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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

action0_client-0.1.0.tar.gz (227.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

action0_client-0.1.0-py3-none-any.whl (62.7 kB view details)

Uploaded Python 3

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

Hashes for action0_client-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a91958c4bc3e8375c8ab0d169508c97b1ed136db5830f1802b1bbada12a81f31
MD5 9d046bc5c29e8ae78b6250bbf3bf004b
BLAKE2b-256 88aef41b92752cea25f6628e6a4b5780f45a2ada986930937a5a1315ee689e17

See more details on using hashes here.

Provenance

The following attestation bundles were made for action0_client-0.1.0.tar.gz:

Publisher: release.yml on LaughInJar/action0-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for action0_client-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 533fe4c0912eaf5fb38a69aa7d0a7cb141a24f5275c7346cb87a279d1ed5c650
MD5 12acb03565241a833ba8e32e80b66289
BLAKE2b-256 afaeef87a77ff54678752fba949d982f49a0f357be1a6a920fc1acf87fae96e5

See more details on using hashes here.

Provenance

The following attestation bundles were made for action0_client-0.1.0-py3-none-any.whl:

Publisher: release.yml on LaughInJar/action0-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page