You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* docs(planning): add docs-ux-restructure design + plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: remove base-client reference from project overview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: slim README to a runnable front-door (G1, G3, G4)
Why-httpware hook, one runnable typed quickstart against jsonplaceholder,
and a Documentation links section. Full quickstart/resilience/streaming/
errors/observability detail now lives canonically in docs/index.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add why-httpware hook and a runnable first example (G1, G4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: order Middleware before Resilience in nav
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: link bare architecture/*.md references to GitHub source
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(planning): mark docs-UX findings resolved + index the bundle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ Guidance for AI agents (Claude Code, etc.) working in this repository.
4
4
5
5
## Project Overview
6
6
7
-
`httpware` is a Python HTTP client framework with sync and async clients for building resilient service clients. It supersedes `community-of-python/base-client` and ships under the `modern-python` org. The framework is a thin opinionated wrapper around `httpx2`: it re-exports `httpx2.Request`/`httpx2.Response`, adds a middleware chain, typed response decoding, and a status-keyed exception tree raised automatically on 4xx/5xx.
7
+
`httpware` is a Python HTTP client framework with sync and async clients for building resilient service clients. It ships under the `modern-python` org and is a thin opinionated wrapper around `httpx2`: it re-exports `httpx2.Request`/`httpx2.Response`, adds a middleware chain, typed response decoding, and a status-keyed exception tree raised automatically on 4xx/5xx.
**A Python HTTP client framework with sync and async clients for building resilient service clients.**
16
16
17
-
`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 resilience suite under `httpware.middleware.resilience` — `AsyncRetry`/`Retry` with a `RetryBudget` (a Finagle-style token bucket that caps the global retry rate to prevent retry storms), `AsyncBulkhead`/`Bulkhead` concurrency limiter, `AsyncCircuitBreaker`/`CircuitBreaker` consecutive-failure breaker, and `AsyncTimeout` for overall-operation wall-clock bounds.
17
+
## Why httpware
18
+
19
+
-**Typed errors, no `raise_for_status()`** — 4xx/5xx automatically raise a status-keyed exception tree (`NotFoundError`, `RateLimitedError`, …), all under `httpware.StatusError`.
20
+
-**Typed response bodies** — `response_model=YourType` decodes the body straight to your pydantic or msgspec model; a missing decoder fails fast, *before* the request goes out.
21
+
-**Production resilience as composable middleware** — retry + retry-budget, bulkhead, circuit breaker, and timeout, composed at construction — all over standard `httpx2`.
22
+
23
+
Built on `httpx2`: httpware re-exports `httpx2.Request`/`httpx2.Response` and stays a thin wrapper, not a new HTTP abstraction.
18
24
19
25
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0.
`AsyncClient()` resolves `decoders=None` against installed extras: pydantic if installed (first), msgspec if installed (second), or an empty tuple if neither. Missing extras never raise at construction. Instead, resolution is deferred to the first `response_model=` call — and if no registered decoder claims the model, `MissingDecoderError` fires *before* the HTTP request goes out.
32
-
33
37
## Quickstart
34
38
35
-
**Async usage:**
39
+
A typed GET against a live API (needs `pip install httpware[pydantic]`):
36
40
37
41
```python
38
42
import asyncio
39
43
40
-
from httpware import AsyncClient
41
-
42
-
asyncdefmain() -> None:
43
-
asyncwith AsyncClient(base_url="https://example.test") as client:
44
-
response =await client.get("/users/42")
45
-
print(response.json())
46
-
47
-
asyncio.run(main())
48
-
```
49
-
50
-
**Sync usage:**
51
-
52
-
```python
53
-
from httpware import Client
54
-
55
-
with Client(base_url="https://example.test") as client:
56
-
response = client.get("/users/42")
57
-
print(response.json())
58
-
```
59
-
60
-
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.
61
-
62
-
```python
63
44
from httpware import AsyncClient
64
45
from pydantic import BaseModel
65
46
@@ -70,88 +51,28 @@ class User(BaseModel):
70
51
71
52
72
53
asyncdefmain() -> None:
73
-
asyncwith AsyncClient(base_url="https://api.example.com") as client:
54
+
asyncwith AsyncClient(base_url="https://jsonplaceholder.typicode.com") as client:
74
55
user =await client.get("/users/1", response_model=User)
75
-
print(user.name)
76
-
```
77
-
78
-
### With resilience middleware
79
-
80
-
Compose resilience middleware at construction; `AsyncBulkhead` goes outside `AsyncRetry` so one slot covers all retry attempts.
56
+
print(user.name) # Leanne Graham
81
57
82
-
The sync `Client` accepts identical `middleware=[...]`; swap `AsyncClient` → `Client` and `AsyncRetry` → `Retry` for the sync version.
83
58
84
-
```python
85
-
from httpware import AsyncClient, AsyncBulkhead, AsyncRetry
86
-
87
-
88
-
asyncdefmain() -> None:
89
-
asyncwith AsyncClient(
90
-
base_url="https://api.example.com",
91
-
middleware=[
92
-
AsyncBulkhead(max_concurrent=10), # cap total in-flight
user =await client.get("/users/1", response_model=User)
97
-
```
98
-
99
-
Need a custom middleware (auth, tracing, request-ID propagation, etc.)? See the [Middleware guide](https://httpware.modern-python.org/middleware/).
100
-
101
-
### Streaming responses
102
-
103
-
For large responses or server-sent events, stream the body chunk-by-chunk. `stream()` is an async context manager:
104
-
105
-
```python
106
-
from httpware import AsyncClient
107
-
108
-
109
-
asyncdefmain() -> None:
110
-
asyncwith AsyncClient(base_url="https://api.example.com") as client:
111
-
asyncwith client.stream("GET", "/big-file") as response:
112
-
asyncfor chunk in response.aiter_bytes():
113
-
process(chunk)
114
-
```
115
-
116
-
`stream()` auto-raises `StatusError` subclasses on 4xx/5xx with the response body pre-read, so `exc.response.content` is accessible from the caught exception.
117
-
118
-
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.)
119
-
120
-
## Errors
121
-
122
-
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`, `BulkheadFullError`, and `CircuitOpenError`. Everything inherits `httpware.ClientError`.
123
-
124
-
## Observability
125
-
126
-
All resilience middleware 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.
127
-
128
-
Logger names and event names are the stable public contract: `httpware.retry` (`retry.giving_up`, `retry.budget_refused`, `retry.streaming_refused`), `httpware.bulkhead` (`bulkhead.rejected`), `httpware.circuit_breaker` (`circuit.opened`, `circuit.rejected`, `circuit.half_open`, `circuit.closed`), and `httpware.timeout` (`timeout.exceeded`).
129
-
130
-
```python
131
-
import logging
132
-
133
-
# Enable visibility into resilience operational events
For OTel attribute enrichment on the active span — install the extra:
141
-
142
-
```bash
143
-
pip install httpware[otel]
59
+
asyncio.run(main())
144
60
```
145
61
146
-
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.
62
+
The sync `Client` is identical — swap `AsyncClient` → `Client` and drop the `await` / `async with`. A 4xx/5xx response raises a typed `StatusError`; a malformed body raises `DecodeError`. Both subclass `httpware.ClientError`.
Copy file name to clipboardExpand all lines: docs/index.md
+10-4Lines changed: 10 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,12 @@
2
2
3
3
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` as the public request/response surface, adds a middleware chain (with a built-in resilience suite: `AsyncRetry`/`Retry` + `RetryBudget`, `AsyncBulkhead`/`Bulkhead`), opt-in typed response decoding, and a status-keyed exception tree raised automatically on 4xx/5xx.
4
4
5
+
## Why httpware
6
+
7
+
-**Typed errors, no `raise_for_status()`** — 4xx/5xx automatically raise a status-keyed exception tree (`NotFoundError`, `RateLimitedError`, …), all under `httpware.StatusError`.
8
+
-**Typed response bodies** — `response_model=YourType` decodes the body straight to your pydantic or msgspec model; a missing decoder fails fast, *before* the request goes out.
9
+
-**Production resilience as composable middleware** — retry + retry-budget, bulkhead, circuit breaker, and timeout, composed at construction — all over standard `httpx2`.
10
+
5
11
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0.
6
12
7
13
## Install
@@ -28,8 +34,8 @@ import asyncio
28
34
from httpware import AsyncClient
29
35
30
36
asyncdefmain() -> None:
31
-
asyncwith AsyncClient(base_url="https://example.test") as client:
32
-
response =await client.get("/users/42")
37
+
asyncwith AsyncClient(base_url="https://jsonplaceholder.typicode.com") as client:
38
+
response =await client.get("/users/1")
33
39
print(response.json())
34
40
35
41
asyncio.run(main())
@@ -40,8 +46,8 @@ asyncio.run(main())
40
46
```python
41
47
from httpware import Client
42
48
43
-
with Client(base_url="https://example.test") as client:
44
-
response = client.get("/users/42")
49
+
with Client(base_url="https://jsonplaceholder.typicode.com") as client:
Copy file name to clipboardExpand all lines: docs/middleware.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -110,7 +110,7 @@ The example pairs naturally with the 0.6.0 observability events: a `httpware.ret
110
110
-**Redaction:** Use a `logging.Filter` on the consumer side. `httpware` deliberately does no redaction in-library (per the 0.6.0 observability design).
111
111
-**URL or header validation:**`httpx2` owns it. Don't reimplement.
112
112
-**Per-call behavior that doesn't apply to other calls:** Pass through `request.extensions=` (or the `extensions=` kwarg at the call site) instead. Middleware exists for *cross-cutting* concerns.
113
-
-**HTTP-level span creation for tracing:** Install `opentelemetry-instrumentation-httpx` instead of writing an OTel middleware in httpware. We retired story `5-4` (standalone OTel middleware) for this reason — `opentelemetry-instrumentation-httpx` already covers transport-level tracing, and a separate httpware layer would duplicate it. See `architecture/middleware.md`.
113
+
-**HTTP-level span creation for tracing:** Install `opentelemetry-instrumentation-httpx` instead of writing an OTel middleware in httpware. We retired story `5-4` (standalone OTel middleware) for this reason — `opentelemetry-instrumentation-httpx` already covers transport-level tracing, and a separate httpware layer would duplicate it. See [`architecture/middleware.md`](https://github.com/modern-python/httpware/blob/main/architecture/middleware.md).
114
114
115
115
## Wiring OpenTelemetry
116
116
@@ -198,6 +198,6 @@ Sync and async middleware classes do not interop: a `Middleware` cannot be passe
198
198
199
199
## See also
200
200
201
-
-**`architecture/middleware.md` (Seam A)** — the formal protocol contract and why the chain is frozen at construction.
201
+
-**[`architecture/middleware.md`](https://github.com/modern-python/httpware/blob/main/architecture/middleware.md) (Seam A)** — the formal protocol contract and why the chain is frozen at construction.
202
202
-**`src/httpware/middleware/resilience/`** — `AsyncRetry`, `AsyncBulkhead`, `RetryBudget` as real-world consumers of this exact protocol.
-**`architecture/testing.md`** — the project's own testing patterns (Hypothesis property-based tests, `pytest-asyncio` auto-mode, the `RecordedTransport`-was-removed history).
114
+
-**[`architecture/testing.md`](https://github.com/modern-python/httpware/blob/main/architecture/testing.md)** — the project's own testing patterns (Hypothesis property-based tests, `pytest-asyncio` auto-mode, the `RecordedTransport`-was-removed history).
0 commit comments