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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file action0_client-0.1.0.tar.gz.
File metadata
- Download URL: action0_client-0.1.0.tar.gz
- Upload date:
- Size: 227.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a91958c4bc3e8375c8ab0d169508c97b1ed136db5830f1802b1bbada12a81f31
|
|
| MD5 |
9d046bc5c29e8ae78b6250bbf3bf004b
|
|
| BLAKE2b-256 |
88aef41b92752cea25f6628e6a4b5780f45a2ada986930937a5a1315ee689e17
|
Provenance
The following attestation bundles were made for action0_client-0.1.0.tar.gz:
Publisher:
release.yml on LaughInJar/action0-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
action0_client-0.1.0.tar.gz -
Subject digest:
a91958c4bc3e8375c8ab0d169508c97b1ed136db5830f1802b1bbada12a81f31 - Sigstore transparency entry: 2380490205
- Sigstore integration time:
-
Permalink:
LaughInJar/action0-client@69d206c6ef929f5c632f515198ac2e8fbb36a361 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/LaughInJar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@69d206c6ef929f5c632f515198ac2e8fbb36a361 -
Trigger Event:
push
-
Statement type:
File details
Details for the file action0_client-0.1.0-py3-none-any.whl.
File metadata
- Download URL: action0_client-0.1.0-py3-none-any.whl
- Upload date:
- Size: 62.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
533fe4c0912eaf5fb38a69aa7d0a7cb141a24f5275c7346cb87a279d1ed5c650
|
|
| MD5 |
12acb03565241a833ba8e32e80b66289
|
|
| BLAKE2b-256 |
afaeef87a77ff54678752fba949d982f49a0f357be1a6a920fc1acf87fae96e5
|
Provenance
The following attestation bundles were made for action0_client-0.1.0-py3-none-any.whl:
Publisher:
release.yml on LaughInJar/action0-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
action0_client-0.1.0-py3-none-any.whl -
Subject digest:
533fe4c0912eaf5fb38a69aa7d0a7cb141a24f5275c7346cb87a279d1ed5c650 - Sigstore transparency entry: 2380490314
- Sigstore integration time:
-
Permalink:
LaughInJar/action0-client@69d206c6ef929f5c632f515198ac2e8fbb36a361 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/LaughInJar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@69d206c6ef929f5c632f515198ac2e8fbb36a361 -
Trigger Event:
push
-
Statement type: