Skip to main content

xtr-service-contracts

What a container drives, not what a service does.

python 3.11+ core dependencies: 0 typed license MIT

Why?

A service's own contract says what it is for — a logger logs, a lock locks, a bus dispatches. This package holds the other kind: what something built by a container must answer to so the container can manage it, whatever it is for.

Keeping the two apart matters. reset() is not a logging idea, so a logging contract that ships it makes every consumer of that contract inherit a concern belonging to the container. Here it is declared once, and any contract or library that needs it depends on a package with no dependencies at all.

That last part is the whole design: this is a leaf, and it should stay one. Every other contract package in the ecosystem is free to depend on it, which only remains true while nothing here can drag anything in.

Install

uv add xtr-service-contracts

Requires Python 3.11+.

ResetInterface

@runtime_checkable
class ResetInterface(Protocol):
    async def reset(self) -> None: ...

A long-running process — a worker consuming messages, a server answering requests — builds its services once and uses them many times. Buffers, caches, accumulated state and generated ids belong to one unit of work rather than to the service holding them. reset() ends that unit, so the next starts clean while configuration survives:

It is awaited, because clearing state is not always a local assignment: a rate limiter's count or a cache's entries may live in another process, and reaching them is I/O. A service whose state is its own awaits nothing and returns at once; one whose state lives elsewhere awaits the trip, and the caller — a worker between messages — is already somewhere it can wait.

reset() fits a unit of work that runs alone — one message, one command — where the service sits idle between units and clearing its state is the whole story. When units overlap, as when one set of services answers concurrent requests, there is no single "between" to reset into: each unit's state belongs to its own execution context and is ended by whatever opened the unit, not by a container calling reset(). A service that must serve overlapping units keeps its per-unit state in context, and reset() stays for the standalone case.

from xtr_service_contracts import ResetInterface


class Buffer:
    def __init__(self, capacity: int) -> None:
        self.capacity = capacity  # configuration — survives
        self.items: list[str] = []  # this unit of work — does not

    async def reset(self) -> None:
        self.items.clear()


assert isinstance(Buffer(10), ResetInterface)

The protocol is structural and @runtime_checkable, so a class that already has a reset() satisfies isinstance as it stands. Being @runtime_checkable costs precision, though: the check confirms a reset attribute is present, not that it is callable, so an object whose reset is a plain value passes and then fails when called. Guard the call rather than trust the check alone:

for service in container.services:
    if callable(getattr(service, "reset", None)):
        await service.reset()

A container is the usual caller: it knows what it built, so it can reset whatever asks for it between units of work, and neither side has to know anything else about the other. Its autoconfiguration keys on inheritance, so a service that means to be reset inherits this interface; a structural reset() alone matches isinstance but the container never sees it.

ContainerInterface

@runtime_checkable
class ContainerInterface(Protocol):
    async def get(self, service: type[T], /, qualifier: Hashable | None = None) -> T: ...
    def has(self, service: type[object], /, qualifier: Hashable | None = None) -> bool: ...
    def get_parameter(self, name: str, /) -> object: ...
    def has_parameter(self, name: str, /) -> bool: ...

What a dependency-injection container answers to.

A service is identified by its type plus an optional qualifier that chooses between several services registered for that same type. get builds asynchronously and raises a LookupError when has() is false; a registered service that cannot be built raises its own error. get_parameter takes a dotted name and raises a LookupError when has_parameter() is false.

ServiceProviderInterface

@runtime_checkable
class ServiceProviderInterface(Protocol[T_co]):
    async def get(self, name: Hashable, /) -> T_co: ...
    def has(self, name: Hashable, /) -> bool: ...
    def provided_services(self) -> Mapping[Hashable, type[object]]: ...

A source of services keyed by name. It knows every name it can answer to and the type each yields, without building any of them; a service is built lazily, on first request.

ServiceCollectionInterface

@runtime_checkable
class ServiceCollectionInterface(ServiceProviderInterface[T_co], Protocol[T_co]):
    def __len__(self) -> int: ...
    def __aiter__(self) -> AsyncIterator[tuple[Hashable, T_co]]: ...

A provider that is also countable and iterable. Iterating yields (name, service) pairs, each built as it is reached, in provided_services() order:

count = len(collection)
async for name, service in collection:
    ...

All three are @runtime_checkable, which checks method presence only — an implementation inherits the protocol explicitly so a type checker verifies its signatures against the contract.

Why providers are not containers

A container is keyed by type (plus an optional qualifier) and carries parameters: it is what an application resolves services from. A provider is keyed by name and carries nothing else: a small, fixed, named collection, the shape a ServiceLocator or a tagged-iterator locator takes. Splitting them keeps each contract about one thing, and lets ContainerInterface speak in types rather than being pinned to string ids.

Who uses it

Package Uses it for
xtr-dependency-injection ContainerInterface, ServiceProviderInterface and ServiceCollectionInterface as its container, locator and tagged-collection contracts, and ResetInterface for kernel.reset
xtr-logging Buffered and fingers-crossed handlers, generated ids, and LoggerFactory.reset() between requests or messages

Development

Developed in the python-xtr monorepo, under packages/xtr-service-contracts; run the commands below from there. The python-xtr-service-contracts repository is a read-only copy, so send issues and pull requests to the monorepo.

uv sync
uv run ruff check . && uv run ruff format --check .
uv run basedpyright
uv run ty check
uv run pytest

License

MIT — see LICENSE.

Metadata

Release files for xtr-service-contracts 4.0.3

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

Source distribution (sdist)

Source distribution for xtr-service-contracts 4.0.3
File Size Uploaded
xtr_service_contracts-4.0.3.tar.gz 9.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for xtr-service-contracts 4.0.3
File Interpreter ABI Platform
xtr_service_contracts-4.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 19.8 kB

Release files / xtr_service_contracts-4.0.3.tar.gz

Download URL xtr_service_contracts-4.0.3.tar.gz
Size 9.2 kB
Tags Source
SHA-256 checksum
How to use checksums
4770f20540d8b84ceaf31939d4aa4ae3dbc5334dec663ff9a3f58ccec4803a85
BLAKE2b-256 checksum
How to use checksums
88d717ad99d8d01ec1ce1df1f9fd1927383df59595feb4356eb2a0fc557f5e6d
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 15, 2026.

Transparency log

Release files / xtr_service_contracts-4.0.3-py3-none-any.whl

Download URL xtr_service_contracts-4.0.3-py3-none-any.whl
Size 10.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b48add72f21eba8826c0f2db2584f960d4bd1087eb6151e4486f02afa98c631f
BLAKE2b-256 checksum
How to use checksums
5b9eee5e558b494519d7ecd2bc655776b4ef6f8a47125737b7393d53d222ee9c
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 15, 2026.

Transparency log

Release history Release notifications | RSS feed

4.1.0

2 release files

4.0.4

2 release files

This release

4.0.3 This release

2 release files

4.0.2

2 release files

3.0.0

2 release files

2.0.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.0.1

2 release files

1.0.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