From f01669f426ad422834f5d707e2470144396e081a Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 17 Oct 2026 21:31:13 +0300 Subject: [PATCH] docs: plain-prose pass over agent and glossary docs --- AGENTS.md | 32 ++++++++++++++++---------------- CONTEXT.md | 20 ++++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3fcc3d6..10a9d62 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,13 +6,13 @@ Guidance for AI agents (Claude Code, etc.) working in this repository. `httpware` is a thin, opinionated wrapper around `httpx2` with sync and async clients for building resilient service clients; [`CONTEXT.md`](CONTEXT.md) opens with what it does and owns the -vocabulary — read it before naming a concept in code, a test name, or an issue title. It ships +vocabulary. Read it before naming a concept in code, a test name, or an issue title. It ships under the `modern-python` org. ## Commands -`just` (task runner) and `uv` (package manager). The [`justfile`](justfile) is the source of truth — -`just --list`, or read it. The one thing it does not say: `just docs-build` runs `mkdocs --strict` +`just` (task runner) and `uv` (package manager). The [`justfile`](justfile) is the source of truth: +run `just --list`, or read it. The one thing it does not say: `just docs-build` runs `mkdocs --strict` over the site only, and `docs/adr/` and `docs/agents/` are excluded from it (`exclude_docs` in `mkdocs.yml`) because they are read on GitHub rather than published. Their links are covered instead by the `links` job in `.github/workflows/_checks.yml`, which runs lychee `--offline` over @@ -20,9 +20,9 @@ every `*.md` in the repo. ## Architecture -`httpx2` is public surface, not an abstraction to hide: `httpx2.Request` and `httpx2.Response` are -re-exported as-is. Three protocol seams — client ↔ middleware chain, client ↔ decoder list, and -httpware ↔ optional extras — are defined in `CONTEXT.md` and named **A**, **B** and **C** in the +`httpx2` is part of the public surface: `httpx2.Request` and `httpx2.Response` are +re-exported as-is. Three protocol seams (client ↔ middleware chain, client ↔ decoder list, and +httpware ↔ optional extras) are defined in `CONTEXT.md` and named **A**, **B** and **C** in the module docstrings that implement them. Never cross a seam except through its protocol. ### Key files @@ -30,8 +30,8 @@ module docstrings that implement them. Never cross a seam except through its pro Every module under `src/httpware/` is named for what it does; read it. What a single-file read will **not** tell you: -- `client.py` holds **both worlds** — `Client` and `AsyncClient` — and they are hand-maintained at - parity, not generated. A feature added to one must be mirrored to the other; +- `client.py` holds **both worlds**, `Client` and `AsyncClient`, hand-maintained at parity; + nothing generates one from the other. A feature added to one must be mirrored to the other; `tests/test_client_parity.py` says why, and names the sole exception (`AsyncTimeout` has no sync sibling). - `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 `RetryBudget`, `_backoff`, `_event_loop_guard`) written once and driven by both worlds. New resilience logic goes in a shared object, not into each world's shell. -**House invariants, review-only** — nothing in CI catches these: no `httpx2._` private API (ruff +**House invariants, review-only.** Nothing in CI catches these: no `httpx2._` private API (ruff `SLF001` flags private *attribute* access but not a used private *import*); no -`from __future__ import annotations` (3.11+ floor); no global logging config — -`logging.getLogger("httpware")` and namespaced children only. +`from __future__ import annotations` (3.11+ floor); no global logging config +(`logging.getLogger("httpware")` and namespaced children only). ### Testing patterns -Transport mocking is `httpx2.MockTransport` passed as `transport=`, never `respx` — `respx` -targets `httpx`, not `httpx2`, and patches its internals. Concurrency-sensitive components carry +Transport mocking is `httpx2.MockTransport` passed as `transport=`, never `respx`, which +targets `httpx` rather than `httpx2` and patches its internals. Concurrency-sensitive components carry Hypothesis property tests in `test_*_props.py`, and `stress`-marked tests drive real thread parallelism: they run under the GIL for coverage, but the proof comes from the free-threaded `3.14t` CI job. @@ -78,10 +78,10 @@ description, and PyPI does not rewrite relative links, so a relative one 404s on - Design principle: thin wrapper, small public surface. `httpware.__all__` is checked against an explicit expected set in `tests/test_public_api.py`, so a symbol cannot appear unnoticed - `Http` is two letters in a class name (`AsyncClient`, not `ASYNCClient`); no `a` prefix on async - methods, matching `httpx2` — `aclose()` is the sole exception + methods, matching `httpx2`; `aclose()` is the sole exception - Docstrings: public API documents the contract; internal helpers get a one-line contract, plus at - most 1–2 lines for a genuinely non-obvious constraint. Never narrate implementation or justify - code to a reviewer — cross-file rationale lives in an invariant test's docstring + most one or two lines for a non-obvious constraint. Never narrate implementation or justify + code to a reviewer; cross-file rationale lives in an invariant test's docstring ## Agent skills diff --git a/CONTEXT.md b/CONTEXT.md index ce6e716..5946fc6 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,7 +1,7 @@ # httpware A thin, opinionated wrapper around `httpx2`: it re-exports `httpx2.Request` and -`httpx2.Response` as the public request/response surface and adds three things — +`httpx2.Response` as the public request/response surface and adds three things: a middleware chain composed at client construction, typed response decoding, and a status-keyed exception tree raised automatically on 4xx and 5xx. `Client` and `AsyncClient` carry the same features. @@ -10,14 +10,14 @@ a status-keyed exception tree raised automatically on 4xx and 5xx. `Client` and A term is listed only when there is a synonym to reject, or a meaning subtle enough that code and docs must agree on it. General HTTP and programming vocabulary does not belong here, however -heavily this project uses it. `httpx2`'s own names — `Request`, `Response`, `Timeout`, `Limits`, -`Auth`, `MockTransport` — keep `httpx2`'s meanings; nothing here redefines one. +heavily this project uses it. `httpx2`'s own names (`Request`, `Response`, `Timeout`, `Limits`, +`Auth`, `MockTransport`) keep `httpx2`'s meanings; nothing here redefines one. **Middleware**: An object satisfying `Middleware` / `AsyncMiddleware`: it takes a request and a continuation (`Next` / `AsyncNext`) and returns a response. The chain is composed once at client construction and frozen for the client's lifetime. -_Avoid_: hook — reserved for `httpx2.event_hooks`, a genuinely different mechanism that runs +_Avoid_: hook, which is reserved for `httpx2.event_hooks`, a different mechanism that runs *below* the chain, once per transport attempt rather than once per logical call. **Terminal**: @@ -37,7 +37,7 @@ optional extras. A seam is not an extension point: an extension point is what a across. The letters are cited from module docstrings, so they must not be renumbered casually. **Decoder**: -An object satisfying `ResponseDecoder` — `can_decode(model)` to claim a model, `decode(content, +An object satisfying `ResponseDecoder`: `can_decode(model)` to claim a model, `decode(content, model)` to turn raw bytes into it in a single parse pass. _Avoid_: adapter, which names the third-party object a decoder builds and memoizes per model (`pydantic.TypeAdapter`, `msgspec.json.Decoder`). Calling the decoder an adapter too leaves @@ -45,20 +45,20 @@ _Avoid_: adapter, which names the third-party object a decoder builds and memoiz **Counted failure**: The circuit breaker's unit of account: a `NetworkError`, an httpware `TimeoutError`, or a -`StatusError` whose status is in the effective failure set (default: all 5xx). Every other 4xx — -429 included — counts as a *success*. A `RetryBudgetExhaustedError` is classified by its +`StatusError` whose status is in the effective failure set (default: all 5xx). Every other 4xx, +429 included, counts as a *success*. A `RetryBudgetExhaustedError` is classified by its `last_exception`, and any other exception type propagates without touching circuit state. "Failure" alone is ambiguous here: a failed request is very often not a counted failure. **Circuit key**: The value a keyed circuit breaker computes from a request to pick its circuit; requests with equal keys share one circuit. The default is the URL's origin: scheme, host and port. -_Avoid_: host — the host alone merges upstreams that differ only in scheme or port. +_Avoid_: host. The host alone merges upstreams that differ only in scheme or port. **Cap**: -`max_response_body_bytes` — the bound on how many bytes httpware buffers on the caller's behalf. +`max_response_body_bytes`: the bound on how many bytes httpware buffers on the caller's behalf. Counted *decoded*, status-agnostic, and never applied to user-driven `stream()` iteration. -_Avoid_: limit — `limits=` on a client is `httpx2.Limits`, the connection-pool configuration, and +_Avoid_: limit. `limits=` on a client is `httpx2.Limits`, the connection-pool configuration, and has nothing to do with body size. The one sanctioned exception is public and cannot be renamed without a breaking change: `ResponseTooLargeError.limit` carries the cap that was exceeded. Prose says cap; that field says `limit`.