This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
The Python counterpart to dexpace/java-sdk.
The architecture follows the same shape (immutable HTTP models, pipeline steps,
context promotion chain) but the public API uses Python idioms — dataclasses
instead of builder objects, Protocols instead of interfaces with implementation
modules, context managers instead of explicit close pairs. The pluggable I/O
seam that exists in the Java SDK was removed in this port: Python's bytes /
bytearray / memoryview / BinaryIO cover the same surface natively, so
bodies are modelled as typed Pythonic abstractions instead.
-
Python 3.12+. Modern union syntax (
X | None), built-in generics (list[X],dict[X, Y],tuple[X, ...]),Selffor fluent returns, PEP 695 type parameters (def f[T](x: T) -> T) andtypestatement aliases where they fit.from __future__ import annotationsat the top of every module so forward refs evaluate lazily. -
mypy --strictclean. Every public signature is typed; noAnyin public API; no unused# type: ignorecomments. -
ruffandruff formatclean (rule set inpyproject.toml). -
No runtime dependencies, with one sanctioned exception:
furl.coreotherwise ships against the standard library only.furlpowershttp.common.Urlparse/serialise; do not introduce any other third-party runtime dep — model it as an adapter behindHttpClientorSerdeinstead. -
Immutable data with slots. Models are
@dataclass(frozen=True, slots=True); mutate viadataclasses.replaceor thewith_*helpers. Builders are a Java idiom — Python's keyword and default arguments make them redundant noise. -
Protocol for SPIs, ABC for shared behaviour. Structural duck-typed seams (
HttpClient,Serde,PipelineStep) aretyping.Protocol. Types that ship default methods (RequestBody,ResponseBody,Span,CallContext) areabc.ABC. -
Context managers for resources.
Response,ResponseBody,CallContext, andTracingScopeall implement__enter__/__exit__so callers canwith …:and rely on deterministic cleanup. -
Bodies are Pythonic.
RequestBodyproduces bytes viaiter_bytes(chunk_size); factories coverfrom_bytes/from_string/from_form/from_stream/from_iter/from_file.ResponseBodyexposesiter_bytes/bytes/string. Single-use bodies (stream / iter) raiseRuntimeErroron second consumption — callto_replayable()before the first send if retries are needed.AsyncRequestBody/AsyncResponseBodyare the async twins (aiter_bytes), andMultipartField/MultipartRequestBodybuildmultipart/form-datapayloads. -
Body capture for logging uses
BytesIO.LoggableRequestBodymirrors writes into aBytesIOtap;LoggableResponseBodycaches drained bytes for repeatable reads. Both honour a configurable byte cap. -
Thread-safety where stated.
ContextStoreis safe under concurrent use; individual bodies and streams are not. Every store operation (get/put/set/remove) acquires athreading.Lock, so the guarantee survives free-threaded CPython (PEP 703) and runtimes without atomic dict ops rather than relying on the GIL. -
Public API is narrow. Helpers and concrete adapter classes are module-private (leading underscore). The public surface for each subpackage is what its
__init__.pyre-exports. -
__all__declares the surface on every module that exports anything; keep it accurate as new symbols land. -
py.typedships with the package (PEP 561) so downstream type-checkers consume our annotations. -
No logging package dependency. Use stdlib
loggingwhen needed; do not addloguruor similar. -
Google-style docstrings. One-line summary, blank line, then details with
Args:/Returns:/Raises:/Yields:sections. Plain backticks for code/type references — no Sphinx:class:/:meth:cross-references. -
Function-size cap: 50 lines. Aim 10–25. Refactor when you push past.
-
Commit style:
chore:for refactors/cleanup;feat:for new features;fix:for bug fixes;docs:for documentation-only changes. -
MIT licence header on every
.pyfile. The project is MIT-licensed (LICENSE.mdat the root and in each package). Every Python source file — src and tests alike — starts with the two-line header before the module docstring:# Copyright (c) 2026 dexpace and Omar Aljarrah. # Licensed under the MIT License. See LICENSE.md in the repository root for details.
The repo is a uv-managed workspace of five distributions. Each member under
packages/ is its own distribution; PEP 420 namespace packages let them share
the dexpace.sdk.* prefix. Commands run from the workspace root via
uv run … — uv sync provisions the virtualenv with all packages installed
in editable mode.
python-sdk/
├── LICENSE.md # MIT (also copied into each package)
├── README.md
├── CLAUDE.md
├── pyproject.toml # workspace root (uv workspace, dev deps,
│ # ruff/mypy/pytest config)
├── uv.lock
├── .github/workflows/ci.yml # pytest + mypy + ruff, python matrix 3.12–3.14
├── docs/ # cross-package documentation: architecture,
│ # auth, bodies, errors, http, pipelines
└── packages/
├── dexpace-sdk-core/ # toolkit (no transports); only runtime dep: furl
│ ├── pyproject.toml
│ ├── README.md
│ ├── src/dexpace/sdk/core/
│ │ ├── http/
│ │ │ ├── common/ # Headers, HttpHeaderName, MediaType,
│ │ │ │ # Protocol, Url, QueryParams, ETag,
│ │ │ │ # HttpRange, RequestConditions,
│ │ │ │ # common_media_types; pagination.py
│ │ │ │ # (ItemPaged/Pager + async twins),
│ │ │ │ # streaming.py (jsonl/chunked-frame iters)
│ │ │ ├── request/ # Request, RequestBody, AsyncRequestBody,
│ │ │ │ # FileRequestBody, LoggableRequestBody,
│ │ │ │ # MultipartField/MultipartRequestBody, Method
│ │ │ ├── response/ # Response, AsyncResponse, ResponseBody,
│ │ │ │ # AsyncResponseBody, LoggableResponseBody,
│ │ │ │ # Status
│ │ │ ├── context/ # CallContext, DispatchContext,
│ │ │ │ # RequestContext, ExchangeContext,
│ │ │ │ # ContextStore
│ │ │ ├── sse/ # Server-Sent Events parser + connection
│ │ │ ├── webhooks/ # webhook signature verification
│ │ │ └── auth/ # TokenCredential, BearerTokenPolicy,
│ │ │ # BasicAuthPolicy, KeyCredentialPolicy,
│ │ │ # ChallengeHandler (Basic/Digest/Composite),
│ │ │ # AuthenticateChallenge, TokenCache
│ │ ├── pipeline/ # Pipeline/AsyncPipeline, Policy/AsyncPolicy,
│ │ │ │ # Stage, StagedPipelineBuilder, defaults,
│ │ │ │ # sans-io + transport runners under the hood
│ │ │ │
│ │ │ ├── policies/ # redirect, idempotency, retry, set_date,
│ │ │ │ # client_identity, logging, tracing
│ │ │ │ # (async twins for all but logging and per-attempt tracing)
│ │ │ └── step/ # PipelineStep, StepMetadata
│ │ ├── client/ # HttpClient + AsyncHttpClient Protocols
│ │ ├── config/ # Configuration
│ │ ├── serde/ # Serde, Serializer, Deserializer Protocols
│ │ ├── errors/ # SDK-level exception hierarchy
│ │ ├── instrumentation/ # InstrumentationContext, Span, Tracer,
│ │ │ # TracingScope, noops, metrics,
│ │ │ # correlation, client_logger, http_tracer,
│ │ │ # identifiers, log_level, url_redactor
│ │ ├── pagination/ # Page, Paginator, link-header + strategy
│ │ └── util/ # clock, proxy helpers
│ └── tests/ # pytest suite — auth/, config/, context/,
│ # errors/, http/, instrumentation/,
│ # pagination/, pipeline/, serde/, sse/,
│ # util/, webhooks/
├── dexpace-sdk-http-stdlib/ # reference stdlib transports:
│ │ # UrllibHttpClient, AsyncioHttpClient
│ └── src/dexpace/sdk/http/stdlib/
├── dexpace-sdk-http-httpx/ # httpx transports (sync + async)
│ └── src/dexpace/sdk/http/httpx/
├── dexpace-sdk-http-aiohttp/ # aiohttp transport (async)
│ └── src/dexpace/sdk/http/aiohttp/
└── dexpace-sdk-http-requests/ # requests transport (sync)
└── src/dexpace/sdk/http/requests/
Community-health and tooling files (CHANGELOG.md, CONTRIBUTING.md,
SECURITY.md, CODE_OF_CONDUCT.md, conftest.py, tools/) are elided from
the tree above.
Every transport package depends on dexpace-sdk-core and adapts its HTTP
library to the HttpClient / AsyncHttpClient Protocols. Namespace
packaging (no __init__.py at src/dexpace/, src/dexpace/sdk/, or
src/dexpace/sdk/http/) is mandatory for every package so the dexpace.sdk
prefix stays shared.
uv sync # install workspace + dev tools
uv run pytest -q # walk all five packages' test suites
uv run mypy --strict # type-check everything in `files = [...]`
uv run ruff check # lint
uv run ruff format --check # formatting gateThe SDK is an HTTP-client toolkit, not an HTTP client. It provides
abstractions, models, and pipelines; consuming libraries plug in a concrete
transport via the HttpClient Protocol.
Layered, bottom-up:
- Bodies (
http.request.RequestBody/http.response.ResponseBody) — typed abstractions for outgoing/incoming payloads.iter_bytes(chunk_size)is the primary streaming surface;bytes()/string()for full reads. Bytes- and file-backed variants are replayable; stream- and iter-backed variants are single-use.Loggable*decorators wrap either side for diagnostic capture with a configurable cap. http.request/http.response/http.common— immutable@dataclass(frozen=True, slots=True)models. Non-destructive mutation viadataclasses.replaceor thewith_*helpers. The HTTP value layer includesHeaders,HttpHeaderName(typed constants for IANA names),MediaType,Url/QueryParams,ETag,HttpRange,RequestConditions.http.context— promotion chainDispatchContext→RequestContext→ExchangeContext, all carrying anInstrumentationContextfor tracing correlation. The thread-safeContextStoreis keyed by trace id; entries evict onCallContext.close().pipeline—Policy(andAsyncPolicy) wrap the downstream chain;Pipeline/AsyncPipelinerun an ordered set of policies grouped intoStages viaStagedPipelineBuilder. Shipped policies: redirect, idempotency, retry, set-date, client-identity, logging, tracing, and operation-tracing. Async twins exist for redirect, idempotency, retry, set-date, client-identity, and operation-tracing; logging and the per-attempt tracing policy are sync-only.default_pipeline()/default_async_pipeline()assemble the standard stack in the order operation-tracing → redirect → idempotency → retry → set-date → client-identity → [auth] → logging → tracing (the async pipeline keeps operation-tracing but omits logging and the per-attempt tracing span). The lower-levelpipeline/step/PipelineStepProtocol ((input, context) -> output) plusStepMetadataremain for custom composition.client/HttpClient— single-method Protocol (execute(request) -> Response). Transport is not provided bycore; thedexpace-sdk-http-*packages (stdlib, httpx, aiohttp, requests) each adapt one HTTP library to the Protocol.
- The HTTP request/response models are frozen — mutate via
dataclasses.replaceor thewith_*helpers, not by reassigning fields. Trying to assign raisesdataclasses.FrozenInstanceError. RequestBody.from_streamandfrom_iterare single-use. The seconditer_bytescall raisesRuntimeError. Callto_replayable()before the first send if you need retries.ResponseBody.bytes()/.string()consume and close the body. Wrap withLoggableResponseBodyif repeatable reads are needed.LoggableRequestBody.snapshot()/LoggableResponseBody.snapshot()are capped atmax_capture_bytes(default = CPython's effectivebytesceiling, ~2 GiB). The primary write path still receives the full payload; only the tap is truncated.Headersis case-insensitive but stores names in lower-case canonical form. Lookups (get,__contains__) compare names case-insensitively; iteration yields the lower-cased name. PassHttpHeaderNameinstances directly to skip the.lower()step on the hot path.- The Java SDK's
Io/IoProviderseam intentionally does NOT exist here. Python's stdlib (bytes,bytearray,memoryview,BytesIO,BinaryIO) is the contract. Don't reintroduce an Okio-style layer. mypyis invoked asuv run mypy --strictfrom the workspace root (config in the rootpyproject.toml).python_version = "3.12"because mypy 2.x requires it; the source still runs on whatever interpreter ≥ 3.12. Don't lower the floor.