"""FastAPI application factory and production entrypoint.""" from typing import Any import psycopg import uvicorn from fastapi import FastAPI, Response, status from app import __version__ from app.api.v1 import demo_router from app.core.config import get_settings from app.core.problems import ApiProblem, api_problem_handler from app.core.request_context import trace_request from app.core.secrets import SecretFileError type HealthPayload = dict[str, str | dict[str, str]] def live() -> dict[str, str]: return {"status": "ok", "version": __version__} def ready(response: Response) -> HealthPayload: settings = get_settings() try: dsn = settings.database_url().set(drivername="postgresql") with psycopg.connect( dsn.render_as_string(hide_password=False), connect_timeout=2, autocommit=True, ) as connection: result = connection.execute("SELECT 1").fetchone() if result != (1,): raise psycopg.DatabaseError("readiness query returned an unexpected result") except (OSError, SecretFileError, psycopg.Error): # Do not expose connection strings, secret paths, hostnames, or driver errors. response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return {"status": "not_ready", "checks": {"database": "unavailable"}} return {"status": "ready", "checks": {"database": "ok"}} def meta() -> dict[str, Any]: settings = get_settings() return { "name": settings.app_name, "environment": settings.app_env, "version": __version__, "models": { "embedding": settings.embedding_model, "rerank": settings.rerank_model, "generation": settings.llm_model, }, } def create_app() -> FastAPI: """Create the API without opening a database or loading model credentials.""" api = FastAPI( title="Geological RAG API", version=__version__, openapi_tags=[ {"name": "health", "description": "Process and database health probes."}, {"name": "meta", "description": "Safe runtime capability metadata."}, {"name": "offline-demo", "description": "Synthetic offline validation only."}, ], ) api.middleware("http")(trace_request) api.add_exception_handler(ApiProblem, api_problem_handler) # type: ignore[arg-type] api.include_router(demo_router) api.add_api_route( "/health/live", live, methods=["GET"], tags=["health"], include_in_schema=False, ) api.add_api_route( "/api/v1/health/live", live, methods=["GET"], tags=["health"], operation_id="getLiveness", ) api.add_api_route( "/health/ready", ready, methods=["GET"], tags=["health"], include_in_schema=False, responses={status.HTTP_503_SERVICE_UNAVAILABLE: {"description": "Database unavailable"}}, ) api.add_api_route( "/api/v1/health/ready", ready, methods=["GET"], tags=["health"], operation_id="getReadiness", responses={status.HTTP_503_SERVICE_UNAVAILABLE: {"description": "Database unavailable"}}, ) api.add_api_route( "/api/v1/meta", meta, methods=["GET"], tags=["meta"], operation_id="getRuntimeMetadata", ) return api app = create_app() if __name__ == "__main__": # Compose publishes this listener only on the host loopback interface. uvicorn.run("app.main:app", host="0.0.0.0", port=8000) # noqa: S104