Backends and their configuration

Every built-in backend takes its library’s native client/session/agent, so nothing of the underlying library is hidden from you:

import httpx

from action0.client.backends.httpx import HttpxBackend

# bring your own client (pooling, HTTP/2, proxies, ...) — it stays yours
# and is not closed by the backend
backend = HttpxBackend(httpx.Client(http2=True))

# or let the backend create one; then the backend owns and closes it
backend = HttpxBackend(timeout=10.0, follow_redirects=False)

The counterparts: RequestsBackend (requests.Session), AsyncHttpxBackend (httpx.AsyncClient, async with / aclose()), AiohttpBackend (aiohttp.ClientSession, created lazily on the first send when none is passed), UrllibBackend (a stdlib urllib.request opener — zero dependencies, for simple needs), Urllib3Backend (a urllib3.PoolManager, for projects on urllib3 without requests on top; also takes a retries= policy) and TwistedBackend (twisted.web.client.Agent, plus a reactor= for the timeout clock). All of them accept timeout=, follow_redirects= and hooks=.

Parallel requests from sync code

ThreadPoolBackend (stdlib, no extra) wraps any synchronous backend and runs its sends on a ThreadPoolExecutor — its execution model is concurrent.futures.Future, and the types follow, including through APIClient:

from action0.client import APIClient
from action0.client.backends.futures import ThreadPoolBackend
from action0.client.backends.requests import RequestsBackend

with RequestsBackend() as inner, ThreadPoolBackend(inner) as backend:
    client = APIClient(backend, "https://api.example.com/v1")
    futures = [client.send(GetItem(item_id=item_id)) for item_id in range(100)]
    items = [future.result() for future in futures]  # each one a Future[Item]

Hooks belong on the wrapped backend (they run on the pool threads, around the actual I/O); the wrapper itself stays out of the way.

Streaming response bodies

By default every backend preloads the response body into memory. For large downloads (or endless feeds) most backends take stream=True: send() then returns as soon as the headers arrived, and the body is consumed chunk by chunk over the still-open connection. Streaming has a page of its own.

Greenlet stacks (gevent, eventlet)

Nothing extra is needed for gevent or eventlet: monkey-patching turns the blocking sockets under RequestsBackend and UrllibBackend cooperative, exactly as it does for plain requests/urllib code. From this library’s point of view those stacks are simply synchronous — send() returns the plain Response, typed by the sync overloads; the yielding to other greenlets happens inside the socket layer. (Don’t combine monkey-patching with the asyncio, trio or Twisted backends in the same process — that caveat comes from the greenlet libraries, not from here.)