from __future__ import annotations from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from app.core.errors import AppError from app.db.session import get_db from app.models import Area, Project from app.providers.registry import fetch_provider_data, get_provider, import_provider_dataset, list_provider_capabilities from app.schemas import ( CoverageCatalogResponse, CoverageResolveRequest, CoverageResolveResponse, Envelope, ExternalFetchRequest, ExternalFetchResponse, ProviderCapabilitiesResponse, ProviderCapabilityResponse, ProviderImportRequest, ProviderImportResponse, ProviderLayersResponse, ProviderStatusResponse, ) from app.services.coverage_registry_service import CoverageRegistryService from app.utils.response import envelope router = APIRouter(prefix="/external", tags=["external"]) def _validate_area_in_project(db: Session, project_id, area_id: str | None) -> None: if area_id is None: return area = db.get(Area, area_id) if not area: raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404) if area.project_id != project_id: raise AppError(code="INVALID_DATASET_SCOPE", message="Area does not belong to this project", status_code=400) def _assert_project_exists(db: Session, project_id): project = db.get(Project, project_id) if not project: raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404) def _normalize_layer_input(layers: list[str] | None) -> list[str]: return [layer.strip() for layer in (layers or []) if isinstance(layer, str) and layer.strip()] def _provider_payload(provider_name: str) -> dict: return get_provider(provider_name).capability.to_dict() @router.get("/providers", response_model=Envelope[ProviderCapabilitiesResponse]) def list_external_providers() -> dict: return envelope({ "providers": [provider.to_dict() for provider in list_provider_capabilities()], }) @router.get("/coverage/catalog", response_model=Envelope[CoverageCatalogResponse]) def get_coverage_catalog() -> dict: return envelope(CoverageRegistryService.catalog().model_dump()) @router.post("/coverage/resolve", response_model=Envelope[CoverageResolveResponse]) def resolve_project_coverage(payload: CoverageResolveRequest, db: Session = Depends(get_db)) -> dict: result = CoverageRegistryService.resolve( db, project_id=payload.project_id, bbox=payload.bbox, themes=payload.themes, ) return envelope(result.model_dump()) @router.get( "/providers/capabilities", response_model=Envelope[ProviderCapabilitiesResponse], ) def get_external_provider_capabilities() -> dict: return envelope({ "providers": [provider.to_dict() for provider in list_provider_capabilities()], }) @router.get( "/providers/{provider_name}", response_model=Envelope[ProviderCapabilityResponse], ) def get_external_provider(provider_name: str) -> dict: return envelope(_provider_payload(provider_name)) @router.get( "/providers/{provider_name}/layers", response_model=Envelope[ProviderLayersResponse], ) def get_external_provider_layers(provider_name: str) -> dict: provider = get_provider(provider_name) return envelope({ "provider_name": provider.provider_name, "layers": provider.supported_layers, }) @router.get( "/providers/{provider_name}/status", response_model=Envelope[ProviderStatusResponse], ) def get_external_provider_status(provider_name: str) -> dict: provider = get_provider(provider_name) return envelope({ "provider_name": provider.provider_name, "configured": provider.is_configured, "status": provider.capability.status, "limitation_message": provider.limitation_message, }) @router.post( "/providers/{provider_name}/import", response_model=Envelope[ProviderImportResponse], ) def import_external_provider_dataset(provider_name: str, payload: ProviderImportRequest) -> dict: result = import_provider_dataset( provider_name=provider_name, project_id=payload.project_id, area_id=payload.area_id, layers=_normalize_layer_input(payload.layers), requested_dataset_role=payload.dataset_role, ) return envelope(result.model_dump()) def _run_fetch(payload: ExternalFetchRequest, provider_name: str) -> ExternalFetchResponse: area_id_str = str(payload.area_id) if payload.area_id else None response = fetch_provider_data( provider_name=provider_name, project_id=str(payload.project_id), area_id=area_id_str, layers=_normalize_layer_input(payload.layers), ) return ExternalFetchResponse( provider=provider_name, status=response.get("status", "not_configured"), message=response.get("message", "Provider fetch executed."), requested_layers=_normalize_layer_input(payload.layers), project_id=payload.project_id, area_id=payload.area_id, ) @router.post("/osm/fetch", response_model=Envelope[ExternalFetchResponse]) def fetch_osm(payload: ExternalFetchRequest, db: Session = Depends(get_db)) -> dict: _assert_project_exists(db, payload.project_id) _validate_area_in_project(db, payload.project_id, payload.area_id) return envelope(_run_fetch(payload, "osm").model_dump()) @router.post("/grb/fetch", response_model=Envelope[ExternalFetchResponse]) def fetch_grb(payload: ExternalFetchRequest, db: Session = Depends(get_db)) -> dict: _assert_project_exists(db, payload.project_id) _validate_area_in_project(db, payload.project_id, payload.area_id) return envelope(_run_fetch(payload, "grb").model_dump())