88 lines
2.9 KiB
Python
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()
|