Skip to content

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 19 May 16:22
· 31 commits to main since this release
a75fed0

Added

  • Default model service factories — six new top-level exports that
    return ready-made service callables for the common case where the
    entire body is a one-line wrapper over the mutation helpers:
    create_model(Model, *, field_map=, exclude_fields=, m2m=),
    update_model(Model, *, field_map=, exclude_fields=, m2m=, update_fields=), and delete_model(Model, *, soft_delete=), plus
    the matching acreate_model / aupdate_model / adelete_model
    async variants that wrap acreate_from_input / aupdate_from_input
    / await instance.adelete(). m2m accepts either a static mapping
    or a callable receiving the validated data — the typical shape
    when M2M values live on the input itself. The returned callables
    conform to the unified CreateService / UpdateService /
    DeleteService Protocols, so they absorb arbitrary framework-pool
    keys (request, user, URL kwargs, ServiceSpec.kwargs returns)
    and the existing view layer routes them — sync or async — without
    changes.

Removed

  • The pre-merge StrictCreateService / StrictUpdateService /
    StrictDeleteService / StrictListSelector / StrictRetrieveSelector
    / StrictOutputSelector classes. Rename every import to its unified
    equivalent (StrictCreateServiceCreateService, etc.) and drop
    the trailing ExtraT type argument from each call site (extras are
    now typed on the function signature instead — see Changed below).
    The @implements(...) decorator pattern keeps working unchanged
    once the names update.
  • NoKwargs (the empty TypedDict previously used as the ExtraT
    slot of strict service Protocols). The slot no longer exists. Drop
    imports of NoKwargs; if you were also writing
    **extras: Unpack[NoKwargs] in a service body, replace it with
    **extras: Any.

Changed

  • ChangeResult is now generic over the model type. The four mutation
    helpers (create_from_input, acreate_from_input,
    update_from_input, aupdate_from_input) thread the model TypeVar
    through their signatures, so create_from_input(Author, ...) returns
    a ChangeResult[Author] whose .instance is typed as Author
    removing the cast(Author, result.instance) boilerplate that used to
    be necessary. Bare ChangeResult (no parameter) keeps resolving to
    ChangeResult[Model], so existing annotations continue to work
    unchanged. Runtime behaviour is identical; this is a typing-only
    change.

  • Breaking (typing only): the lenient and strict service / selector
    Protocols are merged into a single shape per kind, with **extras
    typed as Any. The strict form's trailing ExtraT type argument is
    gone. Names and call sites:

    • CreateService[InputT, ResultT].
    • UpdateService[InputT, InstanceT, ResultT].
    • DeleteService[InputT, InstanceT, ResultT].
    • ListSelector[ResultT].
    • RetrieveSelector[ResultT].
    • OutputSelector[InT, OutT].

    Strict-typed extras stay possible on your own function signature:
    declare a TypedDict with total=False (or per-field NotRequired)
    and annotate **extras: Unpack[YourKw]. Inside the function body,
    extras["foo"] is typed by YourKw. The Protocol does not enforce
    a kwargs-shape match — that cross-check only worked under one minor
    version of one type checker (ty 0.0.32) and never under mypy or
    pyright. Putting the typing on the function instead works on every
    modern checker.

    Three migration notes:

    1. Drop the trailing ExtraT from every parameterised call site:
      StrictCreateService[AuthorIn, MyKw, Author]
      CreateService[AuthorIn, Author]. Keep **extras: Unpack[MyKw]
      on your function for typed extras.
    2. Strict extras TypedDicts must declare keys as NotRequired
      (or set total=False on the class) — required keys would make
      the function reject callers that omit them, breaking Protocol
      conformance under PEP 692.
    3. The lenient Protocols no longer name request and user as
      fixed parameters — they flow through **extras like any other
      framework-pool key (matching the strict Protocols, which already
      dropped these in 0.9.0). Services that declared
      def fn(*, data, request, user, **kwargs) keep working at
      runtime; to satisfy the new Protocol annotation either drop the
      named request / user parameters and read them off **extras,
      or subclass HttpExtras[YourUser] (now total=False) and use
      it as the Unpack target.

    Runtime behaviour of every callable is unchanged. The merge unblocks
    the default model service factories above and restores Python 3.10+
    support (the previous design needed PEP 728's extra_items=Any,
    which is not in mypy yet and forced a 3.13 floor).

  • HttpExtras[UserT] is now declared total=False: every key is
    optional, matching the framework's runtime contract (the kwargs
    pool may or may not contain request / user depending on the
    caller). Subclass with total=False (or annotate fields as
    NotRequired) for the same reason — see migration note 2 above.

  • Version is now tracked in a single source of truth at
    rest_framework_services/version.py. pyproject.toml declares
    dynamic = ["version"] and hatchling reads the value from version.py
    at build time. rest_framework_services.__version__ continues to
    re-export the same value. Pure refactor — no behaviour change.