AutoCRUD
AutoCRUD gives you a complete, consistent CRUD surface for your models without the boilerplate. It pairs cleanly with Kwik’s dependencies and patterns so you can focus on business logic.
Quick Mental Model¶
- Define a small subclass of
AutoCRUDper model. - Pass a
context(session + user) to every call. - Get stable list endpoints with pagination, sorting, and filters out of the box.
- Let AutoCRUD set audit fields automatically when a user is available.
The Generics Explained¶
AutoCRUD’s type parameters define how it operates:
from kwik.crud import AutoCRUD
from kwik.crud.context import UserCtx, NoUserCtx, Context
class MyCRUD(AutoCRUD[UserCtx, Model, CreateSchema, UpdateSchema, int]):
...
Ctx: one ofUserCtx,NoUserCtx, orMaybeUserCtxfromkwik.crud.context.Model: your SQLAlchemy model (subclass ofkwik.models.Base).CreateSchema/UpdateSchema: Pydantic models used for input.PkType: the primary key type, usuallyint.
Important: - If your model has audit columns (creator_user_id, last_modifier_user_id), you must use UserCtx. AutoCRUD validates this at construction and raises a ValueError otherwise. - AutoCRUD infers the model type from the generics. If you forget to specify them, it raises ValueError explaining what’s missing.
Context and Audit Fields¶
AutoCRUD inspects your model for audit columns:
- If found and you used
UserCtx, it will setcreator_user_idoncreate()andlast_modifier_user_idonupdate()whencontext.useris present. - If you try to use
NoUserCtxwith a model that has audit columns, AutoCRUD raises aValueErrorat instantiation time.
Example behavior (simplified):
db_obj = crud.create(obj_in=create_schema, context=context) # sets creator_user_id if available
db_obj = crud.update(entity_id=42, obj_in=update_schema, context=context) # sets last_modifier_user_id if available
Listing: Pagination, Sorting, Filters¶
get_multi() implements the standard list flow with validation:
count, rows = crud.get_multi(
context=context,
skip=0,
limit=100,
sort=[("id", "desc")],
status="active", # any allowed filter field
)
Notes: - Sorting uses a list of (field, direction) pairs. If not set, results default to primary key(s) ascending for stable pagination. - Filters are passed as keyword arguments. AutoCRUD validates both sort and filter fields; invalid fields raise ValueError which Kwik maps to HTTP 400. - In routes, prefer the ListQuery dependency to build these parameters consistently:
from kwik.dependencies import ListQuery, UserContext
from kwik.schemas import Paginated, MyProfile
@router.get("/", response_model=Paginated[MyProfile])
def read_items(q: ListQuery, context: UserContext):
total, data = crud.get_multi(context=context, **q)
return {"total": total, "data": data}
Core Methods¶
get(entity_id, context)→ Model | None: Fetch by id.get_if_exist(entity_id, context)→ Model: Fetch by id or raiseEntityNotFoundError.get_multi(context, skip=0, limit=100, sort=None, **filters)→(count, list[Model]): List with pagination/sorting/filters.create(obj_in, context)→ Model: Insert a new row; appliescreator_user_idif available.create_if_not_exist(obj_in, context, filters: dict[str, str], raise_on_error=False)→ Model: Insert or return existing; raisesDuplicatedEntityErrorifraise_on_error=Trueand a match exists.update(entity_id, obj_in, context)→ Model: Update a row; applieslast_modifier_user_idif available.delete(entity_id, context)→ Model: Delete and return the deleted object.
Allowed Fields for List Queries¶
By default, all mapped columns are allowed for sorting and filtering. To restrict these, set list_allowed_fields on your subclass:
class ProductCRUD(AutoCRUD[UserCtx, Product, ProductCreate, ProductUpdate, int]):
list_allowed_fields = {"id", "name", "status"}
Invalid fields cause ValueError → HTTP 400 via Kwik’s value_error_handler.
Complete Example¶
Here’s a compact example mirroring the pattern from the home page.
from decimal import Decimal
from pydantic import BaseModel
from sqlalchemy.orm import Mapped, mapped_column
from kwik.crud import AutoCRUD
from kwik.crud.context import UserCtx
from kwik.models import Base, RecordInfoMixin
# Model with audit fields (from RecordInfoMixin)
class Product(Base, RecordInfoMixin):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
price: Mapped[Decimal]
status: Mapped[str]
class ProductCreate(BaseModel):
name: str
price: Decimal
status: str = "active"
class ProductUpdate(BaseModel):
name: str | None = None
price: Decimal | None = None
status: str | None = None
class ProductCRUD(AutoCRUD[UserCtx, Product, ProductCreate, ProductUpdate, int]):
list_allowed_fields = {"id", "name", "status"}
crud_products = ProductCRUD(Product)
Use it in routes exactly like the Endpoints guide shows: inject UserContext, accept ListQuery, return Paginated[...] responses, and add has_permission(...) where appropriate.
Tips¶
- Mind the two “context” types: routes inject
UserContext(dependency), while AutoCRUD generics useUserCtx/NoUserCtx(fromkwik.crud.context). They’re related but not the same. - Override methods to add business rules (e.g., soft‑delete, invariants). Keep signatures consistent and call
super()when appropriate. - If you need conflict‑free create, use
create_if_not_existwith a filter dict that uniquely identifies the row. - Keep list responses predictable: restrict
list_allowed_fieldsto business‑meaningful fields.