Skip to content

Commit dc390cf

Browse files
authored
docs: plain-prose pass over agent and glossary docs (#193)
1 parent a7cfa3a commit dc390cf

2 files changed

Lines changed: 26 additions & 26 deletions

File tree

‎AGENTS.md‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,32 @@ Guidance for AI agents (Claude Code, etc.) working in this repository.
66

77
`httpware` is a thin, opinionated wrapper around `httpx2` with sync and async clients for building
88
resilient service clients; [`CONTEXT.md`](CONTEXT.md) opens with what it does and owns the
9-
vocabulary — read it before naming a concept in code, a test name, or an issue title. It ships
9+
vocabulary. Read it before naming a concept in code, a test name, or an issue title. It ships
1010
under the `modern-python` org.
1111

1212
## Commands
1313

14-
`just` (task runner) and `uv` (package manager). The [`justfile`](justfile) is the source of truth —
15-
`just --list`, or read it. The one thing it does not say: `just docs-build` runs `mkdocs --strict`
14+
`just` (task runner) and `uv` (package manager). The [`justfile`](justfile) is the source of truth:
15+
run `just --list`, or read it. The one thing it does not say: `just docs-build` runs `mkdocs --strict`
1616
over the site only, and `docs/adr/` and `docs/agents/` are excluded from it (`exclude_docs` in
1717
`mkdocs.yml`) because they are read on GitHub rather than published. Their links are covered
1818
instead by the `links` job in `.github/workflows/_checks.yml`, which runs lychee `--offline` over
1919
every `*.md` in the repo.
2020

2121
## Architecture
2222

23-
`httpx2` is public surface, not an abstraction to hide: `httpx2.Request` and `httpx2.Response` are
24-
re-exported as-is. Three protocol seams — client ↔ middleware chain, client ↔ decoder list, and
25-
httpware ↔ optional extras — are defined in `CONTEXT.md` and named **A**, **B** and **C** in the
23+
`httpx2` is part of the public surface: `httpx2.Request` and `httpx2.Response` are
24+
re-exported as-is. Three protocol seams (client ↔ middleware chain, client ↔ decoder list, and
25+
httpware ↔ optional extras) are defined in `CONTEXT.md` and named **A**, **B** and **C** in the
2626
module docstrings that implement them. Never cross a seam except through its protocol.
2727

2828
### Key files
2929

3030
Every module under `src/httpware/` is named for what it does; read it. What a single-file read will
3131
**not** tell you:
3232

33-
- `client.py` holds **both worlds** — `Client` and `AsyncClient` — and they are hand-maintained at
34-
parity, not generated. A feature added to one must be mirrored to the other;
33+
- `client.py` holds **both worlds**, `Client` and `AsyncClient`, hand-maintained at parity;
34+
nothing generates one from the other. A feature added to one must be mirrored to the other;
3535
`tests/test_client_parity.py` says why, and names the sole exception (`AsyncTimeout` has no sync
3636
sibling).
3737
- `errors.py` owns the tree and both construction rules. Status-keyed `StatusError` subclasses take
@@ -54,15 +54,15 @@ Every module under `src/httpware/` is named for what it does; read it. What a si
5454
`RetryBudget`, `_backoff`, `_event_loop_guard`) written once and driven by both worlds. New
5555
resilience logic goes in a shared object, not into each world's shell.
5656

57-
**House invariants, review-only** — nothing in CI catches these: no `httpx2._` private API (ruff
57+
**House invariants, review-only.** Nothing in CI catches these: no `httpx2._` private API (ruff
5858
`SLF001` flags private *attribute* access but not a used private *import*); no
59-
`from __future__ import annotations` (3.11+ floor); no global logging config —
60-
`logging.getLogger("httpware")` and namespaced children only.
59+
`from __future__ import annotations` (3.11+ floor); no global logging config
60+
(`logging.getLogger("httpware")` and namespaced children only).
6161

6262
### Testing patterns
6363

64-
Transport mocking is `httpx2.MockTransport` passed as `transport=`, never `respx` — `respx`
65-
targets `httpx`, not `httpx2`, and patches its internals. Concurrency-sensitive components carry
64+
Transport mocking is `httpx2.MockTransport` passed as `transport=`, never `respx`, which
65+
targets `httpx` rather than `httpx2` and patches its internals. Concurrency-sensitive components carry
6666
Hypothesis property tests in `test_*_props.py`, and `stress`-marked tests drive real thread
6767
parallelism: they run under the GIL for coverage, but the proof comes from the free-threaded
6868
`3.14t` CI job.
@@ -78,10 +78,10 @@ description, and PyPI does not rewrite relative links, so a relative one 404s on
7878
- Design principle: thin wrapper, small public surface. `httpware.__all__` is checked against an
7979
explicit expected set in `tests/test_public_api.py`, so a symbol cannot appear unnoticed
8080
- `Http` is two letters in a class name (`AsyncClient`, not `ASYNCClient`); no `a` prefix on async
81-
methods, matching `httpx2` — `aclose()` is the sole exception
81+
methods, matching `httpx2`; `aclose()` is the sole exception
8282
- Docstrings: public API documents the contract; internal helpers get a one-line contract, plus at
83-
most 1–2 lines for a genuinely non-obvious constraint. Never narrate implementation or justify
84-
code to a reviewer — cross-file rationale lives in an invariant test's docstring
83+
most one or two lines for a non-obvious constraint. Never narrate implementation or justify
84+
code to a reviewer; cross-file rationale lives in an invariant test's docstring
8585

8686
## Agent skills
8787

‎CONTEXT.md‎

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

33
A thin, opinionated wrapper around `httpx2`: it re-exports `httpx2.Request` and
4-
`httpx2.Response` as the public request/response surface and adds three things —
4+
`httpx2.Response` as the public request/response surface and adds three things:
55
a middleware chain composed at client construction, typed response decoding, and
66
a status-keyed exception tree raised automatically on 4xx and 5xx. `Client` and
77
`AsyncClient` carry the same features.
@@ -10,14 +10,14 @@ a status-keyed exception tree raised automatically on 4xx and 5xx. `Client` and
1010

1111
A term is listed only when there is a synonym to reject, or a meaning subtle enough that code and
1212
docs must agree on it. General HTTP and programming vocabulary does not belong here, however
13-
heavily this project uses it. `httpx2`'s own names — `Request`, `Response`, `Timeout`, `Limits`,
14-
`Auth`, `MockTransport` — keep `httpx2`'s meanings; nothing here redefines one.
13+
heavily this project uses it. `httpx2`'s own names (`Request`, `Response`, `Timeout`, `Limits`,
14+
`Auth`, `MockTransport`) keep `httpx2`'s meanings; nothing here redefines one.
1515

1616
**Middleware**:
1717
An object satisfying `Middleware` / `AsyncMiddleware`: it takes a request and a continuation
1818
(`Next` / `AsyncNext`) and returns a response. The chain is composed once at client construction
1919
and frozen for the client's lifetime.
20-
_Avoid_: hook — reserved for `httpx2.event_hooks`, a genuinely different mechanism that runs
20+
_Avoid_: hook, which is reserved for `httpx2.event_hooks`, a different mechanism that runs
2121
*below* the chain, once per transport attempt rather than once per logical call.
2222

2323
**Terminal**:
@@ -37,28 +37,28 @@ optional extras. A seam is not an extension point: an extension point is what a
3737
across. The letters are cited from module docstrings, so they must not be renumbered casually.
3838

3939
**Decoder**:
40-
An object satisfying `ResponseDecoder` — `can_decode(model)` to claim a model, `decode(content,
40+
An object satisfying `ResponseDecoder`: `can_decode(model)` to claim a model, `decode(content,
4141
model)` to turn raw bytes into it in a single parse pass.
4242
_Avoid_: adapter, which names the third-party object a decoder builds and memoizes per model
4343
(`pydantic.TypeAdapter`, `msgspec.json.Decoder`). Calling the decoder an adapter too leaves
4444
"the adapter cache" ambiguous about which of the two it caches.
4545

4646
**Counted failure**:
4747
The circuit breaker's unit of account: a `NetworkError`, an httpware `TimeoutError`, or a
48-
`StatusError` whose status is in the effective failure set (default: all 5xx). Every other 4xx —
49-
429 included — counts as a *success*. A `RetryBudgetExhaustedError` is classified by its
48+
`StatusError` whose status is in the effective failure set (default: all 5xx). Every other 4xx,
49+
429 included, counts as a *success*. A `RetryBudgetExhaustedError` is classified by its
5050
`last_exception`, and any other exception type propagates without touching circuit state. "Failure"
5151
alone is ambiguous here: a failed request is very often not a counted failure.
5252

5353
**Circuit key**:
5454
The value a keyed circuit breaker computes from a request to pick its circuit; requests with equal
5555
keys share one circuit. The default is the URL's origin: scheme, host and port.
56-
_Avoid_: host — the host alone merges upstreams that differ only in scheme or port.
56+
_Avoid_: host. The host alone merges upstreams that differ only in scheme or port.
5757

5858
**Cap**:
59-
`max_response_body_bytes` — the bound on how many bytes httpware buffers on the caller's behalf.
59+
`max_response_body_bytes`: the bound on how many bytes httpware buffers on the caller's behalf.
6060
Counted *decoded*, status-agnostic, and never applied to user-driven `stream()` iteration.
61-
_Avoid_: limit — `limits=` on a client is `httpx2.Limits`, the connection-pool configuration, and
61+
_Avoid_: limit. `limits=` on a client is `httpx2.Limits`, the connection-pool configuration, and
6262
has nothing to do with body size. The one sanctioned exception is public and cannot be renamed
6363
without a breaking change: `ResponseTooLargeError.limit` carries the cap that was exceeded. Prose
6464
says cap; that field says `limit`.

0 commit comments

Comments
 (0)