Action0-Client
Backend-agnostic, fully typed HTTP API clients: describe your API once —
as typed operations — and run it synchronously, on asyncio or on Twisted,
just by plugging in a different backend. The type checker follows along:
the same send() returns a value, an Awaitable or a Deferred,
depending on the backend. Built on
action0-req
(request/response representation) and
action0-url (URL
representation).
The same typed operation, driven by three different backends:
client = APIClient(RequestsBackend(), "https://api.example.com/v1")
item = client.send(GetItem(item_id=42)) # Item
client = APIClient(AsyncHttpxBackend(), "https://api.example.com/v1")
item = await client.send(GetItem(item_id=42)) # Awaitable[Item]
client = APIClient(TwistedBackend(), "https://api.example.com/v1")
deferred = client.send(GetItem(item_id=42)) # Deferred[Item]
(GetItem is an ordinary typed operation class, written once — see
Usage for its definition.)
Requires Python 3.11 or newer.
Full documentation including the API reference: https://laughinjar.github.io/action0-client/
Status: the core API is complete — the backend protocol with
requests/httpx/Twisted implementations and instrumentation hooks, the raw
Client, the typed Operation/JsonOperation/APIClient layer, and the
stub backends for testing.
Usage
Raw requests, any execution model
A backend implements one structural protocol, Backend[W], generic over
what its send wraps the response in (W is Response,
Awaitable[Response], Deferred[Response], Future[Response], ...);
Client sends
action0-req Requests
through it, and the return type of send() follows the backend:
from action0.client import Client
from action0.client.backends.requests import RequestsBackend
from action0.req import Request
with RequestsBackend() as backend:
response = Client(backend).send(Request("https://example.com/")) # Response
from action0.client.backends.httpx import AsyncHttpxBackend
async with AsyncHttpxBackend() as backend:
response = await Client(backend).send(Request("https://example.com/"))
from action0.client.backends.twisted import TwistedBackend
deferred = Client(TwistedBackend()).send(Request("https://example.com/"))
deferred.addCallback(lambda response: print(response.status)) # Deferred[Response]
Typed APIs: operations
Endpoints are dataclasses: HTTP method and path template are fixed on the
class, the variable parts are typed fields placed via specifiers
(query, header, path_param, json_field, json_body, form_field,
body), and
the generic parameter is the parsed result type:
from dataclasses import dataclass
from typing import Any
from action0.client import APIClient, JsonOperation, path_param, query
from action0.req import Method
@dataclass
class Item:
id: int
name: str
class GetItem(JsonOperation[Item]):
method = Method.GET
path = "/items/{item_id}"
item_id: int = path_param()
expand: bool | None = query(default=None) # None = not sent
def load_json(self, data: Any) -> Item:
return Item(id=data["id"], name=data["name"])
APIClient binds backend + base URL + default headers and runs the whole
pipeline — request building, send, status check, parsing — with the
result type following operation and backend (checked by mypy strict,
pyright and ty):
client = APIClient(RequestsBackend(), "https://api.example.com/v1")
item = client.send(GetItem(item_id=42)) # Item
client = APIClient(AsyncHttpxBackend(), "https://api.example.com/v1")
item = await client.send(GetItem(item_id=42)) # Awaitable[Item]
client = APIClient(TwistedBackend(), "https://api.example.com/v1")
deferred = client.send(GetItem(item_id=42)) # Deferred[Item]
Transport problems surface uniformly as TransportError/TimeoutError,
API-level problems (unexpected status, malformed payload) as APIError —
regardless of the HTTP library underneath.
Instrumentation and testing
Backends run Hooks (logging, metrics, tracing, request decoration)
around every send — the bundled LoggingHook logs redacted requests and
responses with timings. action0.client.testing ships recording stub
backends for all three execution models, so API clients are testable
without a server:
from action0.client.testing import StubBackend
from action0.req import Response
backend = StubBackend(Response(200, body='{"id": 42, "name": "Thing"}'))
client = APIClient(backend, "https://api.example.com/v1")
assert client.send(GetItem(item_id=42)) == Item(id=42, name="Thing")
assert backend.requests[0].url.path == "/v1/items/42"
A complete example client (models, operations, auth, all three execution models, runnable demo) lives in examples/petstore.py.
Installation
Install from PyPI. The HTTP
libraries are optional extras — pick what you need (requests, httpx,
aiohttp, urllib3, twisted, all); the stdlib urllib and thread-pool backends
work without any extra:
uv add "action0-client[httpx]"
Development
The project is managed with uv; uv run
creates and syncs the virtual environment automatically (the dev group
includes all backend libraries):
uv run pytest # run the tests (incl. the docstring examples as doctests)
uv run ruff check # lint
uv run ruff format # format
uv run mypy # type-check (also: uv run pyright, uv run ty check)
# build the docs (Sphinx; deployed to GitHub Pages on push to main)
uv run --group docs sphinx-build -W docs docs/_build/html
Releasing
The version lives only in src/action0/client/__init__.py
(__version__). To release: bump it, merge to main, then tag the
release commit and push the tag — the release workflow re-runs all
checks, verifies the tag matches __version__, builds sdist + wheel and
publishes to PyPI via trusted publishing:
git tag v0.1.0
git push origin v0.1.0
AI-assisted development
In the spirit of transparency: most of this project's code, tests and
documentation are written by Claude Code,
Anthropic's coding agent — under human direction and review. The designs
are specified, discussed and iterated by a human, and every change is
reviewed before it lands in main or in a release. AI-authored commits
carry a Co-Authored-By: Claude ... trailer.
About action0
This is just the namespace I like to use for my personal projects. I quite like namespaces.
Release files for action0-client 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| action0_client-0.1.0.tar.gz | 227.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| action0_client-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 290.3 kB
Release files / action0_client-0.1.0.tar.gz
| Download URL | action0_client-0.1.0.tar.gz |
|---|---|
| Size | 227.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a91958c4bc3e8375c8ab0d169508c97b1ed136db5830f1802b1bbada12a81f31
|
|
BLAKE2b-256 checksum How to use checksums |
88aef41b92752cea25f6628e6a4b5780f45a2ada986930937a5a1315ee689e17
|
| 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 Aug 8, 2026.
Transparency logRelease files / action0_client-0.1.0-py3-none-any.whl
| Download URL | action0_client-0.1.0-py3-none-any.whl |
|---|---|
| Size | 62.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
533fe4c0912eaf5fb38a69aa7d0a7cb141a24f5275c7346cb87a279d1ed5c650
|
|
BLAKE2b-256 checksum How to use checksums |
afaeef87a77ff54678752fba949d982f49a0f357be1a6a920fc1acf87fae96e5
|
| 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 Aug 8, 2026.
Transparency log