99 lines
2.3 KiB
Python
99 lines
2.3 KiB
Python
|
|
"""FastAPI application entry point."""
|
||
|
|
|
||
|
|
import uuid
|
||
|
|
from contextlib import asynccontextmanager
|
||
|
|
from typing import AsyncGenerator
|
||
|
|
|
||
|
|
from fastapi import FastAPI, Request
|
||
|
|
from fastapi.responses import JSONResponse
|
||
|
|
from sqlalchemy.exc import IntegrityError
|
||
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||
|
|
|
||
|
|
from app.config import settings
|
||
|
|
from app.db import engine
|
||
|
|
from app.routes import (
|
||
|
|
agents_router,
|
||
|
|
health_router,
|
||
|
|
playbooks_router,
|
||
|
|
tasks_router,
|
||
|
|
tenants_router,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# --- Middleware ---
|
||
|
|
|
||
|
|
|
||
|
|
class RequestIDMiddleware(BaseHTTPMiddleware):
|
||
|
|
"""Middleware that adds a unique request ID to each request."""
|
||
|
|
|
||
|
|
async def dispatch(self, request: Request, call_next):
|
||
|
|
request_id = str(uuid.uuid4())
|
||
|
|
request.state.request_id = request_id
|
||
|
|
response = await call_next(request)
|
||
|
|
response.headers["X-Request-ID"] = request_id
|
||
|
|
return response
|
||
|
|
|
||
|
|
|
||
|
|
# --- Lifespan ---
|
||
|
|
|
||
|
|
|
||
|
|
@asynccontextmanager
|
||
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||
|
|
"""Application lifespan handler for startup and shutdown."""
|
||
|
|
# Startup
|
||
|
|
yield
|
||
|
|
# Shutdown
|
||
|
|
await engine.dispose()
|
||
|
|
|
||
|
|
|
||
|
|
# --- Application ---
|
||
|
|
|
||
|
|
|
||
|
|
app = FastAPI(
|
||
|
|
title=settings.APP_NAME,
|
||
|
|
version=settings.APP_VERSION,
|
||
|
|
description="Control-plane backend for the LetsBe Cloud platform",
|
||
|
|
lifespan=lifespan,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Add middleware
|
||
|
|
app.add_middleware(RequestIDMiddleware)
|
||
|
|
|
||
|
|
|
||
|
|
# --- Exception Handlers ---
|
||
|
|
|
||
|
|
|
||
|
|
@app.exception_handler(IntegrityError)
|
||
|
|
async def integrity_error_handler(request: Request, exc: IntegrityError) -> JSONResponse:
|
||
|
|
"""Handle database integrity errors (unique constraint violations, etc.)."""
|
||
|
|
return JSONResponse(
|
||
|
|
status_code=409,
|
||
|
|
content={
|
||
|
|
"detail": "Resource conflict: a record with these values already exists",
|
||
|
|
"request_id": getattr(request.state, "request_id", None),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# --- Routers ---
|
||
|
|
|
||
|
|
|
||
|
|
app.include_router(health_router)
|
||
|
|
app.include_router(tenants_router, prefix="/api/v1")
|
||
|
|
app.include_router(tasks_router, prefix="/api/v1")
|
||
|
|
app.include_router(agents_router, prefix="/api/v1")
|
||
|
|
app.include_router(playbooks_router, prefix="/api/v1")
|
||
|
|
|
||
|
|
|
||
|
|
# --- Root endpoint ---
|
||
|
|
|
||
|
|
|
||
|
|
@app.get("/")
|
||
|
|
async def root():
|
||
|
|
"""Root endpoint redirecting to docs."""
|
||
|
|
return {
|
||
|
|
"message": f"Welcome to {settings.APP_NAME}",
|
||
|
|
"docs": "/docs",
|
||
|
|
"health": "/health",
|
||
|
|
}
|