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:
d3vyce
2026-02-19 16:43:38 +01:00
committed by GitHub
parent 73fae04333
commit 6714ceeb92
42 changed files with 2008 additions and 40 deletions

View File

@@ -2,11 +2,15 @@
import importlib
import sys
from typing import TYPE_CHECKING, Any, Literal, overload
import typer
from .pyproject import find_pyproject, load_pyproject
if TYPE_CHECKING:
from ..fixtures import FixtureRegistry
def _ensure_project_in_path():
"""Add project root to sys.path if not installed in editable mode."""
@@ -17,7 +21,7 @@ def _ensure_project_in_path():
sys.path.insert(0, project_root)
def import_from_string(import_path: str):
def import_from_string(import_path: str) -> Any:
"""Import an object from a dotted string path.
Args:
@@ -51,7 +55,13 @@ def import_from_string(import_path: str):
return getattr(module, attr_name)
def get_config_value(key: str, required: bool = False):
@overload
def get_config_value(key: str, required: Literal[True]) -> Any: ... # pragma: no cover
@overload
def get_config_value(
key: str, required: bool = False
) -> Any | None: ... # pragma: no cover
def get_config_value(key: str, required: bool = False) -> Any | None:
"""Get a configuration value from pyproject.toml.
Args:
@@ -76,7 +86,7 @@ def get_config_value(key: str, required: bool = False):
return value
def get_fixtures_registry():
def get_fixtures_registry() -> FixtureRegistry:
"""Import and return the fixtures registry from config."""
from ..fixtures import FixtureRegistry
@@ -91,7 +101,7 @@ def get_fixtures_registry():
return registry
def get_db_context():
def get_db_context() -> Any:
"""Import and return the db_context function from config."""
import_path = get_config_value("db_context", required=True)
return import_from_string(import_path)

View File

@@ -13,11 +13,13 @@ def async_command(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
"""Decorator to run an async function as a sync CLI command.
Example:
```python
@fixture_cli.command("load")
@async_command
async def load(ctx: typer.Context) -> None:
async with get_db_context() as session:
await load_fixtures(session, registry)
```
"""
@functools.wraps(func)

View File

@@ -682,6 +682,7 @@ def CrudFactory(
AsyncCrud subclass bound to the model
Example:
```python
from fastapi_toolsets.crud import CrudFactory
from myapp.models import User, Post
@@ -724,6 +725,7 @@ def CrudFactory(
joins=[(Post, Post.user_id == User.id)],
outer_join=True,
)
```
"""
cls = type(
f"Async{model.__name__}Crud",

View File

@@ -35,6 +35,7 @@ def create_db_dependency(
An async generator function usable with FastAPI's Depends()
Example:
```python
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from fastapi_toolsets.db import create_db_dependency
@@ -46,6 +47,7 @@ def create_db_dependency(
@app.get("/users")
async def list_users(session: AsyncSession = Depends(get_db)):
...
```
"""
async def get_db() -> AsyncGenerator[AsyncSession, None]:
@@ -72,6 +74,7 @@ def create_db_context(
An async context manager function
Example:
```python
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from fastapi_toolsets.db import create_db_context
@@ -83,6 +86,7 @@ def create_db_context(
async with get_db_context() as session:
user = await UserCrud.get(session, [User.id == 1])
...
```
"""
get_db = create_db_dependency(session_maker)
return asynccontextmanager(get_db)
@@ -104,9 +108,11 @@ async def get_transaction(
The session within the transaction context
Example:
```python
async with get_transaction(session):
session.add(model)
# Auto-commits on exit, rolls back on exception
```
"""
if session.in_transaction():
async with session.begin_nested():
@@ -158,6 +164,7 @@ async def lock_tables(
SQLAlchemyError: If lock cannot be acquired within timeout
Example:
```python
from fastapi_toolsets.db import lock_tables, LockMode
async with lock_tables(session, [User, Account]):
@@ -169,6 +176,7 @@ async def lock_tables(
async with lock_tables(session, [Order], mode=LockMode.EXCLUSIVE):
# Exclusive lock - no other transactions can access
await process_order(session, order_id)
```
"""
table_names = ",".join(table.__tablename__ for table in tables)
@@ -212,6 +220,7 @@ async def wait_for_row_change(
TimeoutError: If timeout expires before a change is detected
Example:
```python
from fastapi_toolsets.db import wait_for_row_change
# Wait for any column to change
@@ -224,6 +233,7 @@ async def wait_for_row_change(
interval=1.0,
timeout=30.0,
)
```
"""
instance = await session.get(model, pk_value)
if instance is None:

View File

@@ -38,12 +38,14 @@ def PathDependency(
NotFoundError: If no matching record is found
Example:
```python
UserDep = PathDependency(User, User.id, session_dep=get_db)
@router.get("/user/{id}")
async def get(
user: User = UserDep,
): ...
```
"""
crud = CrudFactory(model)
name = (
@@ -102,6 +104,7 @@ def BodyDependency(
NotFoundError: If no matching record is found
Example:
```python
UserDep = BodyDependency(
User, User.ctfd_id, session_dep=get_db, body_field="user_id"
)
@@ -110,6 +113,7 @@ def BodyDependency(
async def assign(
user: User = UserDep,
): ...
```
"""
crud = CrudFactory(model)
python_type = field.type.python_type

View File

@@ -12,6 +12,7 @@ class ApiException(Exception):
The exception handler will use api_error to generate the response.
Example:
```python
class CustomError(ApiException):
api_error = ApiError(
code=400,
@@ -19,6 +20,7 @@ class ApiException(Exception):
desc="The request was invalid.",
err_code="CUSTOM-400",
)
```
"""
api_error: ClassVar[ApiError]
@@ -114,6 +116,7 @@ def generate_error_responses(
Dict suitable for FastAPI's responses parameter
Example:
```python
from fastapi_toolsets.exceptions import generate_error_responses, UnauthorizedError, ForbiddenError
@app.get(
@@ -122,6 +125,7 @@ def generate_error_responses(
)
async def admin_endpoint():
...
```
"""
responses: dict[int | str, dict[str, Any]] = {}

View File

@@ -25,11 +25,13 @@ def init_exceptions_handlers(app: FastAPI) -> FastAPI:
The same FastAPI instance (for chaining)
Example:
```python
from fastapi import FastAPI
from fastapi_toolsets.exceptions import init_exceptions_handlers
app = FastAPI()
init_exceptions_handlers(app)
```
"""
_register_exception_handlers(app)
app.openapi = lambda: _custom_openapi(app) # type: ignore[method-assign]

View File

@@ -26,6 +26,7 @@ class FixtureRegistry:
"""Registry for managing fixtures with dependencies.
Example:
```python
from fastapi_toolsets.fixtures import FixtureRegistry, Context
fixtures = FixtureRegistry()
@@ -48,6 +49,7 @@ class FixtureRegistry:
return [
Post(id=1, title="Test", user_id=1),
]
```
"""
def __init__(
@@ -80,6 +82,7 @@ class FixtureRegistry:
contexts: List of contexts this fixture belongs to
Example:
```python
@fixtures.register
def roles():
return [Role(id=1, name="admin")]
@@ -87,6 +90,7 @@ class FixtureRegistry:
@fixtures.register(depends_on=["roles"], contexts=[Context.TESTING])
def test_users():
return [User(id=1, username="test", role_id=1)]
```
"""
def decorator(
@@ -124,6 +128,7 @@ class FixtureRegistry:
ValueError: If a fixture name already exists in the current registry
Example:
```python
registry = FixtureRegistry()
dev_registry = FixtureRegistry()
@@ -132,6 +137,7 @@ class FixtureRegistry:
return [...]
registry.include_registry(registry=dev_registry)
```
"""
for name, fixture in registry._fixtures.items():
if name in self._fixtures:

View File

@@ -59,9 +59,11 @@ async def load_fixtures(
Dict mapping fixture names to loaded instances
Example:
```python
# Loads 'roles' first (dependency), then 'users'
result = await load_fixtures(session, fixtures, "users")
print(result["users"]) # [User(...), ...]
```
"""
ordered = registry.resolve_dependencies(*names)
return await _load_ordered(session, registry, ordered, strategy)
@@ -85,11 +87,13 @@ async def load_fixtures_by_context(
Dict mapping fixture names to loaded instances
Example:
```python
# Load base + testing fixtures
await load_fixtures_by_context(
session, fixtures,
Context.BASE, Context.TESTING
)
```
"""
ordered = registry.resolve_context_dependencies(*contexts)
return await _load_ordered(session, registry, ordered, strategy)

View File

@@ -37,10 +37,12 @@ def configure_logging(
The configured Logger instance.
Example:
```python
from fastapi_toolsets.logger import configure_logging
logger = configure_logging("DEBUG")
logger.info("Application started")
```
"""
formatter = logging.Formatter(fmt)
@@ -83,11 +85,13 @@ def get_logger(name: str | None = _SENTINEL) -> logging.Logger: # type: ignore[
A Logger instance.
Example:
```python
from fastapi_toolsets.logger import get_logger
logger = get_logger() # uses caller's __name__
logger = get_logger("myapp") # explicit name
logger = get_logger(None) # root logger
```
"""
if name is _SENTINEL:
name = sys._getframe(1).f_globals.get("__name__")

View File

@@ -40,12 +40,14 @@ def init_metrics(
The same FastAPI instance (for chaining).
Example:
```python
from fastapi import FastAPI
from fastapi_toolsets.metrics import MetricsRegistry, init_metrics
metrics = MetricsRegistry()
app = FastAPI()
init_metrics(app, registry=metrics)
```
"""
for provider in registry.get_providers():
logger.debug("Initialising metric provider '%s'", provider.name)

View File

@@ -22,6 +22,7 @@ class MetricsRegistry:
"""Registry for managing Prometheus metric providers and collectors.
Example:
```python
from prometheus_client import Counter, Gauge
from fastapi_toolsets.metrics import MetricsRegistry
@@ -38,6 +39,7 @@ class MetricsRegistry:
@metrics.register(collect=True)
def collect_queue_depth(gauge=Gauge("queue_depth", "Current queue depth")):
gauge.set(get_current_queue_depth())
```
"""
def __init__(self) -> None:
@@ -61,6 +63,7 @@ class MetricsRegistry:
If ``False`` (default), called once at init time.
Example:
```python
@metrics.register
def my_counter():
return Counter("my_counter", "A counter")
@@ -68,6 +71,7 @@ class MetricsRegistry:
@metrics.register(collect=True, name="queue")
def collect_queue_depth():
gauge.set(compute_depth())
```
"""
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
@@ -93,6 +97,7 @@ class MetricsRegistry:
ValueError: If a metric name already exists in the current registry.
Example:
```python
main = MetricsRegistry()
sub = MetricsRegistry()
@@ -101,6 +106,7 @@ class MetricsRegistry:
return Counter("sub_total", "Sub counter")
main.include_registry(sub)
```
"""
for metric_name, definition in registry._metrics.items():
if metric_name in self._metrics:

View File

@@ -35,6 +35,7 @@ def register_fixtures(
List of created fixture names
Example:
```python
# conftest.py
from app.fixtures import fixtures
from fastapi_toolsets.pytest_plugin import register_fixtures
@@ -45,6 +46,7 @@ def register_fixtures(
# - fixture_roles
# - fixture_users (depends on fixture_roles if users depends on roles)
# - fixture_posts (depends on fixture_users if posts depends on users)
```
"""
created_fixtures: list[str] = []

View File

@@ -37,6 +37,7 @@ async def create_async_client(
An AsyncClient configured for the app.
Example:
```python
from fastapi import FastAPI
from fastapi_toolsets.pytest import create_async_client
@@ -50,8 +51,10 @@ async def create_async_client(
async def test_endpoint(client: AsyncClient):
response = await client.get("/health")
assert response.status_code == 200
```
Example with dependency overrides:
```python
from fastapi_toolsets.pytest import create_async_client, create_db_session
from app.db import get_db
@@ -69,6 +72,7 @@ async def create_async_client(
app, dependency_overrides={get_db: override}
) as c:
yield c
```
"""
if dependency_overrides:
app.dependency_overrides.update(dependency_overrides)
@@ -111,6 +115,7 @@ async def create_db_session(
An AsyncSession ready for database operations.
Example:
```python
from fastapi_toolsets.pytest import create_db_session
from app.models import Base
@@ -127,6 +132,7 @@ async def create_db_session(
user = User(name="test")
db_session.add(user)
await db_session.commit()
```
"""
engine = create_async_engine(database_url, echo=echo)
@@ -186,6 +192,7 @@ def worker_database_url(database_url: str, default_test_db: str) -> str:
A database URL with a worker- or default-specific database name.
Example:
```python
# With PYTEST_XDIST_WORKER="gw0":
url = worker_database_url(
"postgresql+asyncpg://user:pass@localhost/test_db",
@@ -199,6 +206,7 @@ def worker_database_url(database_url: str, default_test_db: str) -> str:
default_test_db="test",
)
# "postgresql+asyncpg://user:pass@localhost/test_db_test"
```
"""
worker = _get_xdist_worker(default_test_db=default_test_db)
@@ -231,6 +239,7 @@ async def create_worker_database(
The worker-specific database URL.
Example:
```python
from fastapi_toolsets.pytest import (
create_worker_database, create_db_session,
)
@@ -248,6 +257,7 @@ async def create_worker_database(
worker_db_url, Base, cleanup=True
) as session:
yield session
```
"""
worker_url = worker_database_url(
database_url=database_url, default_test_db=default_test_db
@@ -288,11 +298,13 @@ async def cleanup_tables(
base: SQLAlchemy DeclarativeBase class containing model metadata.
Example:
```python
@pytest.fixture
async def db_session(worker_db_url):
async with create_db_session(worker_db_url, Base) as session:
yield session
await cleanup_tables(session, Base)
```
"""
tables = base.metadata.sorted_tables
if not tables:

View File

@@ -71,7 +71,9 @@ class Response(BaseResponse, Generic[DataT]):
"""Generic API response with data payload.
Example:
```python
Response[UserRead](data=user, message="User retrieved")
```
"""
data: DataT | None = None
@@ -108,10 +110,12 @@ class PaginatedResponse(BaseResponse, Generic[DataT]):
"""Paginated API response for list endpoints.
Example:
```python
PaginatedResponse[UserRead](
data=users,
pagination=Pagination(total_count=100, items_per_page=10, page=1, has_more=True)
)
```
"""
data: list[DataT]