Skip to content

Commit 28ab9da

Browse files
authored
Merge pull request #30 from modern-python/feat/async-prefix-rename
refactor: rename async middleware with Async*/async_* prefix; drop attempt_timeout
2 parents d254ca2 + 5d023d2 commit 28ab9da

27 files changed

Lines changed: 3844 additions & 404 deletions

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
**Async HTTP client framework for Python.**
99

10-
`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`.
10+
`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`.
1111

1212
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0.
1313
@@ -44,18 +44,18 @@ async def main() -> None:
4444

4545
### With resilience middleware
4646

47-
Compose resilience middleware at construction; `Bulkhead` goes outside `Retry` so one slot covers all retry attempts.
47+
Compose resilience middleware at construction; `AsyncBulkhead` goes outside `AsyncRetry` so one slot covers all retry attempts.
4848

4949
```python
50-
from httpware import AsyncClient, Bulkhead, Retry
50+
from httpware import AsyncClient, AsyncBulkhead, AsyncRetry
5151

5252

5353
async def main() -> None:
5454
async with AsyncClient(
5555
base_url="https://api.example.com",
5656
middleware=[
57-
Bulkhead(max_concurrent=10), # cap total in-flight
58-
Retry(), # default: 3 attempts, full-jitter backoff
57+
AsyncBulkhead(max_concurrent=10), # cap total in-flight
58+
AsyncRetry(), # default: 3 attempts, full-jitter backoff
5959
],
6060
) as client:
6161
user = await client.get("/users/1", response_model=User)
@@ -80,15 +80,15 @@ async def main() -> None:
8080

8181
`stream()` auto-raises `StatusError` subclasses on 4xx/5xx with the response body pre-read, so `exc.response.content` is accessible from the caught exception.
8282

83-
It does NOT pass through the middleware chain: `Retry`, `Bulkhead`, and any custom middleware are bypassed. (Retry separately refuses to retry any request — stream or non-stream — whose body was an async-iterable, since streams can't replay across attempts.)
83+
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.)
8484

8585
## Errors
8686

8787
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`.
8888

8989
## Observability
9090

91-
`Retry` and `Bulkhead` emit operational events via two channels — stdlib `logging` records (always on) and OpenTelemetry span events (when `opentelemetry-api` is installed).
91+
`AsyncRetry` and `AsyncBulkhead` emit operational events via two channels — stdlib `logging` records (always on) and OpenTelemetry span events (when `opentelemetry-api` is installed).
9292

9393
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.
9494

docs/errors.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ For the resilience-specific errors (`RetryBudgetExhaustedError`, `BulkheadFullEr
99
```
1010
ClientError (catch-all for anything httpware raises)
1111
├── TransportError (connection/network/protocol failure pre-response)
12-
│ └── NetworkError (transient — safe to retry; covered by Retry's defaults)
12+
│ └── NetworkError (transient — safe to retry; covered by AsyncRetry's defaults)
1313
├── TimeoutError (also inherits builtins.TimeoutError — except OSError catches it)
1414
├── StatusError (got a response but its status was 4xx/5xx)
1515
│ ├── ClientStatusError (any 4xx — fallback for unknown 4xx codes)
@@ -72,7 +72,7 @@ async def fetch(client: AsyncClient, user_id: int) -> dict | None:
7272
_LOGGER.warning("upstream returned %s for %s", exc.response.status_code, exc.response.request.url)
7373
raise
7474
except NetworkError:
75-
# Transient transport failure. Already retried by the default Retry middleware
75+
# Transient transport failure. Already retried by the default AsyncRetry middleware
7676
# (if installed) when the method was idempotent. Seeing this means retries
7777
# exhausted or the method was non-idempotent.
7878
raise
@@ -128,6 +128,6 @@ except RetryBudgetExhaustedError as exc:
128128

129129
## See also
130130

131-
- **[Resilience reference](resilience.md)**`Retry`, `RetryBudget`, `Bulkhead` parameter tables.
132-
- **[Middleware guide](middleware.md)** — the `@on_error` decorator can translate exceptions into responses.
131+
- **[Resilience reference](resilience.md)**`AsyncRetry`, `RetryBudget`, `AsyncBulkhead` parameter tables.
132+
- **[Middleware guide](middleware.md)** — the `@async_on_error` decorator can translate exceptions into responses.
133133
- **`planning/engineering.md` §4** — the formal exception contract.

docs/index.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# httpware
22

3-
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.
3+
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: `AsyncRetry` + `RetryBudget`, `AsyncBulkhead`), opt-in typed response decoding, and a status-keyed exception tree raised automatically on 4xx/5xx.
44

55
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0.
66
@@ -42,18 +42,18 @@ asyncio.run(main())
4242

4343
### With resilience middleware
4444

45-
Compose resilience middleware at construction; `Bulkhead` goes outside `Retry` so one slot covers all retry attempts.
45+
Compose resilience middleware at construction; `AsyncBulkhead` goes outside `AsyncRetry` so one slot covers all retry attempts.
4646

4747
```python
48-
from httpware import AsyncClient, Bulkhead, Retry
48+
from httpware import AsyncClient, AsyncBulkhead, AsyncRetry
4949

5050

5151
async def main() -> None:
5252
async with AsyncClient(
5353
base_url="https://api.example.com",
5454
middleware=[
55-
Bulkhead(max_concurrent=10), # cap total in-flight
56-
Retry(), # default: 3 attempts, full-jitter backoff
55+
AsyncBulkhead(max_concurrent=10), # cap total in-flight
56+
AsyncRetry(), # default: 3 attempts, full-jitter backoff
5757
],
5858
) as client:
5959
user = await client.get("/users/1", response_model=User)
@@ -76,15 +76,15 @@ async def main() -> None:
7676

7777
`stream()` auto-raises `StatusError` subclasses on 4xx/5xx with the response body pre-read, so `exc.response.content` is accessible from the caught exception.
7878

79-
It does NOT pass through the middleware chain: `Retry`, `Bulkhead`, and any custom middleware are bypassed. (Retry separately refuses to retry any request — stream or non-stream — whose body was an async-iterable, since streams can't replay across attempts.)
79+
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.)
8080

8181
## Errors
8282

8383
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`.
8484

8585
## Observability
8686

87-
`Retry` and `Bulkhead` emit operational events via two channels — stdlib `logging` records (always on) and OpenTelemetry span events (when `opentelemetry-api` is installed).
87+
`AsyncRetry` and `AsyncBulkhead` emit operational events via two channels — stdlib `logging` records (always on) and OpenTelemetry span events (when `opentelemetry-api` is installed).
8888

8989
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.
9090

@@ -106,8 +106,8 @@ When installed, `_emit_event` calls `trace.get_current_span().add_event(name, at
106106

107107
## Where to go next
108108

109-
- **[Resilience reference](resilience.md)** — every parameter on `Retry`, `RetryBudget`, and `Bulkhead`; the retry-rule matrix; Retry-After parsing; budget sharing.
110-
- **[Middleware guide](middleware.md)** — write your own middleware. Covers the Middleware Protocol, the phase decorators, a worked Request-ID propagation example, and OpenTelemetry wiring.
109+
- **[Resilience reference](resilience.md)** — every parameter on `AsyncRetry`, `RetryBudget`, and `AsyncBulkhead`; the retry-rule matrix; Retry-After parsing; budget sharing.
110+
- **[Middleware guide](middleware.md)** — write your own middleware. Covers the AsyncMiddleware Protocol, the phase decorators, a worked Request-ID propagation example, and OpenTelemetry wiring.
111111
- **[Errors reference](errors.md)** — the full exception tree, catching strategies, `exc.response.*` access pattern.
112112
- **[Testing guide](testing.md)** — mock-transport injection pattern for testing code that uses `httpware`.
113113
- **[Recipes](recipes/modern-di.md)** — wiring `AsyncClient` into a `modern-di` container.

0 commit comments

Comments
 (0)