main.py 4.3 KB

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