Skip to content

Commit 1222c64

Browse files
authored
Merge pull request #21 from modern-python/feat/v0.3-pydantic-optional
feat: pydantic as an optional extra (0.3.0)
2 parents f6af165 + 9be5c97 commit 1222c64

14 files changed

Lines changed: 1693 additions & 29 deletions

README.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,25 @@
77

88
**Async HTTP client framework for Python.**
99

10-
`httpware` is a typed, async HTTP client library with a protocol-based seam so the transport is swappable (`httpx2` ships as the default). Middleware composes via an onion model. Pydantic and msgspec response decoding ship out of the box. `RecordedTransport` replaces `respx` for transport-level tests.
10+
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request`/`httpx2.Response`, adds a middleware chain composed at client construction, supports opt-in typed response decoding (pydantic and msgspec are both extras), and raises a status-keyed exception tree automatically on 4xx/5xx.
1111

12-
> **Status:** Pre-1.0 (0.1.0 alpha). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped.
12+
> **Status:** Pre-1.0 (0.3.0). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped.
1313
1414
## Install
1515

1616
```bash
17-
pip install httpware
17+
pip install httpware # core only — no decoder
18+
pip install httpware[pydantic] # + PydanticDecoder (the default-decoder path)
19+
pip install httpware[msgspec] # + MsgspecDecoder
20+
pip install httpware[all] # everything declared above (pydantic, msgspec, otel)
1821
```
1922

20-
Optional extras:
21-
22-
```bash
23-
pip install httpware[msgspec] # MsgspecDecoder
24-
```
25-
26-
(`otel`, `niquests`, and `all` extras are declared; integrations have not shipped yet.)
23+
`AsyncClient()` with no `decoder=` argument defaults to constructing a `PydanticDecoder`; that path requires the `pydantic` extra and raises `ImportError` at `AsyncClient.__init__` if it is missing. The `otel` extra is declared but the OpenTelemetry middleware (Epic 5) has not shipped yet.
2724

2825
## Quickstart
2926

27+
> Requires: `pip install httpware[pydantic]`
28+
3029
```python
3130
from httpware import AsyncClient
3231
from pydantic import BaseModel

planning/engineering.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This doc is the single distilled reference for `httpware` design rationale, prot
44

55
## 1. Project intent
66

7-
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request` and `httpx2.Response` as the public request/response surface and adds three things on top: typed response decoding (via a `ResponseDecoder` protocol; Pydantic ships as the default, msgspec as an opt-in extra), a middleware chain composed at client construction, and a status-keyed exception tree raised automatically on 4xx and 5xx.
7+
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request` and `httpx2.Response` as the public request/response surface and adds three things on top: typed response decoding (via a `ResponseDecoder` protocol; pydantic and msgspec are both opt-in extras as of 0.3.0), a middleware chain composed at client construction, and a status-keyed exception tree raised automatically on 4xx and 5xx. `AsyncClient(decoder=None)` defaults to constructing a `PydanticDecoder` and so requires the `pydantic` extra; callers can supply an explicit `decoder=` argument to escape the default.
88

99
The 0.1.0 release attempted to own a full abstraction over the underlying HTTP client. v0.2 walks that back: `httpx2` is part of the public surface.
1010

@@ -43,6 +43,7 @@ The 0.1.0 seams numbered 1 (Middleware↔Transport) and 4 (Transport↔httpx2) h
4343
- **Where:** `pyproject.toml` extras (`[project.optional-dependencies]`) ↔ the adapter modules that import them.
4444
- **Contract:** each optional dependency is imported only inside its own dedicated module (e.g., `pydantic` in `decoders/pydantic.py`; `msgspec` in `decoders/msgspec.py`; `opentelemetry` in `middleware/observability/otel.py` when Epic 5 lands).
4545
- **Rule:** never import an extra at package top-level. The package must import cleanly when the extra is not installed.
46+
- **Verification:** `tests/test_optional_extras_isolation.py` runs a fresh-subprocess `import httpware` and asserts that neither pydantic nor msgspec ends up in `sys.modules`. New extras must add the same isolation test.
4647

4748
## 4. Exception contract
4849

@@ -102,7 +103,7 @@ msgspec = ["msgspec>=0.18"]
102103
otel = ["opentelemetry-api>=1.20", "opentelemetry-sdk>=1.20"]
103104
```
104105

105-
Each extra's code lives in a single dedicated module (e.g., `decoders/pydantic.py`, `decoders/msgspec.py`, `middleware/observability/otel.py`). The `import` of the extra happens **inside** that module — never at package top level. This way, `import httpware` works cleanly without the extras installed, and the seam stays observable: grep for `import pydantic` should return exactly one file.
106+
Each extra's code lives in a single dedicated module (`decoders/pydantic.py`, `decoders/msgspec.py`, `middleware/observability/otel.py` when Epic 5 lands). The `import` of the extra happens **inside** that module behind an `is_<extra>_installed` guard from `_internal/import_checker.py` — never at package top level. This way, `import httpware` works cleanly without the extras installed, and the seam stays observable: `grep -rnE 'from pydantic|import pydantic' src/httpware/ | grep -v import_checker` returns exactly one indented line (the guarded import in `decoders/pydantic.py`), and the same is true for `msgspec`.
106107

107108
Caller-facing pattern: consumers select the implementation by passing it explicitly, e.g., `AsyncClient(decoder=PydanticDecoder())`. There is no auto-detection or implicit registry.
108109

0 commit comments

Comments
 (0)