Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

**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.
`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 — `Retry` middleware with a Finagle-style `RetryBudget`, plus a `Bulkhead` concurrency limiter — under `httpware.middleware.resilience`.

> **Status:** Pre-1.0 (0.3.0). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped.
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0. Streaming and observability are not yet shipped.

## Install

Expand Down Expand Up @@ -42,7 +42,30 @@ async def main() -> None:
print(user.name)
```

## 📚 [Documentation](https://httpware.readthedocs.io)
### With resilience middleware

Compose resilience middleware at construction; `Bulkhead` goes outside `Retry` so one slot covers all retry attempts.

```python
from httpware import AsyncClient, Bulkhead, Retry


async def main() -> None:
async with AsyncClient(
base_url="https://api.example.com",
middleware=[
Bulkhead(max_concurrent=10), # cap total in-flight
Retry(), # default: 3 attempts, full-jitter backoff
],
) as client:
user = await client.get("/users/1", response_model=User)
```

## 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`.

## 🗒️ [Release notes](https://github.com/modern-python/httpware/releases)

## 📦 [PyPI](https://pypi.org/project/httpware)

Expand Down
2 changes: 1 addition & 1 deletion docs/dev/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ just test # pytest with coverage

These are enforced by CI grep gates. Do not break them in pull requests:

- No `import httpx2` outside `src/httpware/transports/httpx2.py`.
- No `httpx2._*` (private API) usage anywhere in the library.
- No `from __future__ import annotations`.
- No `print()` calls.
- No `logging.basicConfig()` or bare `logging.getLogger()`.
- Type suppressions use `# ty: ignore[<rule>]`, never `# type: ignore` or `# mypy: ignore`.

## Code of Conduct

Expand Down
31 changes: 28 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# httpware

A Python async HTTP client framework for building resilient service clients. `httpware` owns the abstraction layer above the underlying HTTP client (`httpx2` by default); consumers never import the transport directly.
A Python async HTTP client framework for building resilient service clients. `httpware` is a thin opinionated wrapper around `httpx2` — it re-exports `httpx2.Request`/`httpx2.Response` as the public request/response surface, adds a middleware chain (with a built-in resilience suite: `Retry` + `RetryBudget`, `Bulkhead`), opt-in typed response decoding, and a status-keyed exception tree raised automatically on 4xx/5xx.

> **Status:** Pre-1.0 (0.1.0 alpha). Public API is subject to change between minor releases until v1.0.
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0. Streaming and observability are not yet shipped.

## Install

Expand All @@ -13,6 +13,7 @@ pip install httpware
Optional extras:

```bash
pip install httpware[pydantic] # PydanticDecoder (the default decoder path)
pip install httpware[msgspec] # MsgspecDecoder
```

Expand All @@ -39,10 +40,34 @@ async def main() -> None:
asyncio.run(main())
```

### With resilience middleware

Compose resilience middleware at construction; `Bulkhead` goes outside `Retry` so one slot covers all retry attempts.

```python
from httpware import AsyncClient, Bulkhead, Retry


async def main() -> None:
async with AsyncClient(
base_url="https://api.example.com",
middleware=[
Bulkhead(max_concurrent=10), # cap total in-flight
Retry(), # default: 3 attempts, full-jitter backoff
],
) as client:
user = await client.get("/users/1", response_model=User)
```

## 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`.

## Where to go next

- **[Engineering Notes](dev/engineering.md)** — design invariants, the five protocol seams, exception contract, module layout, testing patterns, optional-extras pattern.
- **[Engineering Notes](https://github.com/modern-python/httpware/blob/main/planning/engineering.md)** — design invariants, the three protocol seams, exception contract, module layout, testing patterns, optional-extras pattern. Lives in the repo at `planning/engineering.md`.
- **[Contributing](dev/contributing.md)** — setup, conventions, workflow.
- **[Release notes](https://github.com/modern-python/httpware/releases)** — per-version changelogs.

## Part of `modern-python`

Expand Down
1 change: 0 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ edit_uri: edit/main/docs/
nav:
- Quick-Start: index.md
- Development:
- Engineering Notes: dev/engineering.md
- Contributing: dev/contributing.md

theme:
Expand Down
12 changes: 9 additions & 3 deletions planning/engineering.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This doc is the single distilled reference for `httpware` design rationale, prot

## 1. Project intent

`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request` and `httpx2.Response` as the public request/response surface and adds three things on top: typed response decoding (via a `ResponseDecoder` protocol; pydantic and msgspec are both opt-in extras as of 0.3.0), a middleware chain composed at client construction, and a status-keyed exception tree raised automatically on 4xx and 5xx. `AsyncClient(decoder=None)` defaults to constructing a `PydanticDecoder` and so requires the `pydantic` extra; callers can supply an explicit `decoder=` argument to escape the default.
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request` and `httpx2.Response` as the public request/response surface and adds three things on top: typed response decoding (via a `ResponseDecoder` protocol; pydantic and msgspec are both opt-in extras as of 0.3.0), a middleware chain composed at client construction, and a status-keyed exception tree raised automatically on 4xx and 5xx. `AsyncClient(decoder=None)` defaults to constructing a `PydanticDecoder` and so requires the `pydantic` extra; callers can supply an explicit `decoder=` argument to escape the default. As of 0.4.0, the package ships a small resilience suite under `httpware.middleware.resilience` — a `Retry` middleware with a Finagle-style `RetryBudget`, plus a `Bulkhead` concurrency limiter — composed via the standard middleware chain.

The 0.1.0 release attempted to own a full abstraction over the underlying HTTP client. v0.2 walks that back: `httpx2` is part of the public surface.

Expand Down Expand Up @@ -70,10 +70,16 @@ src/httpware/
├── __init__.py # public exports
├── py.typed
├── client.py # AsyncClient
├── errors.py # status-keyed exception tree (response: httpx2.Response)
├── errors.py # status-keyed exception tree + NetworkError + RetryBudgetExhaustedError + BulkheadFullError
├── middleware/
│ ├── __init__.py # Middleware protocol, Next type, @before_request/@after_response/@on_error
│ └── chain.py # compose(middleware, terminal) -> Next
│ ├── chain.py # compose(middleware, terminal) -> Next
│ └── resilience/
│ ├── __init__.py # re-exports Bulkhead, Retry, RetryBudget
│ ├── bulkhead.py # Bulkhead middleware (concurrency limiter)
│ ├── budget.py # RetryBudget (Finagle-style token bucket)
│ ├── retry.py # Retry middleware
│ └── _backoff.py # full-jitter exponential backoff helper (private)
├── decoders/
│ ├── __init__.py # ResponseDecoder protocol
│ ├── pydantic.py # PydanticDecoder (extra: pydantic)
Expand Down
Loading