main.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """Main FastAPI app instance declaration."""
  2. import typing
  3. from contextlib import asynccontextmanager
  4. from fastapi import FastAPI
  5. from fastapi.middleware.cors import CORSMiddleware
  6. from fastapi.middleware.trustedhost import TrustedHostMiddleware
  7. from fastapi.openapi.utils import get_openapi
  8. from app.api.api import api_router
  9. from app.core import config
  10. from app.core.events import broadcast
  11. @asynccontextmanager
  12. async def lifespan(app: FastAPI):
  13. await broadcast.connect()
  14. yield
  15. await broadcast.disconnect()
  16. app = FastAPI(
  17. title=config.settings.PROJECT_NAME,
  18. version=config.settings.VERSION,
  19. description=config.settings.DESCRIPTION,
  20. openapi_url="/openapi.json",
  21. docs_url="/",
  22. lifespan=lifespan,
  23. )
  24. app.include_router(api_router)
  25. # Sets all CORS enabled origins
  26. app.add_middleware(
  27. CORSMiddleware,
  28. allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS],
  29. allow_credentials=True,
  30. allow_methods=["*"],
  31. allow_headers=["*"],
  32. )
  33. # Guards against HTTP Host Header attacks
  34. if config.settings.ENVIRONMENT != "PYTEST":
  35. app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.settings.ALLOWED_HOSTS)
  36. def custom_openapi():
  37. if app.openapi_schema:
  38. return app.openapi_schema
  39. openapi_schema = get_openapi(
  40. title=config.settings.PROJECT_NAME,
  41. version=config.settings.VERSION,
  42. description=config.settings.DESCRIPTION,
  43. routes=app.routes,
  44. )
  45. one_of_schemas = []
  46. for route in app.routes:
  47. if hasattr(route, "openapi_extra") and route.openapi_extra:
  48. event_name = route.openapi_extra.get("sse_event")
  49. if not event_name:
  50. continue
  51. method = list(route.methods)[0] if route.methods else "GET"
  52. payload_schema = {}
  53. # Determine payload schema for DELETE (Path params)
  54. model = getattr(route, "response_model", None)
  55. if method == "DELETE" and model is None:
  56. props = {}
  57. for param in getattr(route.dependant, "path_params", []):
  58. props[param.name] = {"type": "string"}
  59. payload_schema = {"type": "object", "properties": props}
  60. # Determine payload schema for POST/PUT (Response Model)
  61. else:
  62. origin = typing.get_origin(model)
  63. if origin is list:
  64. item_model = typing.get_args(model)[0]
  65. if hasattr(item_model, "__name__"):
  66. model_name = item_model.__name__
  67. payload_schema = {
  68. "type": "array",
  69. "items": {"$ref": f"#/components/schemas/{model_name}"},
  70. }
  71. elif hasattr(model, "__name__"):
  72. model_name = model.__name__
  73. payload_schema = {"$ref": f"#/components/schemas/{model_name}"}
  74. # Build the discriminated union object with traceability in the description
  75. event_schema = {
  76. "type": "object",
  77. "description": f"Triggered by `{method}` `{route.path}`",
  78. "properties": {
  79. "event": {"type": "string", "enum": [event_name]},
  80. "data": payload_schema,
  81. },
  82. "required": ["event", "data"],
  83. }
  84. one_of_schemas.append(event_schema)
  85. if one_of_schemas:
  86. if "components" not in openapi_schema:
  87. openapi_schema["components"] = {"schemas": {}}
  88. elif "schemas" not in openapi_schema["components"]:
  89. openapi_schema["components"]["schemas"] = {}
  90. openapi_schema["components"]["schemas"]["SseEventPayload"] = {
  91. "title": "SseEventPayload",
  92. "description": "Discriminated union of all possible SSE events and their payloads.",
  93. "oneOf": one_of_schemas,
  94. }
  95. for path, path_item in openapi_schema["paths"].items():
  96. if path.endswith("/stream") and "get" in path_item:
  97. path_item["get"]["responses"]["200"]["content"] = {
  98. "text/event-stream": {
  99. "schema": {"$ref": "#/components/schemas/SseEventPayload"}
  100. }
  101. }
  102. app.openapi_schema = openapi_schema
  103. return app.openapi_schema
  104. # 5. Override the default OpenAPI method
  105. app.openapi = custom_openapi