Version 1.0.0 (#80)

* docs: fix typos

* chore: build docs only when release

* Version 1.0.0
This commit is contained in:
d3vyce
2026-02-20 14:09:01 +01:00
committed by GitHub
parent 823a0b3e36
commit 31678935aa
14 changed files with 194 additions and 124 deletions

View File

@@ -1,12 +1,14 @@
name: Documentation name: Documentation
on: on:
push: release:
branches: types: [published]
- main
permissions: permissions:
contents: read contents: read
pages: write pages: write
id-token: write id-token: write
jobs: jobs:
deploy: deploy:
environment: environment:

View File

@@ -1,6 +1,6 @@
# CLI # CLI
Typer-based command-line interface for managing your FastAPI application, with built-in fixture loading. Typer-based command-line interface for managing your FastAPI application, with built-in fixture commands integration.
## Installation ## Installation
@@ -16,7 +16,7 @@ Typer-based command-line interface for managing your FastAPI application, with b
## Overview ## 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. The `cli` module provides a `manager` entry point built with [Typer](https://typer.tiangolo.com/). It allow custom commands to be added in addition of the fixture commands when a [`FixtureRegistry`](../reference/fixtures.md#fastapi_toolsets.fixtures.registry.FixtureRegistry) and a database context are configured.
## Configuration ## Configuration
@@ -24,24 +24,48 @@ Configure the CLI in your `pyproject.toml`:
```toml ```toml
[tool.fastapi-toolsets] [tool.fastapi-toolsets]
cli = "myapp.cli:cli" # optional: your custom Typer app cli = "myapp.cli:cli" # Custom Typer app
fixtures = "myapp.fixtures:registry" # FixtureRegistry instance fixtures = "myapp.fixtures:registry" # FixtureRegistry instance
db_context = "myapp.db:db_context" # async context manager for sessions 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. All fields are optional. Without configuration, the `manager` command still works but no command are available.
## Usage ## Usage
```bash ```bash
# List available commands # Manager commands
manager --help manager --help
# Load fixtures for a specific context Usage: manager [OPTIONS] COMMAND [ARGS]...
manager fixtures load --context testing
# Load all fixtures (no context filter) FastAPI utilities CLI.
manager fixtures load
╭─ Options ────────────────────────────────────────────────────────────────────────╮
│ --install-completion Install completion for the current shell. │
│ --show-completion Show completion for the current shell, to copy it │
│ or customize the installation. │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────────╮
│ check-db │
│ fixtures Manage database fixtures. │
╰──────────────────────────────────────────────────────────────────────────────────╯
# Fixtures commands
manager fixtures --help
Usage: manager fixtures [OPTIONS] COMMAND [ARGS]...
Manage database fixtures.
╭─ Options ────────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────────╮
│ list List all registered fixtures. │
│ load Load fixtures into the database. │
╰──────────────────────────────────────────────────────────────────────────────────╯
``` ```
## Custom CLI ## Custom CLI
@@ -64,14 +88,6 @@ def hello():
cli = "myapp.cli:cli" 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) [:material-api: API Reference](../reference/cli.md)

View File

@@ -1,6 +1,9 @@
# CRUD # CRUD
Generic async CRUD operations for SQLAlchemy models with search, pagination, and many-to-many support. Generic async CRUD operations for SQLAlchemy models with search, pagination, and many-to-many support. This module has features that are only compatible with Postgres.
!!! info
This module has been coded and tested to be compatible with PostgreSQL only.
## Overview ## Overview
@@ -12,10 +15,7 @@ The `crud` module provides [`AsyncCrud`](../reference/crud.md#fastapi_toolsets.c
from fastapi_toolsets.crud import CrudFactory from fastapi_toolsets.crud import CrudFactory
from myapp.models import User from myapp.models import User
UserCrud = CrudFactory( UserCrud = CrudFactory(model=User)
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. [`CrudFactory`](../reference/crud.md#fastapi_toolsets.crud.factory.CrudFactory) dynamically creates a class named `AsyncUserCrud` with `User` as its model.
@@ -24,51 +24,56 @@ UserCrud = CrudFactory(
```python ```python
# Create # Create
user = await UserCrud.create(session, obj=UserCreateSchema(username="alice")) user = await UserCrud.create(session=session, obj=UserCreateSchema(username="alice"))
# Get one (raises NotFoundError if not found) # Get one (raises NotFoundError if not found)
user = await UserCrud.get(session, filters=[User.id == user_id]) user = await UserCrud.get(session=session, filters=[User.id == user_id])
# Get first or None # Get first or None
user = await UserCrud.first(session, filters=[User.email == email]) user = await UserCrud.first(session=session, filters=[User.email == email])
# Get multiple # Get multiple
users = await UserCrud.get_multi(session, filters=[User.is_active == True]) users = await UserCrud.get_multi(session=session, filters=[User.is_active == True])
# Update # Update
user = await UserCrud.update(session, obj=UserUpdateSchema(username="bob"), filters=[User.id == user_id]) user = await UserCrud.update(session=session, obj=UserUpdateSchema(username="bob"), filters=[User.id == user_id])
# Delete # Delete
await UserCrud.delete(session, filters=[User.id == user_id]) await UserCrud.delete(session=session, filters=[User.id == user_id])
# Count / exists # Count / exists
count = await UserCrud.count(session, filters=[User.is_active == True]) count = await UserCrud.count(session=session, filters=[User.is_active == True])
exists = await UserCrud.exists(session, filters=[User.email == email]) exists = await UserCrud.exists(session=session, filters=[User.email == email])
``` ```
## Pagination ## Pagination
```python ```python
result = await UserCrud.paginate( @router.get(
session, "",
filters=[User.is_active == True], response_model=PaginatedResponse[User],
order_by=[User.created_at.desc()], )
page=1, async def get_users(
items_per_page=20, session: SessionDep,
search="alice", items_per_page: int = 50,
search_fields=[User.username, User.email], page: int = 1,
):
return await crud.UserCrud.paginate(
session=session,
items_per_page=items_per_page,
page=page,
) )
# result.data: list of users
# result.pagination: Pagination(total_count, items_per_page, page, has_more)
``` ```
The [`paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.paginate) function will return a [`PaginatedResponse`](../reference/schemas.md#fastapi_toolsets.schemas.PaginatedResponse).
## Search ## Search
Declare searchable fields on the CRUD class. Relationship traversal is supported via tuples: Declare searchable fields on the CRUD class. Relationship traversal is supported via tuples:
```python ```python
PostCrud = CrudFactory( PostCrud = CrudFactory(
Post, model=Post,
searchable_fields=[ searchable_fields=[
Post.title, Post.title,
Post.content, Post.content,
@@ -77,18 +82,38 @@ PostCrud = CrudFactory(
) )
``` ```
This allow to do a search with the [`paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.paginate) function:
```python
@router.get(
"",
response_model=PaginatedResponse[User],
)
async def get_users(
session: SessionDep,
items_per_page: int = 50,
page: int = 1,
search: str | None = None,
):
return await crud.UserCrud.paginate(
session=session,
items_per_page=items_per_page,
page=page,
search=search,
)
```
## Many-to-many relationships ## 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: 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 ```python
PostCrud = CrudFactory( PostCrud = CrudFactory(
Post, model=Post,
m2m_fields={"tag_ids": Post.tags}, m2m_fields={"tag_ids": Post.tags},
) )
# schema: PostCreateSchema(title="Hello", tag_ids=[1, 2, 3]) post = await PostCrud.create(session=session, obj=PostCreateSchema(title="Hello", tag_ids=[1, 2, 3]))
post = await PostCrud.create(session, obj=PostCreateSchema(...))
``` ```
## Upsert ## Upsert
@@ -97,7 +122,7 @@ Atomic `INSERT ... ON CONFLICT DO UPDATE` using PostgreSQL:
```python ```python
await UserCrud.upsert( await UserCrud.upsert(
session, session=session,
obj=UserCreateSchema(email="alice@example.com", username="alice"), obj=UserCreateSchema(email="alice@example.com", username="alice"),
index_elements=[User.email], index_elements=[User.email],
set_={"username"}, set_={"username"},
@@ -106,11 +131,22 @@ await UserCrud.upsert(
## `as_response` ## `as_response`
Pass `as_response=True` to any write operation to get a [`Response[ModelType]`](../reference/schemas.md#fastapi_toolsets.schemas.Response) back directly: Pass `as_response=True` to any write operation to get a [`Response[ModelType]`](../reference/schemas.md#fastapi_toolsets.schemas.Response) back directly for API usage:
```python ```python
response = await UserCrud.create(session, obj=schema, as_response=True) @router.get(
# response: Response[User] "/{uuid}",
response_model=Response[User],
responses=generate_error_responses(NotFoundError),
)
async def get_user(session: SessionDep, uuid: UUID):
return await crud.UserCrud.get(
session=session,
filters=[User.id == uuid],
as_response=True,
)
``` ```
---
[:material-api: API Reference](../reference/crud.md) [:material-api: API Reference](../reference/crud.md)

View File

@@ -2,9 +2,12 @@
SQLAlchemy async session management with transactions, table locking, and row-change polling. SQLAlchemy async session management with transactions, table locking, and row-change polling.
!!! info
This module has been coded and tested to be compatible with PostgreSQL only.
## Overview ## 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. The `db` module provides helpers to create FastAPI dependencies and context managers for `AsyncSession`, along with utilities for nested transactions, table lock and polling for row changes.
## Session dependency ## Session dependency
@@ -14,10 +17,10 @@ Use [`create_db_dependency`](../reference/db.md#fastapi_toolsets.db.create_db_de
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from fastapi_toolsets.db import create_db_dependency from fastapi_toolsets.db import create_db_dependency
engine = create_async_engine("postgresql+asyncpg://...") engine = create_async_engine(url="postgresql+asyncpg://...", future=True)
session_maker = async_sessionmaker(engine) session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
get_db = create_db_dependency(session_maker) get_db = create_db_dependency(session_maker=session_maker)
@router.get("/users") @router.get("/users")
async def list_users(session: AsyncSession = Depends(get_db)): async def list_users(session: AsyncSession = Depends(get_db)):
@@ -31,11 +34,11 @@ Use [`create_db_context`](../reference/db.md#fastapi_toolsets.db.create_db_conte
```python ```python
from fastapi_toolsets.db import create_db_context from fastapi_toolsets.db import create_db_context
db_context = create_db_context(session_maker) db_context = create_db_context(session_maker=session_maker)
async def seed(): async def seed():
async with db_context() as session: async with db_context() as session:
session.add(User(name="admin")) ...
``` ```
## Nested transactions ## Nested transactions
@@ -45,11 +48,11 @@ async def seed():
```python ```python
from fastapi_toolsets.db import get_transaction from fastapi_toolsets.db import get_transaction
async def create_user_with_role(session): async def create_user_with_role(session=session):
async with get_transaction(session): async with get_transaction(session=session):
session.add(role) ...
async with get_transaction(session): # uses savepoint async with get_transaction(session=session): # uses savepoint
session.add(user) ...
``` ```
## Table locking ## Table locking
@@ -59,7 +62,7 @@ async def create_user_with_role(session):
```python ```python
from fastapi_toolsets.db import lock_tables from fastapi_toolsets.db import lock_tables
async with lock_tables(session, tables=[User], mode="EXCLUSIVE"): async with lock_tables(session=session, tables=[User], mode="EXCLUSIVE"):
# No other transaction can modify User until this block exits # No other transaction can modify User until this block exits
... ...
``` ```
@@ -75,7 +78,7 @@ from fastapi_toolsets.db import wait_for_row_change
# Wait up to 30s for order.status to change # Wait up to 30s for order.status to change
await wait_for_row_change( await wait_for_row_change(
session, session=session,
model=Order, model=Order,
pk_value=order_id, pk_value=order_id,
columns=[Order.status], columns=[Order.status],

View File

@@ -13,17 +13,17 @@ The `dependencies` module provides two factory functions that create FastAPI dep
```python ```python
from fastapi_toolsets.dependencies import PathDependency from fastapi_toolsets.dependencies import PathDependency
UserDep = PathDependency(User, User.id, session_dep=get_db) UserDep = PathDependency(model=User, field=User.id, session_dep=get_db)
@router.get("/users/{user_id}") @router.get("/users/{user_id}")
async def get_user(user: User = UserDep): async def get_user(user: User = UserDep):
return user return user
``` ```
The parameter name is inferred from the field (`user_id` for `User.id`). You can override it: By default the parameter name is inferred from the field (`user_id` for `User.id`). You can override it:
```python ```python
UserDep = PathDependency(User, User.id, session_dep=get_db, param_name="id") UserDep = PathDependency(model=User, field=User.id, session_dep=get_db, param_name="id")
@router.get("/users/{id}") @router.get("/users/{id}")
async def get_user(user: User = UserDep): async def get_user(user: User = UserDep):
@@ -37,7 +37,7 @@ async def get_user(user: User = UserDep):
```python ```python
from fastapi_toolsets.dependencies import BodyDependency from fastapi_toolsets.dependencies import BodyDependency
RoleDep = BodyDependency(Role, Role.id, session_dep=get_db, body_field="role_id") RoleDep = BodyDependency(model=Role, field=Role.id, session_dep=get_db, body_field="role_id")
@router.post("/users") @router.post("/users")
async def create_user(body: UserCreateSchema, role: Role = RoleDep): async def create_user(body: UserCreateSchema, role: Role = RoleDep):
@@ -45,4 +45,6 @@ async def create_user(body: UserCreateSchema, role: Role = RoleDep):
... ...
``` ```
---
[:material-api: API Reference](../reference/dependencies.md) [:material-api: API Reference](../reference/dependencies.md)

View File

@@ -4,7 +4,7 @@ Structured API exceptions with consistent error responses and automatic OpenAPI
## Overview ## 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. 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).
## Setup ## Setup
@@ -15,7 +15,7 @@ from fastapi import FastAPI
from fastapi_toolsets.exceptions import init_exceptions_handlers from fastapi_toolsets.exceptions import init_exceptions_handlers
app = FastAPI() app = FastAPI()
init_exceptions_handlers(app) init_exceptions_handlers(app=app)
``` ```
This registers handlers for: This registers handlers for:
@@ -36,11 +36,11 @@ This registers handlers for:
| [`NoSearchableFieldsError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.NoSearchableFieldsError) | 400 | No searchable fields | | [`NoSearchableFieldsError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.NoSearchableFieldsError) | 400 | No searchable fields |
```python ```python
from fastapi_toolsets.exceptions import NotFoundError, ForbiddenError from fastapi_toolsets.exceptions import NotFoundError
@router.get("/users/{id}") @router.get("/users/{id}")
async def get_user(id: int, session: AsyncSession = Depends(get_db)): async def get_user(id: int, session: AsyncSession = Depends(get_db)):
user = await UserCrud.first(session, filters=[User.id == id]) user = await UserCrud.first(session=session, filters=[User.id == id])
if not user: if not user:
raise NotFoundError raise NotFoundError
return user return user
@@ -77,6 +77,9 @@ from fastapi_toolsets.exceptions import generate_error_responses, NotFoundError,
async def get_user(...): ... async def get_user(...): ...
``` ```
!!! info
The pydantic validation error is automatically added by FastAPI.
--- ---
[:material-api: API Reference](../reference/exceptions.md) [:material-api: API Reference](../reference/exceptions.md)

View File

@@ -32,22 +32,22 @@ Dependencies declared via `depends_on` are resolved topologically — `roles` wi
## Loading fixtures ## Loading fixtures
### By context By context with [`load_fixtures_by_context`](../reference/fixtures.md#fastapi_toolsets.fixtures.utils.load_fixtures_by_context):
```python ```python
from fastapi_toolsets.fixtures import load_fixtures_by_context from fastapi_toolsets.fixtures import load_fixtures_by_context
async with db_context() as session: async with db_context() as session:
await load_fixtures_by_context(session, registry=fixtures, context=Context.TESTING) await load_fixtures_by_context(session=session, registry=fixtures, context=Context.TESTING)
``` ```
### Directly Directly with [`load_fixtures`](../reference/fixtures.md#fastapi_toolsets.fixtures.utils.load_fixtures):
```python ```python
from fastapi_toolsets.fixtures import load_fixtures from fastapi_toolsets.fixtures import load_fixtures
async with db_context() as session: async with db_context() as session:
await load_fixtures(session, registry=fixtures) await load_fixtures(session=session, registry=fixtures)
``` ```
## Contexts ## Contexts
@@ -60,7 +60,7 @@ async with db_context() as session:
| `Context.TESTING` | Data only loaded during tests | | `Context.TESTING` | Data only loaded during tests |
| `Context.PRODUCTION` | Data only loaded in production | | `Context.PRODUCTION` | Data only loaded in production |
A fixture with no `contexts` argument is loaded in all contexts. A fixture with no `contexts` defined takes `Context.BASE` by default.
## Load strategies ## Load strategies
@@ -72,9 +72,21 @@ A fixture with no `contexts` argument is loaded in all contexts.
| `LoadStrategy.UPSERT` | Insert or update on conflict | | `LoadStrategy.UPSERT` | Insert or update on conflict |
| `LoadStrategy.SKIP` | Skip rows that already exist | | `LoadStrategy.SKIP` | Skip rows that already exist |
## Merging registries
Split fixtures definitions across modules and merge them:
```python
from myapp.fixtures.dev import dev_fixtures
from myapp.fixtures.prod import prod_fixtures
fixtures = fixturesRegistry()
fixtures.include_registry(registry=dev_fixtures)
fixtures.include_registry(registry=prod_fixtures)
## Pytest integration ## 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}`: 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}` by default:
```python ```python
# conftest.py # conftest.py
@@ -95,10 +107,8 @@ register_fixtures(registry=registry, namespace=globals())
```python ```python
# test_users.py # test_users.py
async def test_user_can_login(fixture_users, fixture_roles, client): async def test_user_can_login(fixture_users: list[User], fixture_roles: list[Role]):
# fixture_roles is loaded first (dependency), then fixture_users ...
response = await client.post("/auth/login", json={"username": "alice"})
assert response.status_code == 200
``` ```

View File

@@ -34,4 +34,6 @@ When called without arguments, [`get_logger`](../reference/logger.md#fastapi_too
logger = get_logger() logger = get_logger()
``` ```
---
[:material-api: API Reference](../reference/logger.md) [:material-api: API Reference](../reference/logger.md)

View File

@@ -27,7 +27,7 @@ from fastapi_toolsets.metrics import MetricsRegistry, init_metrics
app = FastAPI() app = FastAPI()
metrics = MetricsRegistry() metrics = MetricsRegistry()
init_metrics(app, registry=metrics) init_metrics(app=app, registry=metrics)
``` ```
This mounts the `/metrics` endpoint that Prometheus can scrape. This mounts the `/metrics` endpoint that Prometheus can scrape.
@@ -70,8 +70,8 @@ from myapp.metrics.http import http_metrics
from myapp.metrics.db import db_metrics from myapp.metrics.db import db_metrics
metrics = MetricsRegistry() metrics = MetricsRegistry()
metrics.include_registry(http_metrics) metrics.include_registry(registry=http_metrics)
metrics.include_registry(db_metrics) metrics.include_registry(registry=db_metrics)
``` ```
## Multi-process mode ## Multi-process mode
@@ -81,10 +81,6 @@ Multi-process support is enabled automatically when the `PROMETHEUS_MULTIPROC_DI
!!! warning "Environment variable name" !!! warning "Environment variable name"
The correct variable is `PROMETHEUS_MULTIPROC_DIR` (not `PROMETHEUS_MULTIPROCESS_DIR`). 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) [:material-api: API Reference](../reference/metrics.md)

View File

@@ -26,8 +26,15 @@ Use [`create_async_client`](../reference/pytest.md#fastapi_toolsets.pytest.utils
from fastapi_toolsets.pytest import create_async_client from fastapi_toolsets.pytest import create_async_client
@pytest.fixture @pytest.fixture
async def client(app): async def http_client(db_session):
async with create_async_client(app=app) as c: async def _override_get_db():
yield db_session
async with create_async_client(
app=app,
base_url="http://127.0.0.1/api/v1",
dependency_overrides={get_db: _override_get_db},
) as c:
yield c yield c
``` ```
@@ -36,36 +43,34 @@ async def client(app):
Use [`create_db_session`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_db_session) to create an isolated `AsyncSession` for a test: Use [`create_db_session`](../reference/pytest.md#fastapi_toolsets.pytest.utils.create_db_session) to create an isolated `AsyncSession` for a test:
```python ```python
from fastapi_toolsets.pytest import create_db_session from fastapi_toolsets.pytest import create_db_session, create_worker_database
@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") @pytest.fixture(scope="session")
async def worker_db_url(): async def worker_db_url():
async with create_worker_database(database_url=DATABASE_URL) as url: async with create_worker_database(
database_url=str(settings.SQLALCHEMY_DATABASE_URI)
) as url:
yield url yield url
@pytest.fixture @pytest.fixture
async def db_session(worker_db_url): async def db_session(worker_db_url):
async with create_db_session(database_url=worker_db_url, base=Base, cleanup=True) as session: async with create_db_session(
database_url=worker_db_url, base=Base, cleanup=True
) as session:
yield session yield session
``` ```
!!! info
In this example, the database is reset between each test using the argument `cleanup=True`.
## Parallel testing with pytest-xdist
The examples above are already compatible with parallel test execution with `pytest-xdist`.
## Cleaning up tables ## Cleaning up tables
[`cleanup_tables`](../reference/pytest.md#fastapi_toolsets.pytest.utils.cleanup_tables) truncates all tables between tests for fast isolation: If you want to manually clean up a database you can use [`cleanup_tables`](../reference/pytest.md#fastapi_toolsets.pytest.utils.cleanup_tables), this will truncates all tables between tests for fast isolation:
```python ```python
from fastapi_toolsets.pytest import cleanup_tables from fastapi_toolsets.pytest import cleanup_tables

View File

@@ -8,7 +8,7 @@ The `schemas` module provides generic response wrappers that enforce a uniform r
## Response models ## Response models
### `Response[T]` ### [`Response[T]`](../reference/schemas.md#fastapi_toolsets.schemas.Response)
The most common wrapper for a single resource response. The most common wrapper for a single resource response.
@@ -20,7 +20,7 @@ async def get_user(user: User = UserDep) -> Response[UserSchema]:
return Response(data=user, message="User retrieved") return Response(data=user, message="User retrieved")
``` ```
### `PaginatedResponse[T]` ### [`PaginatedResponse[T]`](../reference/schemas.md#fastapi_toolsets.schemas.PaginatedResponse)
Wraps a list of items with pagination metadata. Wraps a list of items with pagination metadata.
@@ -40,15 +40,10 @@ async def list_users() -> PaginatedResponse[UserSchema]:
) )
``` ```
### `ErrorResponse` ### [`ErrorResponse`](../reference/schemas.md#fastapi_toolsets.schemas.ErrorResponse)
Returned automatically by the exceptions handler. Can also be used as a response model for OpenAPI docs. Returned automatically by the exceptions handler.
```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) [:material-api: API Reference](../reference/schemas.md)

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "fastapi-toolsets" name = "fastapi-toolsets"
version = "0.10.0" version = "1.0.0"
description = "Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL" description = "Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"

View File

@@ -21,4 +21,4 @@ Example usage:
return Response(data={"user": user.username}, message="Success") return Response(data={"user": user.username}, message="Success")
""" """
__version__ = "0.10.0" __version__ = "1.0.0"

2
uv.lock generated
View File

@@ -251,7 +251,7 @@ wheels = [
[[package]] [[package]]
name = "fastapi-toolsets" name = "fastapi-toolsets"
version = "0.10.0" version = "1.0.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "asyncpg" }, { name = "asyncpg" },