mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-03-02 01:10:47 +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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]] = {}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user