Guidance for AI agents (Claude Code, etc.) working in this repository.
httpware is a Python HTTP client framework with sync and async clients for building resilient service clients. It ships under the modern-python org and is a thin opinionated wrapper around httpx2: it re-exports httpx2.Request/httpx2.Response, adds a middleware chain, typed response decoding, and a status-keyed exception tree raised automatically on 4xx/5xx.
Where to find what:
architecture/(repo root) — the per-capability living truth (overview, client, middleware, decoders, errors, resilience, optional extras, testing); the promotion target on every ship. Read the relevant file before changing that capability.planning/README.md— the planning conventions (two axes, change bundles, three lanes, frontmatter) + the change Index.planning/changes/{active,archive}/<YYYY-MM-DD.NN-slug>/— per-change bundles (design.md+plan.md, orchange.mdfor the lightweight lane).planning/audits/— findings reports +scripts/tooling.planning/retros/— retrospectives.planning/releases/— per-version release notes (also published on GitHub Releases).planning/deferred.md— review-surfaced, not-yet-actionable items.planning/_templates/— design/plan/change templates.
Per-feature workflow: brainstorming → design.md in planning/changes/active/<id>/ → writing-plans → plan.md in the same bundle → executing-plans (or subagent-driven-development) → requesting-code-review → finishing-a-development-branch. On ship, promote the conclusions into the affected architecture/<capability>.md by hand and move the bundle to planning/changes/archive/. Topic slugs are kebab-case descriptions (msgspec-decoder-adapter), not story IDs.
This project uses just (task runner) and uv (package manager).
just install # uv lock --upgrade && uv sync --all-extras --frozen --group lint
just lint # eof-fixer + ruff format + ruff check --fix + ty check
just lint-ci # same checks without auto-fixing (used in CI)
just test # uv run pytest (with coverage by default)
just test-branch # pytest with branch coveragejust test passes extra args to pytest:
just test tests/test_client.py
just test tests/test_client.py -k test_get_returns_typed_responseWithout just:
uv run ruff format . && uv run ruff check . --fix && uv run ty check
uv run pytestThese 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.
- No
httpx2private API:grep -rE 'httpx2\._' src/httpware/should return zero matches (run in review — not wired into CI). Public symbols only. - No
from __future__ import annotations: Python 3.11+ floor; PEP 604/585 syntax is native. - No
print(): enforced by ruff. - No global logging config: no
logging.basicConfig(), no barelogging.getLogger(). Acquirelogging.getLogger("httpware")orlogging.getLogger(f"httpware.{module}")only. - Type suppressions: use
# ty: ignore[<rule>], never# type: ignoreor# mypy: ignore.
- Modules:
snake_case(client.py,errors.py,middleware/chain.py). - Classes:
PascalCase.Httpis two letters:AsyncClient, notASYNCClient. - Methods:
snake_case. Noaprefix on async methods (matchhttpx2);aclose()is the sole exception. - Private symbols:
_leading_underscore. Cross-module private code lives in_internal/. - Imports: absolute paths inside
src/httpware/; relative imports only within the same subpackage. - Docstrings: PEP 257. Module/class/public-method required;
D1(missing docstring) is ignored. - Exception construction: status-keyed
StatusErrorsubclasses (the 4xx/5xx tree) take a single positionalresponse: httpx2.Responseand do NOT override__init__— all fields viaexc.response.*. This rule scopes toStatusErroronly; non-statusClientErrorsubclasses such asDecodeError,MissingDecoderError,BulkheadFullError,RetryBudgetExhaustedError, andCircuitOpenErrordeliberately define__init__with keyword-only fields. Seearchitecture/errors.md.
src/httpware/
├── __init__.py # public exports + __all__
├── client.py # AsyncClient + Client (thin wrappers over httpx2.AsyncClient / httpx2.Client)
├── errors.py # status-keyed exception hierarchy holding httpx2.Response
├── middleware/ # protocol, Next type, chain composition, phase decorators
├── decoders/ # ResponseDecoder protocol + Pydantic/msgspec adapters
├── _internal/ # private cross-module helpers
└── py.typed
Three documented internal boundaries. AI agents must respect them — never cross a seam except through its documented protocol.
- Seam A —
Client/AsyncClient↔Middleware/AsyncMiddleware— middleware chain composed atClient.__init__andAsyncClient.__init__, frozen for the client's lifetime. Internal terminal callshttpx2.Client.sendorhttpx2.AsyncClient.send, maps exceptions, raisesStatusErroron 4xx/5xx. Sync and async surfaces are kept at parity. - Seam B —
Client/AsyncClient↔ResponseDecoderlist — both clients takedecoders: Sequence[ResponseDecoder] | None(a list, not a single decoder;Noneresolves against installed extras, pydantic-first). Whenresponse_modelis provided,send/send_with_response(sync and async alike) walk the list and the first decoder whosecan_decode(model: type) -> boolreturns True runsdecode(content: bytes, model: type[T]) -> T; if no decoder claims the model,MissingDecoderErroris raised before the HTTP call. Decoder exceptions are wrapped asDecodeErrorat the seam. Full contract:architecture/decoders.md. - Seam C —
httpware↔ optional extras — each opt-in dependency imported only inside its dedicated module.
pytest-asyncioauto mode — async tests do NOT need@pytest.mark.asyncio.- Property-based tests (Hypothesis) for concurrency-sensitive code:
RetryBudget,Bulkhead, retry interleaving. Files namedtest_*_props.py. - Tests inject
httpx2.MockTransportviaAsyncClient(httpx2_client=httpx2.AsyncClient(transport=mock))for async orClient(httpx2_client=httpx2.Client(transport=mock))for sync. Norespx, noRecordedTransport.
- Check the relevant
architecture/capability file before adding a new module or extension point. - Surface ambiguity as a documentation gap rather than improvising.