fix: ruff warnings

This commit is contained in:
2026-07-28 13:55:31 +02:00
committed by d3vyce
parent a407677be0
commit fd83a7142d
36 changed files with 255 additions and 138 deletions
+10 -3
View File
@@ -85,13 +85,17 @@ The `paginate` shorthand was an alias for `offset_paginate`. It has been removed
=== "Before (`v1`)"
```python
result = await UserCrud.paginate(session=session, page=2, items_per_page=20, schema=UserRead)
result = await UserCrud.paginate(
session=session, page=2, items_per_page=20, schema=UserRead
)
```
=== "Now (`v2`)"
```python
result = await UserCrud.offset_paginate(session=session, page=2, items_per_page=20, schema=UserRead)
result = await UserCrud.offset_paginate(
session=session, page=2, items_per_page=20, schema=UserRead
)
```
---
@@ -124,8 +128,11 @@ For shared base classes that are not meant to be raised directly, use `abstract=
class BillingError(ApiException, abstract=True):
"""Base for all billing-related errors — not raised directly."""
class PaymentRequiredError(BillingError):
api_error = ApiError(code=402, msg="Payment Required", desc="...", err_code="BILLING-402")
api_error = ApiError(
code=402, msg="Payment Required", desc="...", err_code="BILLING-402"
)
```
---
+13 -2
View File
@@ -45,12 +45,16 @@ Each new method accepts `search`, `filter`, and `order` boolean toggles (all `Tr
```python
from fastapi_toolsets.crud import OrderByClause
@router.get("/offset")
async def list_articles_offset(
session: SessionDep,
params: Annotated[dict, Depends(ArticleCrud.offset_params(default_page_size=20))],
filter_by: Annotated[dict, Depends(ArticleCrud.filter_params())],
order_by: Annotated[OrderByClause | None, Depends(ArticleCrud.order_params(default_field=Article.created_at))],
order_by: Annotated[
OrderByClause | None,
Depends(ArticleCrud.order_params(default_field=Article.created_at)),
],
search: str | None = None,
) -> OffsetPaginatedResponse[ArticleRead]:
return await ArticleCrud.offset_paginate(
@@ -79,7 +83,9 @@ Each new method accepts `search`, `filter`, and `order` boolean toggles (all `Tr
),
],
) -> OffsetPaginatedResponse[ArticleRead]:
return await ArticleCrud.offset_paginate(session=session, **params, schema=ArticleRead)
return await ArticleCrud.offset_paginate(
session=session, **params, schema=ArticleRead
)
```
The same pattern applies to `cursor_paginate_params()` and `paginate_params()`. To disable a feature, pass the toggle:
@@ -109,6 +115,7 @@ Model method callbacks (`on_create`, `on_delete`, `on_update`) and the `@watch`
```python
from fastapi_toolsets.models import WatchedFieldsMixin, watch
@watch("status")
class Order(Base, UUIDMixin, WatchedFieldsMixin):
__tablename__ = "orders"
@@ -131,21 +138,25 @@ Model method callbacks (`on_create`, `on_delete`, `on_update`) and the `@watch`
```python
from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for
class Order(Base, UUIDMixin):
__tablename__ = "orders"
__watched_fields__ = ("status",)
status: Mapped[str]
@listens_for(Order, [ModelEvent.CREATE])
async def on_order_created(order: Order, event_type: ModelEvent, changes: None):
await notify_new_order(order.id)
@listens_for(Order, [ModelEvent.UPDATE])
async def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):
if "status" in changes:
await notify_status_change(order.id, changes["status"])
@listens_for(Order, [ModelEvent.DELETE])
async def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):
await notify_order_cancelled(order.id)
+3 -1
View File
@@ -35,6 +35,8 @@ The function creates and manages its own **dedicated session** internally, yield
user.balance += 100
# With a custom lock mode
async with lock_tables(session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE) as session:
async with lock_tables(
session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE
) as session:
await process_order(session, order_id)
```
+10 -5
View File
@@ -24,9 +24,10 @@ Build one `Database` with your URL (or an existing `engine=`), then use the inst
get_db = create_db_dependency(session_maker=SessionLocal)
get_db_context = create_db_context(session_maker=SessionLocal)
@app.get("/users")
async def list_users(session: AsyncSession = Depends(get_db)):
...
async def list_users(session: AsyncSession = Depends(get_db)): ...
async def seed():
async with get_db_context() as session:
@@ -40,9 +41,10 @@ Build one `Database` with your URL (or an existing `engine=`), then use the inst
db = Database(url="postgresql+asyncpg://...")
@app.get("/users")
async def list_users(session: AsyncSession = Depends(db)):
...
async def list_users(session: AsyncSession = Depends(db)): ...
async def seed():
async with db.session() as session:
@@ -89,7 +91,9 @@ The free `lock_tables(session_maker, tables, ...)` function still exists for cal
```python
from fastapi_toolsets.db import lock_tables, LockMode
async with lock_tables(session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE) as session:
async with lock_tables(
session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE
) as session:
await process_order(session, order_id)
```
@@ -127,6 +131,7 @@ Both also change their first argument: instead of the fixture *function*, pass t
```python
from fastapi_toolsets.fixtures import get_obj_by_attr, get_field_by_attr
@fixtures.register(depends_on=["roles"])
def users():
admin_role = get_obj_by_attr(fixtures=roles, attr_name="name", value="admin")
+1
View File
@@ -92,6 +92,7 @@ import typer
cli = typer.Typer()
@cli.command()
def hello():
print("Hello from my app!")
+26 -6
View File
@@ -30,6 +30,7 @@ UserCrud = CrudFactory(model=User)
from fastapi_toolsets.crud.factory import AsyncCrud
from myapp.models import User
class UserCrud(AsyncCrud[User]):
model = User
searchable_fields = [User.username, User.email]
@@ -61,6 +62,7 @@ from fastapi_toolsets.crud.factory import AsyncCrud
T = TypeVar("T", bound=DeclarativeBase)
class AuditedCrud(AsyncCrud[T], Generic[T]):
"""Base CRUD with custom function"""
@@ -101,7 +103,9 @@ user = await UserCrud.first(session=session, filters=[User.email == email])
users = await UserCrud.get_multi(session=session, filters=[User.is_active == True])
# Update
user = await UserCrud.update(session=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
await UserCrud.delete(session=session, filters=[User.id == user_id])
@@ -160,10 +164,14 @@ user = await UserCrud.get(session, [User.id == user_id], with_for_update=True)
user = await UserCrud.get(session, [User.id == user_id], with_for_update="nowait")
# Skip rows already locked by another transaction (e.g. job queues)
rows = await JobCrud.get_multi(session, filters=[Job.status == "pending"], with_for_update="skip_locked")
rows = await JobCrud.get_multi(
session, filters=[Job.status == "pending"], with_for_update="skip_locked"
)
# Lock atomically as part of update (prevents race between SELECT and UPDATE)
user = await UserCrud.update(session, UserUpdate(credits=10), [User.id == user_id], with_for_update=True)
user = await UserCrud.update(
session, UserUpdate(credits=10), [User.id == user_id], with_for_update=True
)
```
!!! warning
@@ -193,6 +201,7 @@ Three pagination methods are available. All return a typed response whose `pagi
from typing import Annotated
from fastapi import Depends
@router.get("")
async def get_users(
session: SessionDep,
@@ -228,7 +237,9 @@ By default `offset_paginate` runs two queries: one for the page items and one `C
@router.get("")
async def get_users(
session: SessionDep,
params: Annotated[dict, Depends(UserCrud.offset_paginate_params(include_total=False))],
params: Annotated[
dict, Depends(UserCrud.offset_paginate_params(include_total=False))
],
) -> OffsetPaginatedResponse[UserRead]:
return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)
```
@@ -303,6 +314,7 @@ PostCrud = CrudFactory(model=Post, cursor_column=Post.created_at)
```python
from fastapi_toolsets.schemas import PaginatedResponse
@router.get("")
async def list_users(
session: SessionDep,
@@ -446,6 +458,7 @@ from typing import Annotated
from fastapi import Depends
@router.get("", response_model_exclude_none=True)
async def list_users(
session: SessionDep,
@@ -540,6 +553,7 @@ from typing import Annotated
from fastapi import Depends
@router.get("")
async def list_users(
session: SessionDep,
@@ -632,7 +646,9 @@ PostCrud = CrudFactory(
m2m_fields={"tag_ids": Post.tags},
)
post = await PostCrud.create(session=session, obj=PostCreateSchema(title="Hello", tag_ids=[1, 2, 3]))
post = await PostCrud.create(
session=session, obj=PostCreateSchema(title="Hello", tag_ids=[1, 2, 3])
)
```
## Upsert
@@ -659,6 +675,7 @@ class UserRead(PydanticBase):
id: UUID
username: str
@router.get(
"/{uuid}",
responses=generate_error_responses(NotFoundError),
@@ -670,12 +687,15 @@ async def get_user(session: SessionDep, uuid: UUID) -> Response[UserRead]:
schema=UserRead,
)
@router.get("")
async def list_users(
session: SessionDep,
params: Annotated[dict, Depends(crud.UserCrud.offset_paginate_params())],
) -> OffsetPaginatedResponse[UserRead]:
return await crud.UserCrud.offset_paginate(session=session, **params, schema=UserRead)
return await crud.UserCrud.offset_paginate(
session=session, **params, schema=UserRead
)
```
The schema must have `from_attributes=True` (or inherit from [`PydanticBase`](../reference/schemas.md#fastapi_toolsets.schemas.PydanticBase)) so it can be built from SQLAlchemy model instances.
+9 -3
View File
@@ -24,9 +24,9 @@ db = Database("postgresql+asyncpg://postgres:postgres@localhost/app")
app = FastAPI()
db.install(app) # commit middleware + engine disposal on shutdown
@app.get("/users")
async def list_users(session: AsyncSession = Depends(db)):
...
async def list_users(session: AsyncSession = Depends(db)): ...
```
The `Database` instance **is** the dependency: use it directly as `Depends(db)`. The whole request runs as a single transaction (CRUD writes use savepoints under it).
@@ -37,9 +37,11 @@ The **URL** may be a plain string or a Pydantic [`PostgresDsn`](https://docs.pyd
from pydantic_settings import BaseSettings
from pydantic import PostgresDsn
class Settings(BaseSettings):
database_url: PostgresDsn
settings = Settings()
db = Database(
@@ -72,12 +74,14 @@ Without `install`, the session commits in the dependency teardown, which runs af
```python
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
await warm_cache() # your startup
await warm_cache() # your startup
yield
await flush_metrics() # your shutdown
app = FastAPI(lifespan=lifespan)
db.install(app) # your shutdown runs first, then the engine is disposed
```
@@ -101,6 +105,7 @@ async def seed():
```python
from fastapi_toolsets.db import transaction
async def create_user_with_role(session):
async with transaction(session):
...
@@ -207,6 +212,7 @@ For test isolation with automatic cleanup, use [`create_worker_database`](../ref
```python
from fastapi_toolsets.db.testing import cleanup_tables
@pytest.fixture(autouse=True)
async def clean(db_session):
yield
+8 -2
View File
@@ -20,6 +20,7 @@ UserDep = PathDependency(model=User, field=User.id, session_dep=get_db)
SessionDep = Annotated[AsyncSession, Depends(get_db)]
UserDep = PathDependency(model=User, field=User.id, session_dep=SessionDep)
@router.get("/users/{user_id}")
async def get_user(user: User = UserDep):
return user
@@ -30,6 +31,7 @@ By default the parameter name is inferred from the field (`user_id` for `User.id
```python
UserDep = PathDependency(model=User, field=User.id, session_dep=get_db, param_name="id")
@router.get("/users/{id}")
async def get_user(user: User = UserDep):
return user
@@ -43,11 +45,15 @@ async def get_user(user: User = UserDep):
from fastapi_toolsets.dependencies import BodyDependency
# Plain callable
RoleDep = BodyDependency(model=Role, field=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"
)
# Annotated
SessionDep = Annotated[AsyncSession, Depends(get_db)]
RoleDep = BodyDependency(model=Role, field=Role.id, session_dep=SessionDep, body_field="role_id")
RoleDep = BodyDependency(
model=Role, field=Role.id, session_dep=SessionDep, body_field="role_id"
)
@router.post("/users")
+12 -3
View File
@@ -53,7 +53,9 @@ All built-in exceptions accept optional keyword arguments to customise the respo
| `data` | Overrides the `data` field |
```python
raise NotFoundError(detail="User 42 not found", desc="No user with that ID exists in the database.")
raise NotFoundError(
detail="User 42 not found", desc="No user with that ID exists in the database."
)
```
## Custom exceptions
@@ -64,6 +66,7 @@ Subclass [`ApiException`](../reference/exceptions.md#fastapi_toolsets.exceptions
from fastapi_toolsets.exceptions import ApiException
from fastapi_toolsets.schemas import ApiError
class PaymentRequiredError(ApiException):
api_error = ApiError(
code=402,
@@ -105,11 +108,17 @@ Use `abstract=True` when creating a shared base that is not meant to be raised d
class BillingError(ApiException, abstract=True):
"""Base for all billing-related errors."""
class PaymentRequiredError(BillingError):
api_error = ApiError(code=402, msg="Payment Required", desc="...", err_code="BILLING-402")
api_error = ApiError(
code=402, msg="Payment Required", desc="...", err_code="BILLING-402"
)
class SubscriptionExpiredError(BillingError):
api_error = ApiError(code=402, msg="Subscription Expired", desc="...", err_code="BILLING-402-EXP")
api_error = ApiError(
code=402, msg="Subscription Expired", desc="...", err_code="BILLING-402-EXP"
)
```
## OpenAPI response documentation
+18 -5
View File
@@ -13,6 +13,7 @@ from fastapi_toolsets.fixtures import FixtureRegistry, Context
fixtures = FixtureRegistry()
@fixtures.register
def roles():
return [
@@ -20,6 +21,7 @@ def roles():
Role(id=2, name="user"),
]
@fixtures.register(depends_on=["roles"], contexts=[Context.TESTING])
def test_users():
return [
@@ -79,14 +81,17 @@ Plain strings and any `Enum` subclass are accepted wherever a `Context` enum is
```python
from enum import Enum
class AppContext(str, Enum):
STAGING = "staging"
DEMO = "demo"
@fixtures.register(contexts=[AppContext.STAGING])
def staging_data():
return [Config(key="feature_x", enabled=True)]
# loads staging_data plus any Context.BASE fixtures
await load_fixtures_by_context(session, fixtures, AppContext.STAGING)
```
@@ -98,7 +103,8 @@ Pass `contexts` to `FixtureRegistry` to set a default for all fixtures registere
```python
testing_registry = FixtureRegistry(contexts=[Context.TESTING])
@testing_registry.register # implicitly contexts=[Context.TESTING]
@testing_registry.register # implicitly contexts=[Context.TESTING]
def test_orders():
return [Order(id=1, total=99)]
```
@@ -112,10 +118,12 @@ The same fixture name may be registered under different (non-overlapping) contex
def users():
return [User(id=1, username="admin")]
@fixtures.register(contexts=[Context.TESTING])
def users():
return [User(id=2, username="tester")]
# loads both admin and tester (Context.BASE is included automatically)
await load_fixtures_by_context(session, fixtures, Context.TESTING)
```
@@ -171,7 +179,9 @@ Looking the fixture up by name (instead of importing the `roles` function direct
```python
@fixtures.register(depends_on=["roles"])
def users():
return [User(id=1, username="alice", role_id=fixtures.field("roles", "name", "admin"))]
return [
User(id=1, username="alice", role_id=fixtures.field("roles", "name", "admin"))
]
```
Both raise `StopIteration` if no matching instance is found, and `KeyError` if the fixture name isn't registered.
@@ -189,18 +199,21 @@ 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:
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: list[User], fixture_roles: list[Role]):
...
async def test_user_can_login(fixture_users: list[User], fixture_roles: list[Role]): ...
```
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.
+3
View File
@@ -47,10 +47,12 @@ If neither of these applies to you, declaring metrics at module level (e.g. `HTT
```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")
@@ -79,6 +81,7 @@ from prometheus_client import Gauge
_queue_depth = Gauge("queue_depth", "Current queue depth")
@metrics.register(collect=True)
def collect_queue_depth():
_queue_depth.set(get_current_queue_depth())
+19 -1
View File
@@ -11,6 +11,7 @@ The `models` module provides mixins that each add a single, well-defined column
```python
from fastapi_toolsets.models import UUIDMixin, TimestampMixin
class Article(Base, UUIDMixin, TimestampMixin):
__tablename__ = "articles"
@@ -31,11 +32,13 @@ Adds a `id: UUID` primary key generated server-side by PostgreSQL using `gen_ran
```python
from fastapi_toolsets.models import UUIDMixin
class User(Base, UUIDMixin):
__tablename__ = "users"
username: Mapped[str]
# id is None before flush
user = User(username="alice")
session.add(user)
@@ -54,11 +57,13 @@ Adds a `id: UUID` primary key generated server-side by PostgreSQL using `uuidv7(
```python
from fastapi_toolsets.models import UUIDv7Mixin
class Event(Base, UUIDv7Mixin):
__tablename__ = "events"
name: Mapped[str]
# id is None before flush
event = Event(name="user.signup")
session.add(event)
@@ -73,6 +78,7 @@ Adds a `created_at: datetime` column set to `clock_timestamp()` on insert. The c
```python
from fastapi_toolsets.models import UUIDMixin, CreatedAtMixin
class Order(Base, UUIDMixin, CreatedAtMixin):
__tablename__ = "orders"
@@ -86,11 +92,13 @@ Adds an `updated_at: datetime` column set to `clock_timestamp()` on insert and a
```python
from fastapi_toolsets.models import UUIDMixin, UpdatedAtMixin
class Post(Base, UUIDMixin, UpdatedAtMixin):
__tablename__ = "posts"
title: Mapped[str]
post = Post(title="Hello")
await session.flush()
await session.refresh(post)
@@ -111,6 +119,7 @@ Convenience mixin that combines [`CreatedAtMixin`](../reference/models.md#fastap
```python
from fastapi_toolsets.models import UUIDMixin, TimestampMixin
class Article(Base, UUIDMixin, TimestampMixin):
__tablename__ = "articles"
@@ -166,10 +175,12 @@ class Order(Base, UUIDMixin):
__watched_fields__ = ("status",)
...
class UrgentOrder(Order):
# inherits __watched_fields__ = ("status",)
...
class PriorityOrder(Order):
__watched_fields__ = ("priority",)
# overrides parent — UPDATE fires only for priority changes
@@ -183,20 +194,24 @@ Register handlers with the [`listens_for`](../reference/models.md#fastapi_toolse
```python
from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for
class Order(Base, UUIDMixin):
__tablename__ = "orders"
__watched_fields__ = ("status",)
status: Mapped[str]
@listens_for(Order, [ModelEvent.CREATE])
async def on_order_created(order: Order, event_type: ModelEvent, changes: None):
await notify_new_order(order.id)
@listens_for(Order, [ModelEvent.DELETE])
async def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):
await notify_order_cancelled(order.id)
@listens_for(Order, [ModelEvent.UPDATE])
async def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):
if "status" in changes:
@@ -212,8 +227,11 @@ A single handler can listen for multiple events at once. When `event_types` is o
async def on_order_changed(order: Order, event_type: ModelEvent, changes: dict | None):
await invalidate_cache(order.id)
@listens_for(Order) # all events
async def on_any_order_event(order: Order, event_type: ModelEvent, changes: dict | None):
async def on_any_order_event(
order: Order, event_type: ModelEvent, changes: dict | None
):
await audit_log(order.id, event_type)
```
+11 -2
View File
@@ -21,6 +21,7 @@ Use [`create_async_client`](../reference/pytest.md#fastapi_toolsets.pytest.utils
```python
from fastapi_toolsets.pytest import create_async_client
@pytest.fixture
async def http_client(db_session):
async def _override_get_db():
@@ -52,6 +53,7 @@ Use [`create_worker_database`](../reference/pytest.md#fastapi_toolsets.pytest.ut
```python
from fastapi_toolsets.pytest import create_worker_database, create_db_session
@pytest.fixture(scope="session")
async def worker_db_url():
async with create_worker_database(
@@ -96,11 +98,17 @@ Use [`worker_database_url`](../reference/pytest.md#fastapi_toolsets.pytest.utils
```python
from fastapi_toolsets.pytest import worker_database_url
url = worker_database_url("postgresql+asyncpg://user:pass@localhost/myapp", default_test_db="test")
url = worker_database_url(
"postgresql+asyncpg://user:pass@localhost/myapp", default_test_db="test"
)
# → "postgresql+asyncpg://user:pass@localhost/gw0" under xdist
# → "postgresql+asyncpg://user:pass@localhost/test" otherwise
url = worker_database_url("postgresql+asyncpg://user:pass@localhost/myapp", default_test_db="test", prefix="myapp")
url = worker_database_url(
"postgresql+asyncpg://user:pass@localhost/myapp",
default_test_db="test",
prefix="myapp",
)
# → "postgresql+asyncpg://user:pass@localhost/myapp_gw0" under xdist
# → "postgresql+asyncpg://user:pass@localhost/myapp_test" otherwise
```
@@ -112,6 +120,7 @@ url = worker_database_url("postgresql+asyncpg://user:pass@localhost/myapp", defa
```python
from fastapi_toolsets.pytest import cleanup_tables
@pytest.fixture(autouse=True)
async def clean(db_session):
yield
+4
View File
@@ -15,6 +15,7 @@ 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")
@@ -39,6 +40,7 @@ Use as the return type when the endpoint always uses [`offset_paginate`](crud.md
```python
from fastapi_toolsets.schemas import OffsetPaginatedResponse
@router.get("/users")
async def list_users(
page: int = 1,
@@ -74,6 +76,7 @@ Use as the return type when the endpoint always uses [`cursor_paginate`](crud.md
```python
from fastapi_toolsets.schemas import CursorPaginatedResponse
@router.get("/events")
async def list_events(
cursor: str | None = None,
@@ -110,6 +113,7 @@ When used as a return annotation, `PaginatedResponse[T]` automatically expands t
from fastapi_toolsets.crud import PaginationType
from fastapi_toolsets.schemas import PaginatedResponse
@router.get("/users")
async def list_users(
pagination_type: PaginationType = PaginationType.OFFSET,
+5 -1
View File
@@ -6,7 +6,11 @@ You can import the main symbols from `fastapi_toolsets.crud`:
```python
from fastapi_toolsets.crud import CrudFactory, AsyncCrud
from fastapi_toolsets.crud.search import SearchConfig, get_searchable_fields, build_search_filters
from fastapi_toolsets.crud.search import (
SearchConfig,
get_searchable_fields,
build_search_filters,
)
```
## ::: fastapi_toolsets.crud.factory.AsyncCrud