v0.12.0
Added
-
permission_classesfield onServiceSpecandSelectorSpec. Accepts a
sequence of DRFBasePermissionsubclasses to override the calling view's
class-levelpermission_classesfor the action the spec backs.None
(the default) inherits the view-level permissions; an empty sequence means
"no permissions" explicitly. Honored by_ActionSpecsMixin.get_permissions
(covers all viewset mixins andServiceViewSet/SelectorViewSet), by
MutationFlowMixin.get_permissions(standalone mutation views), by the
selector views' newget_permissionsoverrides, and by the
@service_action/@selector_actiondecorators which forward the value
into DRF's@action(permission_classes=...). Misconfigurations (non-
BasePermissionsubclasses, permission instances rather than classes)
fail fast atas_view()time withImproperlyConfigured. -
input_serializer_contextandoutput_serializer_contextfields on
ServiceSpec, plusoutput_serializer_contextonSelectorSpec. Each
takes aCallable[[ServiceView, Request], Mapping[str, Any]]returning
extra context keys to merge into the serializer'scontext=dict. They
sit at the most-specific layer of the existing resolution chain
(DRF default → directionalget_<direction>_serializer_contexthook →
per-actionget_<action>_<direction>_serializer_contexthook → spec
callable), so the spec wins on overlapping keys. Wired through
dispatch_mutation_for_spec(input + output),selector_action
(output), and a newget_serializer_contextoverride on
_ActionSpecsMixinand the standaloneSelectorListView/
SelectorRetrieveViewso the spec's output context is honored by
ListModelMixin/RetrieveModelMixindispatch. Closes the gap where
the selector list/retrieve mixins previously ignored the per-action
get_<action>_output_serializer_contexthook entirely. -
Per-spec queryset shaping on both
SelectorSpecandServiceSpec.
Four new fields cover the static and dynamic cases:select_related: Sequence[str] | None— forwarded to
qs.select_related(*spec.select_related).prefetch_related: Sequence[str | Prefetch] | None— forwarded to
qs.prefetch_related(*spec.prefetch_related); accepts plain names or
fullPrefetchobjects.annotations: Mapping[str, Any] | None— merged into a single
qs.annotate(**spec.annotations)call.extend_queryset: Callable[[QuerySet, ServiceView, Request], QuerySet] | None— dynamic escape hatch, invoked after the declarative fields
so it always sees the fully statically-shaped queryset. Synchronous
only (no DB I/O — manipulates the lazy expression tree).
On
SelectorSpec, shaping runs insidedispatch_selector_for_spec, so
both list and retrieve flows pick it up. OnServiceSpec, shaping
applies to the QuerySet returned byoutput_selector— a typical
pattern isoutput_selector=lambda result: Model.objects.filter(pk=result.pk)
with the spec declaring the eager-loading. After shaping, a QuerySet
return is materialized via.first()(the matchingdispatch_retrieve_selector
behaviour also added below).Configuring shaping with no
selector(onSelectorSpec) or no
output_selector(onServiceSpec) raisesImproperlyConfiguredat
as_view()time. A non-QuerySet return when shaping is set raises at
request time. -
dispatch_retrieve_selectornow materializes a QuerySet return via
.first(), so retrieve selectors can return a filtered QuerySet and
let the framework apply shaping before pulling the single object.
Backward-compatible — selectors that already returned an instance
continue to work unchanged.
Changed
- The field order on
ServiceSpecandSelectorSpechas been reorganized
to group related fields together: the callable, then the input pipeline
(input_*), then the output pipeline (output_*+ the new shaping
fields), then the cross-cuttingkwargsandpermission_classes.
Backward-compatible for keyword-argument call sites (which is every
documented usage); positional construction must follow the new order.