Spaces:
Sleeping
Sleeping
| # backend/app/data/store.py | |
| from __future__ import annotations | |
| import json, sqlite3 | |
| from datetime import datetime, timezone, timedelta | |
| from typing import Dict, Any, List, Optional | |
| from pathlib import Path | |
| from ..config.settings import settings | |
| from .geo import haversine_km | |
| DB_PATH: Path = settings.REPORTS_DB | |
| # Ensure parent exists & fail early if unwritable | |
| DB_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| try: | |
| DB_PATH.touch(exist_ok=True) | |
| except Exception as e: | |
| raise RuntimeError(f"Cannot create DB file at {DB_PATH}: {e}") | |
| _CONN = sqlite3.connect(str(DB_PATH), check_same_thread=False) | |
| _CONN.execute(""" | |
| CREATE TABLE IF NOT EXISTS reports ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| lat REAL NOT NULL, | |
| lon REAL NOT NULL, | |
| text TEXT NOT NULL, | |
| props_json TEXT, | |
| created_at TEXT NOT NULL | |
| ) | |
| """) | |
| _CONN.commit() | |
| def _row_to_feature(row: tuple) -> Dict[str, Any]: | |
| _id, lat, lon, text, props_json, created_at = row | |
| props = {"type": "user_report", "text": text, "reported_at": created_at} | |
| if props_json: | |
| try: | |
| props.update(json.loads(props_json)) | |
| except Exception: | |
| props["raw_props"] = props_json | |
| props.setdefault("rid", str(_id)) | |
| props.setdefault("id", str(_id)) | |
| return { | |
| "type": "Feature", | |
| "geometry": {"type": "Point", "coordinates": [lon, lat]}, | |
| "properties": props, | |
| } | |
| def add_report(lat: float, lon: float, text: str = "User report", props: dict | None = None): | |
| created_at = datetime.now(timezone.utc).isoformat() | |
| props = dict(props or {}) | |
| props_json = json.dumps(props) | |
| cur = _CONN.execute( | |
| "INSERT INTO reports (lat, lon, text, props_json, created_at) VALUES (?,?,?,?,?)", | |
| (float(lat), float(lon), text, props_json, created_at) | |
| ) | |
| _CONN.commit() | |
| rid = str(cur.lastrowid) | |
| out_props = {"type": "user_report", "text": text, "reported_at": created_at, **props} | |
| out_props.setdefault("rid", rid) | |
| out_props.setdefault("id", rid) | |
| return { | |
| "type": "Feature", | |
| "geometry": {"type": "Point", "coordinates": [float(lon), float(lat)]}, | |
| "properties": out_props, | |
| } | |
| def get_feature_collection() -> Dict[str, Any]: | |
| cur = _CONN.execute("SELECT id, lat, lon, text, props_json, created_at FROM reports ORDER BY id DESC") | |
| feats = [_row_to_feature(r) for r in cur.fetchall()] | |
| return {"type": "FeatureCollection", "features": feats} | |
| def find_reports_near( | |
| lat: float, | |
| lon: float, | |
| radius_km: float = 10.0, | |
| limit: int = 20, | |
| max_age_hours: Optional[int] = None, | |
| ) -> List[Dict[str, Any]]: | |
| params: list[Any] = [] | |
| sql = "SELECT id, lat, lon, text, props_json, created_at FROM reports" | |
| if max_age_hours is not None: | |
| cutoff = datetime.now(timezone.utc) - timedelta(hours=int(max_age_hours)) | |
| sql += " WHERE datetime(created_at) >= datetime(?)" | |
| params.append(cutoff.isoformat()) | |
| sql += " ORDER BY id DESC LIMIT 2000" | |
| cur = _CONN.execute(sql, params) | |
| center = (lat, lon) | |
| cand = [] | |
| for r in cur.fetchall(): | |
| _, lat2, lon2, *_ = r | |
| d = haversine_km(center, (lat2, lon2)) | |
| if d <= radius_km: | |
| cand.append((d, r)) | |
| cand.sort(key=lambda x: x[0]) | |
| out = [_row_to_feature(r) for _, r in cand[:max(1, limit)]] | |
| return out | |
| def clear_reports() -> dict[str, Any]: | |
| _CONN.execute("DELETE FROM reports") | |
| _CONN.commit() | |
| return {"ok": True, "message": "All reports cleared."} | |