Initial commit
Some checks failed
Run linters on applied template / Python 3.13 lint and build (push) Failing after 34s

This is a FastAPI backend microservice template used with `copier` utility.

Features of applied template are:
- Configuration file processing logic
- Metrics and tracing (both optional) configuration available
- Debug endpoints
- Database migration commands, prepared Alembic environment
- Database usage example in ping_db endpoint
- gitea sanity check pipeline
This commit is contained in:
2025-11-29 21:42:27 +03:00
commit 5dd68b7114
52 changed files with 4563 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
"""FastAPI application initialization is performed here."""
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
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
from {{project_slug}}.exceptions.mapper import ExceptionMapper
from {{project_slug}}.handlers.debug import DebugException, DebugExceptionWithParams
from {{project_slug}}.middlewares.exception_handler import ExceptionHandlerMiddleware
from {{project_slug}}.middlewares.observability import ObservabilityMiddleware
from {{project_slug}}.observability.metrics import init_metrics
from {{project_slug}}.observability.otel_agent import OpenTelemetryAgent
from {{project_slug}}.utils.observability import URLsMapper, configure_logging
from .handlers import list_of_routers
from .version import LAST_UPDATE, VERSION
def _get_exception_mapper(debug: bool) -> ExceptionMapper:
mapper = ExceptionMapper()
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
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.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.state.config = app_config
exception_mapper = _get_exception_mapper(app_config.app.debug)
metrics = init_metrics()
urls_mapper = URLsMapper(app_config.observability.prometheus.urls_mapping)
application.add_middleware(
ObservabilityMiddleware,
exception_mapper=exception_mapper,
metrics=metrics,
urls_mapper=urls_mapper,
)
application.add_middleware(
ExceptionHandlerMiddleware,
debug=[False], # reinitialized on startup
exception_mapper=exception_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
loggers_dict = {logger_config.filename: logger_config.level for logger_config in app_config.logging.files}
logger = configure_logging(app_config.logging.level, loggers_dict)
await logger.ainfo("application is being configured", config=app_config.to_order_dict())
connection_manager = PostgresConnectionManager(
master=app_config.db.master,
replicas=app_config.db.replicas,
logger=logger,
application_name=f"{{project_slug}}_{VERSION}",
)
connection_manager_dep.init_dispencer(application, connection_manager)
logger_dep.init_dispencer(application, logger)
for middleware in application.user_middleware:
if middleware.cls == ExceptionHandlerMiddleware:
middleware.kwargs["debug"][0] = app_config.app.debug
otel_agent = OpenTelemetryAgent(
app_config.observability.prometheus,
app_config.observability.jaeger,
)
yield
otel_agent.shutdown()
app = get_app()