from threading import Lock from fastapi import FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from .config import get_settings from .schemas import ObjectSearchRequest, TrailRequest from .services import AfterimageService settings = get_settings() _service_lock = Lock() _service_instance: AfterimageService | None = None app = FastAPI(title="Afterimage API", version="0.1.0") app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origin_list, allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) app.mount("/assets", StaticFiles(directory=settings.resolved_asset_root), name="assets") def service() -> AfterimageService: global _service_instance if _service_instance is None: with _service_lock: if _service_instance is None: _service_instance = AfterimageService(settings) return _service_instance def bad_request(exc: Exception) -> HTTPException: return HTTPException(status_code=400, detail=str(exc)) @app.get("/api/health") def health(): try: return service().health() except Exception as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc @app.get("/api/scenarios") def scenarios(): try: return service().scenarios() except Exception as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc @app.get("/api/memories") def memories( scenario: str = Query(default="vault", max_length=60), zone: str | None = Query(default=None), region_id: str | None = Query(default=None), is_baseline: bool | None = Query(default=None), ): try: return service().memories(scenario=scenario, zone=zone, region_id=region_id, is_baseline=is_baseline) except ValueError as exc: raise bad_request(exc) from exc @app.post("/api/anomaly/scan") def anomaly_scan(scenario: str = Query(default="vault", max_length=60)): try: return service().scan_anomalies(scenario=scenario) except (FileNotFoundError, ValueError) as exc: raise bad_request(exc) from exc @app.post("/api/object/search") def object_search(request: ObjectSearchRequest): try: return service().object_search( scenario=request.scenario, asset_id=request.asset_id or request.crop_id, image_ref=request.image_ref, filters=request.filters, limit=request.limit, ) except (FileNotFoundError, ValueError) as exc: raise bad_request(exc) from exc @app.post("/api/trail") def trail(request: TrailRequest): try: return service().trail( scenario=request.scenario, asset_id=request.asset_id or request.crop_id, image_ref=request.image_ref, score_cutoff=request.score_cutoff, ) except (FileNotFoundError, ValueError) as exc: raise bad_request(exc) from exc @app.get("/api/outliers") def outliers(scenario: str = Query(default="vault", max_length=60)): try: return service().outliers(scenario=scenario) except ValueError as exc: raise bad_request(exc) from exc @app.get("/api/matrix") def matrix(scenario: str = Query(default="vault", max_length=60), sample: int = Query(default=24, ge=1, le=100)): try: return service().matrix(scenario=scenario, sample=sample) except ValueError as exc: raise bad_request(exc) from exc @app.get("/api/text_search") def text_search(q: str = Query(..., max_length=120), scenario: str | None = Query(default=None, max_length=60)): try: return service().text_search(query=q, scenario=scenario) except (FileNotFoundError, ValueError) as exc: raise bad_request(exc) from exc