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

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 685ea5e5f4
52 changed files with 4563 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
"""All FastAPI handlers are exported from this module."""
import importlib
from pathlib import Path
from .routers import routers_list
for file in sorted(Path(__file__).resolve().parent.iterdir()):
if file.name.endswith(".py"):
importlib.import_module(f".{file.name[:-3]}", __package__)
list_of_routers = [
*routers_list,
]
__all__ = [
"list_of_routers",
]

View File

@@ -0,0 +1,114 @@
"""Debug and testing endpoints are defined here. They should be included in router only in debug-mode."""
import asyncio
from typing import Literal
import aiohttp
from fastapi import HTTPException
from fastapi.responses import JSONResponse
from opentelemetry import trace
from starlette import status
from {{project_slug}}.schemas import PingResponse
from {{project_slug}}.utils.observability import get_span_headers
from .routers import debug_errors_router
_tracer = trace.get_tracer(__name__)
class DebugException(Exception):
pass
class DebugExceptionWithParams(Exception):
def __init__(self, status_code: int, message: str):
self.status_code = status_code
self.message = message
@debug_errors_router.get(
"/status_code",
response_model=PingResponse,
status_code=status.HTTP_200_OK,
)
async def get_status_code(status_code: int, as_exception: bool = False):
"""
Return given status code. If `as_exception` is set to True, return as HTTPException.
"""
if as_exception:
raise HTTPException(
status_code=status_code, detail=f"debugging with status code {status_code} as http_exception"
)
return JSONResponse(PingResponse().model_dump(), status_code=status_code)
@debug_errors_router.get(
"/exception",
response_model=PingResponse,
status_code=status.HTTP_200_OK,
)
async def get_exception(error_type: Literal["RuntimeError", "DebugException", "DebugExceptionWithParams"]):
"""
Raise exception: `RuntimeError`, `DebugException` or `DebugExceptionWithParams` depending on given parameter.
"""
if error_type == "DebugException":
raise DebugException()
if error_type == "DebugExceptionWithParams":
raise DebugExceptionWithParams(522, "Message goes here")
raise RuntimeError("That's how runtime errors look like")
@debug_errors_router.get(
"/tracing_check",
status_code=status.HTTP_200_OK,
)
async def tracing_check():
"""
Add event to span, and sleep 2 seconds inside an inner span.
"""
span = trace.get_current_span()
span.add_event("successful log entry", attributes={"parameters": "go here"})
with _tracer.start_as_current_span("long operation") as inner_span:
inner_span.add_event("going to sleep")
await asyncio.sleep(2)
inner_span.add_event("woke up")
return JSONResponse({"finished": "ok"})
@debug_errors_router.get(
"/inner_get_tracing",
status_code=status.HTTP_200_OK,
)
async def inner_get(host: str = "http://127.0.0.1:8080"):
"""
Perform GET request with span proxying to get more complicated trace.
"""
def perform_get(session: aiohttp.ClientSession) -> asyncio.Task:
return asyncio.create_task(
session.get(
"/api/debug/tracing_check",
headers=get_span_headers(),
)
)
with _tracer.start_as_current_span("inner request"):
inner_results = []
async with aiohttp.ClientSession(host) as session:
requests_futures = [perform_get(session) for _ in range(2)]
results: list[aiohttp.ClientResponse] = await asyncio.gather(*requests_futures)
results.append(await perform_get(session))
for response in results:
inner_results.append(
{
"inner_response": await response.json(),
"headers": dict(response.headers.items()),
"status_code": response.status,
}
)
return JSONResponse({"inner_results": inner_results})

View File

@@ -0,0 +1,18 @@
"""API routers are defined here.
It is needed to import files which use these routers to initialize handlers.
"""
from fastapi import APIRouter
system_router = APIRouter(tags=["system"])
debug_errors_router = APIRouter(tags=["debug"], prefix="/debug")
routers_list = [
system_router,
debug_errors_router,
]
__all__ = [
"routers_list",
]

View File

@@ -0,0 +1,43 @@
"""System endpoints are defined here."""
import fastapi
from starlette import status
from {{project_slug}}.dependencies import connection_manager_dep
from {{project_slug}}.logic import system as system_logic
from {{project_slug}}.schemas import PingResponse
from .routers import system_router
@system_router.get("/", status_code=status.HTTP_307_TEMPORARY_REDIRECT, include_in_schema=False)
@system_router.get("/api/", status_code=status.HTTP_307_TEMPORARY_REDIRECT, include_in_schema=False)
async def redirect_to_swagger_docs():
"""Redirects to **/api/docs** from **/**"""
return fastapi.responses.RedirectResponse("/api/docs", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
@system_router.get(
"/health_check/ping",
response_model=PingResponse,
status_code=status.HTTP_200_OK,
)
async def ping():
"""
Check that application is alive.
"""
return PingResponse()
@system_router.get(
"/health_check/ping_db",
response_model=PingResponse,
status_code=status.HTTP_200_OK,
)
async def ping_db(request: fastapi.Request, readonly: bool = False):
"""
Check that database connection is valid.
"""
connection_manager = connection_manager_dep.obtain(request)
return await system_logic.ping_db(connection_manager, readonly)