Files
template-fastapi/{{project_slug}}/fastapi_init.py.jinja
Aleksei Sokol 53f14a8624
All checks were successful
Run linters on applied template / Python 3.13 lint and build (push) Successful in 1m40s
Version 0.4.0
Changes:
- put ObservabilityMiddleware before ExceptionHandlerMiddleware to avoid repetative code
- add application startup and last metrics update metrics along with CPU usage metric and threads count
- move host and port to new uvicorn section at config along with new reload and forwarded_allow_ips
- add request_id and remove trace_id/span_id generation if tracing is disabled
- move logging logic from utils to observability
- pass trace_id/span_id in HEX form
2026-01-03 16:29:58 +03:00

162 lines
5.8 KiB
Django/Jinja

"""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()