Initial public release
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
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 _assert_guest_project_scope(request: Request, project_id) -> None:
|
||||
principal = getattr(request.state, "auth_principal", None)
|
||||
if (
|
||||
getattr(principal, "role", None) == "guest"
|
||||
and getattr(principal, "project_id", None) != project_id
|
||||
):
|
||||
raise AppError(
|
||||
code="GUEST_PROJECT_SCOPE_REQUIRED",
|
||||
message="Deze gastensessie heeft alleen toegang tot de GeoIntel-demowerkruimte.",
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
_assert_guest_project_scope(request, payload.project_id)
|
||||
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())
|
||||
Reference in New Issue
Block a user