Resilience-first async HTTP client framework for Python
Project description
httpware
Async HTTP client framework for Python.
httpware is a thin opinionated wrapper around httpx2. It re-exports httpx2.Request/httpx2.Response, adds a middleware chain composed at client construction, supports opt-in typed response decoding (pydantic and msgspec are both extras), and raises a status-keyed exception tree automatically on 4xx/5xx. It also ships a small resilience suite — AsyncRetry middleware with a Finagle-style RetryBudget, plus an AsyncBulkhead concurrency limiter — under httpware.middleware.resilience.
Status: Pre-1.0. Public API is subject to change between minor releases until v1.0.
Install
pip install httpware # core only — no decoder
pip install httpware[pydantic] # + PydanticDecoder (the default-decoder path)
pip install httpware[msgspec] # + MsgspecDecoder
pip install httpware[all] # everything declared above (pydantic, msgspec, otel)
AsyncClient() with no decoder= argument defaults to constructing a PydanticDecoder; that path requires the pydantic extra and raises ImportError at AsyncClient.__init__ if it is missing.
Quickstart
Async usage:
import asyncio
from httpware import AsyncClient
async def main() -> None:
async with AsyncClient(base_url="https://example.test") as client:
response = await client.get("/users/42")
print(response.json())
asyncio.run(main())
Sync usage:
from httpware import Client
with Client(base_url="https://example.test") as client:
response = client.get("/users/42")
print(response.json())
Typed decoding via response_model= works in both worlds — requires pip install httpware[pydantic]:
from httpware import AsyncClient
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
async def main() -> None:
async with AsyncClient(base_url="https://api.example.com") as client:
user = await client.get("/users/1", response_model=User)
print(user.name)
With resilience middleware
Compose resilience middleware at construction; AsyncBulkhead goes outside AsyncRetry so one slot covers all retry attempts.
from httpware import AsyncClient, AsyncBulkhead, AsyncRetry
async def main() -> None:
async with AsyncClient(
base_url="https://api.example.com",
middleware=[
AsyncBulkhead(max_concurrent=10), # cap total in-flight
AsyncRetry(), # default: 3 attempts, full-jitter backoff
],
) as client:
user = await client.get("/users/1", response_model=User)
Need a custom middleware (auth, tracing, request-ID propagation, etc.)? See the Middleware guide.
Streaming responses
For large responses or server-sent events, stream the body chunk-by-chunk. stream() is an async context manager:
from httpware import AsyncClient
async def main() -> None:
async with AsyncClient(base_url="https://api.example.com") as client:
async with client.stream("GET", "/big-file") as response:
async for chunk in response.aiter_bytes():
process(chunk)
stream() auto-raises StatusError subclasses on 4xx/5xx with the response body pre-read, so exc.response.content is accessible from the caught exception.
It does NOT pass through the middleware chain: AsyncRetry, AsyncBulkhead, and any custom middleware are bypassed. (AsyncRetry separately refuses to retry any request — stream or non-stream — whose body was an async-iterable, since streams can't replay across attempts.)
Errors
All 4xx/5xx responses raise typed exceptions automatically: NotFoundError, ServiceUnavailableError, RateLimitedError, etc. — all subclasses of httpware.StatusError. Transport-layer transient failures raise NetworkError; the resilience middleware raise RetryBudgetExhaustedError and BulkheadFullError. Everything inherits httpware.ClientError.
Observability
AsyncRetry and AsyncBulkhead emit operational events via two channels — stdlib logging records (always on) and OpenTelemetry span events (when opentelemetry-api is installed).
Logger names (httpware.retry, httpware.bulkhead) and event names (retry.giving_up, retry.budget_refused, retry.streaming_refused, bulkhead.rejected) are the stable public contract.
import logging
# Enable visibility into retry / bulkhead operational events
logging.getLogger("httpware.retry").setLevel(logging.WARNING)
logging.getLogger("httpware.bulkhead").setLevel(logging.WARNING)
For OTel attribute enrichment on the active span — install the extra:
pip install httpware[otel]
When installed, _emit_event calls trace.get_current_span().add_event(name, attributes=...) automatically. We never create our own spans; for HTTP-level tracing install opentelemetry-instrumentation-httpx separately.
🗒️ Release notes
📦 PyPI
📝 License
Part of modern-python
Browse the full list of templates and libraries in modern-python — see the org profile for the categorized index.
Project details
Release history Release notifications | RSS feed
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 httpware-0.8.0.tar.gz.
File metadata
- Download URL: httpware-0.8.0.tar.gz
- Upload date:
- Size: 18.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb4af08b9a0ad5ded73d92993aa59aa9a5ba0c662990474f44265051df1433c8
|
|
| MD5 |
b922ae27db81c2d3152bb473beccc51f
|
|
| BLAKE2b-256 |
b479afbbb6142ec1eeb9349682e2c54f520b29de691ef35a469056627cb39162
|
File details
Details for the file httpware-0.8.0-py3-none-any.whl.
File metadata
- Download URL: httpware-0.8.0-py3-none-any.whl
- Upload date:
- Size: 25.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15102ac6852421b147b5ed6f22bd24afb37accebd7bcf93da2d0452f1ad3b770
|
|
| MD5 |
05ed4b5946addf06be961ae808742601
|
|
| BLAKE2b-256 |
c822f8c1c29b3f7f9b3ccaa646843d85d94ffc8aa4f3df52874a56b6722e786a
|