mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-03-01 17:00:48 +01:00
doc: add missing docstring + add missing feature to README (#57)
This commit is contained in:
10
README.md
10
README.md
@@ -26,11 +26,15 @@ uv add fastapi-toolsets
|
||||
|
||||
## Features
|
||||
|
||||
- **CRUD**: Generic async CRUD operations with `CrudFactory`
|
||||
- **Fixtures**: Fixture system with dependency management, context support and pytest integration
|
||||
- **CRUD**: Generic async CRUD operations with `CrudFactory`, built-in search with relationship traversal
|
||||
- **Database**: Session management, transaction helpers, table locking, and polling-based row change detection
|
||||
- **Dependencies**: FastAPI dependency factories (`PathDependency`, `BodyDependency`) for automatic DB lookups from path or body parameters
|
||||
- **Fixtures**: Fixture system with dependency management, context support, and pytest integration
|
||||
- **CLI**: Django-like command-line interface with fixture management and custom commands support
|
||||
- **Standardized API Responses**: Consistent response format across your API
|
||||
- **Standardized API Responses**: Consistent response format with `Response`, `PaginatedResponse`, and `PydanticBase`
|
||||
- **Exception Handling**: Structured error responses with automatic OpenAPI documentation
|
||||
- **Logging**: Logging configuration with uvicorn integration via `configure_logging` and `get_logger`
|
||||
- **Pytest Helpers**: Async test client, database session management, `pytest-xdist` support, and table cleanup utilities
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -18,10 +18,16 @@ def _ensure_project_in_path():
|
||||
|
||||
|
||||
def import_from_string(import_path: str):
|
||||
"""Import an object from a string path like 'module.submodule:attribute'.
|
||||
"""Import an object from a dotted string path.
|
||||
|
||||
Args:
|
||||
import_path: Import path in ``"module.submodule:attribute"`` format
|
||||
|
||||
Returns:
|
||||
The imported attribute
|
||||
|
||||
Raises:
|
||||
typer.BadParameter: If the import path is invalid or import fails.
|
||||
typer.BadParameter: If the import path is invalid or import fails
|
||||
"""
|
||||
if ":" not in import_path:
|
||||
raise typer.BadParameter(
|
||||
|
||||
@@ -10,6 +10,12 @@ def find_pyproject(start_path: Path | None = None) -> Path | None:
|
||||
"""Find pyproject.toml by walking up the directory tree.
|
||||
|
||||
Similar to how pytest, black, and ruff discover their config files.
|
||||
|
||||
Args:
|
||||
start_path: Directory to start searching from. Defaults to cwd.
|
||||
|
||||
Returns:
|
||||
Path to pyproject.toml, or None if not found.
|
||||
"""
|
||||
path = (start_path or Path.cwd()).resolve()
|
||||
|
||||
|
||||
@@ -210,6 +210,20 @@ async def wait_for_row_change(
|
||||
Raises:
|
||||
LookupError: If the row does not exist or is deleted during polling
|
||||
TimeoutError: If timeout expires before a change is detected
|
||||
|
||||
Example:
|
||||
from fastapi_toolsets.db import wait_for_row_change
|
||||
|
||||
# Wait for any column to change
|
||||
updated = await wait_for_row_change(session, User, user_id)
|
||||
|
||||
# Watch specific columns with a timeout
|
||||
updated = await wait_for_row_change(
|
||||
session, User, user_id,
|
||||
columns=["status", "email"],
|
||||
interval=1.0,
|
||||
timeout=30.0,
|
||||
)
|
||||
"""
|
||||
instance = await session.get(model, pk_value)
|
||||
if instance is None:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Standardized API exceptions and error response handlers."""
|
||||
|
||||
from .exceptions import (
|
||||
ApiError,
|
||||
ApiException,
|
||||
|
||||
@@ -87,6 +87,12 @@ class InsufficientRolesError(ForbiddenError):
|
||||
)
|
||||
|
||||
def __init__(self, required_roles: list[str], user_roles: set[str] | None = None):
|
||||
"""Initialize the exception.
|
||||
|
||||
Args:
|
||||
required_roles: Roles needed to access the resource
|
||||
user_roles: Roles the current user has, if known
|
||||
"""
|
||||
self.required_roles = required_roles
|
||||
self.user_roles = user_roles
|
||||
|
||||
@@ -130,6 +136,11 @@ class NoSearchableFieldsError(ApiException):
|
||||
)
|
||||
|
||||
def __init__(self, model: type) -> None:
|
||||
"""Initialize the exception.
|
||||
|
||||
Args:
|
||||
model: The SQLAlchemy model class that has no searchable fields
|
||||
"""
|
||||
self.model = model
|
||||
detail = (
|
||||
f"No searchable fields found for model '{model.__name__}'. "
|
||||
|
||||
@@ -12,6 +12,25 @@ from .exceptions import ApiException
|
||||
|
||||
|
||||
def init_exceptions_handlers(app: FastAPI) -> FastAPI:
|
||||
"""Register exception handlers and custom OpenAPI schema on a FastAPI app.
|
||||
|
||||
Installs handlers for :class:`ApiException`, validation errors, and
|
||||
unhandled exceptions, and replaces the default 422 schema with a
|
||||
consistent error format.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
|
||||
Returns:
|
||||
The same FastAPI instance (for chaining)
|
||||
|
||||
Example:
|
||||
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]
|
||||
return app
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Fixture system for seeding databases with dependency resolution."""
|
||||
|
||||
from .enum import LoadStrategy
|
||||
from .registry import Context, FixtureRegistry
|
||||
from .utils import get_obj_by_attr, load_fixtures, load_fixtures_by_context
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Enums for fixture loading strategies and contexts."""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Fixture loading utilities for database seeding."""
|
||||
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, TypeVar
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@ def configure_logging(
|
||||
|
||||
Returns:
|
||||
The configured Logger instance.
|
||||
|
||||
Example:
|
||||
from fastapi_toolsets.logger import configure_logging
|
||||
|
||||
logger = configure_logging("DEBUG")
|
||||
logger.info("Application started")
|
||||
"""
|
||||
formatter = logging.Formatter(fmt)
|
||||
|
||||
@@ -75,6 +81,13 @@ def get_logger(name: str | None = _SENTINEL) -> logging.Logger: # type: ignore[
|
||||
|
||||
Returns:
|
||||
A Logger instance.
|
||||
|
||||
Example:
|
||||
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__")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Pytest helpers for FastAPI testing: sessions, clients, and fixtures."""
|
||||
|
||||
from .plugin import register_fixtures
|
||||
from .utils import (
|
||||
cleanup_tables,
|
||||
|
||||
@@ -33,7 +33,6 @@ 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
|
||||
|
||||
@@ -47,7 +46,6 @@ async def create_async_client(
|
||||
async def test_endpoint(client: AsyncClient):
|
||||
response = await client.get("/health")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
"""
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url=base_url) as client:
|
||||
@@ -79,7 +77,6 @@ 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
|
||||
|
||||
@@ -94,7 +91,6 @@ 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)
|
||||
|
||||
@@ -151,7 +147,6 @@ 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",
|
||||
@@ -165,7 +160,6 @@ 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)
|
||||
|
||||
@@ -198,7 +192,6 @@ async def create_worker_database(
|
||||
The worker-specific database URL.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from fastapi_toolsets.pytest import (
|
||||
create_worker_database, create_db_session, cleanup_tables
|
||||
)
|
||||
@@ -215,7 +208,6 @@ async def create_worker_database(
|
||||
async with create_db_session(worker_db_url, Base) as session:
|
||||
yield session
|
||||
await cleanup_tables(session, Base)
|
||||
```
|
||||
"""
|
||||
worker_url = worker_database_url(
|
||||
database_url=database_url, default_test_db=default_test_db
|
||||
@@ -256,13 +248,11 @@ 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:
|
||||
|
||||
Reference in New Issue
Block a user