from collections.abc import Awaitable, Callable from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from sqlalchemy import text from sqlalchemy.orm.exc import StaleDataError from app.analytics.router import router as analytics_router from app.budgeting.router import router as budgeting_router from app.db.session import get_engine from app.errors import AppError from app.exports.router import router as exports_router from app.identity.router import router as identity_router from app.imports.router import router as imports_router from app.ledger.router import router as ledger_router from app.spaces.router import router as spaces_router app = FastAPI( title="Finance Tracker API", version="0.1.0", docs_url="/api/docs", openapi_url="/api/v1/openapi.json", ) @app.exception_handler(AppError) async def app_error_handler(_request: Request, exc: AppError) -> JSONResponse: return JSONResponse( status_code=exc.status_code, content={"error": {"code": exc.code, "message": exc.message, "details": exc.details}}, ) @app.exception_handler(StaleDataError) async def stale_data_handler(_request: Request, _exc: StaleDataError) -> JSONResponse: return JSONResponse( status_code=409, content={"error": {"code": "VERSION_CONFLICT", "message": "Resource changed"}}, ) @app.middleware("http") async def security_headers(request: Request, call_next: Callable[[Request], Awaitable]): response = await call_next(request) response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["Referrer-Policy"] = "no-referrer" response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()" response.headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'" return response @app.get("/health") def health() -> dict: return {"status": "ok"} @app.get("/healthz", include_in_schema=False) def healthz() -> dict: return health() @app.get("/ready") def readiness() -> dict: with get_engine().connect() as connection: connection.execute(text("select 1")) return {"status": "ready"} for api_router in ( identity_router, spaces_router, ledger_router, budgeting_router, imports_router, analytics_router, exports_router, ): app.include_router(api_router, prefix="/api/v1")