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
Second batch of docs-audit follow-ups (audit: planning/audits/2026-06-13-docs-audit.md).
Invariant enforcement (triage item, option 2 — fix the claim):
Empirically confirmed against ruff --select ALL that the "(CI-enforced) / CI
rejects PRs" heading was overstated. Only print() (T201) and a blanket
# type: ignore (PGH003) are machine-checked; httpx2._ is partial (SLF001 catches
attribute access, not a used private import); the future-import, global-logging,
and # ty:-vs-# type: rules are review-only. Rewrote the heading + intro in
CLAUDE.md and architecture/overview.md to the real split and reframed the
httpx2._ grep as a review check, not a CI gate.
Readability + small-gap findings:
- R3 errors.md: examples used _LOGGER undefined; added import logging +
_LOGGER = logging.getLogger("myapp") and a note.
- G5 errors.md: documented the public STATUS_TO_EXCEPTION mapping (lone
undocumented __all__ export).
- R2 README: glossed Finagle-style RetryBudget (token bucket capping global
retry rate) and PEP 678 note.
- R1 README + resilience.md: de-densified the decoder-resolution sentence and
the respect_retry_after table cell.
Change bundle: planning/changes/active/2026-06-13.05-docs-audit-followups/
mkdocs build --strict and just lint both clean.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Copy file name to clipboardExpand all lines: CLAUDE.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,11 +45,11 @@ uv run ruff format . && uv run ruff check . --fix && uv run ty check
45
45
uv run pytest
46
46
```
47
47
48
-
## Architecture invariants (CI-enforced)
48
+
## Architecture invariants
49
49
50
-
These are non-negotiable. CI rejects PRs that violate them.
50
+
These are non-negotiable, but **most are NOT machine-checked — don't rely on CI to catch a violation.** Enforced by ruff: `print()` (`T201`) and a blanket `# type: ignore` (`PGH003`). Partially: `httpx2._` (ruff `SLF001` catches attribute access, not a *used* private import). Review-only: the future-import and global-logging bans.
51
51
52
-
-**No `httpx2` private API**: `grep -rE 'httpx2\._' src/httpware/`must return zero matches. Public symbols only.
52
+
-**No `httpx2` private API**: `grep -rE 'httpx2\._' src/httpware/`should return zero matches (run in review — not wired into CI). Public symbols only.
53
53
-**No `from __future__ import annotations`**: Python 3.11+ floor; PEP 604/585 syntax is native.
54
54
-**No `print()`**: enforced by ruff.
55
55
-**No global logging config**: no `logging.basicConfig()`, no bare `logging.getLogger()`. Acquire `logging.getLogger("httpware")` or `logging.getLogger(f"httpware.{module}")` only.
Copy file name to clipboardExpand all lines: README.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
@@ -14,7 +14,7 @@
14
14
15
15
**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 Finagle-style `RetryBudget`, `AsyncBulkhead`/`Bulkhead` concurrency limiter, `AsyncCircuitBreaker`/`CircuitBreaker` consecutive-failure breaker, and `AsyncTimeout` for overall-operation wall-clock bounds.
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.
18
18
19
19
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0.
20
20
@@ -28,7 +28,7 @@ pip install httpware[pydantic,msgspec] # both extras — both decoders registe
`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.
31
+
`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.
Copy file name to clipboardExpand all lines: architecture/overview.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,11 +4,11 @@
4
4
5
5
`httpx2` is part of the public surface. Exposing `httpx2.Request`/`httpx2.Response` is the design — `httpware` does not own a full abstraction over the underlying HTTP client.
6
6
7
-
## Architectural invariants (CI-enforced)
7
+
## Architectural invariants
8
8
9
-
These are non-negotiable. CI rejects PRs that violate them. The "why" exists so future contributors can judge edge cases instead of blindly following the rule.
9
+
These are non-negotiable, but **enforcement varies — do not assume CI will catch a violation.** Machine-checked: `print()` (ruff `T201`) and a blanket `# type: ignore` (ruff `PGH003`). Partially checked: the `httpx2._` ban — ruff `SLF001` flags private *attribute* access (`httpx2._foo`) but not a *used* private import (`from httpx2._internal import …`). Review-only: the future-import and global-logging bans, and `# type: ignore[<code>]` vs `# ty: ignore[<code>]`. The "why" exists so future contributors can judge edge cases instead of blindly following the rule.
10
10
11
-
-**No `httpx2._` private API.***Why:* private symbols can change between patch releases. We accept the public-API surface as the contract.
11
+
-**No `httpx2._` private API.***Why:* private symbols can change between patch releases. We accept the public-API surface as the contract.*Check:*`grep -rE 'httpx2\._' src/httpware/` should return zero matches — run in review; it is not wired into CI.
12
12
-**No `from __future__ import annotations`.***Why:* Python 3.11+ floor. PEP 604/585 syntax is native; the future-import would only add noise and inconsistency.
13
13
-**No `print()`.***Why:* ruff-enforced. Libraries log; they do not print to stdout. Stray prints leak into consumer applications.
14
14
-**No global logging config.***Why:*`logging.basicConfig()` from a library mutates the consumer's logging tree. We only acquire `logging.getLogger("httpware")` or namespaced child loggers and let consumers configure handlers.
Copy file name to clipboardExpand all lines: docs/errors.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,9 +50,15 @@ ClientError (catch-all for anything httpware raises)
50
50
51
51
The fallback assumes `400 ≤ status < 600`. Statuses outside that range don't raise (they return the response as-is).
52
52
53
+
The explicit rows above are also exported as the public `STATUS_TO_EXCEPTION` mapping (`Mapping[int, type[StatusError]]`) — `from httpware import STATUS_TO_EXCEPTION` — so you can look up the class for a status code programmatically (e.g. `STATUS_TO_EXCEPTION.get(404)`). The two fallback rows are not in the mapping; they're applied by the raise logic for any unmapped in-range status.
54
+
53
55
## Catching strategies
54
56
57
+
The examples below assume a module logger in your own namespace (not under `httpware.*`): `_LOGGER = logging.getLogger("myapp")`.
Copy file name to clipboardExpand all lines: docs/resilience.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
@@ -21,7 +21,7 @@ from httpware.middleware.resilience import AsyncRetry
21
21
|`max_delay`|`5.0` (s) | Ceiling for backoff. |
22
22
|`retry_status_codes`|`frozenset({408, 429, 502, 503, 504})`| Status codes considered retryable. |
23
23
|`retry_methods`|`frozenset({"GET", "HEAD", "OPTIONS", "PUT", "DELETE"})`| Idempotent methods only by default. POST excluded; pass an explicit frozenset including `"POST"` to retry it. |
24
-
|`respect_retry_after`|`True`| When the response carries a `Retry-After` header on a retryable status, sleep for the header value instead of the jittered backoff. If the header value exceeds `max_delay`, AsyncRetry gives up and re-raises the underlying `StatusError` with a PEP 678 note `httpware: Retry-After (Ns) exceeded max_delay (Ms); giving up`. Set `max_delay` higher (or `respect_retry_after=False`) to opt out. |
24
+
|`respect_retry_after`|`True`| When a retryable response carries a `Retry-After` header, sleep for that value instead of the jittered backoff. If it exceeds `max_delay`, AsyncRetry gives up and re-raises the underlying `StatusError`, attaching an exception note (PEP 678): `httpware: Retry-After (Ns) exceeded max_delay (Ms); giving up`. Opt out with `respect_retry_after=False` or a higher `max_delay`. |
25
25
|`budget`|`RetryBudget()` (default-configured) | The token bucket. Pass a shared `RetryBudget` instance to apply one budget across multiple clients. |
26
26
27
27
For a whole-operation wall-clock bound across all retry attempts, compose `AsyncTimeout` outermost — see [AsyncTimeout](#asynctimeout) below. For a per-request bound, use `httpx2.Timeout` on the client or pass `timeout=` per request.
Copy file name to clipboardExpand all lines: planning/README.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -70,6 +70,7 @@ carry **no** frontmatter — living prose, dated by git.
70
70
71
71
### Active
72
72
73
+
-**[docs-audit-followups](changes/active/2026-06-13.05-docs-audit-followups/change.md)** (draft, 2026-06-13) — Second batch from the [docs audit](audits/2026-06-13-docs-audit.md): fix the overstated invariant-enforcement claims in `CLAUDE.md` + `architecture/overview.md` (only `print()`/blanket-`type: ignore` are machine-checked), plus readability findings R1–R3 and documenting the public `STATUS_TO_EXCEPTION` (G5).
73
74
-**[docs-accuracy-fixes](changes/active/2026-06-13.04-docs-accuracy-fixes/change.md)** (draft, 2026-06-13) — Fix the 5 verified factual errors from the [docs audit](audits/2026-06-13-docs-audit.md): RetryBudget formula, modern-di 2.x recipe, contributing-doc CI/grep claim, `just lint` comment, middleware stable-contracts list (+ AsyncTimeout non-finite wording).
0 commit comments