From 665c0e04b3f0145e53871443392bd80fbadd9299 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 5 Jun 2026 15:25:15 +0300 Subject: [PATCH 1/5] docs(readme): sync with shipped 0.4 features - Drop the '0.3.0' version from the status line (pyproject is source of truth) - Drop the now-false 'retry / timeout / bulkhead not yet shipped' claim - Mention the resilience suite (Retry + RetryBudget, Bulkhead) in the project description - Add a 'With resilience middleware' Quickstart subsection showing the recommended [Bulkhead, Retry] ordering - Add an 'Errors' section so users know StatusError / NetworkError / RetryBudgetExhaustedError / BulkheadFullError exist - Replace the dead readthedocs.io link with the GH Releases page --- README.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3dfbfbe..5df1d34 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) From a975d1239d569a901a7843a17de784bee792d259 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 5 Jun 2026 15:29:39 +0300 Subject: [PATCH 2/5] docs(index): sync with shipped 0.4 features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop '0.1.0 alpha' from the status line - Rewrite the project description (pre-v0.2 wording claimed 'owns the abstraction layer / consumers never import the transport' โ€” both walked back in v0.2) - Add the pydantic extra to the install block (was added in 0.3.0) - Add a 'With resilience middleware' subsection mirroring the README - Add an 'Errors' section - Fix the dead 'Engineering Notes' link (target file doesn't exist in docs/ tree; point to GH URL of planning/engineering.md instead) - Fix the now-incorrect 'five protocol seams' claim (v0.2 collapsed to three) - Add a Release notes link --- docs/index.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 2e0a51b..70f0e0f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 @@ -13,6 +13,7 @@ pip install httpware Optional extras: ```bash +pip install httpware[pydantic] # PydanticDecoder (the default decoder path) pip install httpware[msgspec] # MsgspecDecoder ``` @@ -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` From 2ffd8fe3f4819e70cbe487f8fbf12fa99d7305e8 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 5 Jun 2026 15:31:59 +0300 Subject: [PATCH 3/5] docs(contributing): drop retired invariant + add ty-suppression rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove 'No import httpx2 outside transports/httpx2.py' โ€” retired in the v0.2 thin-wrapper pivot; transports/ directory no longer exists. Continuing to list this rule misleads contributors. - Add 'Type suppressions use # ty: ignore[]' โ€” current CI-enforced invariant that was missing from this list. --- docs/dev/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index af4029e..5fc9ca5 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -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[]`, never `# type: ignore` or `# mypy: ignore`. ## Code of Conduct From 91e7ca7145e386996b9b2d9aaf7e2009e133cc9e Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 5 Jun 2026 15:33:50 +0300 Subject: [PATCH 4/5] docs(mkdocs): drop dead nav entry for dev/engineering.md The file doesn't exist (the live engineering reference is planning/engineering.md). mkdocs build --strict was emitting a 'file referenced in nav not found' warning. Drop the broken entry; docs/index.md now links to the GH URL of planning/engineering.md. site_url left as-is (fictional RTD URL is unrelated project hygiene to be addressed separately). --- mkdocs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 0cc2299..cf0d0aa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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: From 153124af71deb763ada3009247ef8a0acb707003 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 5 Jun 2026 15:35:33 +0300 Subject: [PATCH 5/5] =?UTF-8?q?docs(engineering):=20=C2=A71=20resilience?= =?UTF-8?q?=20suite=20+=20=C2=A75=20module-layout=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ยง1: append a sentence noting the resilience suite ships in 0.4 - ยง5: add middleware/resilience/ subpackage (bulkhead, budget, retry, _backoff) to the module tree; extend errors.py comment to mention NetworkError + RetryBudgetExhaustedError + BulkheadFullError --- planning/engineering.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/planning/engineering.md b/planning/engineering.md index bece5d2..444da81 100644 --- a/planning/engineering.md +++ b/planning/engineering.md @@ -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. @@ -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)