import uuid from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from sqlalchemy import text from starlette.middleware.sessions import SessionMiddleware from app.api.routers import ( audit, auth, bookings, customers, dashboard, data_quality, demo, integration_status, integrations, knowledge, mcp_integrations, search, users, vehicles, workflows, ) from app.api.routers.auth import bootstrap_initial_admin from app.core.config import PRODUCT_NAME, get_settings from app.core.db import SessionLocal from app.core.errors import AppError, error_body from app.services.dispatcher import start_background_dispatcher, stop_background_dispatcher settings = get_settings() @asynccontextmanager async def lifespan(_app: FastAPI): with SessionLocal() as db: bootstrap_initial_admin(db) start_background_dispatcher() yield stop_background_dispatcher() app = FastAPI(title=f"{PRODUCT_NAME} API", version="0.1.0", lifespan=lifespan) app.add_middleware( SessionMiddleware, secret_key=settings.app_secret, session_cookie="mobilityops_oidc_state", max_age=600, same_site="lax", https_only=settings.session_cookie_secure, ) app.add_middleware( CORSMiddleware, allow_origins=[o.strip() for o in settings.cors_allow_origins.split(",")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(AppError) def handle_app_error(_request: Request, exc: AppError) -> JSONResponse: return JSONResponse( status_code=exc.status_code, content=error_body(exc.code, exc.message, exc.correlation_id, exc.details), ) @app.exception_handler(HTTPException) def handle_http_exception(_request: Request, exc: HTTPException) -> JSONResponse: return JSONResponse( status_code=exc.status_code, content=error_body( code=str(exc.status_code), message=str(exc.detail), correlation_id=str(uuid.uuid4()), details={}, ), ) @app.get("/health") def health() -> dict[str, str]: return {"status": "ok", "service": "mobilityops-api"} @app.get("/health/live") def liveness() -> dict[str, str]: """Process liveness only; external dependencies deliberately do not affect it.""" return {"status": "ok", "service": "mobilityops-api"} @app.get("/health/ready") def readiness() -> JSONResponse: """Traffic readiness: the API is useful only while its canonical database responds.""" try: with SessionLocal() as db: db.execute(text("SELECT 1")) except Exception: # noqa: BLE001 -- readiness must convert infrastructure errors to 503 return JSONResponse( status_code=503, content={"status": "not_ready", "service": "mobilityops-api", "database": "down"}, ) return JSONResponse(content={"status": "ready", "service": "mobilityops-api", "database": "up"}) @app.get("/api/v1/system/status") def system_status() -> dict[str, object]: return { "service": "mobilityops-api", "environment": settings.mobilityops_env, "demo_mode": settings.mobilityops_demo_mode, "knowledge_provider": settings.knowledge_provider, "oidc_enabled": auth.oidc_status().enabled, "oidc_provider_name": auth.oidc_status().provider_name, } app.include_router(demo.router) app.include_router(auth.router) app.include_router(dashboard.router) app.include_router(vehicles.router) app.include_router(bookings.router) app.include_router(customers.router) app.include_router(audit.router) app.include_router(data_quality.router) app.include_router(workflows.router) app.include_router(integrations.router) app.include_router(knowledge.router) app.include_router(mcp_integrations.router) app.include_router(search.router) app.include_router(integration_status.router) app.include_router(users.router)