Skip to main content

Resilience-first async HTTP client framework for Python

Project description

httpware

Test PyPI version Python versions License: MIT

A Python HTTP client framework with sync and async clients for building resilient service clients.

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/Retry middleware with a Finagle-style RetryBudget, plus an AsyncBulkhead/Bulkhead 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 — handles BaseModel + dataclasses + primitives + generics
pip install httpware[msgspec]       # + MsgspecDecoder — handles Struct + dataclasses + primitives + generics
pip install httpware[pydantic,msgspec]   # both extras — both decoders register; BaseModel routes to pydantic, Struct to msgspec
pip install httpware[all]           # everything declared above (pydantic, msgspec, otel)

AsyncClient() resolves decoders=None against installed extras: pydantic if installed (first), msgspec if installed (second), or an empty tuple if neither. AsyncClient() never raises on missing extras — failure is deferred to the first response_model= call, where MissingDecoderError fires before the HTTP request if no registered decoder claims the model.

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 — install either pip install httpware[pydantic] or pip install httpware[msgspec] (or both; pydantic is tried first when both are present). Decode failures (malformed body, schema mismatch) raise httpware.DecodeError, a ClientError subclass — so except httpware.ClientError covers them alongside transport and status errors.

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.

The sync Client accepts identical middleware=[...]; swap AsyncClientClient and AsyncRetryRetry for the sync version.

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/Retry and AsyncBulkhead/Bulkhead emit operational events via two channels — stdlib logging records (always on) and OpenTelemetry span events (when opentelemetry-api is installed). Event names and payloads are identical across sync and async; dashboards built against one class apply unchanged to the other.

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.

📚 Documentation

🗒️ 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


Download files

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

Source Distribution

httpware-0.9.1.tar.gz (23.1 kB view details)

Uploaded Source

Built Distribution

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

httpware-0.9.1-py3-none-any.whl (30.3 kB view details)

Uploaded Python 3

File details

Details for the file httpware-0.9.1.tar.gz.

File metadata

  • Download URL: httpware-0.9.1.tar.gz
  • Upload date:
  • Size: 23.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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

Hashes for httpware-0.9.1.tar.gz
Algorithm Hash digest
SHA256 437c33d90e6ee0e400b99db12a728b69b83cc6bcb4e65357e69ac770f03746a2
MD5 3456f0d6bc668de7c9124e012ea6dbf1
BLAKE2b-256 c46394808e12726b997e8d54ca558a16aadb62003bc44719ef70051ddeed783e

See more details on using hashes here.

File details

Details for the file httpware-0.9.1-py3-none-any.whl.

File metadata

  • Download URL: httpware-0.9.1-py3-none-any.whl
  • Upload date:
  • Size: 30.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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

Hashes for httpware-0.9.1-py3-none-any.whl
Algorithm Hash digest
SHA256 84229603244e4a8a880afb0361c4ead3caf00280a4a55710f377d9e4a642b805
MD5 04a011c46b8f1dc567243400fd8adee4
BLAKE2b-256 c8cf9b7ece4912f0084a50f38e0c272fdb6ad67dd4233a04cc26f0e25eab765e

See more details on using hashes here.

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