action0-client

Backend-agnostic, fully typed HTTP API clients: describe your API once — as typed operations — and run it synchronously, on asyncio, on Twisted — or on an execution model of your own — just by plugging in a different backend. Built on action0-req (the request/response representation) and action0-url (the URL representation).

The same typed operation, driven by three of the 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 — the guide shows its definition.)

Eight backends are included — requests, httpx (sync and async), aiohttp, urllib3 and Twisted, each behind an optional extra, plus stdlib-only urllib and thread-pool backends — and the list is open: a backend is one small structural protocol, so writing your own takes two methods, and it drives the same clients and operations.

Two companion projects build on this one: action0-client-openapi generates the typed operations, models and client for a whole API from its OpenAPI schema — readable, checked-in code that depends on action0-client alone; and action0-github-api is a fully typed GitHub REST API client that showcases the patterns on a real API: typed pagination, conditional requests via a hook, a tuned retry policy and streaming release-asset downloads.

uv add "action0-client[httpx]"

Highlights:

  • One Client / APIClient, four execution models: the backend decides whether send() returns a value, an awaitable, a Twisted Deferred or a concurrent.futures.Future — and the type checker knows which, including the per-operation result type (Item, Awaitable[Item], Deferred[Item], Future[Item]).

  • One structural Backend protocol, generic over the execution model’s wrapper type — implement two methods and anything can drive the same clients, including execution models this library has never heard of (Client.send returns whatever your backend’s send returns).

  • Endpoints as typed dataclasses: Operation fixes method and path per class, the field specifiers of action0.client.fields place typed fields into query, headers, path templates or the JSON body.

  • Instrumentation Hooks (logging, metrics, tracing, request decoration) and uniform error translation into one exception family, in every execution model.

  • Batteries for testing API clients without a server: action0.client.testing ships recording stub backends for all three execution models.

  • Fully typed (checked with mypy strict, pyright and ty), Python 3.11+.

The action0 namespace is simply the one the author likes to use for personal projects.