|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +Guidance for AI agents (Claude Code, etc.) working in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 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. |
| 8 | + |
| 9 | +**Source-of-truth planning artifacts** live in the sibling `base-client` repo (until httpware is fully self-hosted): |
| 10 | + |
| 11 | +- `base-client/docs/prd.md` — Product Requirements Document (47 FRs, 25 NFRs) |
| 12 | +- `base-client/docs/architecture.md` — 12 architectural decisions, 5 protocol seams, full module layout |
| 13 | +- `base-client/docs/epics.md` — 6 epics, 32 stories with Given/When/Then acceptance criteria |
| 14 | + |
| 15 | +Stories under active dev live in `httpware/docs/stories/`. |
| 16 | + |
| 17 | +## Commands |
| 18 | + |
| 19 | +This project uses `just` (task runner) and `uv` (package manager). |
| 20 | + |
| 21 | +```bash |
| 22 | +just install # uv lock --upgrade && uv sync --all-extras --frozen --group lint |
| 23 | +just lint # eof-fixer + ruff format + ruff check --fix + ty check |
| 24 | +just lint-ci # same checks without auto-fixing (used in CI) |
| 25 | +just test # uv run pytest (with coverage by default) |
| 26 | +just test-branch # pytest with branch coverage |
| 27 | +``` |
| 28 | + |
| 29 | +`just test` passes extra args to pytest: |
| 30 | + |
| 31 | +```bash |
| 32 | +just test tests/test_client.py |
| 33 | +just test tests/test_client.py -k test_get_returns_typed_response |
| 34 | +``` |
| 35 | + |
| 36 | +Without `just`: |
| 37 | + |
| 38 | +```bash |
| 39 | +uv run ruff format . && uv run ruff check . --fix && uv run ty check |
| 40 | +uv run pytest |
| 41 | +``` |
| 42 | + |
| 43 | +## Architecture invariants (CI-enforced) |
| 44 | + |
| 45 | +These are non-negotiable. CI rejects PRs that violate them. |
| 46 | + |
| 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. |
| 49 | +- **No `from __future__ import annotations`**: Python 3.11+ floor; PEP 604/585 syntax is native. |
| 50 | +- **No `print()`**: enforced by ruff. |
| 51 | +- **No global logging config**: no `logging.basicConfig()`, no bare `logging.getLogger()`. Acquire `logging.getLogger("httpware")` or `logging.getLogger(f"httpware.{module}")` only. |
| 52 | +- **Type suppressions**: use `# ty: ignore[<rule>]`, never `# type: ignore` or `# mypy: ignore`. |
| 53 | + |
| 54 | +## Code conventions |
| 55 | + |
| 56 | +- **Modules**: `snake_case` (`client.py`, `request.py`, `transports/httpx2.py`). |
| 57 | +- **Classes**: `PascalCase`. `Http` is two letters: `Httpx2Transport`, not `HTTPX2Transport`. |
| 58 | +- **Methods**: `snake_case`. No `a` prefix on async methods (match `httpx2`); `aclose()` is the sole exception. |
| 59 | +- **Private symbols**: `_leading_underscore`. Cross-module private code lives in `_internal/`. |
| 60 | +- **Imports**: absolute paths inside `src/httpware/`; relative imports only within the same subpackage. |
| 61 | +- **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`. |
| 63 | + |
| 64 | +## Module layout |
| 65 | + |
| 66 | +``` |
| 67 | +src/httpware/ |
| 68 | +├── __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 |
| 77 | +├── _internal/ # private cross-module helpers |
| 78 | +└── py.typed |
| 79 | +``` |
| 80 | + |
| 81 | +Story 1.1 ships only the scaffold; subsequent stories add modules. |
| 82 | + |
| 83 | +## Protocol seams |
| 84 | + |
| 85 | +Five documented internal boundaries. AI agents must respect them — never cross a seam except through its documented protocol. |
| 86 | + |
| 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. |
| 92 | + |
| 93 | +## Testing |
| 94 | + |
| 95 | +- `pytest-asyncio` auto mode — async tests do NOT need `@pytest.mark.asyncio`. |
| 96 | +- 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`. |
| 98 | + |
| 99 | +## When in doubt |
| 100 | + |
| 101 | +- Check the architecture document (`base-client/docs/architecture.md`) before adding a new module or extension point. |
| 102 | +- Surface ambiguity as a documentation gap rather than improvising. |
0 commit comments