66[ ![ Ruff] ( https://img.shields.io/badge/lint-ruff-blue.svg )] ( https://docs.astral.sh/ruff/ )
77[ ![ License: MIT] ( https://img.shields.io/badge/license-MIT-green.svg )] ( LICENSE.md )
88
9- A toolkit for building Python HTTP client libraries — immutable models, a
10- stage-based pipeline runtime, pluggable transports, and an authentication
11- pillar including OAuth bearer tokens and RFC 7616 Digest. Targets Python
12- 3.12+, ships strict type annotations ( ` mypy --strict ` ), and keeps the core
13- distribution dependency-free .
14-
15- The SDK is ** not** an HTTP client. It defines the contracts ( ` HttpClient ` ,
16- ` AsyncHttpClient ` ) and supplies models, policies, and observability hooks;
17- transport packages bring the actual networking. Pick the transport that
18- matches your dependency constraints (or write your own — the Protocol is
19- six lines) .
9+ A toolkit for building Python HTTP client libraries. It provides immutable
10+ request and response models, a staged policy pipeline, pluggable transports,
11+ and an authentication pillar that speaks OAuth bearer tokens and RFC 7616
12+ Digest. Everything is typed end to end under ` mypy --strict ` and targets
13+ Python 3.12 or later .
14+
15+ The SDK is deliberately not an HTTP client. It defines the contracts
16+ ( ` HttpClient ` , ` AsyncHttpClient ` ) and supplies the models, policies, and
17+ observability hooks that surround them; the networking itself arrives
18+ through a transport package of your choosing. Pick the adapter that fits
19+ your dependency budget, or write your own: the Protocol is six lines.
2020
2121## Packages
2222
2323The repository is a [ ` uv ` ] ( https://docs.astral.sh/uv/ ) -managed workspace of
24- five distributions sharing the ` dexpace.sdk.* ` namespace:
24+ five distributions sharing the ` dexpace.sdk.* ` namespace. Each transport
25+ package depends on ` dexpace-sdk-core ` and exactly one HTTP library.
2526
26- | Package | Purpose | Runtime dependencies |
27- | ---------------------------------- | ------------------------------------------------------------------- | --- --------------------------|
28- | ` dexpace-sdk-core ` | Toolkit: models , pipeline, policies, auth, observability | None (stdlib only) |
29- | ` dexpace-sdk-http-stdlib ` | ` UrllibHttpClient ` (sync) + ` AsyncioHttpClient ` (async) | ` dexpace-sdk-core ` |
30- | ` dexpace-sdk-http-httpx ` | ` HttpxHttpClient ` (sync) + ` AsyncHttpxHttpClient ` | ` httpx ` |
31- | ` dexpace-sdk-http-aiohttp ` | ` AiohttpHttpClient ` (async only ) | ` aiohttp ` |
32- | ` dexpace-sdk-http-requests ` | ` RequestsHttpClient ` (sync only ) | ` requests ` |
27+ | Package | Provides | Third-party dependencies |
28+ | -----------------------------| -----------------------------------------------------------| --------------------------|
29+ | ` dexpace-sdk-core ` | Models , pipeline, policies, auth, observability | ` furl ` |
30+ | ` dexpace-sdk-http-stdlib ` | ` UrllibHttpClient ` (sync), ` AsyncioHttpClient ` (async) | none (stdlib only) |
31+ | ` dexpace-sdk-http-httpx ` | ` HttpxHttpClient ` (sync), ` AsyncHttpxHttpClient ` (async) | ` httpx ` |
32+ | ` dexpace-sdk-http-aiohttp ` | ` AiohttpHttpClient ` (async) | ` aiohttp ` |
33+ | ` dexpace-sdk-http-requests ` | ` RequestsHttpClient ` (sync) | ` requests ` |
3334
34- End-users install only the transport they need:
35+ Install the core plus whichever transport you need:
3536
3637``` bash
3738pip install dexpace-sdk-core dexpace-sdk-http-httpx
3839```
3940
40- ## What's in ` dexpace-sdk-core `
41-
42- | Subpackage | Surface |
43- | ---------------------------------| ----------------------------------------------------------------------------------------------------------|
44- | ` http.request ` | ` Request ` , ` RequestBody ` , ` FileRequestBody ` , ` LoggableRequestBody ` , ` MultipartRequestBody ` , ` Method ` |
45- | ` http.response ` | ` Response ` , ` AsyncResponse ` , ` ResponseBody ` , ` AsyncResponseBody ` , ` LoggableResponseBody ` , ` Status ` |
46- | ` http.common ` | ` Headers ` , ` HttpHeaderName ` , ` MediaType ` , ` Protocol ` , ` Url ` , ` QueryParams ` , ` ETag ` , ` HttpRange ` , ` RequestConditions ` , paging primitives |
47- | ` http.context ` | ` CallContext ` → ` DispatchContext ` → ` RequestContext ` → ` ExchangeContext ` chain, ` ContextStore ` |
48- | ` http.auth ` | ` BearerTokenPolicy ` , ` BasicAuthPolicy ` , ` KeyCredentialPolicy ` , ` DigestChallengeHandler ` , RFC 7235 challenge parser, ` TokenCache ` |
49- | ` http.sse ` | ` SseParser ` for Server-Sent Events streams |
50- | ` pipeline ` | ` Pipeline ` , ` AsyncPipeline ` , ` Policy ` ABC, ` Stage ` enum, ` StagedPipelineBuilder ` , ` default_pipeline() ` |
51- | ` pipeline.policies ` | ` RetryPolicy ` , ` RedirectPolicy ` , ` SetDatePolicy ` , ` LoggingPolicy ` , ` TracingPolicy ` (+ async twins) |
52- | ` client ` | ` HttpClient ` and ` AsyncHttpClient ` Protocols |
53- | ` serde ` | ` Serde ` , ` Serializer ` , ` Deserializer ` Protocols + ` JsonSerde ` reference impl |
54- | ` instrumentation ` | ` ClientLogger ` , ` UrlRedactor ` , ` Tracer ` , ` Span ` , ` InstrumentationContext ` , noop singletons |
55- | ` errors ` | ` SdkError ` hierarchy: ` ServiceRequestError ` , ` ServiceResponseError ` , ` HttpResponseError[ModelT] ` , … |
56- | ` util ` | ` Clock ` , ` AsyncClock ` , ` ProxyOptions ` |
57- | ` config ` | ` Configuration ` (layered env-var + override lookup) + ` ConfigurationBuilder ` |
58-
5941## Quick start
6042
61- ### Workspace setup (contributors)
62-
63- ``` bash
64- git clone https://github.com/dexpace/python-sdk.git
65- cd python-sdk
66- uv sync
67- ```
68-
69- ` uv sync ` provisions a virtualenv with every workspace package installed in
70- editable mode plus the dev toolchain (` pytest ` , ` mypy ` , ` ruff ` ). All
71- commands below run via ` uv run … ` .
72-
7343### A minimal request
7444
7545``` python
@@ -109,8 +79,8 @@ with UrllibHttpClient() as client, client.execute(request) as response:
10979### A configured pipeline
11080
11181` default_pipeline() ` returns a ` StagedPipelineBuilder ` pre-wired with the
112- canonical policy stack (redirect → retry → set-date → logging → tracing).
113- Add authentication and customise as needed :
82+ canonical policy stack (redirect, retry, set-date, logging, tracing). Add
83+ authentication and adjust whatever the defaults get wrong for you :
11484
11585``` python
11686from dexpace.sdk.core.http.auth import BearerTokenPolicy
@@ -128,10 +98,10 @@ with pipeline:
12898 response = pipeline.run(request, dispatch_context)
12999```
130100
131- ` StagedPipelineBuilder ` enforces stage-based ordering and pillar
132- constraints (one redirect policy, one retry policy, one auth policy)
133- through type-checked surgical edits — ` replace ` , ` insert_after ` ,
134- ` insert_before ` , ` remove ` operate by policy class.
101+ The builder enforces stage ordering and pillar constraints (one redirect
102+ policy, one retry policy, one auth policy) and supports surgical,
103+ type-checked edits: ` replace ` , ` insert_after ` , ` insert_before ` , and
104+ ` remove ` all operate by policy class.
135105
136106### Streaming and replayable bodies
137107
@@ -142,97 +112,126 @@ from dexpace.sdk.core.http.request import RequestBody
142112RequestBody.from_stream(open (" payload.bin" , " rb" ))
143113RequestBody.from_iter([b " chunk-1" , b " chunk-2" ])
144114
145- # Replayable (transport may use zero-copy sendfile)
115+ # Replayable (transports may use zero-copy sendfile)
146116RequestBody.from_file(" upload.bin" )
147117
148118# Convert any single-use body into a replayable one before retrying
149119replayable = body.to_replayable()
150120```
151121
152- ` RetryPolicy ` automatically buffers single-use bodies before the first
153- attempt when retries are configured .
122+ When retries are configured, ` RetryPolicy ` buffers single-use bodies
123+ before the first attempt so a retry never finds the stream already drained .
154124
155125## Architecture
156126
157- The pipeline runs request flow through ordered ` Policy ` instances; each
158- policy can mutate the request, invoke the downstream chain, and
159- post-process the response . The terminal ` Policy ` is wired to an
160- ` HttpClient ` transport.
127+ A request flows through ordered ` Policy ` instances. Each policy may rewrite
128+ the request, invoke the chain below it , and post-process the response on
129+ the way back up . The terminal policy hands the request to an ` HttpClient `
130+ transport.
161131
162132```
163133caller → Pipeline → REDIRECT → RETRY → SET_DATE → AUTH → LOGGING → POST_LOGGING → HttpClient → wire
164134 (pillar) (pillar) (pillar) (pillar)
165135```
166136
167- Stage ordering is enforced by ` Stage ` , an ` IntEnum ` with sparse 100- apart
168- values that leave room for new stages without renumbering. Pillar stages
169- admit at most one policy; non-pillar stages stack with deque semantics. The
170- list-form ` Pipeline(client, policies=[...]) ` constructor remains available
171- for callers who want explicit ordering .
137+ Ordering is governed by ` Stage ` , an ` IntEnum ` whose values sit 100 apart so
138+ new stages can land without renumbering. Pillar stages admit a single
139+ policy; non-pillar stages stack with deque semantics. Callers who prefer
140+ explicit ordering can still use the list form,
141+ ` Pipeline(client, policies=[...]) ` .
172142
173- Layered, bottom -up:
143+ Bottom -up, the layers are :
174144
1751451 . ** Bodies.** ` RequestBody.iter_bytes(chunk) ` produces bytes on demand;
176- ` ResponseBody.iter_bytes ` / ` bytes() ` / ` string() ` consume them.
146+ ` ResponseBody.iter_bytes ` , ` bytes() ` , and ` string() ` consume them.
177147 Stream-backed variants are single-use; bytes-backed and file-backed
178- variants are replayable.
179- 2 . ** Models.** ` Request ` , ` Response ` , ` Headers ` , ` Url ` , etc. are
180- ` @dataclass(frozen=True, slots=True) ` . Mutate via ` dataclasses.replace `
181- or ` with_* ` helpers.
182- 3 . ** Context.** ` DispatchContext ` → ` RequestContext ` → ` ExchangeContext `
183- carry the ` InstrumentationContext ` (trace id, span id, flags) and are
184- registered in the thread-safe ` ContextStore ` keyed by trace id.
185- 4 . ** Pipeline.** ` Policy ` ABC + ` Stage ` enum. Policies declare
186- ` STAGE: ClassVar[Stage] ` (enforced at class-creation via
187- ` __init_subclass__ ` ) and slot into ` StagedPipelineBuilder ` accordingly.
148+ variants replay freely.
149+ 2 . ** Models.** ` Request ` , ` Response ` , ` Headers ` , ` Url ` , and their kin are
150+ ` @dataclass(frozen=True, slots=True) ` . Mutation happens through
151+ ` dataclasses.replace ` or the ` with_* ` helpers, never in place.
152+ 3 . ** Context.** ` DispatchContext ` promotes to ` RequestContext ` and then
153+ ` ExchangeContext ` , carrying an ` InstrumentationContext ` (trace id, span
154+ id, flags) throughout. The thread-safe ` ContextStore ` indexes live
155+ contexts by trace id.
156+ 4 . ** Pipeline.** The ` Policy ` ABC pairs with the ` Stage ` enum. Each policy
157+ declares ` STAGE: ClassVar[Stage] ` , checked at class creation via
158+ ` __init_subclass__ ` , and slots into the builder accordingly.
1881595 . ** Client.** ` HttpClient.execute(request) → Response ` and its async twin
189- are the only transport contracts. Concrete transports live in separate
160+ are the only transport contracts. Concrete transports live in their own
190161 distributions.
191162
163+ ## Inside the core
164+
165+ | Subpackage | Surface |
166+ | ---------------------| ----------------------------------------------------------------------------------------------------------|
167+ | ` http.request ` | ` Request ` , ` RequestBody ` , ` FileRequestBody ` , ` LoggableRequestBody ` , ` MultipartRequestBody ` , ` Method ` |
168+ | ` http.response ` | ` Response ` , ` AsyncResponse ` , ` ResponseBody ` , ` AsyncResponseBody ` , ` LoggableResponseBody ` , ` Status ` |
169+ | ` http.common ` | ` Headers ` , ` HttpHeaderName ` , ` MediaType ` , ` Protocol ` , ` Url ` , ` QueryParams ` , ` ETag ` , ` HttpRange ` , ` RequestConditions ` , paging primitives |
170+ | ` http.context ` | ` CallContext ` → ` DispatchContext ` → ` RequestContext ` → ` ExchangeContext ` chain, ` ContextStore ` |
171+ | ` http.auth ` | ` BearerTokenPolicy ` , ` BasicAuthPolicy ` , ` KeyCredentialPolicy ` , ` DigestChallengeHandler ` , RFC 7235 challenge parser, ` TokenCache ` |
172+ | ` http.sse ` | ` SseParser ` for Server-Sent Events streams |
173+ | ` pipeline ` | ` Pipeline ` , ` AsyncPipeline ` , ` Policy ` ABC, ` Stage ` enum, ` StagedPipelineBuilder ` , ` default_pipeline() ` |
174+ | ` pipeline.policies ` | ` RetryPolicy ` , ` RedirectPolicy ` , ` SetDatePolicy ` , ` LoggingPolicy ` , ` TracingPolicy ` (+ async twins) |
175+ | ` client ` | ` HttpClient ` and ` AsyncHttpClient ` Protocols |
176+ | ` serde ` | ` Serde ` , ` Serializer ` , ` Deserializer ` Protocols + ` JsonSerde ` reference impl |
177+ | ` instrumentation ` | ` ClientLogger ` , ` UrlRedactor ` , ` Tracer ` , ` Span ` , ` InstrumentationContext ` , noop singletons |
178+ | ` errors ` | ` SdkError ` hierarchy: ` ServiceRequestError ` , ` ServiceResponseError ` , ` HttpResponseError[ModelT] ` , … |
179+ | ` util ` | ` Clock ` , ` AsyncClock ` , ` ProxyOptions ` |
180+ | ` config ` | ` Configuration ` (layered env-var + override lookup) + ` ConfigurationBuilder ` |
181+
192182## Highlights
193183
194- - ** Strictly typed.** ` mypy --strict ` clean; no ` Any ` in public API; PEP
195- 695 type parameters where they fit; ` Literal[Stage.X] ` to lock policy
196- stages at the type level.
197- - ** Immutable data with slots.** Frozen dataclasses with ` __slots__ ` for
198- every model; non-destructive updates via ` with_* ` helpers.
199- - ** Pluggable everything.** ` HttpClient ` / ` AsyncHttpClient ` /
200- ` ChallengeHandler ` / ` Serde ` / ` TokenCache ` / ` Clock ` /
201- ` Configuration ` are all duck-typed Protocols.
202- - ** Real auth.** OAuth bearer with concurrent-refresh serialization,
184+ - ** Strictly typed.** Clean under ` mypy --strict ` , with no ` Any ` in the
185+ public API. PEP 695 type parameters appear where they fit, and
186+ ` Literal[Stage.X] ` pins policy stages at the type level.
187+ - ** Immutable data with slots.** Every model is a frozen dataclass with
188+ ` __slots__ ` ; updates are non-destructive through ` with_* ` helpers.
189+ - ** Pluggable everything.** ` HttpClient ` , ` AsyncHttpClient ` ,
190+ ` ChallengeHandler ` , ` Serde ` , ` TokenCache ` , ` Clock ` , and ` Configuration `
191+ are all duck-typed Protocols, so a conforming class is a valid
192+ implementation with no registration step.
193+ - ** Real auth.** OAuth bearer with serialized concurrent refresh, a
203194 ` WWW-Authenticate ` challenge parser per RFC 7235, RFC 7616 Digest
204- (MD5 / MD5-sess / SHA-256 / SHA-256-sess), basic, and key credential.
195+ (MD5, MD5-sess, SHA-256, SHA-256-sess), basic, and key credential.
205196- ** Retry done right.** Exponential backoff with jitter, ` Retry-After `
206- awareness, automatic single-use-body replay, deterministic via
207- injectable ` Clock ` .
208- - ** Redirects done right.** Loop detection, ` Authorization ` stripping on
197+ awareness, automatic replay of single-use bodies, and deterministic
198+ behaviour under test through an injectable ` Clock ` .
199+ - ** Redirects done right.** Loop detection, ` Authorization ` stripped on
209200 reissue, userinfo dropped from ` Location ` URLs, configurable allowed
210201 methods and 303 handling.
211- - ** Observability.** Structured logging via ` LoggingPolicy ` , OpenTelemetry-
212- compatible spans via ` TracingPolicy ` , URL redaction with allowlisted
213- query parameters, body capture caps for diagnostic logging .
214- - ** Server-Sent Events.** WHATWG-compliant ` SseParser ` with bounded line
215- buffer.
216- - ** Zero core dependencies .** ` dexpace-sdk-core ` ships against the
217- standard library only; transport adapters bring exactly one third-party
218- HTTP library each .
202+ - ** Observability.** Structured logging via ` LoggingPolicy ` ,
203+ OpenTelemetry- compatible spans via ` TracingPolicy ` , URL redaction with
204+ allowlisted query parameters, and capped body capture for diagnostics .
205+ - ** Server-Sent Events.** A WHATWG-compliant ` SseParser ` with a bounded
206+ line buffer.
207+ - ** A lean core .** ` dexpace-sdk-core ` carries a single runtime dependency
208+ ( ` furl ` , which backs ` Url ` parsing); each transport adapter adds exactly
209+ one HTTP library.
219210
220211## Development
221212
213+ The workspace is managed with ` uv ` ; one sync provisions every package in
214+ editable mode along with the dev toolchain (` pytest ` , ` mypy ` , ` ruff ` ).
215+
216+ ``` bash
217+ git clone https://github.com/dexpace/python-sdk.git
218+ cd python-sdk
219+ uv sync
220+ ```
221+
222222``` bash
223- uv sync # provision workspace + dev tools
224223uv run pytest -q # 646 tests across 5 packages
225224uv run mypy --strict # type-check (171 source files)
226225uv run ruff check # lint
227226uv run ruff format --check # formatting gate
228227```
229228
230229CI runs the same gates on Python 3.12, 3.13, and 3.14 for every push and
231- pull request — see [ ` .github/workflows/ci.yml ` ] ( .github/workflows/ci.yml ) .
230+ pull request; see [ ` .github/workflows/ci.yml ` ] ( .github/workflows/ci.yml ) .
232231
233232## Conventions
234233
235- Documented in [ ` CLAUDE.md ` ] ( CLAUDE.md ) . Highlights :
234+ Documented in [ ` CLAUDE.md ` ] ( CLAUDE.md ) . The short version :
236235
237236- Python 3.12+; modern syntax (` X | None ` , ` list[X] ` , ` Self ` , PEP 695
238237 generics); ` from __future__ import annotations ` everywhere.
@@ -241,15 +240,14 @@ Documented in [`CLAUDE.md`](CLAUDE.md). Highlights:
241240- Context managers for resources (` Response ` , ` ResponseBody ` ,
242241 ` CallContext ` ).
243242- ` mypy --strict ` and ` ruff ` clean on every commit; Google-style
244- docstrings on every public symbol; function-size cap of 50 lines.
243+ docstrings on every public symbol; functions capped at 50 lines.
245244
246245## Contributing
247246
248- External pull requests are welcome. Run the gates above (` pytest ` , `mypy
249- --strict` , ` ruff check` , ` ruff format --check`) before opening one, and
250- follow the conventions in [ ` CLAUDE.md ` ] ( CLAUDE.md ) .
247+ External pull requests are welcome. Run the gates above before opening
248+ one, and follow the conventions in [ ` CLAUDE.md ` ] ( CLAUDE.md ) .
251249
252250## License
253251
254- Licensed under the [ MIT License] ( LICENSE.md ) .
252+ Released under the [ MIT License] ( LICENSE.md ) .
255253Copyright © 2026 dexpace and Omar Aljarrah.
0 commit comments