letsbe-orchestrator/app/main.py

103 lines
2.5 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,
env_router,
files_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")
app.include_router(env_router, prefix="/api/v1")
app.include_router(files_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",
}