Migrating to v3.0¶
This page covers every breaking change introduced in v3.0 and the steps required to update your code.
CRUD¶
Facet keys now always use the full relationship chain¶
In v2, relationship facet fields used only the terminal column key (e.g. "name" for Role.name) and only prepended the relationship name when two facet fields shared the same column key. In v3, facet keys always include the full relationship chain joined by __, regardless of collisions.
*_params dependencies consolidated into per-paginate methods¶
The six individual dependency methods (offset_params, cursor_params, paginate_params, filter_params, search_params, order_params) have been removed and replaced by three consolidated methods that bundle pagination, search, filter, and order into a single Depends() call.
| Removed | Replacement |
|---|---|
offset_params() + filter_params() + search_params() + order_params() |
offset_paginate_params() |
cursor_params() + filter_params() + search_params() + order_params() |
cursor_paginate_params() |
paginate_params() + filter_params() + search_params() + order_params() |
paginate_params() |
Each new method accepts search, filter, and order boolean toggles (all True by default) to disable features you don't need.
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))],
search: str | None = None,
) -> OffsetPaginatedResponse[ArticleRead]:
return await ArticleCrud.offset_paginate(
session=session,
**params,
search=search,
filter_by=filter_by or None,
order_by=order_by,
schema=ArticleRead,
)
@router.get("/offset")
async def list_articles_offset(
session: SessionDep,
params: Annotated[
dict,
Depends(
ArticleCrud.offset_paginate_params(
default_page_size=20,
default_order_field=Article.created_at,
)
),
],
) -> OffsetPaginatedResponse[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:
# No search or ordering, only pagination + filtering
ArticleCrud.offset_paginate_params(search=False, order=False)
Models¶
The lifecycle event system has been rewritten. Callbacks are now registered with a module-level listens_for decorator and dispatched by EventSession, replacing the mixin-based approach from v2.
WatchedFieldsMixin and @watch removed¶
Importing WatchedFieldsMixin or watch will raise ImportError.
Model method callbacks (on_create, on_delete, on_update) and the @watch decorator are replaced by:
__watched_fields__— a plain class attribute to restrict which field changes triggerUPDATEevents (replaces@watch).@listens_for— a module-level decorator to register callbacks for one or moreModelEventtypes (replaceson_create/on_delete/on_updatemethods).
from fastapi_toolsets.models import WatchedFieldsMixin, watch
@watch("status")
class Order(Base, UUIDMixin, WatchedFieldsMixin):
__tablename__ = "orders"
status: Mapped[str]
async def on_create(self):
await notify_new_order(self.id)
async def on_update(self, changes):
if "status" in changes:
await notify_status_change(self.id, changes["status"])
async def on_delete(self):
await notify_order_cancelled(self.id)
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)
EventSession now required¶
Without EventSession, lifecycle callbacks will silently stop firing.
Callbacks are now dispatched inside EventSession.commit() rather than via background tasks. Pass it as the session class when creating your session factory:
Note
If you use create_db_session from fastapi_toolsets.pytest, the session already uses EventSession — no changes needed in tests.