@@ -5,40 +5,65 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
55## Repository
66
77The Python counterpart to [ ` dexpace/java-sdk ` ] ( https://github.com/dexpace/java-sdk ) .
8- The architecture follows the same shape (pluggable I/O, immutable HTTP models,
9- pipeline steps, context promotion chain) but the public API uses Python idioms
10- — dataclasses instead of builder objects, Protocols instead of interfaces with
11- implementation modules, context managers instead of explicit close pairs.
8+ The architecture follows the same shape (immutable HTTP models, pipeline steps,
9+ context promotion chain) but the public API uses Python idioms — dataclasses
10+ instead of builder objects, Protocols instead of interfaces with implementation
11+ modules, context managers instead of explicit close pairs. The pluggable I/O
12+ seam that exists in the Java SDK was removed in this port: Python's ` bytes ` /
13+ ` bytearray ` / ` memoryview ` / ` BinaryIO ` cover the same surface natively, so
14+ bodies are modelled as typed Pythonic abstractions instead.
1215
1316## Conventions (enforced — match these when adding code)
1417
15- - ** Python 3.8 syntax.** No ` match ` (3.10+), no ` X | Y ` runtime unions, no ` Self `
16- (3.11+), no ` TypeAlias ` (3.10+), no PEP 695 syntax. Use
17- ` from __future__ import annotations ` at the top of every module so forward refs
18- and PEP 604 unions in annotations work without runtime cost.
19- - ** No runtime dependencies.** ` core ` ships against the standard library only. If
20- you reach for a third-party package, stop — model it as an adapter behind
21- ` IoProvider ` / ` HttpClient ` / ` Serde ` .
22- - ** Immutable data, no builders.** Models are ` @dataclass(frozen=True) ` ; mutate via
23- ` dataclasses.replace ` or the ` with_* ` helpers. Builders are a Java idiom —
24- Python's keyword and default arguments make them redundant noise.
25- - ** Protocol for SPIs, ABC for shared behaviour.** Structural duck-typed seams
26- (` HttpClient ` , ` Serde ` , ` PipelineStep ` ) are ` typing.Protocol ` . Types that ship
27- default methods (` Source ` , ` Sink ` , ` Span ` , ` CallContext ` ) are ` abc.ABC ` .
28- - ** Context managers for resources.** ` Source ` , ` Sink ` , ` Response ` , ` ResponseBody ` ,
29- ` CallContext ` , and ` TracingScope ` all implement ` __enter__ ` / ` __exit__ ` so
30- callers can ` with ...: ` and rely on deterministic cleanup.
31- - ** Thread-safety where stated.** ` Io ` and ` ContextStore ` are safe under
32- concurrent use; individual buffers / sources / sinks are not. ` Io.install_provider `
33- uses a ` threading.Lock ` ; per-context lookups rely on CPython's GIL for the
34- atomic dict ops and use the lock only for check-and-set sequences.
35- - ** Public API is narrow.** Helpers and concrete adapter classes are module-private
36- (leading underscore). The public surface for each subpackage is what its
37- ` __init__.py ` re-exports.
38- - ** No logging package dependency.** Use stdlib ` logging ` when needed; do not add
39- ` loguru ` or similar.
18+ - ** Python 3.12+.** Modern union syntax (` X | None ` ), built-in generics
19+ (` list[X] ` , ` dict[X, Y] ` , ` tuple[X, ...] ` ), ` Self ` for fluent returns, PEP
20+ 695 type parameters (` def f[T](x: T) -> T ` ) and ` type ` statement aliases
21+ where they fit. ` from __future__ import annotations ` at the top of every
22+ module so forward refs evaluate lazily.
23+ - ** ` mypy --strict ` clean.** Every public signature is typed; no ` Any ` in
24+ public API; no unused ` # type: ignore ` comments.
25+ - ** ` ruff ` and ` ruff format ` clean** (rule set in ` pyproject.toml ` ).
26+ - ** No runtime dependencies.** ` core ` ships against the standard library only.
27+ If you reach for a third-party package, stop — model it as an adapter
28+ behind ` HttpClient ` or ` Serde ` .
29+ - ** Immutable data with slots.** Models are
30+ ` @dataclass(frozen=True, slots=True) ` ; mutate via ` dataclasses.replace ` or
31+ the ` with_* ` helpers. Builders are a Java idiom — Python's keyword and
32+ default arguments make them redundant noise.
33+ - ** Protocol for SPIs, ABC for shared behaviour.** Structural duck-typed
34+ seams (` HttpClient ` , ` Serde ` , ` PipelineStep ` ) are ` typing.Protocol ` . Types
35+ that ship default methods (` RequestBody ` , ` ResponseBody ` , ` Span ` ,
36+ ` CallContext ` ) are ` abc.ABC ` .
37+ - ** Context managers for resources.** ` Response ` , ` ResponseBody ` ,
38+ ` CallContext ` , and ` TracingScope ` all implement ` __enter__ ` / ` __exit__ `
39+ so callers can ` with …: ` and rely on deterministic cleanup.
40+ - ** Bodies are Pythonic.** ` RequestBody ` produces bytes via
41+ ` iter_bytes(chunk_size) ` ; factories cover ` from_bytes ` / ` from_string ` /
42+ ` from_form ` / ` from_stream ` / ` from_iter ` / ` from_file ` . ` ResponseBody `
43+ exposes ` iter_bytes ` / ` bytes ` / ` string ` . Single-use bodies (stream /
44+ iter) raise ` RuntimeError ` on second consumption — call ` to_replayable() `
45+ before the first send if retries are needed.
46+ - ** Body capture for logging uses ` BytesIO ` .** ` LoggableRequestBody ` mirrors
47+ writes into a ` BytesIO ` tap; ` LoggableResponseBody ` caches drained bytes
48+ for repeatable reads. Both honour a configurable byte cap.
49+ - ** Thread-safety where stated.** ` ContextStore ` is safe under concurrent
50+ use; individual bodies and streams are not. Per-context lookups rely on
51+ CPython's GIL for atomic dict ops and use a lock only for check-and-set.
52+ - ** Public API is narrow.** Helpers and concrete adapter classes are
53+ module-private (leading underscore). The public surface for each subpackage
54+ is what its ` __init__.py ` re-exports.
55+ - ** ` __all__ ` declares the surface** on every module that exports anything;
56+ keep it accurate as new symbols land.
57+ - ** ` py.typed ` ** ships with the package (PEP 561) so downstream type-checkers
58+ consume our annotations.
59+ - ** No logging package dependency.** Use stdlib ` logging ` when needed; do
60+ not add ` loguru ` or similar.
61+ - ** Google-style docstrings.** One-line summary, blank line, then details
62+ with ` Args: ` / ` Returns: ` / ` Raises: ` / ` Yields: ` sections. Plain backticks
63+ for code/type references — no Sphinx ` :class: ` / ` :meth: ` cross-references.
64+ - ** Function-size cap: 50 lines.** Aim 10–25. Refactor when you push past.
4065- ** Commit style:** ` chore: ` for refactors/cleanup; ` feat: ` for new features;
41- ` fix: ` for bug fixes.
66+ ` fix: ` for bug fixes; ` docs: ` for documentation-only changes .
4267
4368## Repository Layout
4469
@@ -47,67 +72,77 @@ python-sdk/
4772├── LICENSE.md
4873├── README.md
4974├── pyproject.toml
50- └── src/dexpace/sdk/core/
51- ├── generics.py # Builder[T] Protocol (optional; not used by core models)
52- ├── io/ # Source/Sink/BufferedSource/BufferedSink/Buffer/IoProvider/Io
53- │ ├── source.py, sink.py
54- │ ├── buffered_source.py, buffered_sink.py
55- │ ├── buffer.py, io_provider.py, io.py
56- │ ├── tee_sink.py # mirror-write helper for body logging
57- │ └── default.py # DefaultIoProvider — bytearray + BinaryIO
58- ├── http/
59- │ ├── common/ # Headers, MediaType, common_media_types, Protocol
60- │ ├── request/ # Request, RequestBody, Method
61- │ ├── response/ # Response, ResponseBody, Status
62- │ └── context/ # CallContext, DispatchContext, RequestContext, ExchangeContext, ContextStore
63- ├── pipeline/
64- │ └── step/ # PipelineStep, RetryableStep, StepMetadata, RetryConfig
65- ├── client/ # HttpClient Protocol
66- ├── serde/ # Serde, Serializer, Deserializer Protocols
67- └── instrumentation/ # InstrumentationContext, Span, Tracer, TracingScope, noops
75+ ├── src/dexpace/sdk/core/
76+ │ ├── http/
77+ │ │ ├── common/ # Headers, HttpHeaderName, MediaType, Protocol, Url,
78+ │ │ │ # QueryParams, ETag, HttpRange, RequestConditions,
79+ │ │ │ # common_media_types
80+ │ │ ├── request/ # Request, RequestBody, FileRequestBody,
81+ │ │ │ # LoggableRequestBody, Method
82+ │ │ ├── response/ # Response, ResponseBody, LoggableResponseBody, Status
83+ │ │ └── context/ # CallContext, DispatchContext, RequestContext,
84+ │ │ # ExchangeContext, ContextStore
85+ │ ├── pipeline/
86+ │ │ └── step/ # PipelineStep, RetryableStep, StepMetadata, RetryConfig
87+ │ ├── client/ # HttpClient Protocol
88+ │ ├── serde/ # Serde, Serializer, Deserializer Protocols
89+ │ └── instrumentation/ # InstrumentationContext, Span, Tracer, TracingScope, noops
90+ └── tests/ # pytest suite — io/, http/, context/, pipeline/
6891```
6992
7093## Architecture — Big Picture
7194
72- The SDK is an ** HTTP-client toolkit, not an HTTP client** . It provides abstractions,
73- models, and pipelines; consuming libraries plug in a concrete transport via the
74- ` HttpClient ` Protocol and a concrete I/O implementation via the ` IoProvider ` ABC .
95+ The SDK is an ** HTTP-client toolkit, not an HTTP client** . It provides
96+ abstractions, models, and pipelines; consuming libraries plug in a concrete
97+ transport via the ` HttpClient ` Protocol .
7598
7699Layered, bottom-up:
77100
78- 1 . ** ` io/ ` contracts** — ` Source ` / ` Sink ` (primitive byte channels), ` BufferedSource `
79- / ` BufferedSink ` (typed reads/writes), ` Buffer ` (both + ` snapshot() ` for body
80- logging). ` IoProvider ` is the single factory seam; ` Io.install_provider(provider) `
81- wires it once. Missing provider raises ` RuntimeError ` with the install
82- instruction. ` DefaultIoProvider ` ships a working implementation backed by
83- ` bytearray ` and ` BinaryIO ` .
84- 2 . ** ` http.request ` / ` http.response ` / ` http.common ` ** — immutable frozen-dataclass
85- models. Non-destructive mutation via ` dataclasses.replace ` or ` with_* ` helpers.
86- ` RequestBody ` exposes ` is_replayable() ` and ` to_replayable(provider) ` ; classmethod
87- factories (` from_bytes ` , ` from_string ` , ` from_form ` , ` from_buffer ` ,
88- ` from_source ` ) cover the common shapes.
89- 3 . ** ` http.context ` ** — promotion chain ` DispatchContext ` → ` RequestContext ` →
90- ` ExchangeContext ` , all carrying an ` InstrumentationContext ` for tracing
101+ 1 . ** Bodies (` http.request.RequestBody ` / ` http.response.ResponseBody ` )** —
102+ typed abstractions for outgoing/incoming payloads. ` iter_bytes(chunk_size) `
103+ is the primary streaming surface; ` bytes() ` / ` string() ` for full reads.
104+ Bytes- and file-backed variants are replayable; stream- and iter-backed
105+ variants are single-use. ` Loggable* ` decorators wrap either side for
106+ diagnostic capture with a configurable cap.
107+ 2 . ** ` http.request ` / ` http.response ` / ` http.common ` ** — immutable
108+ ` @dataclass(frozen=True, slots=True) ` models. Non-destructive mutation
109+ via ` dataclasses.replace ` or the ` with_* ` helpers. The HTTP value layer
110+ includes ` Headers ` , ` HttpHeaderName ` (typed constants for IANA names),
111+ ` MediaType ` , ` Url ` / ` QueryParams ` , ` ETag ` , ` HttpRange ` ,
112+ ` RequestConditions ` .
113+ 3 . ** ` http.context ` ** — promotion chain ` DispatchContext ` → ` RequestContext `
114+ → ` ExchangeContext ` , all carrying an ` InstrumentationContext ` for tracing
91115 correlation. The thread-safe ` ContextStore ` is keyed by trace id; entries
92116 evict on ` CallContext.close() ` .
93- 4 . ** ` pipeline/step/PipelineStep ` ** — a ` Protocol ` with signature
94- ` (input, context) -> output ` . Steps compose into chains; ` RetryableStep ` adds
95- a retry hook. ` StepMetadata ` and ` RetryConfig ` provide optional configuration.
96- 5 . ** ` client/HttpClient ` ** — single-method ` Protocol ` (` execute(request) -> Response ` ).
97- Transport is ** not** provided by ` core ` .
117+ 4 . ** ` pipeline/step/PipelineStep ` ** — a Protocol with signature
118+ ` (input, context) -> output ` . Steps compose into chains; the provisional
119+ ` RetryableStep ` adds a retry hook (M2 replaces it with a ` Policy ` ABC
120+ that wraps the downstream chain — see ` to-implement.md ` ). ` StepMetadata `
121+ and ` RetryConfig ` provide optional configuration.
122+ 5 . ** ` client/HttpClient ` ** — single-method Protocol
123+ (` execute(request) -> Response ` ). Transport is ** not** provided by ` core ` .
98124
99125## Things That Will Bite You
100126
101- - ` Io.install_provider(...) ` must run before any code that calls ` Io.provider ` .
102- Tests that touch buffers should install ` DefaultIoProvider ` in their setup.
103- - The HTTP request/response models are frozen — mutate via ` dataclasses.replace `
104- or the ` with_* ` helpers, not by reassigning fields.
105- - The default ` Buffer ` is in-memory and unbounded. Don't drain a multi-GB response
106- body into a single buffer; stream via ` BufferedSource.input_stream() ` or read
107- in chunks.
108- - ` ResponseBody ` is single-use — once ` .bytes() ` / ` .string() ` / ` .source().read_* `
109- consumes the underlying source, the bytes are gone. Wrap with a logging body
110- if you need repeatable reads.
111- - ` Headers ` is case-insensitive but case-preserving on the wire form. Lookups
112- (` get ` , ` __contains__ ` ) compare names case-insensitively; iteration yields
113- the lower-cased canonical form.
127+ - The HTTP request/response models are frozen — mutate via
128+ ` dataclasses.replace ` or the ` with_* ` helpers, not by reassigning fields.
129+ Trying to assign raises ` dataclasses.FrozenInstanceError ` .
130+ - ` RequestBody.from_stream ` and ` from_iter ` are ** single-use** . The second
131+ ` iter_bytes ` call raises ` RuntimeError ` . Call ` to_replayable() ` before the
132+ first send if you need retries.
133+ - ` ResponseBody.bytes() ` / ` .string() ` consume and close the body. Wrap with
134+ ` LoggableResponseBody ` if repeatable reads are needed.
135+ - ` LoggableRequestBody.snapshot() ` / ` LoggableResponseBody.snapshot() ` are
136+ capped at ` max_capture_bytes ` (default = CPython's effective ` bytes `
137+ ceiling, ~ 2 GiB). The primary write path still receives the full payload;
138+ only the tap is truncated.
139+ - ` Headers ` is case-insensitive but stores names in lower-case canonical
140+ form. Lookups (` get ` , ` __contains__ ` ) compare names case-insensitively;
141+ iteration yields the lower-cased name. Pass ` HttpHeaderName ` instances
142+ directly to skip the ` .lower() ` step on the hot path.
143+ - The Java SDK's ` Io ` /` IoProvider ` seam intentionally does NOT exist here.
144+ Python's stdlib (` bytes ` , ` bytearray ` , ` memoryview ` , ` BytesIO ` ,
145+ ` BinaryIO ` ) is the contract. Don't reintroduce an Okio-style layer.
146+ - ` mypy ` is invoked as ` python3 -m mypy ` (config in ` pyproject.toml ` ).
147+ ` python_version = "3.12" ` because mypy 2.x requires it; the source still
148+ runs on whatever interpreter ≥ 3.12. Don't lower the floor.
0 commit comments