Files
2026-04-04 13:53:57 -04:00

88 lines
2.9 KiB
Python

"""FastAPI application entry point.
Assembles middleware, lifespan (poller start/stop), auth, routes.
All request handlers live in app/routes/*.
"""
from __future__ import annotations
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from .logging_config import RequestIdMiddleware, configure_logging
from .poller import cancel_all_tasks, poll_loop
from .rate_limit import limiter
from .routes import api_alerts, api_auth, api_config, api_events, api_health, api_ups, pages, sse
from .security import SecurityHeadersMiddleware
from .settings import settings
logger = logging.getLogger(__name__)
_background_tasks: list[asyncio.Task] = []
@asynccontextmanager
async def lifespan(app: FastAPI):
configure_logging()
if not settings.session_secret:
logger.warning(
"SESSION_SECRET not set; using insecure ephemeral secret. "
"Set SESSION_SECRET in your environment for production."
)
task = asyncio.create_task(poll_loop())
_background_tasks.append(task)
logger.info("Poller started")
try:
yield
finally:
logger.info("Shutting down: cancelling background tasks")
for t in _background_tasks:
t.cancel()
for t in _background_tasks:
try:
await t
except (asyncio.CancelledError, Exception):
pass
await cancel_all_tasks()
logger.info("Shutdown complete")
def create_app() -> FastAPI:
application = FastAPI(title="APC UPS Dashboard", lifespan=lifespan)
application.state.limiter = limiter
application.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# Middleware (outermost first in registration; Starlette wraps inside-out)
application.add_middleware(SecurityHeadersMiddleware)
application.add_middleware(RequestIdMiddleware)
application.mount(
"/static", StaticFiles(directory="app/static"), name="static"
)
# Routers
application.include_router(pages.router)
application.include_router(api_auth.router)
application.include_router(api_health.router)
application.include_router(api_config.router)
application.include_router(api_ups.router)
application.include_router(api_alerts.router)
application.include_router(api_events.router)
application.include_router(sse.router)
@application.exception_handler(Exception)
async def unhandled_exc(request: Request, exc: Exception): # noqa: ARG001
logger.exception("Unhandled error", extra={"path": request.url.path})
return JSONResponse({"detail": "Internal server error"}, status_code=500)
return application
app = create_app()