"""FastAPI application initialization is performed here.""" import os from contextlib import asynccontextmanager from typing import NoReturn from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.openapi.docs import get_swagger_ui_html from fastapi.responses import JSONResponse from {{project_slug}}.config import {{ProjectName}}Config from {{project_slug}}.db.connection.manager import PostgresConnectionManager from {{project_slug}}.dependencies import connection_manager_dep, logger_dep, metrics_dep from {{project_slug}}.exceptions.mapper import ExceptionMapper from {{project_slug}}.handlers.debug import DebugException, DebugExceptionWithParams from {{project_slug}}.middlewares.exception_handler import ExceptionHandlerMiddleware, HandlerNotFoundError from {{project_slug}}.middlewares.observability import ObservabilityMiddleware from {{project_slug}}.observability.logging import configure_logging from {{project_slug}}.observability.metrics import setup_metrics from {{project_slug}}.observability.otel_agent import OpenTelemetryAgent from {{project_slug}}.observability.utils import URLsMapper from .handlers import list_of_routers from .version import LAST_UPDATE, VERSION def bind_routes(application: FastAPI, prefix: str, debug: bool) -> None: """Bind all routes to application.""" for router in list_of_routers: if not debug: to_remove = [] for i, route in enumerate(router.routes): if "debug" in route.path: to_remove.append(i) for i in to_remove[::-1]: del router.routes[i] if len(router.routes) > 0: application.include_router(router, prefix=(prefix if "/" not in {r.path for r in router.routes} else "")) def get_app(prefix: str = "/api") -> FastAPI: """Create application and all dependable objects.""" if "CONFIG_PATH" not in os.environ: raise ValueError("CONFIG_PATH environment variable is not set") app_config: {{ProjectName}}Config = {{ProjectName}}Config.from_file(os.getenv("CONFIG_PATH")) description = "{{project_description}}" application = FastAPI( title="{{project_name}}", description=description, docs_url=None, openapi_url=f"{prefix}/openapi", version=f"{VERSION} ({LAST_UPDATE})", terms_of_service="http://swagger.io/terms/", contact={"email": "idu@itmo.ru"}, license_info={"name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html"}, lifespan=lifespan, ) bind_routes(application, prefix, app_config.app.debug) @application.get(f"{prefix}/docs", include_in_schema=False) async def custom_swagger_ui_html(): return get_swagger_ui_html( openapi_url=application.openapi_url, title=application.title + " - Swagger UI", oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, swagger_js_url="https://unpkg.com/swagger-ui-dist@5.11.7/swagger-ui-bundle.js", swagger_css_url="https://unpkg.com/swagger-ui-dist@5.11.7/swagger-ui.css", ) @application.exception_handler(404) async def handle_404(request: Request, exc: Exception) -> NoReturn: raise HandlerNotFoundError() from exc application.add_middleware( CORSMiddleware, allow_origins=app_config.app.cors.allow_origins, allow_credentials=app_config.app.cors.allow_credentials, allow_methods=app_config.app.cors.allow_methods, allow_headers=app_config.app.cors.allow_headers, ) application.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=5) application.state.config = app_config logger = configure_logging( app_config.observability.logging, tracing_enabled=app_config.observability.jaeger is not None, ) metrics = setup_metrics() exception_mapper = ExceptionMapper() _register_exceptions(exception_mapper, debug=app_config.app.debug) connection_manager = PostgresConnectionManager( master=app_config.db.master, replicas=app_config.db.replicas, logger=logger, application_name=f"{{project_slug}}_{VERSION}", ) urls_mapper = URLsMapper() urls_mapper.add_routes(application.routes) connection_manager_dep.init_dispencer(application, connection_manager) metrics_dep.init_dispencer(application, metrics) logger_dep.init_dispencer(application, logger) application.add_middleware( ExceptionHandlerMiddleware, debug=app_config.app.debug, exception_mapper=exception_mapper, urls_mapper=urls_mapper, errors_metric=metrics.http.errors, ) application.add_middleware( ObservabilityMiddleware, metrics=metrics, urls_mapper=urls_mapper, ) return application @asynccontextmanager async def lifespan(application: FastAPI): """Lifespan function. Initializes database connection in pass_services_dependencies middleware. """ app_config: {{ProjectName}}Config = application.state.config logger = logger_dep.from_app(application) await logger.ainfo("application is starting", config=app_config.to_order_dict()) otel_agent = OpenTelemetryAgent( app_config.observability.prometheus, app_config.observability.jaeger, ) yield otel_agent.shutdown() def _register_exceptions(mapper: ExceptionMapper, debug: bool) -> None: if debug: mapper.register_simple(DebugException, 506, "That's how a debug exception look like") mapper.register_func( DebugExceptionWithParams, lambda exc: JSONResponse( {"error": "That's how a debug exception with params look like", "message": exc.message}, status_code=exc.status_code, ), ) return mapper app = get_app()