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]
Structural shape for a create-action service callable.
data carries the validated input from the framework. **extras
absorbs whatever else the framework's kwargs pool delivers — request,
user, and the ServiceSpec.kwargs / get_service_kwargs returns —
without the service having to declare each key. The Protocol types
**extras as Any so services on every major type checker (ty,
mypy, pyright) conform.
Strict-typed extras stay possible on your own function signature: declare
your extras as a TypedDict with NotRequired keys and annotate
**extras: Unpack[YourKw]. Inside the function body, extras["foo"]
is then typed by YourKw. The Protocol no longer carries a third type
argument for the kwargs shape — that cross-check only ever worked under
one minor version of one type checker (ty 0.0.32) and is not portable.
UpdateService ¶
Bases: Protocol[InputT, InstanceT, ResultT]
Structural shape for an update-action service callable.
Receives the resolved instance plus the validated data. Returning
None instructs the framework to render the in-memory instance
(mirroring DRF's UpdateAPIView shape).
See :class:CreateService for the extras-typing notes.
DeleteService ¶
Bases: Protocol[InputT, InstanceT, ResultT]
Structural shape for a delete-action service callable.
Receives the resolved instance. Most delete services return None;
if you need a response body, return a value and configure
ServiceSpec.output_selector_spec with an output_serializer
(and optionally a re-fetch selector).
For delete with payload — when the spec carries an input_serializer
— bind InputT to your input dataclass and declare data on the
service. data is optional in the Protocol (default :data:Ellipsis)
so services that don't read a body can still match the shape by binding
InputT to :class:~rest_framework_services.types.no_input.NoInput.
See :class:CreateService for the extras-typing notes.
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,
children: Mapping[str, ChildSpec] | None = None,
) -> Callable[..., ModelT]
Return a service callable that builds model from validated input.
Equivalent to writing the canonical glue stub by hand::
def create_author(*, data: AuthorIn, **_: Any) -> Author:
return create_from_input(Author, data).instance
field_map and exclude_fields are forwarded to
:func:~rest_framework_services.mutations.create_from_input.
m2m accepts either a static mapping (passed straight through) or a
callable that receives the validated data and returns the mapping —
the common case where M2M values live on the input dataclass / dict
itself::
create_model(
Post,
m2m=lambda data: {"tags": data.tags},
)
children is forwarded to
:func:~rest_framework_services.mutations.create_from_input to write
reverse-FK child collections declaratively (no hand-written service)::
create_model(
Author,
children={"books": ChildSpec(model=Book, fk="author")},
)
The returned closure accepts **kwargs so the framework's kwargs pool
(request, user, URL kwargs, ServiceSpec.kwargs returns) is
absorbed without the service caring — matching the unified
:class:~rest_framework_services.services.CreateService Protocol's
default ExtraT (open extras).
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,
children: Mapping[str, ChildSpec] | None = None,
) -> Callable[..., ModelT]
Return a service callable that updates the resolved instance in place.
Equivalent to::
def update_author(*, instance: Author, data: AuthorIn, **_: Any) -> Author:
return update_from_input(instance, data).instance
model is accepted for symmetry with
:func:create_model / :func:delete_model and to bind ModelT for
the type checker; the instance itself comes from the view's
get_object(). field_map, exclude_fields, m2m,
update_fields, and children are forwarded to
:func:~rest_framework_services.mutations.update_from_input. m2m
accepts either a static mapping or a callable receiving the validated
data (see :func:create_model for the common shape); children
reconciles reverse-FK collections from data[relation] per its
:class:~rest_framework_services.ChildSpec.
delete_model¶
delete_model ¶
delete_model(
model: type[ModelT],
*,
soft_delete: Callable[[ModelT], None] | None = None,
children: Mapping[str, ChildSpec] | None = None,
) -> Callable[..., None]
Return a service callable that deletes the resolved instance.
Equivalent to::
def delete_author(*, instance: Author, **_: Any) -> None:
instance.delete()
model is accepted for symmetry / type binding; the instance comes
from the view's get_object().
soft_delete is an optional hook called instead of
instance.delete() — covers the common archive case::
def _archive(instance: Author) -> None:
instance.is_archived = True
instance.save(update_fields=["is_archived"])
delete_model(Author, soft_delete=_archive)
children declares reverse-FK collections to remove before the
parent goes (grandchildren first), each nullable-FK child unlinked and the
rest deleted — mirroring on_delete=SET_NULL / CASCADE. Use it to
cascade explicitly when the FK can't (a PROTECT relation, or a
soft_delete that Django won't cascade through). The ChildSpec's
write-only fields (match_key / mode / field_map / m2m) are
ignored here; only model / fk / children apply.
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,
children: Mapping[str, ChildSpec] | None = None,
) -> Callable[..., Awaitable[ModelT]]
Async sibling of
:func:~rest_framework_services.services.create_model.
Returns an async def closure that wraps
:func:~rest_framework_services.mutations.acreate_from_input. The
framework's :func:~rest_framework_services.is_async.is_async
detection routes it through the async dispatch path automatically.
children is forwarded for declarative reverse-FK writes (see
:func:~rest_framework_services.services.create_model).
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,
children: Mapping[str, ChildSpec] | None = None,
) -> Callable[..., Awaitable[ModelT]]
Async sibling of
:func:~rest_framework_services.services.update_model.
adelete_model¶
adelete_model ¶
adelete_model(
model: type[ModelT],
*,
soft_delete: Callable[[ModelT], Awaitable[None]] | None = None,
children: Mapping[str, ChildSpec] | None = None,
) -> Callable[..., Awaitable[None]]
Async sibling of
:func:~rest_framework_services.services.delete_model.
Calls await instance.adelete() by default (Django 4.1+; the
package floor is 4.2, so this is always available). soft_delete
is an optional async hook called instead of adelete. children
removes the declared reverse-FK collections first (see
:func:~rest_framework_services.services.delete_model).
delete_collection¶
delete_collection ¶
delete_collection(
model: type[ModelT], *, soft_delete: Callable[[Any], None] | None = None
) -> Callable[..., None]
Return a service that deletes the resolved collection (bulk).
Pairs with :attr:ServiceSpec.collection_selector_spec, which seeds the
target set into the pool as collection. Equivalent to::
def delete_books(*, collection, **_: Any) -> None:
collection.delete()
The default calls collection.delete() — a single queryset bulk delete.
An empty collection is a no-op (deletes nothing), so the action is
idempotent. model is accepted for symmetry / type binding (like
:func:delete_model); the set itself comes from the spec.
soft_delete is an optional hook called with the collection instead
of delete() — e.g. lambda qs: qs.update(is_archived=True).
adelete_collection¶
adelete_collection ¶
adelete_collection(
model: type[ModelT], *, soft_delete: Callable[[Any], Awaitable[None]] | None = None
) -> Callable[..., Awaitable[None]]
Async sibling of
:func:~rest_framework_services.services.delete_collection.
Calls await collection.adelete() by default (Django 4.1+; the package
floor is 4.2). soft_delete is an optional async hook called with the
collection instead.
Decorators¶
implements ¶
Identity decorator: assert fn structurally matches proto.
proto is a parameterised service or selector :class:~typing.Protocol::
@implements(CreateService[AuthorIn, Author])
def create_author(
*,
data: AuthorIn,
**extras: Any,
) -> Author: ...
Strict-typed extras stay on your function: declare a TypedDict with
NotRequired keys (so the function still conforms to a Protocol whose
caller may not supply those keys) and annotate
**extras: Unpack[YourKw]. The Protocol itself does not carry an
extras-shape parameter — see :class:CreateService for the rationale.
Drift between the decorated function and proto is reported at the
decorator line by ty. mypy refuses type[Protocol] arguments (the
type-abstract rule); mypy users either silence that with
# type: ignore[type-abstract] or keep using the legacy
_: CreateService[...] = create_author shim alongside the def.
Returns the function unchanged at runtime.
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.
request is required — the helper is HTTP-scoped by design. user
is derived from request.user (None if the request bypassed
authentication middleware), matching the framework's own pool
construction.
data and instance are passed through when not UNSET;
omitting them mirrors the create / list call shape. Anything else
goes into **extras and merges into the pool — the framework's
standard signature filter (:func:resolve_callable_kwargs) decides
which keys actually reach the service.
map_errors mirrors what the mutation view layer does with a
:class:~rest_framework_services.exceptions.service_error.ServiceError.
By default (False) a raised ServiceError propagates unchanged, so a
caller that forgets to handle it lets the error surface as a 500. Set
True and a ServiceError is translated to the same DRF exception the
framework raises on the normal view path — ServiceValidationError →
ValidationError (400), any other ServiceError → 422 — so DRF's
exception handler renders it as a proper response. Only meaningful because
call_service is HTTP-scoped; a non-HTTP caller wants the raw
ServiceError (hence the False default).
Async services are bridged transparently via async_to_sync;
sync services are called inline.
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 :func:call_service, including map_errors: when
True a raised ServiceError is translated to the matching DRF
exception (validation → 400, else 422); False (the default) propagates
it raw. Async services are awaited directly; sync services are called inline
(no thread hop) — caller is responsible for any sync-side I/O safety.