from __future__ import annotations from importlib import import_module from pathlib import Path from tempfile import NamedTemporaryFile from alembic.config import Config from alembic.script import ScriptDirectory from fastapi import APIRouter, Response, status from sqlalchemy import text from app.core.config import get_settings from app.db.session import get_engine from app.providers.registry import list_provider_capabilities from app.schemas.health import ( HealthResponse, SystemCapabilities, SystemCapabilitiesEnvelope, ) from app.services.model_registry_service import ModelRegistryService router = APIRouter() def _dependency_enabled(module: str) -> bool: try: import_module(module) return True except Exception: return False def _expected_migration_heads() -> list[str]: backend_root = Path(__file__).resolve().parents[3] config = Config(str(backend_root / "alembic.ini")) config.set_main_option("script_location", str(backend_root / "alembic")) return list(ScriptDirectory.from_config(config).get_heads()) def _database_checks() -> dict[str, str]: checks = { "database": "degraded", "postgis": "degraded", "migration": "degraded", } try: with get_engine().connect() as connection: connection.execute(text("SELECT 1")) checks["database"] = "ok" connection.execute( text("SELECT PostGIS_Version()") ).scalar_one() checks["postgis"] = "ok" database_head = connection.execute( text("SELECT version_num FROM alembic_version") ).scalar_one() expected_heads = _expected_migration_heads() if len(expected_heads) == 1 and database_head == expected_heads[0]: checks["migration"] = "ok" else: checks["migration"] = "degraded" except Exception: return checks return checks def _storage_check(storage_root: str) -> str: root = Path(storage_root).expanduser() try: root.mkdir(parents=True, exist_ok=True) with NamedTemporaryFile( prefix=".geointel-readiness-", dir=root, delete=True, ) as handle: handle.write(b"ok") handle.flush() return "ok" except OSError: return "degraded" def _readiness_payload() -> HealthResponse: settings = get_settings() checks = _database_checks() checks["storage"] = _storage_check(settings.storage_root) ready = all( value == "ok" or value.startswith("ok:") for value in checks.values() ) return HealthResponse( status="ok" if ready else "degraded", service="geointel-backend", version="public", database=checks["database"], postgis=checks["postgis"], migration=checks["migration"], storage=checks["storage"], checks=checks, ) @router.get("/health/live", response_model=HealthResponse) def liveness() -> HealthResponse: return HealthResponse( status="ok", service="geointel-backend", version="public", ) def _readiness_response(response: Response) -> HealthResponse: payload = _readiness_payload() if payload.status != "ok": response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return payload @router.get("/health", response_model=HealthResponse) def readiness(response: Response) -> HealthResponse: return _readiness_response(response) @router.get("/health/ready", response_model=HealthResponse) def readiness_explicit(response: Response) -> HealthResponse: return _readiness_response(response) @router.get( "/api/v1/system/capabilities", response_model=SystemCapabilitiesEnvelope, ) def capabilities() -> SystemCapabilitiesEnvelope: settings = get_settings() providers = [item.to_dict() for item in list_provider_capabilities()] configured_yolo = ModelRegistryService.get_model_capability( settings.yolo_model_id, settings=settings, ) yolo_configured = bool(configured_yolo and configured_yolo.configured) yolo_status = configured_yolo.status if configured_yolo else "not_configured" configured_sam = ModelRegistryService.get_model_capability( settings.sam_model_id, settings=settings, task_type="segmentation", ) postgis_ready = _database_checks()["postgis"].startswith("ok:") return SystemCapabilitiesEnvelope( data=SystemCapabilities( postgis=postgis_ready, rasterio=_dependency_enabled("rasterio"), geopandas=_dependency_enabled("geopandas"), yolo=yolo_configured, yolo_status=yolo_status, sam=bool(configured_sam and configured_sam.configured), grb="bounded", sentinel="planned", version=settings.app_version, build_sha=settings.build_sha, providers=providers, ) )