Skip to content

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,
) -> 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},
)

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,
) -> 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, and update_fields 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).

delete_model

delete_model

delete_model(
    model: type[ModelT], *, soft_delete: Callable[[ModelT], None] | 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)

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,
) -> 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.

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,
) -> 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,
) -> 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.

Decorators

implements

implements(proto: type[F]) -> Callable[[F], F]

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,
    **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.

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,
    **extras: Any,
) -> ResultT

Invoke service from async code with the framework's kwargs pool.

Same contract as :func:call_service. Async services are awaited directly; sync services are called inline (no thread hop) — caller is responsible for any sync-side I/O safety.