satdetect-dev / app /dda /dept_export.py
coderuday21's picture
Fix DDA dev bugs and polish from full review pass.
bec7397
Raw
History Blame Contribute Delete
5.1 kB
"""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()
@dataclass
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()