YuraBodnar's picture
feat(api): bootstrap FastAPI skeleton, DI stubs, Docker, CI/CD
6ca3a1d
Raw History Blame Contribute Delete
3 kB
"""FastAPI app factory: lifespan, CORS, error handling, routing, static UI.
See ARCHITECTURE_AND_PLAN.md §3 (System Architecture) and §9 (API Surface).
"""
from __future__ import annotations
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from chokepoint.api.middleware import RequestContextMiddleware
from chokepoint.api.routers import health, simulate
from chokepoint.config import get_settings
from chokepoint.logging import configure_logging, get_logger
WEB_DIR = Path(__file__).resolve().parent.parent / "web"
logger = get_logger("chokepoint.api")
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
settings = get_settings()
configure_logging(settings.log_level)
logger.info(
"startup",
extra={
"method": "-",
"path": "-",
"status_code": 0,
"duration_ms": 0.0,
},
)
yield
logger.info(
"shutdown",
extra={
"method": "-",
"path": "-",
"status_code": 0,
"duration_ms": 0.0,
},
)
def create_app() -> FastAPI:
settings = get_settings()
app = FastAPI(
title="ChokePoint AI",
description="Supply Chain Shock Simulator — OSINT-driven cascading impact forecasts.",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(RequestContextMiddleware)
if settings.app_env == "dev":
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
request_id = getattr(request.state, "request_id", "unknown")
logger.error(
"unhandled exception",
extra={
"request_id": request_id,
"method": request.method,
"path": request.url.path,
"status_code": 500,
"duration_ms": 0.0,
},
exc_info=exc,
)
return JSONResponse(
status_code=500,
media_type="application/problem+json",
content={
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred. This has been logged.",
"request_id": request_id,
},
)
app.include_router(health.router)
app.include_router(simulate.router)
if WEB_DIR.is_dir():
app.mount("/", StaticFiles(directory=str(WEB_DIR), html=True), name="web")
return app
app = create_app()