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 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( 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, } 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)