| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- """Main FastAPI app instance declaration."""
- import typing
- from contextlib import asynccontextmanager
- from fastapi import FastAPI
- from fastapi.middleware.cors import CORSMiddleware
- from fastapi.middleware.trustedhost import TrustedHostMiddleware
- from fastapi.openapi.utils import get_openapi
- from app.api.api import api_router
- from app.core import config
- from app.core.events import broadcast
- @asynccontextmanager
- async def lifespan(app: FastAPI):
- await broadcast.connect()
- yield
- await broadcast.disconnect()
- app = FastAPI(
- title=config.settings.PROJECT_NAME,
- version=config.settings.VERSION,
- description=config.settings.DESCRIPTION,
- openapi_url="/openapi.json",
- docs_url="/",
- lifespan=lifespan,
- )
- app.include_router(api_router)
- # Sets all CORS enabled origins
- app.add_middleware(
- CORSMiddleware,
- allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
- )
- # Guards against HTTP Host Header attacks
- if config.settings.ENVIRONMENT != "PYTEST":
- app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.settings.ALLOWED_HOSTS)
- def custom_openapi():
- if app.openapi_schema:
- return app.openapi_schema
- openapi_schema = get_openapi(
- title=config.settings.PROJECT_NAME,
- version=config.settings.VERSION,
- description=config.settings.DESCRIPTION,
- routes=app.routes,
- )
- one_of_schemas = []
- for route in app.routes:
- if hasattr(route, "openapi_extra") and route.openapi_extra:
- event_name = route.openapi_extra.get("sse_event")
- if not event_name:
- continue
- method = list(route.methods)[0] if route.methods else "GET"
- payload_schema = {}
- # Determine payload schema for DELETE (Path params)
- model = getattr(route, "response_model", None)
- if method == "DELETE" and model is None:
- props = {}
- for param in getattr(route.dependant, "path_params", []):
- props[param.name] = {"type": "string"}
- payload_schema = {"type": "object", "properties": props}
- # Determine payload schema for POST/PUT (Response Model)
- else:
- origin = typing.get_origin(model)
- if origin is list:
- item_model = typing.get_args(model)[0]
- if hasattr(item_model, "__name__"):
- model_name = item_model.__name__
- payload_schema = {
- "type": "array",
- "items": {"$ref": f"#/components/schemas/{model_name}"},
- }
- elif hasattr(model, "__name__"):
- model_name = model.__name__
- payload_schema = {"$ref": f"#/components/schemas/{model_name}"}
- # Build the discriminated union object with traceability in the description
- event_schema = {
- "type": "object",
- "description": f"Triggered by `{method}` `{route.path}`",
- "properties": {
- "event": {"type": "string", "enum": [event_name]},
- "data": payload_schema,
- },
- "required": ["event", "data"],
- }
- one_of_schemas.append(event_schema)
- if one_of_schemas:
- if "components" not in openapi_schema:
- openapi_schema["components"] = {"schemas": {}}
- elif "schemas" not in openapi_schema["components"]:
- openapi_schema["components"]["schemas"] = {}
- openapi_schema["components"]["schemas"]["SseEventPayload"] = {
- "title": "SseEventPayload",
- "description": "Discriminated union of all possible SSE events and their payloads.",
- "oneOf": one_of_schemas,
- }
- for path, path_item in openapi_schema["paths"].items():
- if path.endswith("/stream") and "get" in path_item:
- path_item["get"]["responses"]["200"]["content"] = {
- "text/event-stream": {
- "schema": {"$ref": "#/components/schemas/SseEventPayload"}
- }
- }
- app.openapi_schema = openapi_schema
- return app.openapi_schema
- # 5. Override the default OpenAPI method
- app.openapi = custom_openapi
|