Skip to content

Commit a132c1e

Browse files
lesnik512claude
andauthored
docs: correct invariant-enforcement claims + readability follow-ups (#58)
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]>
1 parent f203821 commit a132c1e

8 files changed

Lines changed: 118 additions & 23 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ uv run ruff format . && uv run ruff check . --fix && uv run ty check
4545
uv run pytest
4646
```
4747

48-
## Architecture invariants (CI-enforced)
48+
## Architecture invariants
4949

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.
5151

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.
5353
- **No `from __future__ import annotations`**: Python 3.11+ floor; PEP 604/585 syntax is native.
5454
- **No `print()`**: enforced by ruff.
5555
- **No global logging config**: no `logging.basicConfig()`, no bare `logging.getLogger()`. Acquire `logging.getLogger("httpware")` or `logging.getLogger(f"httpware.{module}")` only.

README.md

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

1515
**A Python HTTP client framework with sync and async clients for building resilient service clients.**
1616

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.
1818

1919
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0.
2020
@@ -28,7 +28,7 @@ pip install httpware[pydantic,msgspec] # both extras — both decoders registe
2828
pip install httpware[all] # everything declared above (pydantic, msgspec, otel)
2929
```
3030

31-
`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.
3232

3333
## Quickstart
3434

architecture/overview.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
`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.
66

7-
## Architectural invariants (CI-enforced)
7+
## Architectural invariants
88

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.
1010

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.
1212
- **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.
1313
- **No `print()`.** *Why:* ruff-enforced. Libraries log; they do not print to stdout. Stray prints leak into consumer applications.
1414
- **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.

docs/errors.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,15 @@ ClientError (catch-all for anything httpware raises)
5050

5151
The fallback assumes `400 ≤ status < 600`. Statuses outside that range don't raise (they return the response as-is).
5252

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+
5355
## Catching strategies
5456

57+
The examples below assume a module logger in your own namespace (not under `httpware.*`): `_LOGGER = logging.getLogger("myapp")`.
58+
5559
```python
60+
import logging
61+
5662
from httpware import (
5763
AsyncClient,
5864
ClientError,
@@ -64,6 +70,8 @@ from httpware import (
6470
BulkheadFullError,
6571
)
6672

73+
_LOGGER = logging.getLogger("myapp")
74+
6775

6876
async def fetch(client: AsyncClient, user_id: int) -> dict | None:
6977
try:

docs/resilience.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ from httpware.middleware.resilience import AsyncRetry
2121
| `max_delay` | `5.0` (s) | Ceiling for backoff. |
2222
| `retry_status_codes` | `frozenset({408, 429, 502, 503, 504})` | Status codes considered retryable. |
2323
| `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`. |
2525
| `budget` | `RetryBudget()` (default-configured) | The token bucket. Pass a shared `RetryBudget` instance to apply one budget across multiple clients. |
2626

2727
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.

planning/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ carry **no** frontmatter — living prose, dated by git.
7070

7171
### Active
7272

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).
7374
- **[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).
7475

7576
### Archived (shipped)

planning/audits/2026-06-13-docs-audit.md

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,20 @@ batch.)
101101
**R1 — dense stacked-qualifier sentences in the hottest spots.** `README.md:31`
102102
(decoder resolution) stacks two qualified clauses; the `respect_retry_after`
103103
cell in `docs/resilience.md:24` is a four-sentence paragraph inside a table cell.
104-
Correct, but hard on first read. *Suggest:* split the densest sentences/cells.
104+
Correct, but hard on first read. **Resolved** (`2026-06-13.05`) — split the
105+
decoder sentence and tightened the table cell.
105106

106107
**R2 — unglossed jargon on first use.** "Finagle-style" (`README.md:17`),
107108
"full-jitter", "bulkhead", "PEP 678 note", "token bucket" — most are defined
108-
later or never, but the README is first contact. *Suggest:* a one-clause gloss on
109-
first use.
109+
later or never, but the README is first contact. **Resolved** (`2026-06-13.05`)
110+
— glossed "Finagle-style `RetryBudget`" (token bucket capping the global retry
111+
rate) and "PEP 678 note" → "an exception note (PEP 678)". ("bulkhead"/"full-jitter"
112+
left as standard resilience vocabulary.)
110113

111114
**R3 — `_LOGGER` used in `errors.md` examples (lines 77, 130, 154) without
112-
definition.** A literal copy-paste hits `NameError`. Minor/conventional.
115+
definition.** A literal copy-paste hits `NameError`. **Resolved** (`2026-06-13.05`)
116+
— added `import logging` + `_LOGGER = logging.getLogger("myapp")` to the first
117+
block and a one-line note that the examples assume it.
113118

114119
### Onboarding & UX gaps (the larger lane — not bugs)
115120

@@ -133,8 +138,9 @@ accompanying change.
133138
real public test endpoint for the leading example.
134139
- **G5 — `STATUS_TO_EXCEPTION` is a public `__all__` export
135140
(`src/httpware/__init__.py:54`) documented nowhere.** The lone undocumented
136-
public symbol. *Suggest:* document it (it is the extensible status→exception
137-
map) or reconsider its place in `__all__`.
141+
public symbol. **Resolved** (`2026-06-13.05`) — documented at the
142+
status-to-exception table in `docs/errors.md` (public `Mapping[int,
143+
type[StatusError]]`, importable, fallback rows excluded).
138144
- **G6 — No custom-`ResponseDecoder` guide and no API reference.** The decoder
139145
seam (Seam B) is a documented extension point but, unlike middleware, gets no
140146
"write your own" guide; and there is no generated symbol reference
@@ -181,14 +187,23 @@ follow them. No orphan pages and no broken nav targets — the nav is otherwise
181187
- **`2026-06-13.04-docs-accuracy-fixes`** (lightweight) — fixes B1, B2, I1, I2, I3,
182188
and the `AsyncTimeout`-validation wording. All verified against code / official
183189
upstream docs.
190+
- **`2026-06-13.05-docs-audit-followups`** (lightweight) — the invariant-enforcement
191+
wording fix (triage item below) plus readability/small-gap findings R1, R2, R3, G5.
184192

185193
## Deferred / triage
186194

187-
- The onboarding & UX gaps (G1–G6) — a separate, larger docs-UX change (de-dup,
188-
why-httpware, base-client migration, runnable quickstart, custom-decoder guide,
189-
API reference). Not yet scheduled.
190-
- The `httpx2._` invariant is documented as CI-enforced (`CLAUDE.md`,
191-
`architecture/overview.md`, and — fixed here — `contributing.md`) but no CI
192-
workflow runs the grep. Either wire `grep -rE 'httpx2\._' src/httpware/` into the
193-
lint workflow (tiny CI change) or downgrade the "CI-enforced" wording in the two
194-
internal truth docs. Parked pending a decision on which.
195+
- The onboarding & UX gaps **G1, G2, G3, G4, G6** (why-httpware, base-client
196+
migration, README ↔ index de-dup, runnable quickstart, custom-decoder guide +
197+
API reference) — a separate, larger docs-UX change that needs design, not
198+
mechanical edits. Not yet scheduled. (G5 resolved in `2026-06-13.05`.)
199+
- ~~The `httpx2._` invariant is documented as CI-enforced but no CI workflow runs
200+
the grep.~~ **Resolved (option 2 — fix the claim).** Empirically confirmed against
201+
the real `ruff --select ALL` ruleset: only `print()` (`T201`) and a *blanket*
202+
`# type: ignore` (`PGH003`) are machine-checked; the `httpx2._` ban is partial
203+
(`SLF001` catches private *attribute* access, e.g. `httpx2._foo`, but **not** a
204+
*used* private import like `from httpx2._internal import x`); the future-import,
205+
global-logging, and `# ty:`-vs-`# type:` rules are review-only. The blanket
206+
"(CI-enforced) / CI rejects PRs" heading in `CLAUDE.md` and
207+
`architecture/overview.md` was rewritten to state the actual enforcement split
208+
and to note the `httpx2._` grep is a review check, not a CI gate.
209+
`contributing.md` was already corrected in the `docs-accuracy-fixes` change.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
status: draft
3+
date: 2026-06-13
4+
slug: docs-audit-followups
5+
supersedes: null
6+
superseded_by: null
7+
pr: null
8+
outcome: null
9+
---
10+
11+
# Change: Docs-audit follow-ups — invariant-enforcement wording + readability
12+
13+
**Lane:** lightweight — docs-only, no code, no public-API change. Touches a
14+
handful of doc/truth files (above the usual ≤2 guard, but the guard proxies
15+
*code* risk; these are mechanical/verified corrections whose thinking lives in
16+
the audit). Spec is the audit, not a `design.md`.
17+
18+
Spec: [`planning/audits/2026-06-13-docs-audit.md`](../../../audits/2026-06-13-docs-audit.md)
19+
— the second batch: the resolved `httpx2._` triage item plus findings R1, R2, R3, G5.
20+
(First batch — the verified bugs B1/B2/I1/I2/I3 — shipped in
21+
[`2026-06-13.04-docs-accuracy-fixes`](../2026-06-13.04-docs-accuracy-fixes/change.md).)
22+
23+
## Goal
24+
25+
Make the invariant-enforcement claims accurate and clear the concrete
26+
readability/small-gap findings. No structural docs-UX work (de-dup, why-httpware,
27+
migration guide, API reference) — that stays a separate, design-led change.
28+
29+
## Approach
30+
31+
- **Invariant enforcement (triage item, option 2 — "fix the claim").** Empirically
32+
confirmed against `ruff --select ALL`: only `print()` (`T201`) and a blanket
33+
`# type: ignore` (`PGH003`) are machine-checked; `httpx2._` is partial (`SLF001`
34+
catches attribute access, not a *used* private import); future-import / logging /
35+
`# ty:`-vs-`# type:` are review-only. Rewrote the overstated "(CI-enforced) / CI
36+
rejects PRs" heading + intro in `CLAUDE.md` and `architecture/overview.md` to the
37+
real split, and reframed the `httpx2._` grep as a review check (not a CI gate).
38+
- **R3** `docs/errors.md` — examples used `_LOGGER` undefined (copy-paste `NameError`).
39+
Added `import logging` + `_LOGGER = logging.getLogger("myapp")` to the first block
40+
and a one-line note that the examples assume it.
41+
- **G5** `docs/errors.md` — documented the public `STATUS_TO_EXCEPTION` mapping at the
42+
status-to-exception table (it was the lone undocumented `__all__` export).
43+
- **R2** `README.md` — glossed "Finagle-style `RetryBudget`" as a token bucket that
44+
caps the global retry rate, on first use.
45+
- **R1** `README.md` + `docs/resilience.md` — de-densified the decoder-resolution
46+
sentence and tightened the run-on `respect_retry_after` table cell; glossed
47+
"PEP 678 note" → "an exception note (PEP 678)".
48+
49+
## Files
50+
51+
- `CLAUDE.md` — invariant-enforcement heading/intro + `httpx2._` bullet
52+
- `architecture/overview.md` — same, truth-home copy
53+
- `docs/errors.md` — R3 (`_LOGGER`) + G5 (`STATUS_TO_EXCEPTION`)
54+
- `README.md` — R2 (Finagle gloss) + R1 (decoder sentence)
55+
- `docs/resilience.md` — R1 (`respect_retry_after` cell)
56+
- `planning/audits/2026-06-13-docs-audit.md` — mark items resolved
57+
58+
## Verification
59+
60+
- [x] `mkdocs build --strict` succeeds (no broken refs).
61+
- [x] `just lint` — clean (no source touched).
62+
- [x] Enforcement claims match the empirical `ruff --select ALL` result
63+
(`T201`/`PGH003` fire; future-import, `basicConfig`, bare `getLogger`,
64+
and `from httpx2._x import …` do not).
65+
66+
## Deferred (still open after this change)
67+
68+
The structural docs-UX gaps need design, not mechanical edits: **G1** why-httpware,
69+
**G2** base-client migration guide, **G3** de-dup README ↔ index.md, **G4** runnable
70+
first quickstart (real endpoint), **G6** custom-`ResponseDecoder` guide + mkdocstrings
71+
API reference, plus the nav-ordering nits. Tracked in the audit's onboarding/UX section.

0 commit comments

Comments
 (0)