Spaces:
Sleeping
Sleeping
| """Department export adapter — API or file fallback (FR-08).""" | |
| from __future__ import annotations | |
| import csv | |
| import io | |
| import logging | |
| import os | |
| from dataclasses import dataclass | |
| from typing import Any, Dict, List, Optional | |
| import requests | |
| from ..models import DetectionRun | |
| from .geo_regions import region_lat_lng | |
| logger = logging.getLogger(__name__) | |
| DEPT_API_URL = os.environ.get("DEPT_API_URL", "").strip() | |
| DEPT_API_KEY = os.environ.get("DEPT_API_KEY", "").strip() | |
| class ExportResult: | |
| ok: bool | |
| mode: str # api | file | |
| message: str | |
| submitted_count: int = 0 | |
| download_path: Optional[str] = None | |
| detail: Optional[Dict[str, Any]] = None | |
| def regions_to_csv_rows(run: DetectionRun, regions: List[dict]) -> str: | |
| """Build CSV string for export.""" | |
| buf = io.StringIO() | |
| writer = csv.writer(buf) | |
| writer.writerow([ | |
| "run_id", "run_title", "zone", "village", "region_id", | |
| "dda_change_type", "internal_type", "confidence", "area_px", | |
| "latitude", "longitude", "area_sq_m", "review_status", "notes", | |
| ]) | |
| for r in regions: | |
| lat, lng = region_lat_lng(r) | |
| writer.writerow([ | |
| run.id, | |
| run.title, | |
| run.zone or "", | |
| run.village or "", | |
| r.get("id", ""), | |
| r.get("ddaChangeType") or "", | |
| r.get("internalObjectType") or r.get("objectType") or "", | |
| f'{(r.get("confidence") or 0):.4f}', | |
| r.get("area", ""), | |
| lat if lat is not None else "", | |
| lng if lng is not None else "", | |
| r.get("areaSqM") if r.get("areaSqM") is not None else "", | |
| r.get("reviewStatus") or "pending", | |
| r.get("reviewNotes") or "", | |
| ]) | |
| return buf.getvalue() | |
| class FileExporter: | |
| """Fallback — client downloads CSV via API.""" | |
| def submit(self, run: DetectionRun, regions: List[dict]) -> ExportResult: | |
| if not regions: | |
| return ExportResult( | |
| ok=False, | |
| mode="file", | |
| message="No confirmed regions to export.", | |
| submitted_count=0, | |
| ) | |
| return ExportResult( | |
| ok=True, | |
| mode="file", | |
| message=f"{len(regions)} confirmed region(s) ready for CSV download.", | |
| submitted_count=len(regions), | |
| download_path=f"/api/dda/reports/{run.id}/export.csv?confirmed=1", | |
| ) | |
| class ApiExporter: | |
| """POST confirmed changes to departmental API.""" | |
| def __init__(self, url: str, api_key: str = ""): | |
| self.url = url.rstrip("/") | |
| self.api_key = api_key | |
| def submit(self, run: DetectionRun, regions: List[dict]) -> ExportResult: | |
| if not regions: | |
| return ExportResult( | |
| ok=False, | |
| mode="api", | |
| message="No confirmed regions to submit.", | |
| ) | |
| payload = { | |
| "runId": run.id, | |
| "title": run.title, | |
| "zone": run.zone or "", | |
| "village": run.village or "", | |
| "changePercentage": run.change_percentage, | |
| "regions": [ | |
| { | |
| "id": r.get("id"), | |
| "ddaChangeType": r.get("ddaChangeType"), | |
| "objectType": r.get("objectType"), | |
| "confidence": r.get("confidence"), | |
| "area": r.get("area"), | |
| "latitude": r.get("latitude") or (r.get("latLng") or {}).get("lat"), | |
| "longitude": r.get("longitude") or (r.get("latLng") or {}).get("lng"), | |
| "areaSqM": r.get("areaSqM"), | |
| } | |
| for r in regions | |
| ], | |
| } | |
| headers = {"Content-Type": "application/json", "Accept": "application/json"} | |
| if self.api_key: | |
| headers["Authorization"] = f"Bearer {self.api_key}" | |
| try: | |
| resp = requests.post(self.url, json=payload, headers=headers, timeout=60) | |
| if 200 <= resp.status_code < 300: | |
| detail = None | |
| try: | |
| detail = resp.json() | |
| except Exception: | |
| detail = {"body": resp.text[:500]} | |
| return ExportResult( | |
| ok=True, | |
| mode="api", | |
| message=f"Submitted {len(regions)} confirmed region(s) to departmental system.", | |
| submitted_count=len(regions), | |
| detail=detail, | |
| ) | |
| return ExportResult( | |
| ok=False, | |
| mode="api", | |
| message=f"Department API returned {resp.status_code}: {resp.text[:200]}", | |
| ) | |
| except Exception as exc: | |
| logger.exception("Department API submit failed") | |
| return ExportResult( | |
| ok=False, | |
| mode="api", | |
| message=f"Department API error: {exc}", | |
| ) | |
| def get_exporter() -> FileExporter | ApiExporter: | |
| if DEPT_API_URL: | |
| return ApiExporter(DEPT_API_URL, DEPT_API_KEY) | |
| return FileExporter() | |