Skip to content

Latest commit

 

History

History
16 lines (12 loc) · 4.11 KB

File metadata and controls

16 lines (12 loc) · 4.11 KB

Decoders

A protocol seam is a documented internal boundary. AI agents and contributors must respect it — never cross a seam except through its protocol.

Seam B: Client/AsyncClientResponseDecoder list

Both clients take decoders: Sequence[ResponseDecoder] | None = None (a list, not a single instance) and dispatch via each decoder's can_decode(model) predicate. AsyncClient() / Client() do not raise on missing extras.

  • Where: src/httpware/client.pysrc/httpware/decoders/.
  • Contract: the client holds _decoders: tuple[ResponseDecoder, ...] composed at __init__ and frozen for the client's lifetime, plus a _decoder_resolver: _DecoderResolver (decoders/_resolver.py) wrapping that tuple — the synchronous Seam B orchestrator both clients share. The Protocol exposes two methods:
    • can_decode(model: type) -> bool — predicate used at send-time to walk _decoders and pick the first claiming decoder (_DecoderResolver.resolve). Built-in decoders claim broadly (pydantic via TypeAdapter(model) probe, msgspec via msgspec.inspect.type_info(model) + CustomType filter); list ordering decides ambiguous shared shapes (dataclass, primitive, generic). Native types of another library MUST be rejected. can_decode MUST NOT raise — it runs in _DecoderResolver.resolve, outside the _BoundDecoder.decode wrap, so a raising probe escapes the ClientError contract; a decoder that cannot decide must return False, not raise (the built-ins treat any probe failure as False). This is a documented obligation on implementers, not an enforced guard.
    • decode(content: bytes, model: type[T]) -> T — the decode itself. Any exception is wrapped by _BoundDecoder.decode — the bound decoder resolve returns, sealing decoder + model together — into httpware.DecodeError (a ClientError subclass carrying response, model, original); this runs for send / send_with_response on both clients when response_model= is set. Decoder implementers do not need to raise DecodeError directly.
  • Pre-flight check: when response_model= is set and no decoder claims it, _DecoderResolver.resolve (called by send / send_with_response before _dispatch) raises MissingDecoderError(model=..., registered_names=...) BEFORE the HTTP call. MissingDecoderError is a sibling of DecodeError under ClientError, and is distinct from it: DecodeError means the decoder ran and the payload was malformed; the two have distinct corrective actions (install an extra or pass decoders=[...]).
  • Default list: decoders=None resolves via client.py:_build_default_decoders() against installed extras — pydantic-first when both are present, either-only when only one is installed, empty tuple when neither. AsyncClient() / Client() never raise on missing extras; failure surfaces only at the first response_model= use site.
  • Rule: the decoder must operate on raw bytes in a single parse pass. Two-pass decoding (json.loads then validate_python) is rejected: a single bytes-in / typed-object-out pass avoids the redundant intermediate dict allocation and parses faster. The Pydantic adapter implements this as TypeAdapter(model).validate_json(content), with the TypeAdapter cached per-instance on PydanticDecoder._adapters: dict[type, TypeAdapter] (populated lazily on first _get_adapter() call); the msgspec adapter mirrors the pattern with MsgspecDecoder._msgspec_decoders: dict[type, msgspec.json.Decoder]. Cache lifetime matches the decoder/client, not the process — no module-level state, no autouse cache-clear fixtures in tests. Both built-ins' _get_adapter/_get_msgspec_decoder and can_decode share one memoizing helper (decoders/_caching.py:_get_or_build).
  • Unhashable models: a model that isn't hashable (e.g. Annotated[X, unhashable_metadata]) bypasses the cache entirely — every call builds fresh, uncached. can_decode's verdict reflects true buildability regardless of hashability: an unhashable model that the underlying library can actually build a schema/decoder for returns True, not False. Hashability is orthogonal to whether a model can be decoded.