Release Notes¶
v0.2.6¶
BetterCRUD v0.2.6 is a packaging, metadata and documentation release — no runtime API changes — all fully tested (193 tests).
🐛 Bug Fixes¶
- License field corrected —
_version.pydeclared"Apache License 2.0", so PyPI published Apache 2.0 as the project license while theLICENSEfile and theLicense :: OSI Approved :: MIT Licenseclassifier both said MIT. The metadata now agrees with the actual license: MIT. py.typedmarker added — theTyping :: Typedclassifier was declared, but the marker file was missing, somypyandpyrighttreated the package as untyped and every inline annotation was invisible to consumers. Type info is now delivered with the wheel.
✨ Enhancements¶
- Discoverable PyPI metadata — the summary now leads with the "FastAPI CRUD" phrase, and keywords grew from 6 to 13 terms (
crudrouter,fastapi-crud,crud-generator,sqlmodel,rest,rest-api,openapi, …) so the package is reachable from the terms developers actually search for. - Classifiers and project URLs — added
Topic :: Software Development :: Libraries :: Application Frameworks,Topic :: Internet :: WWW/HTTP,Topic :: Internet :: WWW/HTTP :: Dynamic ContentandFramework :: AsyncIO, plus Changelog and Issue Tracker project URLs.
📚 Documentation¶
- Docs site metadata — added
site_urlandsite_description, so the publishedsitemap.xmlis no longer empty and every page now has a canonical URL and meta description.
🔧 Infrastructure¶
- Wider test matrix — the pytest workflow ran on Python 3.9 only; it now runs on 3.9, 3.10, 3.11 and 3.12 (the versions the classifiers declare), with
fail-fast: falseso a single version failure does not mask the rest.
📦 Installation¶
For the complete history, see the Changelog.
v0.2.5¶
BetterCRUD v0.2.5 adds query.filters — declarative, typed, OpenAPI-visible query parameters for get_many that automatically participate in WHERE — all fully tested (193 tests).
🚀 Highlights¶
- Declared query filters — expose business filters like
?period_id=/?company_id=as first-classQueryparameters: documented in OpenAPI, validated by FastAPI, and AND-combined into the query automatically. Previously the only options wereauth.filterorjoins.*.additional_filter_fn, neither of which is visible in/docs. - Relationship fields supported — filter through a join (
company.name) without hand-writing the endpoint. - Fail-fast configuration — misconfiguration is caught at route registration, not in production queries.
✨ Enhancements¶
query.filters¶
@crud(
router,
feature="user",
serialize={"base": UserPublic},
query={
"filters": {
"company_id": {"field": "company_id", "type": "int", "description": "filter by company"},
"by_company": {"field": "company.name", "type": "str", "description": "filter by company name"},
},
"joins": {"company": {"join": True, "select": False}},
}
)
class UserController():
service: UserService = Depends(UserService)
GET /user?company_id=1&filter=user_name||$eq||bob now works out of the box: company_id appears in /docs as an integer parameter, ?company_id=abc returns 422, and the value is AND-combined with the generic filter/s conditions.
Each filter accepts:
| Key | Type | Description |
|---|---|---|
field |
str |
Model field path, e.g. company_id or company.name |
type |
"str"/"int"/"float"/"bool" |
Query value type (OpenAPI + validation) |
description |
str |
OpenAPI description |
required |
bool |
Reject requests omitting it (422). Defaults to False |
Registration-time validation:
- A
fieldcontaining.references a relationship and must be declared inquery.joinswithjoin=True— otherwise aWHEREon an unjoined table would silently cross-join. RaisesValueError. - Filter names must not collide with reserved
get_manyparameters (request,search,sorts,joins,include_deleted, path params). RaisesValueError.
Existing routes are unaffected: with no query.filters configured, the code path is skipped entirely.
See Query params for the full guide.
📦 Installation¶
For the complete history, see the Changelog.
v0.2.4¶
BetterCRUD v0.2.4 fixes explicit response_model handling in crud_action: the value you pass is now the final response shape, used as-is — no more silent double-wrapping — all fully tested (184 tests).
🐛 Bug Fixes¶
- Explicit
response_modelno longer double-wrapped — passing a full parameterized response shell likeresponse_model=ResponseModel[CurrentUser]used to registerResponseModel[ResponseModel[CurrentUser]](a nested shell). The explicit value is now used as the final response model as-is.
✨ Enhancements¶
crud_action explicit response model¶
response_model is the final response shape. Pass the parameterized response shell directly (ResponseModel below is the response schema configured globally via BetterCrudGlobalConfig.init(response_schema=ResponseModel)):
from better_crud import crud, crud_action
@crud(pet_router, serialize={"base": PetPublic})
class PetController():
service: PetService = Depends(PetService)
@crud_action(method="GET", path="/summary", response_model=ResponseModel[PetSummary])
async def summary(self) -> PetSummary:
return PetSummary(adopted=3, available=7)
The registered route's response_model is exactly ResponseModel[PetSummary] — matching your OpenAPI docs and runtime validation.
📦 Installation¶
For the complete history, see the Changelog.
v0.2.3¶
BetterCRUD v0.2.3 makes crud_action response models follow your type annotations, fixes a route-registration crash on unresolvable annotations, and stops double-wrapping payloads — all fully tested (183 tests).
🚀 Highlights¶
- Annotation-driven response models — when a global
response_schemais configured,crud_actionnow infers the inner response model from the endpoint's return annotation instead of always falling back toserialize.base. Return-> CustomResultand the OpenAPI schema becomesResponseModel[CustomResult]. - No more double-wrapped payloads — endpoints that already return a response schema instance are passed through as-is instead of being wrapped a second time.
- Robust annotation resolution — a return annotation that cannot be resolved at import time (e.g.
from __future__ import annotationsreferencing a model that is not in the module globals) no longer crashes route registration; it falls back to the bareresponse_schema(data unconstrained) and emits aUserWarningso you notice the untyped endpoint.
✨ Enhancements¶
crud_action response model inference¶
from pydantic import BaseModel
from better_crud import crud, crud_action
class PetSummary(BaseModel):
adopted: int
available: int
@crud(pet_router, serialize={"base": PetPublic})
class PetController():
service: PetService = Depends(PetService)
@crud_action(method="GET", path="/summary")
async def summary(self) -> PetSummary:
return PetSummary(adopted=3, available=7)
With response_schema=ResponseModel configured globally, this registers with response_model=ResponseModel[PetSummary] — the payload is validated against PetSummary, and OpenAPI documents it precisely. Previously it would have been typed as ResponseModel[PetPublic] regardless of what the endpoint actually returned.
Inference rules:
-> X→ResponseModel[X]-> Optional[X]/-> X | None→ResponseModel[Optional[X]](aNoneresponse stays valid)-> List[X]/-> Page[X]→ preserved as containers- no annotation, or an annotation that cannot be resolved → bare
ResponseModel(data unconstrained, with aUserWarningin the unresolvable case) - no
response_schemaconfigured → FastAPI infers from the return value as before
You can still force a model explicitly with crud_action(..., response_model=...) — the explicit value always wins.
🐛 Bug Fixes¶
- Route registration crash —
crud_actionendpoints with unresolvable return annotations (string annotations referencing non-global names) raisedNameError/TypeErrorat import time, taking down the whole app. Now caught: the endpoint registers with the bareresponse_schemaand aUserWarningis emitted. - Double-wrapped payloads — an endpoint returning a response schema instance produced
{code, msg, data: {code, msg, data: ...}}. Now passed through untouched.
📦 Installation¶
For the complete history, see the Changelog.
v0.2.2¶
BetterCRUD v0.2.2 adds custom endpoints via a new crud_action decorator, makes bulk operations atomic, and completes the soft-delete story with an opt-in recover route — all fully tested (177 tests).
🚀 Highlights¶
crud_actiondecorator — attach business endpoints (adopt,approve,reset, ...) to your generated routes while keeping them inside the CRUD ecosystem:serviceinjection, ACL action names, response schema wrapping, andrequest.statescoping all work automatically.- Atomic bulk operations —
crud_create_many/crud_update_manynow run in a single transaction. A mid-batch failure rolls back every item instead of leaving partial data. - Soft-delete recover route —
PATCH /{resource}/{id}/recoverrestores soft-deleted records. Opt-in viaquery={"soft_delete": True, "allow_recover": True}; routes are not generated otherwise (zero footprint).
✨ Enhancements¶
crud_action custom endpoints¶
from better_crud import crud, crud_action
@crud(pet_router, serialize={"base": PetPublic})
class PetController():
service: PetService = Depends(PetService)
@crud_action(method="POST", path="/{id}/adopt", action="adopt")
async def adopt(self, id: int):
return {"id": id, "adopted": True}
See Custom Actions for the full guide.
Atomic bulk operations¶
crud_create_many and crud_update_many now wrap the batch in a single transaction. On any failure, all changes are rolled back — no partial writes.
Recover route¶
@crud(
router,
serialize={"base": UserPublic},
query={
"soft_delete": True,
"allow_recover": True, # exposes PATCH /{id}/recover
}
)
🐛 Bug Fixes¶
- Bulk
create_many/update_manywere non-atomic (per-item commits) — a mid-batch failure left partial data. Now fully transactional.
📦 Installation¶
For the complete history, see the Changelog.
v0.2.1¶
BetterCRUD v0.2.1 adds optional pagination to the get_many route. List endpoints can now return a full array (no pagination) when the caller omits pagination params — ideal for small datasets like enum-like tables, reference data, and dropdown options.
🚀 Highlights¶
- Optional pagination —
GET /{resource}now supports three modes viapagination_mode, configurable globally or per-route. - Backward compatible by default — the default
"optional"mode matches previous behavior exactly (no pagination params → full array;page/size→ paginated response). All 165 tests pass.
✨ Enhancements¶
pagination_mode global config¶
BetterCrudGlobalConfig.init() accepts a new pagination_mode option:
BetterCrudGlobalConfig.init(
backend_config={"sqlalchemy": {"db_session": get_session}},
pagination_mode="optional", # "always" | "optional" | "disabled"
)
| Mode | No page/size |
?page=1&size=20 |
|---|---|---|
always |
Paginated (default page=1, size=50) |
Paginated |
optional (default) |
Full array | Paginated |
disabled |
Full array | Full array (params ignored) |
Per-route override¶
Individual @crud decorators can override the global mode:
@crud(
router,
serialize={"base": PetPublic},
pagination_mode="always",
)
class PetController():
service: PetService = Depends(PetService)
Response format¶
- Paginated:
{items: [...], total, page, size, pages} - Non-paginated:
[...]— plain array of all matching records
filter/s/sort work identically in both modes. Frontends should check whether the response is an array or an object:
📦 Installation¶
For the complete history, see the Changelog.
v0.2.0¶
BetterCRUD v0.2.0 brings full compatibility with the latest FastAPI releases, more powerful lifecycle hooks, and a cleaner dependency footprint — all backed by a fully passing test suite (154 tests).
🚀 Highlights¶
- FastAPI 0.141+ compatibility — fixes route registration failures caused by pydantic serializing
Dependsobjects into plain dicts. Routes now register correctly across FastAPI>=0.111.0,<1.0. - Richer lifecycle hooks —
on_after_createandon_after_updatenow receive the validatedmodelpayload, not just the persisted entity, giving you full access to the input data in your hook logic. $notanyfilter now supports empty lists, making the operator consistent with$in/$ninand simpler to use from dynamic query builders.
✨ Enhancements¶
Lifecycle hooks receive the input model¶
on_after_create / on_after_update gained a new model parameter carrying the original validated create/update schema:
async def on_after_create(
self,
entity: Entity, # persisted instance
model: EntityCreate, # NEW: validated create payload
background_tasks: BackgroundTasks,
) -> None:
...
Warning
This is a breaking change for anyone overriding these two hooks — update your signatures to accept the new model argument.
$notany supports empty list¶
🐛 Bug Fixes¶
- FastAPI
>=0.141route registration —Dependsinstances restored after pydanticmodel_dump()serialization soroutes.dependenciesattach correctly. $notanyoperator — no longer fails when given an empty list.
🔧 Dependency & Compatibility¶
- FastAPI:
>=0.111.0,<1.0(previously pinned versions could not resolve from older PyPI mirrors) - SQLAlchemy:
>=2.0.30,<3.0 - fastapi-pagination:
>=0.12.24,<1.0 - Pydantic:
>=2.7.3,<3.0 - Development requirements locked to versions reachable from all PyPI mirrors (fastapi 0.128.8, sqlalchemy 2.0.30, pydantic 2.7.3), with
greenletandbcrypt<5added for SQLAlchemy async and passlib compatibility.
✅ Verified Combinations¶
All tested with the full suite — 154 passed:
| FastAPI | SQLAlchemy | fastapi-pagination | Pydantic | SQLModel |
|---|---|---|---|---|
| 0.111.0 | 2.0.30 | 0.12.24 | 2.7.3 | 0.0.22 |
| 0.128.8 | 2.0.30 | 0.12.24 | 2.7.3 | 0.0.22 |
| 0.128.8 | 2.0.51 | 0.15.16 | 2.13.4 | — |
| 0.135.1 | 2.0.48 | 0.12.24 | 2.13.4 | 0.0.14 |
| 0.140.13 | 2.0.51 | 0.15.16 | 2.13.4 | — |
| 0.141.1 | 2.0.51 | 0.15.16 | 2.13.4 | — |
📦 Installation¶
For the complete history, see the Changelog.