"""FastAPI application entrypoint. Opens a database connection on startup, runs Alembic migrations, registers routers, locks CORS to the configured frontend origin(s), and attaches a rate-limiter to the app (applied per-endpoint via decorators). """ import asyncio import logging from contextlib import asynccontextmanager from collections.abc import AsyncIterator from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from app.config import settings from app.db.connection import connect, run_migrations from app.rate_limit import limiter from app.routers import vacation_days from app.routers.analytics import router as analytics_router from app.routers.auth import router as auth_router from app.routers.calendar import router as calendar_router from app.routers.day_overrides import router as day_overrides_router from app.routers.fetch_data import router as fetch_data_router from app.routers.fetch_data import scraper_router from app.routers.leave_wishes import router as leave_wishes_router from app.routers.plan import router as plan_router from app.routers.planned_days import router as planned_days_router from app.routers.settings import child_router, settings_child_router, tips_router from app.routers.workspace import _inv_router as invitations_router from app.routers.workspace import router as workspace_router logger = logging.getLogger(__name__) def _configure_logging() -> None: level = logging.getLevelName(settings.log_level.upper()) handler = logging.StreamHandler() handler.setLevel(level) handler.setFormatter(logging.Formatter("%(levelname)s %(name)s %(message)s")) app_logger = logging.getLogger("app") app_logger.handlers.clear() app_logger.addHandler(handler) app_logger.setLevel(level) app_logger.propagate = False app_logger.disabled = False for name, log in logging.Logger.manager.loggerDict.items(): if (name == "app" or name.startswith("app.")) and isinstance(log, logging.Logger): log.disabled = False @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: _configure_logging() if settings.database_type == "sqlite": try: await asyncio.to_thread(run_migrations, f"sqlite:///{settings.sqlite_path}") except Exception as exc: logger.error("Migration failed — DB schema may be incomplete: %s", exc, exc_info=True) # Re-enable after migrations: alembic's env.py calls fileConfig() which may # still touch logger state even with disable_existing_loggers=False. for name, log in logging.Logger.manager.loggerDict.items(): if (name == "app" or name.startswith("app.")) and isinstance(log, logging.Logger): log.disabled = False app.state.db_connection = await connect(settings.sqlite_path) try: yield finally: await app.state.db_connection.close() def create_app() -> FastAPI: app = FastAPI(title="Föräldraledigplannerare API", lifespan=lifespan) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) @app.middleware("http") async def log_error_responses(request: Request, call_next): response = await call_next(request) if response.status_code >= 400: logger.warning("%s %s %d", request.method, request.url.path, response.status_code) return response app.add_middleware( CORSMiddleware, allow_origins=list(settings.cors_origins), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) _register_routers(app) return app def _register_routers(app: FastAPI) -> None: app.include_router(auth_router) app.include_router(workspace_router) app.include_router(invitations_router) app.include_router(fetch_data_router) app.include_router(scraper_router) app.include_router(analytics_router) app.include_router(tips_router) app.include_router(leave_wishes_router) app.include_router(plan_router) app.include_router(child_router) app.include_router(settings_child_router) app.include_router(planned_days_router) app.include_router(vacation_days.router) app.include_router(day_overrides_router) app.include_router(calendar_router) app = create_app() @app.get("/health", tags=["health"]) async def health() -> dict[str, str]: return {"status": "ok"}