Skip to content

Commit f172ff1

Browse files
OmarAlJarrahclaude
andcommitted
docs: polish README prose and repair stale facts
- Tighten the opening into a thesis plus design-philosophy paragraph - Reorder sections: quick start before the core reference table, and contributor workspace setup moved into Development where it belongs - Fix the packages table: core depends on furl (stale 'stdlib only' claim predated the Url migration); clarify the column covers third-party deps since every transport depends on core - Smooth the architecture and highlights prose; drop em dashes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2e8f751 commit f172ff1

1 file changed

Lines changed: 109 additions & 111 deletions

File tree

README.md

Lines changed: 109 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -6,70 +6,40 @@
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

2323
The 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
3738
pip 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 (redirectretryset-dateloggingtracing).
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
11686
from 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
142112
RequestBody.from_stream(open("payload.bin", "rb"))
143113
RequestBody.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)
146116
RequestBody.from_file("upload.bin")
147117

148118
# Convert any single-use body into a replayable one before retrying
149119
replayable = 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
```
163133
caller → 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

175145
1. **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.
188159
5. **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
224223
uv run pytest -q # 646 tests across 5 packages
225224
uv run mypy --strict # type-check (171 source files)
226225
uv run ruff check # lint
227226
uv run ruff format --check # formatting gate
228227
```
229228

230229
CI 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).
255253
Copyright © 2026 dexpace and Omar Aljarrah.

0 commit comments

Comments
 (0)