Skip to content

Commit 3edef19

Browse files
authored
Merge pull request #20 from modern-python/feat/v0.2-thin-httpx2-wrapper
v0.2: thin httpx2 wrapper rewrite
2 parents 1240493 + 1e4a027 commit 3edef19

39 files changed

Lines changed: 4060 additions & 4109 deletions

CLAUDE.md

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

55
## Project Overview
66

7-
`httpware` is a Python async HTTP client framework for building resilient service clients. It supersedes `community-of-python/base-client` and ships under the `modern-python` org. The framework owns the abstraction layer above the underlying HTTP client (`httpx2` by default); consumers never import the transport.
7+
`httpware` is a Python async HTTP client framework for building resilient service clients. It supersedes `community-of-python/base-client` and ships under the `modern-python` org. The framework 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.
88

99
**Where to find what:**
1010

11-
- [`docs/dev/engineering.md`](docs/dev/engineering.md) — the distilled design reference: invariants and *why*, the five protocol seams, exception contract, module layout, testing patterns, optional-extras pattern, remaining roadmap. Read this before adding any new module or extension point.
11+
- [`planning/engineering.md`](planning/engineering.md) — the distilled design reference: invariants and *why*, the three protocol seams, exception contract, module layout, testing patterns, optional-extras pattern, remaining roadmap. Read this before adding any new module or extension point.
1212
- [`planning/deferred-work.md`](planning/deferred-work.md) — review-surfaced items that are real but not actionable now.
1313
- [`planning/specs/`](planning/specs/) and [`planning/plans/`](planning/plans/) — per-feature design specs and implementation plans (active work).
1414

@@ -44,59 +44,50 @@ uv run pytest
4444

4545
These are non-negotiable. CI rejects PRs that violate them.
4646

47-
- **No `httpx2` leakage**: `import httpx2` / `from httpx2` is allowed ONLY inside `src/httpware/transports/httpx2.py`. The mapping of `httpx2` exceptions to `httpware` exceptions happens at that single seam.
48-
- **No `httpx2` private API**: `grep -rE 'httpx2\._' src/httpware/` must return zero matches.
47+
- **No `httpx2` private API**: `grep -rE 'httpx2\._' src/httpware/` must return zero matches. Public symbols only.
4948
- **No `from __future__ import annotations`**: Python 3.11+ floor; PEP 604/585 syntax is native.
5049
- **No `print()`**: enforced by ruff.
5150
- **No global logging config**: no `logging.basicConfig()`, no bare `logging.getLogger()`. Acquire `logging.getLogger("httpware")` or `logging.getLogger(f"httpware.{module}")` only.
5251
- **Type suppressions**: use `# ty: ignore[<rule>]`, never `# type: ignore` or `# mypy: ignore`.
5352

5453
## Code conventions
5554

56-
- **Modules**: `snake_case` (`client.py`, `request.py`, `transports/httpx2.py`).
57-
- **Classes**: `PascalCase`. `Http` is two letters: `Httpx2Transport`, not `HTTPX2Transport`.
55+
- **Modules**: `snake_case` (`client.py`, `errors.py`, `middleware/chain.py`).
56+
- **Classes**: `PascalCase`. `Http` is two letters: `AsyncClient`, not `ASYNCClient`.
5857
- **Methods**: `snake_case`. No `a` prefix on async methods (match `httpx2`); `aclose()` is the sole exception.
5958
- **Private symbols**: `_leading_underscore`. Cross-module private code lives in `_internal/`.
6059
- **Imports**: absolute paths inside `src/httpware/`; relative imports only within the same subpackage.
6160
- **Docstrings**: PEP 257. Module/class/public-method required; `D1` (missing docstring) is ignored.
62-
- **Exception construction**: keyword arguments only. Mandatory fields: `status: int`, `body: bytes`, `headers: Mapping`, `json: Any | None`, `request_method: str`, `request_url: str`.
61+
- **Exception construction**: status-keyed errors take a single positional `response: httpx2.Response`. Subclasses do not override `__init__`. All fields available via `exc.response.*`.
6362

6463
## Module layout
6564

6665
```
6766
src/httpware/
6867
├── __init__.py # public exports + __all__
69-
├── client.py # AsyncClient
70-
├── request.py # Request + with_*
71-
├── response.py # Response, StreamResponse
72-
├── errors.py # status-keyed exception hierarchy
73-
├── config.py # Limits, Timeout, ClientConfig, Redactor
74-
├── middleware/ # protocols + built-in middleware
75-
├── transports/ # Transport protocol + Httpx2Transport + RecordedTransport
76-
├── decoders/ # ResponseDecoder protocol + adapters
68+
├── client.py # AsyncClient (thin wrapper over httpx2.AsyncClient)
69+
├── errors.py # status-keyed exception hierarchy holding httpx2.Response
70+
├── middleware/ # protocol, Next type, chain composition, phase decorators
71+
├── decoders/ # ResponseDecoder protocol + Pydantic/msgspec adapters
7772
├── _internal/ # private cross-module helpers
7873
└── py.typed
7974
```
8075

81-
Story 1.1 ships only the scaffold; subsequent stories add modules.
82-
8376
## Protocol seams
8477

85-
Five documented internal boundaries. AI agents must respect them — never cross a seam except through its documented protocol.
78+
Three documented internal boundaries. AI agents must respect them — never cross a seam except through its documented protocol.
8679

87-
1. **`Middleware ↔ Transport`** — chain bottom calls `transport.__call__`.
88-
2. **`AsyncClient ↔ Middleware`** — chain composed at construction.
89-
3. **`AsyncClient ↔ ResponseDecoder`** — called when `response_model` provided.
90-
4. **`Httpx2Transport ↔ httpx2`** — only `transports/httpx2.py` imports `httpx2`.
91-
5. **`httpware ↔ optional extras`** — extras imported only inside their dedicated modules.
80+
1. **`AsyncClient ↔ Middleware`** — middleware chain composed at `AsyncClient.__init__`, frozen for the client's lifetime. Internal terminal calls `httpx2.AsyncClient.send`, maps exceptions, raises `StatusError` on 4xx/5xx.
81+
2. **`AsyncClient ↔ ResponseDecoder`** — called when `response_model` is provided. Signature: `decode(content: bytes, model: type[T]) -> T`.
82+
3. **`httpware ↔ optional extras`** — each opt-in dependency imported only inside its dedicated module.
9283

9384
## Testing
9485

9586
- `pytest-asyncio` auto mode — async tests do NOT need `@pytest.mark.asyncio`.
9687
- Property-based tests (Hypothesis) for concurrency-sensitive code: `RetryBudget`, `Bulkhead`, retry interleaving. Files named `test_*_props.py`.
97-
- Tests for transport-level mocking use `RecordedTransport` (shipped with the library); not `respx`.
88+
- Tests inject `httpx2.MockTransport` via `AsyncClient(httpx2_client=httpx2.AsyncClient(transport=mock))`. No `respx`, no `RecordedTransport`.
9889

9990
## When in doubt
10091

101-
- Check [`docs/dev/engineering.md`](docs/dev/engineering.md) before adding a new module or extension point.
92+
- Check [`planning/engineering.md`](planning/engineering.md) before adding a new module or extension point.
10293
- Surface ambiguity as a documentation gap rather than improvising.

docs/dev/engineering.md

Lines changed: 0 additions & 185 deletions
This file was deleted.

0 commit comments

Comments
 (0)