Skip to content

Commit fe4df95

Browse files
lesnik512claude
andcommitted
chore: initial project scaffold (Story 1.1)
- uv init --lib scaffold with src/httpware/ package and py.typed marker - pyproject.toml: uv_build backend, src layout, httpx2>=2.0,<3.0 + pydantic v2 - Install extras: [msgspec], [otel], [niquests], [all] - Org conventions ported from modern-python/modern-di: - Justfile (install / lint / test / publish recipes) - .github/workflows/ci.yml (Python 3.11-3.14 matrix; ruff, ty, pytest) - [tool.ruff] (select=ALL, line-length=120, target=py311, modern-di ignore set) - [tool.pytest.ini_options] (asyncio_mode=auto, src/ pythonpath, coverage) - Dev/lint dep groups - Root files: LICENSE (MIT), SECURITY.md (90-day disclosure window), CONTRIBUTING.md, CHANGELOG.md, CLAUDE.md, context7.json - Type checker: ty (not mypy); Python 3.11+ floor; no from __future__ import annotations - Story 1.1 spec in docs/stories/ tracks task completion against AC Verified: ruff check ✓ ty check ✓ pytest ✓ (0 tests, scaffold story) uv build → wheel + sdist Clean-venv install: 12 deps resolved; httpx2 2.0.0, pydantic 2.13.4 import httpware → OK with empty __all__ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0 parents  commit fe4df95

14 files changed

Lines changed: 650 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: main
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request: {}
8+
9+
concurrency:
10+
group: ${{ github.head_ref || github.run_id }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
lint:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: extractions/setup-just@v2
19+
- uses: astral-sh/setup-uv@v3
20+
with:
21+
enable-cache: true
22+
cache-dependency-glob: "**/pyproject.toml"
23+
- run: uv python install 3.11
24+
- run: just install lint-ci
25+
26+
pytest:
27+
runs-on: ubuntu-latest
28+
strategy:
29+
fail-fast: false
30+
matrix:
31+
python-version:
32+
- "3.11"
33+
- "3.12"
34+
- "3.13"
35+
- "3.14"
36+
steps:
37+
- uses: actions/checkout@v4
38+
- uses: extractions/setup-just@v2
39+
- uses: astral-sh/setup-uv@v3
40+
with:
41+
enable-cache: true
42+
cache-dependency-glob: "**/pyproject.toml"
43+
- run: uv python install ${{ matrix.python-version }}
44+
- run: just install
45+
- run: just test --cov=src/httpware --cov-report xml
46+
- uses: codecov/codecov-action@v4.0.1
47+
env:
48+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
49+
with:
50+
files: ./coverage.xml
51+
flags: unittests
52+
name: codecov-${{ matrix.python-version }}

‎.gitignore‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generic things
2+
*.pyc
3+
*~
4+
__pycache__/*
5+
*.swp
6+
*.sqlite3
7+
*.map
8+
.vscode
9+
.idea
10+
.DS_Store
11+
.env
12+
.pytest_cache
13+
.ruff_cache
14+
.coverage
15+
htmlcov/
16+
coverage.xml
17+
pytest.xml
18+
dist/
19+
build/
20+
wheels/
21+
*.egg-info
22+
.python-version
23+
.venv
24+
uv.lock
25+
plan.md

‎CHANGELOG.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Changelog
2+
3+
All notable changes to `httpware` will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
9+
### Added
10+
11+
- Initial project scaffold: `src/httpware/` package, `py.typed` marker, `pyproject.toml` with `uv_build` backend.
12+
- Org conventions ported from `modern-python/modern-di`: `Justfile`, `.github/workflows/ci.yml`, `[tool.ruff]` config, `[tool.pytest.ini_options]`, dev and lint dep groups.
13+
- Declared dependencies: `httpx2>=2.0.0b1,<3.0`, `pydantic>=2.0,<3.0`.
14+
- Declared install extras: `[msgspec]`, `[otel]`, `[niquests]`, `[all]`.
15+
- `SECURITY.md` with 90-day private-disclosure window.
16+
- `CONTRIBUTING.md` with development workflow.
17+
- `CLAUDE.md` with AI-agent guidance.
18+
19+
[Unreleased]: https://github.com/modern-python/httpware/compare/HEAD...HEAD

‎CLAUDE.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.

‎CONTRIBUTING.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Contributing to httpware
2+
3+
Thank you for your interest in contributing. `httpware` is an open-source resilience-first async HTTP client framework for Python, maintained under the [`modern-python`](https://github.com/modern-python) org.
4+
5+
## Quick start
6+
7+
```bash
8+
git clone https://github.com/modern-python/httpware.git
9+
cd httpware
10+
just install # uv lock --upgrade && uv sync --all-extras --frozen --group lint
11+
just lint # ruff format + ruff check + ty check
12+
just test # pytest with coverage
13+
```
14+
15+
## Development workflow
16+
17+
1. **Open an issue first** for non-trivial changes — design discussion catches issues earlier than code review.
18+
2. **Branch from `main`**, use a descriptive name (`feat/retry-budget-jitter`, `fix/transport-cancel-leak`).
19+
3. **Run `just lint` and `just test`** locally before pushing. CI will reject changes that fail either.
20+
4. **Add tests** for any code change. Property-based tests (via Hypothesis) are required for concurrency-sensitive code (retry budget, bulkhead, retry interleaving).
21+
5. **Update `CHANGELOG.md`** in the `Unreleased` section.
22+
6. **Open a pull request** against `main`. PR titles use conventional-commits style (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`).
23+
24+
## Code style
25+
26+
- `ruff format` enforces formatting; do not hand-format.
27+
- Type-check with `ty` (Astral). Use `# ty: ignore[<rule>]` for suppressions, not `# type: ignore`.
28+
- Do NOT use `from __future__ import annotations`. Python 3.11+ is the floor.
29+
- Module docstrings are required; per-method docstrings only when types alone are insufficient.
30+
31+
See `docs/concepts/middleware.md` and `docs/recipes/custom-middleware.md` (once published) for architecture conventions.
32+
33+
## Architecture invariants
34+
35+
These are enforced by CI grep gates. Do not break them in pull requests:
36+
37+
- No `import httpx2` outside `src/httpware/transports/httpx2.py`.
38+
- No `httpx2._*` (private API) usage anywhere in the library.
39+
- No `from __future__ import annotations`.
40+
- No `print()` calls.
41+
- No `logging.basicConfig()` or bare `logging.getLogger()`.
42+
43+
## Code of Conduct
44+
45+
By participating in this project, you agree to abide by its Code of Conduct. Be excellent to one another.
46+
47+
## License
48+
49+
By contributing, you agree that your contributions will be licensed under the project's MIT license.

‎Justfile‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
default: install lint test
2+
3+
install:
4+
uv lock --upgrade
5+
uv sync --all-extras --frozen --group lint
6+
7+
lint:
8+
uv run eof-fixer .
9+
uv run ruff format
10+
uv run ruff check --fix
11+
uv run ty check
12+
13+
lint-ci:
14+
uv run eof-fixer . --check
15+
uv run ruff format --check
16+
uv run ruff check --no-fix
17+
uv run ty check
18+
19+
test *args:
20+
uv run --no-sync pytest {{ args }}
21+
22+
test-branch:
23+
@just test --cov-branch
24+
25+
publish:
26+
rm -rf dist
27+
uv version $GITHUB_REF_NAME
28+
uv build
29+
uv publish --token $PYPI_TOKEN

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Modern Python contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# httpware
2+
3+
[![Test](https://github.com/modern-python/httpware/actions/workflows/ci.yml/badge.svg)](https://github.com/modern-python/httpware/actions/workflows/ci.yml)
4+
[![PyPI version](https://badge.fury.io/py/httpware.svg)](https://pypi.org/project/httpware/)
5+
[![Python versions](https://img.shields.io/pypi/pyversions/httpware.svg)](https://pypi.org/project/httpware/)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7+
8+
**Resilience-first async HTTP client framework for Python.**
9+
10+
`httpware` is to Python what Polly is to .NET and resilience4j is to the JVM — a canonical resilience-first HTTP framework. The public API is transport-agnostic (the underlying client is `httpx2` by default, sitting behind a swappable `Transport` protocol). Retries, timeouts, bulkheads, and a Finagle-style **retry budget** ship as composable middleware. Tests use a `RecordedTransport` and never see the underlying client.
11+
12+
> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0. See [CHANGELOG.md](./CHANGELOG.md).
13+
14+
## Install
15+
16+
```bash
17+
pip install httpware
18+
```
19+
20+
Optional extras:
21+
22+
```bash
23+
pip install httpware[msgspec] # msgspec ResponseDecoder
24+
pip install httpware[otel] # OpenTelemetry instrumentation
25+
pip install httpware[all] # all of the above
26+
```
27+
28+
## Quickstart
29+
30+
```python
31+
from httpware import AsyncClient
32+
from pydantic import BaseModel
33+
34+
35+
class User(BaseModel):
36+
id: int
37+
name: str
38+
39+
40+
async def main() -> None:
41+
async with AsyncClient(base_url="https://api.example.com") as client:
42+
user = await client.get("/users/1", response_model=User)
43+
print(user.name)
44+
```
45+
46+
## Highlights
47+
48+
- **Transport-agnostic API.** No `httpx2` symbols leak through `httpware`. Swap to a different backend with one constructor argument.
49+
- **Onion middleware** with phase shortcuts (`@before_request`, `@after_response`, `@on_error`). Built-in middleware: `Retry`, `RetryBudget`, `Bulkhead`, `Timeout`, `Observability`.
50+
- **Retry budget by default** — token-bucket admission control (Finagle defaults). Caps retry storms before they happen.
51+
- **Pluggable validation.** Default pydantic decoder with cached `TypeAdapter`; msgspec decoder via extras; bring your own.
52+
- **`RecordedTransport` for tests.** A 3-line fixture replaces respx routes and transport-level mocking.
53+
- **Status-keyed exceptions** with plain fields (`status: int`, `body: bytes`, `headers`, `json`). No transport exception types in user code.
54+
- **First-class OpenTelemetry** instrumentation via `httpware[otel]`.
55+
56+
## Documentation
57+
58+
Full docs (in progress): https://httpware.readthedocs.io
59+
60+
## License
61+
62+
MIT — see [LICENSE](./LICENSE).

0 commit comments

Comments
 (0)