satdetect-dev / app /dda /training_routes.py
coderuday21's picture
Complete Phase 7: session users, RBAC, training export, and UAT hardening.
ac7ea7c
Raw
History Blame Contribute Delete
3.23 kB
"""False-positive training archive export (FR-08 Phase 2 / Phase 7)."""
from __future__ import annotations
import csv
import io
import json
import logging
from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import Response
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import DetectionRun, User
from .dda_auth import current_dda_user, require_admin_or_key
from .geo_regions import region_lat_lng
from .models import RegionReview
from .review_service import load_regions
logger = logging.getLogger(__name__)
router = APIRouter()
def _require_dda():
from .config import IS_DDA_MODE
if not IS_DDA_MODE:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="DDA mode is not enabled")
@router.get("/training/export")
def export_false_positives(
request: Request,
fmt: str = Query("csv"),
db: Session = Depends(get_db),
user: User = Depends(current_dda_user),
):
"""Export all false-positive regions for model training (admin or export key)."""
_require_dda()
require_admin_or_key(request, user, db)
if fmt not in ("csv", "json"):
from fastapi import HTTPException
raise HTTPException(status_code=400, detail="fmt must be csv or json")
reviews = (
db.query(RegionReview)
.filter(RegionReview.status == "false_positive")
.order_by(RegionReview.reviewed_at.desc())
.all()
)
rows = []
for rev in reviews:
run = db.query(DetectionRun).filter(DetectionRun.id == rev.run_id).first()
if not run:
continue
regions = load_regions(run)
region = next((r for r in regions if int(r.get("id", -1)) == rev.region_id), None)
if not region:
continue
lat, lng = region_lat_lng(region)
rows.append({
"runId": run.id,
"runTitle": run.title,
"regionId": rev.region_id,
"ddaChangeType": region.get("ddaChangeType") or region.get("objectType"),
"internalType": region.get("internalObjectType") or region.get("objectType"),
"confidence": region.get("confidence"),
"areaPx": region.get("area"),
"latitude": lat,
"longitude": lng,
"notes": rev.notes or "",
"reviewedAt": rev.reviewed_at.isoformat() if rev.reviewed_at else None,
})
if fmt == "json":
return {"count": len(rows), "falsePositives": rows}
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow([
"run_id", "run_title", "region_id", "dda_change_type", "internal_type",
"confidence", "area_px", "latitude", "longitude", "notes", "reviewed_at",
])
for r in rows:
writer.writerow([
r["runId"], r["runTitle"], r["regionId"], r["ddaChangeType"], r["internalType"],
r.get("confidence"), r.get("areaPx"), r.get("latitude"), r.get("longitude"),
r.get("notes"), r.get("reviewedAt"),
])
return Response(
content=buf.getvalue(),
media_type="text/csv",
headers={"Content-Disposition": 'attachment; filename="dda_false_positives.csv"'},
)