Skip to main content

django-service-specs

CI PyPI Python versions Django versions Docs Coverage Ruff License

A service contract for Django: declare an operation's parameters, permission check, validation and output once, and dispatch it from any transport - an HTTP view, an MCP tool, an agent tool, a management command or a background task.

Django is the base and the only dependency. Django REST Framework is not a dependency, and nothing in the package imports it, so a project with business logic and no API framework can hand its operations to an agent, a command or a queue with Django alone. djangorestframework-services will depend on this package and become its DRF adapter; this package will never depend on it.

No agent transport reads a spec without DRF yet. The family's MCP server (djangorestframework-mcp-server) and its Pydantic AI toolset (djangorestframework-pydantic-ai) both require DRF and djangorestframework-services today. What a tool-building transport reads off a spec besides dispatch - affordances, paging, the agent projection of an output, progress - is here, and those transports are planned to read kernel specs through it, with DRF behind an extra, once djangorestframework-services is this package's DRF adapter. Until then a project without DRF can dispatch its specs from its own views, commands, tasks and agent tools, and no ready-made MCP server or agent toolset serves them.

Status: early. The public API may still change between minor releases, and every change is recorded in the changelog.

Install

pip install django-service-specs

The package has no models and needs no entry in INSTALLED_APPS. A principal is a user of django.contrib.auth, and a generic-relation write needs django.contrib.contenttypes. Django 4.2 or later, on Python 3.10 or later.

The pydantic adapter is the one part that needs more, and it comes with an extra:

pip install "django-service-specs[pydantic]"

Quickstart

One write, declared once: what it takes (a dataclass behind a Validator), which row it acts on (an instance selector), who may run it (permissions=[...]), and what it returns (a Presenter). Note is a model with a title and an owner who is a user.

from dataclasses import dataclass
from typing import Any

from django_service_specs import (
    DataclassPresenter,
    DataclassValidator,
    Parameter,
    Parameters,
    PermissionCheck,
    SelectorKind,
    SelectorSpec,
    ServiceSpec,
    dispatch,
    present,
)
from notes.models import Note  # a title, and an owner who is a user


@dataclass
class Rename:  # what the operation takes
    title: str


@dataclass
class NoteOut:  # what it returns
    id: int
    title: str


class IsOwner(PermissionCheck):
    message = "Only the note's owner may rename it."

    def has_permission(self, principal: Any, spec: Any) -> bool:
        return principal.is_authenticated

    def has_object_permission(self, principal: Any, spec: Any, target: Any) -> bool:
        return target.owner_id == principal.pk


def rename_note(*, instance: Note, title: str) -> Note:
    instance.title = title
    instance.save(update_fields=["title"])
    return instance


rename_note_spec = ServiceSpec(
    service=rename_note,
    permissions=[IsOwner()],
    validator=DataclassValidator(Rename),
    instance_selector_spec=SelectorSpec(
        kind=SelectorKind.RETRIEVE,
        selector=lambda *, pk: Note.objects.filter(pk=pk),  # the row, or none
        reads=Parameters.of(Parameter("pk", "integer", required=True)),
    ),
    presenter=DataclassPresenter(NoteOut),
)


def rename(user: Any, arguments: dict[str, Any]) -> Any:
    result = dispatch(rename_note_spec, principal=user, arguments=arguments)
    if result.kind == "not_found":
        return None  # a transport answers this in its own terms: a 404, an exit code
    return present(rename_note_spec, result)

rename(user, {"pk": 1, "title": "Final"}) returns {"id": 1, "title": "Final"} for the note's owner, None when there is no note 1, and raises NotPermitted for anyone else. The same rename_note_spec can be dispatched from an HTTP view, an MCP tool, a management command or a task: each supplies a principal and the arguments, and answers the result in its own terms; over HTTP, SpecView.as_view(spec=rename_note_spec) is that view. This code is docs/examples/quickstart.py, where Note comes from the test suite's own app, and the test suite runs it.

The order dispatch runs in

dispatch() and adispatch() refuse a deactivated principal first, as PrincipalUnavailable, before anything about the call is checked. Then they take the same steps, in the same order, for a write and a read:

  1. Shape check and closed argument set. Every argument against the declared parameters, at every level of nesting, with no query.
  2. Class-level authorization. Every permission check's has_permission, before any row is resolved, so a refused principal learns nothing about which rows exist.
  3. Target resolution. The selector finds the row or rows. A missing row is returned as DispatchResult(kind="not_found"), never raised.
  4. Object-level authorization. has_object_permission on a retrieved row.
  5. Validation. The Validator, with the resolved row in its context, so an update's uniqueness check can exclude the row being updated.
  6. Affordances. A write's declared affordances, in order. The first one the row or the moment does not meet refuses the call with ActionUnavailable, before the run's transaction opens.
  7. The run. The service, inside transaction.atomic() unless the spec says atomic=False, then the output selector if one is declared.

A read stops after step four: its selector is its run. Its affordances are answers about each row, for a list to report, and refuse nothing.

present() renders the result afterwards, and only when the transport asks, so one that hands the row to a template never pays for rendering it.

What it refuses

Refused Raised Who refused
An argument of the wrong type, missing, or not declared InvalidArguments, with every problem in .detail Dispatch
Arguments the Validator rejects InvalidArguments Dispatch
A permission check says no, or a Grant does not cover the call NotPermitted Dispatch
A deactivated principal, or an identifier that names no active user PrincipalUnavailable Dispatch
A business rule, raised by the service ServiceError, ServiceValidationError, ServiceConflict, ServiceNotFound The operation
A declared affordance the row or the moment does not meet ActionUnavailable, a ServiceConflict carrying a stable code Dispatch, before the run
A required row that does not exist Nothing: DispatchResult(kind="not_found") -
An operation that declares no permission check ImproperlyConfigured, at registration and at dispatch Configuration

The first four share the base DispatchError; the next two share ServiceError, an unavailable action included, because the caller adapts to it as it does to a business rule. Neither base subclasses the other. A consumer reads a service refusal as something the caller adapts to - an agent routes around it and keeps going - so a denial declared as one becomes something a model retries. And an MCP server tells arguments of the wrong shape from a business rule on well-shaped ones by exactly this type difference.

An operation that is open to anyone says so with permissions=[Unrestricted()]. Off HTTP there is no view whose policy it could inherit, so leaving the list out is refused rather than read as "no restriction".

Documentation

artui.github.io/django-service-specs:

License

MIT

Metadata

Release files for django-service-specs 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-service-specs 0.4.0
File Size Uploaded
django_service_specs-0.4.0.tar.gz 540.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-service-specs 0.4.0
File Interpreter ABI Platform
django_service_specs-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 798.4 kB

Release files / django_service_specs-0.4.0.tar.gz

Download URL django_service_specs-0.4.0.tar.gz
Size 540.7 kB
Tags Source
SHA-256 checksum
How to use checksums
2c325691a72ffbaf6966cea4c57deb4adc8f237e27cc6aef780765748d7b74c3
BLAKE2b-256 checksum
How to use checksums
502309422153ba5c289e2b6b2a348ffcdeb788a2e6eff1310b0a396c237cfc7a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / django_service_specs-0.4.0-py3-none-any.whl

Download URL django_service_specs-0.4.0-py3-none-any.whl
Size 257.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5288c4425e55de0d12771f3e70da99051903710a6bb9c7d437ac8489f7190a47
BLAKE2b-256 checksum
How to use checksums
706e7d08e5b45849161004a88af0b72bd7466c90b4f40449fe741b568c545976
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

2 release files

0.4.2

2 release files

0.4.1

2 release files

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page