Skip to main content

Action0-Client

CI PyPI

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)

Source distribution for action0-client 0.1.0
File Size Uploaded
action0_client-0.1.0.tar.gz 227.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for action0-client 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

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