Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ These are non-negotiable. CI rejects PRs that violate them.
- **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 errors take a single positional `response: httpx2.Response`. Subclasses do not override `__init__`. All fields available via `exc.response.*`.
- **Exception construction**: status-keyed `StatusError` subclasses (the 4xx/5xx tree) take a single positional `response: httpx2.Response` and do NOT override `__init__` — all fields via `exc.response.*`. This rule scopes to `StatusError` only; non-status `ClientError` subclasses such as `DecodeError`, `MissingDecoderError`, `BulkheadFullError`, and `RetryBudgetExhaustedError` deliberately define `__init__` with keyword-only fields. See `engineering.md` §4.

## Module layout

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ with Client(base_url="https://example.test") as client:
print(response.json())
```

Typed decoding via `response_model=` works in both worlds — requires `pip install httpware[pydantic]`. Decode failures (malformed body, schema mismatch) raise `httpware.DecodeError`, a `ClientError` subclass — so `except httpware.ClientError` covers them alongside transport and status errors.
Typed decoding via `response_model=` works in both worlds — install either `pip install httpware[pydantic]` or `pip install httpware[msgspec]` (or both; pydantic is tried first when both are present). Decode failures (malformed body, schema mismatch) raise `httpware.DecodeError`, a `ClientError` subclass — so `except httpware.ClientError` covers them alongside transport and status errors.

```python
from httpware import AsyncClient
Expand Down
11 changes: 8 additions & 3 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,15 @@ Raised by `send()` / `send_with_response()` / verb methods when `response_model=
- `model: type` — the `response_model=` value that wasn't claimed.
- `registered_names: tuple[str, ...]` — class names of the registered decoders that all rejected the model. Empty tuple means no decoders were registered.

Corrective action depends on the message hint:
The message reads `no decoder for response_model=<Model>: <hint>`, and the corrective action depends on the hint. The two hints, verbatim:

- `no decoders registered. Install pip install httpware[pydantic] or pip install httpware[msgspec], or pass decoders=[...] explicitly.` — install an extra or pass an explicit decoder list.
- `registered decoders (PydanticDecoder + MsgspecDecoder) all rejected it.` — your `response_model` type is exotic enough that neither built-in claims it. Pass a custom `ResponseDecoder` via `decoders=[...]`.
- **No decoders were registered** — install an extra or pass an explicit decoder list:

no decoders registered. Install `pip install httpware[pydantic]` or `pip install httpware[msgspec]`, or pass decoders=[...] explicitly.

- **Registered decoders all rejected the model** — your `response_model` type is exotic enough that neither built-in claims it; pass a custom `ResponseDecoder` via `decoders=[...]`:

registered decoders (PydanticDecoder + MsgspecDecoder) all rejected it. Pass a custom decoder via decoders=[...].

Unlike `DecodeError`, this error fires *before* the HTTP request — no traffic is sent.

Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ decoders claim broadly within their library; the ordering encodes your
preference for shared shapes (`dict`, `list[Foo]`, dataclasses, primitives):

```python
from httpware import AsyncClient
from httpware.decoders.msgspec import MsgspecDecoder
from httpware.decoders.pydantic import PydanticDecoder

# pydantic-first (the default when both extras are installed):
# - BaseModel -> pydantic
# - Struct -> msgspec
Expand Down
8 changes: 4 additions & 4 deletions planning/deferred-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ As of 0.7.0, all planned epics (3, 4, 5, 6) are closed — see [`engineering.md`

## Open

### Decoder-side

- **`_get_adapter` `lru_cache` is module-global, not per-decoder instance** — keyed by `model` only; two `PydanticDecoder()` instances with different configurations (none today) would share adapters, and the cache survives across tests unless explicitly cleared. Revisit if/when a configurable `PydanticDecoder(mode=..., strict=...)` lands. No such configurability is on the roadmap, so this item is dormant unless a real use-case surfaces. (`src/httpware/decoders/pydantic.py:12-14`)

### Client API surface

- **Per-verb-with-response siblings** (`get_with_response`, `post_with_response`, `request_with_response`) — the v0.8.2 spec deliberately ships only `send_with_response`; the verb-method shape would add ~400 LOC of overload boilerplate per side for a pattern (response headers + typed body) that's almost always paired with a GET and `build_request`. Revisit if a concrete consumer demand surfaces. (`src/httpware/client.py`)

## Resolved

- **Decoder caches are now per-instance** (was: *`_get_adapter` `lru_cache` is module-global*) — PR #42 (0.9.0) replaced the module-level `@functools.lru_cache` with per-instance `_adapters` / `_msgspec_decoders` dicts on each decoder, so configurations no longer share state across instances and caches don't survive across tests. 0.9.1 added a per-instance `can_decode` verdict cache on the same model. No module-global decoder cache remains.
2 changes: 1 addition & 1 deletion planning/engineering.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This doc is the single distilled reference for `httpware` design rationale, prot

## 1. Project intent

`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. As of 0.9.0, both clients take `decoders: Sequence[ResponseDecoder] | None = None` (a *list*, not a single instance) and dispatch via each decoder's `can_decode(model)` predicate; the default resolves against installed extras (pydantic-first when both present) and `AsyncClient()` / `Client()` no longer raise on missing extras. A new `MissingDecoderError` (sibling of `DecodeError` under `ClientError`) fires before the HTTP call when `response_model=` is set but no registered decoder claims the model. As of 0.4.0, the package ships a small resilience suite under `httpware.middleware.resilience` — a `Retry` middleware with a Finagle-style `RetryBudget`, plus a `Bulkhead` concurrency limiter — composed via the standard middleware chain. As of 0.5.0, `AsyncClient.stream()` provides a context-manager API for chunked response bodies; it bypasses the middleware chain by design (see planning/archive/specs/2026-06-05-streaming-design.md). As of 0.6.0, `Retry` and `Bulkhead` emit operational events via stdlib `logging` records (`httpware.retry` / `httpware.bulkhead` loggers) and — when `opentelemetry-api` is installed — OpenTelemetry span events on the active span. As of 0.7.0, the first-cut user-docs surface is live at <https://httpware.readthedocs.io/> (Middleware, Resilience, Errors, Testing guides) and Epic 3 is closed.
`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. As of 0.9.0, both clients take `decoders: Sequence[ResponseDecoder] | None = None` (a *list*, not a single instance) and dispatch via each decoder's `can_decode(model)` predicate; the default resolves against installed extras (pydantic-first when both present) and `AsyncClient()` / `Client()` no longer raise on missing extras. A new `MissingDecoderError` (sibling of `DecodeError` under `ClientError`) fires before the HTTP call when `response_model=` is set but no registered decoder claims the model. As of 0.4.0, the package ships a small resilience suite under `httpware.middleware.resilience` — a `Retry` middleware with a Finagle-style `RetryBudget`, plus a `Bulkhead` concurrency limiter — composed via the standard middleware chain. As of 0.5.0, `AsyncClient.stream()` provides a context-manager API for chunked response bodies; it bypasses the middleware chain by design (see planning/archive/specs/2026-06-05-streaming-design.md). As of 0.6.0, `Retry` and `Bulkhead` emit operational events via stdlib `logging` records (`httpware.retry` / `httpware.bulkhead` loggers) and — when `opentelemetry-api` is installed — OpenTelemetry span events on the active span. As of 0.7.0, the first-cut user-docs surface is live at <https://httpware.modern-python.org/> (Middleware, Resilience, Errors, Testing guides) and Epic 3 is closed.

As of 0.8.0 the async middleware surface uses the `Async*`/`async_*` prefix (aligning with httpx2's convention); the `attempt_timeout=` kwarg was removed from `AsyncRetry` in the same release — see `planning/specs/2026-06-07-sync-client-design.md` for the rationale.

Expand Down