mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-03-01 17:00:48 +01:00
chore: documentation (#76)
* chore: update docstring example to use python code block * docs: add documentation * feat: add docs build + fix other workdlows * fix: add missing return type
This commit is contained in:
77
docs/module/cli.md
Normal file
77
docs/module/cli.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# CLI
|
||||
|
||||
Typer-based command-line interface for managing your FastAPI application, with built-in fixture loading.
|
||||
|
||||
## Installation
|
||||
|
||||
=== "uv"
|
||||
``` bash
|
||||
uv add "fastapi-toolsets[cli]"
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
``` bash
|
||||
pip install "fastapi-toolsets[cli]"
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
The `cli` module provides a `manager` entry point built with [Typer](https://typer.tiangolo.com/). It auto-discovers fixture commands when a [`FixtureRegistry`](../reference/fixtures.md#fastapi_toolsets.fixtures.registry.FixtureRegistry) and a database context are configured.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure the CLI in your `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.fastapi-toolsets]
|
||||
cli = "myapp.cli:cli" # optional: your custom Typer app
|
||||
fixtures = "myapp.fixtures:registry" # FixtureRegistry instance
|
||||
db_context = "myapp.db:db_context" # async context manager for sessions
|
||||
```
|
||||
|
||||
All fields are optional. Without configuration, the `manager` command still works but only includes the built-in commands.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# List available commands
|
||||
manager --help
|
||||
|
||||
# Load fixtures for a specific context
|
||||
manager fixtures load --context testing
|
||||
|
||||
# Load all fixtures (no context filter)
|
||||
manager fixtures load
|
||||
```
|
||||
|
||||
## Custom CLI
|
||||
|
||||
You can extend the CLI by providing your own Typer app. The `manager` entry point will merge your app's commands with the built-in ones:
|
||||
|
||||
```python
|
||||
# myapp/cli.py
|
||||
import typer
|
||||
|
||||
cli = typer.Typer()
|
||||
|
||||
@cli.command()
|
||||
def hello():
|
||||
print("Hello from my app!")
|
||||
```
|
||||
|
||||
```toml
|
||||
[tool.fastapi-toolsets]
|
||||
cli = "myapp.cli:cli"
|
||||
```
|
||||
|
||||
## Entry point
|
||||
|
||||
The `manager` script is registered automatically when the package is installed:
|
||||
|
||||
```bash
|
||||
manager --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
[:material-api: API Reference](../reference/cli.md)
|
||||
116
docs/module/crud.md
Normal file
116
docs/module/crud.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# CRUD
|
||||
|
||||
Generic async CRUD operations for SQLAlchemy models with search, pagination, and many-to-many support.
|
||||
|
||||
## Overview
|
||||
|
||||
The `crud` module provides [`AsyncCrud`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud), an abstract base class with a full suite of async database operations, and [`CrudFactory`](../reference/crud.md#fastapi_toolsets.crud.factory.CrudFactory), a convenience function to instantiate it for a given model.
|
||||
|
||||
## Creating a CRUD class
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.crud import CrudFactory
|
||||
from myapp.models import User
|
||||
|
||||
UserCrud = CrudFactory(
|
||||
User,
|
||||
searchable_fields=[User.username, User.email],
|
||||
)
|
||||
```
|
||||
|
||||
[`CrudFactory`](../reference/crud.md#fastapi_toolsets.crud.factory.CrudFactory) dynamically creates a class named `AsyncUserCrud` with `User` as its model.
|
||||
|
||||
## Basic operations
|
||||
|
||||
```python
|
||||
# Create
|
||||
user = await UserCrud.create(session, obj=UserCreateSchema(username="alice"))
|
||||
|
||||
# Get one (raises NotFoundError if not found)
|
||||
user = await UserCrud.get(session, filters=[User.id == user_id])
|
||||
|
||||
# Get first or None
|
||||
user = await UserCrud.first(session, filters=[User.email == email])
|
||||
|
||||
# Get multiple
|
||||
users = await UserCrud.get_multi(session, filters=[User.is_active == True])
|
||||
|
||||
# Update
|
||||
user = await UserCrud.update(session, obj=UserUpdateSchema(username="bob"), filters=[User.id == user_id])
|
||||
|
||||
# Delete
|
||||
await UserCrud.delete(session, filters=[User.id == user_id])
|
||||
|
||||
# Count / exists
|
||||
count = await UserCrud.count(session, filters=[User.is_active == True])
|
||||
exists = await UserCrud.exists(session, filters=[User.email == email])
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
```python
|
||||
result = await UserCrud.paginate(
|
||||
session,
|
||||
filters=[User.is_active == True],
|
||||
order_by=[User.created_at.desc()],
|
||||
page=1,
|
||||
items_per_page=20,
|
||||
search="alice",
|
||||
search_fields=[User.username, User.email],
|
||||
)
|
||||
# result.data: list of users
|
||||
# result.pagination: Pagination(total_count, items_per_page, page, has_more)
|
||||
```
|
||||
|
||||
## Search
|
||||
|
||||
Declare searchable fields on the CRUD class. Relationship traversal is supported via tuples:
|
||||
|
||||
```python
|
||||
PostCrud = CrudFactory(
|
||||
Post,
|
||||
searchable_fields=[
|
||||
Post.title,
|
||||
Post.content,
|
||||
(Post.author, User.username), # search across relationship
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Many-to-many relationships
|
||||
|
||||
Use `m2m_fields` to map schema fields containing lists of IDs to SQLAlchemy relationships. The CRUD class resolves and validates all IDs before persisting:
|
||||
|
||||
```python
|
||||
PostCrud = CrudFactory(
|
||||
Post,
|
||||
m2m_fields={"tag_ids": Post.tags},
|
||||
)
|
||||
|
||||
# schema: PostCreateSchema(title="Hello", tag_ids=[1, 2, 3])
|
||||
post = await PostCrud.create(session, obj=PostCreateSchema(...))
|
||||
```
|
||||
|
||||
## Upsert
|
||||
|
||||
Atomic `INSERT ... ON CONFLICT DO UPDATE` using PostgreSQL:
|
||||
|
||||
```python
|
||||
await UserCrud.upsert(
|
||||
session,
|
||||
obj=UserCreateSchema(email="alice@example.com", username="alice"),
|
||||
index_elements=[User.email],
|
||||
set_={"username"},
|
||||
)
|
||||
```
|
||||
|
||||
## `as_response`
|
||||
|
||||
Pass `as_response=True` to any write operation to get a [`Response[ModelType]`](../reference/schemas.md#fastapi_toolsets.schemas.Response) back directly:
|
||||
|
||||
```python
|
||||
response = await UserCrud.create(session, obj=schema, as_response=True)
|
||||
# response: Response[User]
|
||||
```
|
||||
|
||||
[:material-api: API Reference](../reference/crud.md)
|
||||
89
docs/module/db.md
Normal file
89
docs/module/db.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# DB
|
||||
|
||||
SQLAlchemy async session management with transactions, table locking, and row-change polling.
|
||||
|
||||
## Overview
|
||||
|
||||
The `db` module provides helpers to create FastAPI dependencies and context managers for `AsyncSession`, along with utilities for nested transactions, PostgreSQL advisory locks, and polling for row changes.
|
||||
|
||||
## Session dependency
|
||||
|
||||
Use [`create_db_dependency`](../reference/db.md#fastapi_toolsets.db.create_db_dependency) to create a FastAPI dependency that yields a session and auto-commits on success:
|
||||
|
||||
```python
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||
from fastapi_toolsets.db import create_db_dependency
|
||||
|
||||
engine = create_async_engine("postgresql+asyncpg://...")
|
||||
session_maker = async_sessionmaker(engine)
|
||||
|
||||
get_db = create_db_dependency(session_maker)
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(session: AsyncSession = Depends(get_db)):
|
||||
...
|
||||
```
|
||||
|
||||
## Session context manager
|
||||
|
||||
Use [`create_db_context`](../reference/db.md#fastapi_toolsets.db.create_db_context) for sessions outside request handlers (e.g. background tasks, CLI commands):
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.db import create_db_context
|
||||
|
||||
db_context = create_db_context(session_maker)
|
||||
|
||||
async def seed():
|
||||
async with db_context() as session:
|
||||
session.add(User(name="admin"))
|
||||
```
|
||||
|
||||
## Nested transactions
|
||||
|
||||
[`get_transaction`](../reference/db.md#fastapi_toolsets.db.get_transaction) handles savepoints automatically, allowing safe nesting:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.db import get_transaction
|
||||
|
||||
async def create_user_with_role(session):
|
||||
async with get_transaction(session):
|
||||
session.add(role)
|
||||
async with get_transaction(session): # uses savepoint
|
||||
session.add(user)
|
||||
```
|
||||
|
||||
## Table locking
|
||||
|
||||
[`lock_tables`](../reference/db.md#fastapi_toolsets.db.lock_tables) acquires PostgreSQL table-level locks before executing critical sections:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.db import lock_tables
|
||||
|
||||
async with lock_tables(session, tables=[User], mode="EXCLUSIVE"):
|
||||
# No other transaction can modify User until this block exits
|
||||
...
|
||||
```
|
||||
|
||||
Available lock modes are defined in [`LockMode`](../reference/db.md#fastapi_toolsets.db.LockMode): `ACCESS_SHARE`, `ROW_SHARE`, `ROW_EXCLUSIVE`, `SHARE_UPDATE_EXCLUSIVE`, `SHARE`, `SHARE_ROW_EXCLUSIVE`, `EXCLUSIVE`, `ACCESS_EXCLUSIVE`.
|
||||
|
||||
## Row-change polling
|
||||
|
||||
[`wait_for_row_change`](../reference/db.md#fastapi_toolsets.db.wait_for_row_change) polls a row until a specific column changes value, useful for waiting on async side effects:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.db import wait_for_row_change
|
||||
|
||||
# Wait up to 30s for order.status to change
|
||||
await wait_for_row_change(
|
||||
session,
|
||||
model=Order,
|
||||
pk_value=order_id,
|
||||
columns=[Order.status],
|
||||
interval=1.0,
|
||||
timeout=30.0,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
[:material-api: API Reference](../reference/db.md)
|
||||
48
docs/module/dependencies.md
Normal file
48
docs/module/dependencies.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Dependencies
|
||||
|
||||
FastAPI dependency factories for automatic model resolution from path and body parameters.
|
||||
|
||||
## Overview
|
||||
|
||||
The `dependencies` module provides two factory functions that create FastAPI dependencies to fetch a model instance from the database automatically — either from a path parameter or from a request body field — and inject it directly into your route handler.
|
||||
|
||||
## `PathDependency`
|
||||
|
||||
[`PathDependency`](../reference/dependencies.md#fastapi_toolsets.dependencies.PathDependency) resolves a model from a URL path parameter and injects it into the route handler. Raises [`NotFoundError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.NotFoundError) automatically if the record does not exist.
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.dependencies import PathDependency
|
||||
|
||||
UserDep = PathDependency(User, User.id, session_dep=get_db)
|
||||
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user(user: User = UserDep):
|
||||
return user
|
||||
```
|
||||
|
||||
The parameter name is inferred from the field (`user_id` for `User.id`). You can override it:
|
||||
|
||||
```python
|
||||
UserDep = PathDependency(User, User.id, session_dep=get_db, param_name="id")
|
||||
|
||||
@router.get("/users/{id}")
|
||||
async def get_user(user: User = UserDep):
|
||||
return user
|
||||
```
|
||||
|
||||
## `BodyDependency`
|
||||
|
||||
[`BodyDependency`](../reference/dependencies.md#fastapi_toolsets.dependencies.BodyDependency) resolves a model from a field in the request body. Useful when a body contains a foreign key and you want the full object injected:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.dependencies import BodyDependency
|
||||
|
||||
RoleDep = BodyDependency(Role, Role.id, session_dep=get_db, body_field="role_id")
|
||||
|
||||
@router.post("/users")
|
||||
async def create_user(body: UserCreateSchema, role: Role = RoleDep):
|
||||
user = User(username=body.username, role=role)
|
||||
...
|
||||
```
|
||||
|
||||
[:material-api: API Reference](../reference/dependencies.md)
|
||||
82
docs/module/exceptions.md
Normal file
82
docs/module/exceptions.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Exceptions
|
||||
|
||||
Structured API exceptions with consistent error responses and automatic OpenAPI documentation.
|
||||
|
||||
## Overview
|
||||
|
||||
The `exceptions` module provides a set of pre-built HTTP exceptions and a FastAPI exception handler that formats all errors — including validation errors — into a uniform [`ErrorResponse`](../reference/schemas.md#fastapi_toolsets.schemas.ErrorResponse) shape.
|
||||
|
||||
## Setup
|
||||
|
||||
Register the exception handlers on your FastAPI app at startup:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from fastapi_toolsets.exceptions import init_exceptions_handlers
|
||||
|
||||
app = FastAPI()
|
||||
init_exceptions_handlers(app)
|
||||
```
|
||||
|
||||
This registers handlers for:
|
||||
|
||||
- [`ApiException`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.ApiException) — all custom exceptions below
|
||||
- `RequestValidationError` — Pydantic request validation (422)
|
||||
- `ResponseValidationError` — Pydantic response validation (422)
|
||||
- `Exception` — unhandled errors (500)
|
||||
|
||||
## Built-in exceptions
|
||||
|
||||
| Exception | Status | Default message |
|
||||
|-----------|--------|-----------------|
|
||||
| [`UnauthorizedError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.UnauthorizedError) | 401 | Unauthorized |
|
||||
| [`ForbiddenError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.ForbiddenError) | 403 | Forbidden |
|
||||
| [`NotFoundError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.NotFoundError) | 404 | Not found |
|
||||
| [`ConflictError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.ConflictError) | 409 | Conflict |
|
||||
| [`NoSearchableFieldsError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.NoSearchableFieldsError) | 400 | No searchable fields |
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.exceptions import NotFoundError, ForbiddenError
|
||||
|
||||
@router.get("/users/{id}")
|
||||
async def get_user(id: int, session: AsyncSession = Depends(get_db)):
|
||||
user = await UserCrud.first(session, filters=[User.id == id])
|
||||
if not user:
|
||||
raise NotFoundError
|
||||
return user
|
||||
```
|
||||
|
||||
## Custom exceptions
|
||||
|
||||
Subclass [`ApiException`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.ApiException) and define an `api_error` class variable:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.exceptions import ApiException
|
||||
from fastapi_toolsets.schemas import ApiError
|
||||
|
||||
class PaymentRequiredError(ApiException):
|
||||
api_error = ApiError(
|
||||
code=402,
|
||||
msg="Payment required",
|
||||
desc="Your subscription has expired.",
|
||||
err_code="PAYMENT_REQUIRED",
|
||||
)
|
||||
```
|
||||
|
||||
## OpenAPI response documentation
|
||||
|
||||
Use [`generate_error_responses`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.generate_error_responses) to add error schemas to your endpoint's OpenAPI spec:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.exceptions import generate_error_responses, NotFoundError, ForbiddenError
|
||||
|
||||
@router.get(
|
||||
"/users/{id}",
|
||||
responses=generate_error_responses(NotFoundError, ForbiddenError),
|
||||
)
|
||||
async def get_user(...): ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
[:material-api: API Reference](../reference/exceptions.md)
|
||||
113
docs/module/fixtures.md
Normal file
113
docs/module/fixtures.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Fixtures
|
||||
|
||||
Dependency-aware database seeding with context-based loading strategies.
|
||||
|
||||
## Overview
|
||||
|
||||
The `fixtures` module lets you define named fixtures with dependencies between them, then load them into the database in the correct order. Fixtures can be scoped to contexts (e.g. base data, testing data) so that only the relevant ones are loaded for each environment.
|
||||
|
||||
## Defining fixtures
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.fixtures import FixtureRegistry, Context
|
||||
|
||||
fixtures = FixtureRegistry()
|
||||
|
||||
@fixtures.register
|
||||
def roles():
|
||||
return [
|
||||
Role(id=1, name="admin"),
|
||||
Role(id=2, name="user"),
|
||||
]
|
||||
|
||||
@fixtures.register(depends_on=["roles"], contexts=[Context.TESTING])
|
||||
def test_users():
|
||||
return [
|
||||
User(id=1, username="alice", role_id=1),
|
||||
User(id=2, username="bob", role_id=2),
|
||||
]
|
||||
```
|
||||
|
||||
Dependencies declared via `depends_on` are resolved topologically — `roles` will always be loaded before `test_users`.
|
||||
|
||||
## Loading fixtures
|
||||
|
||||
### By context
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.fixtures import load_fixtures_by_context
|
||||
|
||||
async with db_context() as session:
|
||||
await load_fixtures_by_context(session, registry=fixtures, context=Context.TESTING)
|
||||
```
|
||||
|
||||
### Directly
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.fixtures import load_fixtures
|
||||
|
||||
async with db_context() as session:
|
||||
await load_fixtures(session, registry=fixtures)
|
||||
```
|
||||
|
||||
## Contexts
|
||||
|
||||
[`Context`](../reference/fixtures.md#fastapi_toolsets.fixtures.enum.Context) is an enum with predefined values:
|
||||
|
||||
| Context | Description |
|
||||
|---------|-------------|
|
||||
| `Context.BASE` | Core data required in all environments |
|
||||
| `Context.TESTING` | Data only loaded during tests |
|
||||
| `Context.PRODUCTION` | Data only loaded in production |
|
||||
|
||||
A fixture with no `contexts` argument is loaded in all contexts.
|
||||
|
||||
## Load strategies
|
||||
|
||||
[`LoadStrategy`](../reference/fixtures.md#fastapi_toolsets.fixtures.enum.LoadStrategy) controls how the fixture loader handles rows that already exist:
|
||||
|
||||
| Strategy | Description |
|
||||
|----------|-------------|
|
||||
| `LoadStrategy.INSERT` | Insert only, fail on duplicates |
|
||||
| `LoadStrategy.UPSERT` | Insert or update on conflict |
|
||||
| `LoadStrategy.SKIP` | Skip rows that already exist |
|
||||
|
||||
## Pytest integration
|
||||
|
||||
Use [`register_fixtures`](../reference/pytest.md#fastapi_toolsets.pytest.plugin.register_fixtures) to expose each fixture in your registry as an injectable pytest fixture named `fixture_{name}`:
|
||||
|
||||
```python
|
||||
# conftest.py
|
||||
import pytest
|
||||
from fastapi_toolsets.pytest import create_db_session, register_fixtures
|
||||
from app.fixtures import registry
|
||||
from app.models import Base
|
||||
|
||||
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/test_db"
|
||||
|
||||
@pytest.fixture
|
||||
async def db_session():
|
||||
async with create_db_session(database_url=DATABASE_URL, base=Base, cleanup=True) as session:
|
||||
yield session
|
||||
|
||||
register_fixtures(registry=registry, namespace=globals())
|
||||
```
|
||||
|
||||
```python
|
||||
# test_users.py
|
||||
async def test_user_can_login(fixture_users, fixture_roles, client):
|
||||
# fixture_roles is loaded first (dependency), then fixture_users
|
||||
response = await client.post("/auth/login", json={"username": "alice"})
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
|
||||
The load order is resolved automatically from the `depends_on` declarations in your registry. Each generated fixture receives `db_session` as a dependency and returns the list of loaded model instances.
|
||||
|
||||
## CLI integration
|
||||
|
||||
Fixtures can be triggered from the CLI. See the [CLI module](cli.md) for setup instructions.
|
||||
|
||||
---
|
||||
|
||||
[:material-api: API Reference](../reference/fixtures.md)
|
||||
37
docs/module/logger.md
Normal file
37
docs/module/logger.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Logger
|
||||
|
||||
Lightweight logging utilities with consistent formatting and uvicorn integration.
|
||||
|
||||
## Overview
|
||||
|
||||
The `logger` module provides two helpers: one to configure the root logger (and uvicorn loggers) at startup, and one to retrieve a named logger anywhere in your codebase.
|
||||
|
||||
## Setup
|
||||
|
||||
Call [`configure_logging`](../reference/logger.md#fastapi_toolsets.logger.configure_logging) once at application startup:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.logger import configure_logging
|
||||
|
||||
configure_logging(level="INFO")
|
||||
```
|
||||
|
||||
This sets up a stdout handler with a consistent format and also configures uvicorn's access and error loggers so all log output shares the same style.
|
||||
|
||||
## Getting a logger
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.logger import get_logger
|
||||
|
||||
logger = get_logger(name=__name__)
|
||||
logger.info("User created")
|
||||
```
|
||||
|
||||
When called without arguments, [`get_logger`](../reference/logger.md#fastapi_toolsets.logger.get_logger) auto-detects the caller's module name via frame inspection:
|
||||
|
||||
```python
|
||||
# Equivalent to get_logger(name=__name__)
|
||||
logger = get_logger()
|
||||
```
|
||||
|
||||
[:material-api: API Reference](../reference/logger.md)
|
||||
90
docs/module/metrics.md
Normal file
90
docs/module/metrics.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Metrics
|
||||
|
||||
Prometheus metrics integration with a decorator-based registry and multi-process support.
|
||||
|
||||
## Installation
|
||||
|
||||
=== "uv"
|
||||
``` bash
|
||||
uv add "fastapi-toolsets[metrics]"
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
``` bash
|
||||
pip install "fastapi-toolsets[metrics]"
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
The `metrics` module provides a [`MetricsRegistry`](../reference/metrics.md#fastapi_toolsets.metrics.registry.MetricsRegistry) to declare Prometheus metrics with decorators, and an [`init_metrics`](../reference/metrics.md#fastapi_toolsets.metrics.handler.init_metrics) function to mount a `/metrics` endpoint on your FastAPI app.
|
||||
|
||||
## Setup
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from fastapi_toolsets.metrics import MetricsRegistry, init_metrics
|
||||
|
||||
app = FastAPI()
|
||||
metrics = MetricsRegistry()
|
||||
|
||||
init_metrics(app, registry=metrics)
|
||||
```
|
||||
|
||||
This mounts the `/metrics` endpoint that Prometheus can scrape.
|
||||
|
||||
## Declaring metrics
|
||||
|
||||
### Providers
|
||||
|
||||
Providers are called once at startup and register metrics that are updated externally (e.g. counters, histograms):
|
||||
|
||||
```python
|
||||
from prometheus_client import Counter, Histogram
|
||||
|
||||
@metrics.register
|
||||
def http_requests():
|
||||
return Counter("http_requests_total", "Total HTTP requests", ["method", "status"])
|
||||
|
||||
@metrics.register
|
||||
def request_duration():
|
||||
return Histogram("request_duration_seconds", "Request duration")
|
||||
```
|
||||
|
||||
### Collectors
|
||||
|
||||
Collectors are called on every scrape. Use them for metrics that reflect current state (e.g. gauges):
|
||||
|
||||
```python
|
||||
@metrics.register(collect=True)
|
||||
def queue_depth():
|
||||
gauge = Gauge("queue_depth", "Current queue depth")
|
||||
gauge.set(get_current_queue_depth())
|
||||
```
|
||||
|
||||
## Merging registries
|
||||
|
||||
Split metrics definitions across modules and merge them:
|
||||
|
||||
```python
|
||||
from myapp.metrics.http import http_metrics
|
||||
from myapp.metrics.db import db_metrics
|
||||
|
||||
metrics = MetricsRegistry()
|
||||
metrics.include_registry(http_metrics)
|
||||
metrics.include_registry(db_metrics)
|
||||
```
|
||||
|
||||
## Multi-process mode
|
||||
|
||||
Multi-process support is enabled automatically when the `PROMETHEUS_MULTIPROC_DIR` environment variable is set. No code changes are required.
|
||||
|
||||
!!! warning "Environment variable name"
|
||||
The correct variable is `PROMETHEUS_MULTIPROC_DIR` (not `PROMETHEUS_MULTIPROCESS_DIR`).
|
||||
|
||||
```bash
|
||||
export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
[:material-api: API Reference](../reference/metrics.md)
|
||||
81
docs/module/pytest.md
Normal file
81
docs/module/pytest.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Pytest
|
||||
|
||||
Testing helpers for FastAPI applications with async client, database sessions, and parallel worker support.
|
||||
|
||||
## Installation
|
||||
|
||||
=== "uv"
|
||||
``` bash
|
||||
uv add "fastapi-toolsets[pytest]"
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
``` bash
|
||||
pip install "fastapi-toolsets[pytest]"
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
The `pytest` module provides utilities for setting up async test clients, managing test database sessions, and supporting parallel test execution with `pytest-xdist`.
|
||||
|
||||
## Creating an async client
|
||||
|
||||
Use [`create_async_client`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_async_client) to get an `httpx.AsyncClient` configured for your FastAPI app:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.pytest import create_async_client
|
||||
|
||||
@pytest.fixture
|
||||
async def client(app):
|
||||
async with create_async_client(app=app) as c:
|
||||
yield c
|
||||
```
|
||||
|
||||
## Database sessions in tests
|
||||
|
||||
Use [`create_db_session`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_db_session) to create an isolated `AsyncSession` for a test:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.pytest import create_db_session
|
||||
|
||||
@pytest.fixture
|
||||
async def db_session():
|
||||
async with create_db_session(database_url=DATABASE_URL, base=Base, cleanup=True) as session:
|
||||
yield session
|
||||
```
|
||||
|
||||
## Parallel testing with pytest-xdist
|
||||
|
||||
When running tests in parallel, each worker needs its own database. Use these helpers to create and identify worker databases:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.pytest import create_worker_database, create_db_session
|
||||
|
||||
# In conftest.py session-scoped fixture
|
||||
@pytest.fixture(scope="session")
|
||||
async def worker_db_url():
|
||||
async with create_worker_database(database_url=DATABASE_URL) as url:
|
||||
yield url
|
||||
|
||||
@pytest.fixture
|
||||
async def db_session(worker_db_url):
|
||||
async with create_db_session(database_url=worker_db_url, base=Base, cleanup=True) as session:
|
||||
yield session
|
||||
```
|
||||
|
||||
## Cleaning up tables
|
||||
|
||||
[`cleanup_tables`](../reference/pytest.md#fastapi_toolsets.pytest.utils.cleanup_tables) truncates all tables between tests for fast isolation:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.pytest import cleanup_tables
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def clean(db_session):
|
||||
yield
|
||||
await cleanup_tables(session=db_session, base=Base)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
[:material-api: API Reference](../reference/pytest.md)
|
||||
54
docs/module/schemas.md
Normal file
54
docs/module/schemas.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Schemas
|
||||
|
||||
Standardized Pydantic response models for consistent API responses across your FastAPI application.
|
||||
|
||||
## Overview
|
||||
|
||||
The `schemas` module provides generic response wrappers that enforce a uniform response structure. All models use `from_attributes=True` for ORM compatibility and `validate_assignment=True` for runtime type safety.
|
||||
|
||||
## Response models
|
||||
|
||||
### `Response[T]`
|
||||
|
||||
The most common wrapper for a single resource response.
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.schemas import Response
|
||||
|
||||
@router.get("/users/{id}")
|
||||
async def get_user(user: User = UserDep) -> Response[UserSchema]:
|
||||
return Response(data=user, message="User retrieved")
|
||||
```
|
||||
|
||||
### `PaginatedResponse[T]`
|
||||
|
||||
Wraps a list of items with pagination metadata.
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.schemas import PaginatedResponse, Pagination
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users() -> PaginatedResponse[UserSchema]:
|
||||
return PaginatedResponse(
|
||||
data=users,
|
||||
pagination=Pagination(
|
||||
total_count=100,
|
||||
items_per_page=10,
|
||||
page=1,
|
||||
has_more=True,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### `ErrorResponse`
|
||||
|
||||
Returned automatically by the exceptions handler. Can also be used as a response model for OpenAPI docs.
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.schemas import ErrorResponse
|
||||
|
||||
@router.delete("/users/{id}", responses={404: {"model": ErrorResponse}})
|
||||
async def delete_user(...): ...
|
||||
```
|
||||
|
||||
[:material-api: API Reference](../reference/schemas.md)
|
||||
Reference in New Issue
Block a user