import asyncio from contextlib import asynccontextmanager, suppress from pathlib import Path from botocore.exceptions import BotoCoreError, ClientError from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware from app.api import v1 from app.config import get_settings from app.db import init_db from app.services import jobs, poller from app.ui import routes as ui @asynccontextmanager async def lifespan(app: FastAPI): init_db() jobs.fail_stale_jobs() task = asyncio.create_task(poller.run()) if get_settings().poll_interval > 0 else None yield if task: task.cancel() with suppress(asyncio.CancelledError): await task def create_app() -> FastAPI: app = FastAPI(title="ros_control", lifespan=lifespan) app.add_middleware( SessionMiddleware, secret_key=get_settings().session_secret, same_site="strict", https_only=False, ) app.mount("/static", StaticFiles(directory=Path(__file__).parent / "ui" / "static"), name="static") app.include_router(v1.router) app.include_router(ui.router) @app.exception_handler(ui.LoginRequired) async def _login(_: Request, e: ui.LoginRequired): return RedirectResponse("/login", status_code=303) @app.exception_handler(LookupError) async def _404(_: Request, e: LookupError): return JSONResponse({"detail": str(e).strip("'\"")}, status_code=404) @app.exception_handler(ValueError) async def _400(_: Request, e: ValueError): return JSONResponse({"detail": str(e)}, status_code=400) @app.exception_handler(BotoCoreError) @app.exception_handler(ClientError) async def _502(_: Request, e: Exception): return JSONResponse({"detail": f"S3: {e}"}, status_code=502) return app app = create_app()