Services¶
Protocols¶
Each Protocol is parameterised by input, instance (where applicable), and
result. **extras is typed Any, so the framework's kwargs pool flows
through without the service having to declare each key. Strict-typed
extras live on the user's function signature via **extras: Unpack[YourKw]
— see Typing services and selectors for the full pattern.
CreateService ¶
Bases: Protocol[InputT, ResultT]
The structural shape of a service that takes validated input and no target.
data is what the spec's
Validator returned,
under the pool's reserved data seed. **extras absorbs whatever else
the keyword pool delivers - user, a registered seed, the spec's
kwargs - without the service declaring each key. It is typed Any so
a service conforms under every type checker.
Nothing reads the Protocol at run time: dispatch binds a service from the
keywords it declares whatever it is annotated with, so conforming is a
claim checked by a type checker, through
implements.
Strictly typed extras stay possible on the service's own signature: declare
them as a TypedDict whose keys are all NotRequired and annotate
**extras: Unpack[YourExtras], so extras["tenant"] is typed in the
body. NotRequired because the Protocol's caller may not supply them.
The Protocol carries no type argument for the extras, since that
cross-check is not portable across type checkers.
UpdateService ¶
Bases: Protocol[InputT, InstanceT, ResultT]
The structural shape of a service that acts on a resolved row with validated input.
instance is the row the spec's instance_selector_spec resolved, under
the pool's reserved instance seed, and data what the spec's
Validator returned.
See CreateService
for **extras and strictly typed extras.
DeleteService ¶
Bases: Protocol[InputT, InstanceT, ResultT]
The structural shape of a service that acts on a resolved row, input optional.
instance is the row the spec's instance_selector_spec resolved. Most
deletes return None.
data has a default in the Protocol, so a service that reads no input
conforms without declaring it: bind InputT to
NoInput and leave data
out. A delete that does take input - a reason, a confirmation - binds
InputT to its input type and declares data with a default of its
own, data: ReasonInput | None = None: declared without one it does not
conform, since the Protocol's caller may leave data out.
See CreateService
for **extras and strictly typed extras.
Default model service factories¶
create_model¶
create_model ¶
create_model(
model: type[ModelT],
*,
field_map: dict[str, str] | None = None,
exclude_fields: list[str] | None = None,
m2m: Mapping[str, Any] | Callable[[Any], Mapping[str, Any]] | None = None,
relations: Mapping[str, RelationSpec] | None = None,
) -> Callable[..., ModelT]
A service that builds a row of model from its validated data, and returns the row.
The service a create operation would otherwise write by hand:
def create_author(*, data: AuthorIn, **_: Any) -> Author:
return create_from_input(Author, data).instance
field_map, exclude_fields and relations are handed to
create_from_input
as they are, so one relations map writes every nested kind, a reverse
foreign key's
ChildSpec among
them. m2m is a static mapping, or a callable handed the validated
data that returns one, which is the usual case when the rows to link
arrive in the arguments:
create_model(Post, m2m=lambda data: {"tags": data.tags})
The service returns the row, not the
ChangeResult,
since the row is what a create's output presents. It opens no transaction:
dispatch runs a service inside one, which covers the row and every nested
write. It takes **kwargs, so the rest of the pool - the user and
any registered seed - is absorbed rather than refused. It names
call_context, which a catch-all is never handed, and the same pool
with the call's context beside it is handed on as the helper's
context, so a row service a relation spec declares can see who is
calling and, by naming call_context, the very dict the call's other
stages share.
update_model¶
update_model ¶
update_model(
model: type[ModelT],
*,
field_map: dict[str, str] | None = None,
exclude_fields: list[str] | None = None,
m2m: Mapping[str, Any] | Callable[[Any], Mapping[str, Any]] | None = None,
update_fields: bool | list[str] = True,
relations: Mapping[str, RelationSpec] | None = None,
) -> Callable[..., ModelT]
A service that writes the validated data onto the resolved row, and returns the row.
The service an update operation would otherwise write by hand:
def update_author(*, instance: Author, data: AuthorIn, **_: Any) -> Author:
return update_from_input(instance, data).instance
model is not read: it is taken for symmetry with
create_model and
to bind the row's type for a type checker, since the row is the
instance the spec's instance selector resolved. field_map,
exclude_fields, update_fields and relations are handed to
update_from_input
as they are, and m2m is read as create_model reads it.
The service returns the row rather than the
ChangeResult,
opens no transaction of its own, absorbs the rest of the pool and hands it
on as context with the call's context beside it, as create_model's
does.
delete_model¶
delete_model ¶
delete_model(
model: type[ModelT],
*,
soft_delete: Callable[[ModelT], None] | None = None,
relations: Mapping[str, RelationSpec] | None = None,
) -> Callable[..., None]
A service that deletes the resolved row, after the rows it owns, and returns None.
The service a delete operation would otherwise write by hand:
def delete_author(*, instance: Author, **_: Any) -> None:
instance.delete()
model is not read, as in
update_model:
the row is the instance the spec's instance selector resolved.
soft_delete is called with the row instead of instance.delete(),
for a row that is retired rather than removed:
def archive(instance: Author) -> None:
instance.is_archived = True
instance.save(update_fields=["is_archived"])
delete_model(Author, soft_delete=archive)
So no row is deleted and Django sends no pre_delete or post_delete
for it.
relations is the map the writes take, and what it declares goes
before the row, deepest first, through
delete_relations:
the rows the instance owns are disposed of, a many-to-many loses its
membership, and a forward relation is left alone. It is the cascade a
database will not run - a PROTECT foreign key, or a soft delete, which
removes no row for Django to cascade from - and the one a declared
delete_service sees row by row. A value that is not a relation spec is
refused with ImproperlyConfigured here, when the service is built,
rather than at its first call. With no map, or an empty one, nothing but
the row is touched.
The service opens no transaction: dispatch runs it inside one, so a refused
cascade leaves every row in place. It absorbs the rest of the pool and
hands it on, with the call's context it names, as the cascade's
context, so a row's delete_service can see who is calling and,
by naming call_context, the very dict the call's other stages share.
acreate_model¶
acreate_model ¶
acreate_model(
model: type[ModelT],
*,
field_map: dict[str, str] | None = None,
exclude_fields: list[str] | None = None,
m2m: Mapping[str, Any] | Callable[[Any], Mapping[str, Any]] | None = None,
relations: Mapping[str, RelationSpec] | None = None,
) -> Callable[..., Awaitable[ModelT]]
The async twin of create_model.
An async def service over
acreate_from_input,
so adispatch awaits it
rather than bridging it. Every row service its relations declare is an
async def too, which the async helpers check.
aupdate_model¶
aupdate_model ¶
aupdate_model(
model: type[ModelT],
*,
field_map: dict[str, str] | None = None,
exclude_fields: list[str] | None = None,
m2m: Mapping[str, Any] | Callable[[Any], Mapping[str, Any]] | None = None,
update_fields: bool | list[str] = True,
relations: Mapping[str, RelationSpec] | None = None,
) -> Callable[..., Awaitable[ModelT]]
The async twin of update_model.
An async def service over
aupdate_from_input.
adelete_model¶
adelete_model ¶
adelete_model(
model: type[ModelT],
*,
soft_delete: Callable[[ModelT], Awaitable[None]] | None = None,
relations: Mapping[str, RelationSpec] | None = None,
) -> Callable[..., Awaitable[None]]
The async twin of delete_model.
Deletes with await instance.adelete(), after the cascade
adelete_relations
runs by the rule delete_model states. soft_delete is an
async def called with the row in place of adelete().
delete_collection¶
delete_collection ¶
delete_collection(
model: type[ModelT], *, soft_delete: Callable[[Any], None] | None = None
) -> Callable[..., None]
A service that deletes the resolved collection in one bulk delete, and returns None.
Pairs with a spec's collection_selector_spec, whose rows dispatch
seeds into the pool as collection. The service a bulk delete would
otherwise write by hand:
def delete_books(*, collection: QuerySet[Book], **_: Any) -> None:
collection.delete()
collection.delete() is Django's bulk delete, which runs each foreign
key's on_delete and sends pre_delete and post_delete for every
row, as a single row's delete does. An empty collection deletes nothing, so
running the operation twice is harmless. model is not read,
as in delete_model.
soft_delete is called with the collection instead of delete(),
as in lambda rows: rows.update(is_archived=True). The service opens no
transaction, since dispatch runs it inside one, and absorbs the rest of
the pool.
adelete_collection¶
adelete_collection ¶
adelete_collection(
model: type[ModelT], *, soft_delete: Callable[[Any], Awaitable[None]] | None = None
) -> Callable[..., Awaitable[None]]
The async twin of delete_collection.
Deletes with await collection.adelete(). soft_delete is an
async def called with the collection in its place.
Decorators¶
implements ¶
Return the decorated callable unchanged, typed as conforming to proto.
proto is a parameterised service or selector Protocol:
@implements(CreateService[NoteInput, Note])
def create_note(*, data: NoteInput, **extras: Any) -> Note: ...
A type checker reports a callable that does not conform - a keyword the Protocol passes and the callable does not take, a return of the wrong type - at the decorator line, as ty does.
It lives in services/ beside the service Protocols, and decorates a
selector the same way, with
ListSelector
or RetrieveSelector.
Nothing changes at run time: dispatch binds a callable from the keywords it
declares, annotated or not.
Helpers¶
call_service¶
call_service ¶
call_service(
service: Callable[..., ResultT],
*,
request: Request,
data: Any = UNSET,
instance: Any = UNSET,
map_errors: bool = False,
**extras: Any,
) -> ResultT
Invoke service with the framework's kwargs pool.
For a view, middleware, or custom action delegating to a service wired to a
different action: the helper builds the pool the framework would build,
filters it against the service's signature, and dispatches sync-or-async so
the caller need not know which — async services are bridged through
async_to_sync, sync services called inline. Outside HTTP scope (Celery
tasks, management commands) call the service directly with whatever kwargs
you have; this helper is not the right tool there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
service
|
Callable[..., ResultT]
|
The service callable, sync or async. |
required |
request
|
Request
|
Required — the helper is HTTP-scoped by design. |
required |
data
|
Any
|
Passed into the pool when not |
UNSET
|
instance
|
Any
|
Passed into the pool when not |
UNSET
|
map_errors
|
bool
|
Translate a raised
|
False
|
**extras
|
Any
|
Merged into the pool; the signature filter
( |
{}
|
acall_service¶
acall_service
async
¶
acall_service(
service: Callable[..., ResultT] | Callable[..., Awaitable[ResultT]],
*,
request: Request,
data: Any = UNSET,
instance: Any = UNSET,
map_errors: bool = False,
**extras: Any,
) -> ResultT
Invoke service from async code with the framework's kwargs pool.
Same contract as
call_service,
map_errors included. Async services are awaited directly; a sync service is
called inline with no thread hop, so the caller owns any sync-side I/O safety.
**extras never overrides a seed this helper derives, exactly as in the sync
twin: a user key spread in from client-supplied data is dropped rather than
replacing request.user.