Skip to content

Commit 901aad3

Browse files
OmarAlJarrahclaude
andcommitted
docs: changelog and docstrings for tracing split and transport fixes
- Add CHANGELOG entries for OperationTracingPolicy and the new outermost Stage.OPERATION, the TracingPolicy lifecycle split (with a migration note for pipelines assembled by hand), and the urllib/asyncio transport corrections (Content-Length under Content-Encoding, chunked-framing detection, and out-of-range status wording). - Document that the tracing and logging policies are sync-only and the async pipeline carries no tracing, in both the tracing module docstring and the changelog's scope boundaries. - List Stage.OPERATION among the pillar stages in the staged-builder docstring. - Remove leftover shorthand prefixes from a few test comments, keeping the explanatory text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7a38203 commit 901aad3

6 files changed

Lines changed: 66 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
A round of platform improvements to `dexpace-sdk-core`: new optional building
11-
blocks (typed serialization, webhook verification, pagination, two pipeline
12-
policies), tightened retry and tracing behaviour, and a batch of correctness
13-
fixes across bodies, SSE parsing, Digest auth, and error reporting. Most of this
14-
lands in `core`; the transport adapters additionally get consistent connect- vs
15-
read-phase timeout classification and tighter resource release. The only removed
16-
public symbol is the unused `RetryConfig` (see Removed); existing code otherwise
17-
continues to work without modification.
11+
blocks (typed serialization, webhook verification, pagination, three pipeline
12+
policies), tightened retry behaviour, a corrected per-operation tracing
13+
lifecycle, and a batch of correctness fixes across bodies, SSE parsing, Digest
14+
auth, and error reporting. Most of this lands in `core`; the transport adapters
15+
additionally get consistent connect- vs read-phase timeout classification,
16+
tighter resource release, and a set of edge-case corrections (status-code
17+
reporting, chunked-framing detection, and content-length under content-encoding).
18+
The only removed public symbol is the unused `RetryConfig` (see Removed);
19+
existing code otherwise continues to work without modification — with one
20+
behavioural note for hand-assembled pipelines (see *Tracing lifecycle* under
21+
Changed).
1822

1923
### Added
2024

@@ -37,6 +41,13 @@ continues to work without modification.
3741
- **Client-identity policy** (`pipeline.policies.client_identity`, plus its
3842
async twin). Sets a consistent `User-Agent` / client-identity header derived
3943
from the configured application id and SDK version.
44+
- **Per-operation tracing policy** (`OperationTracingPolicy` in
45+
`pipeline.policies.tracing_policy`, with a new outermost `Stage.OPERATION`).
46+
Emits the per-operation `HttpTracer` lifecycle (`operation_started`, then
47+
exactly one `operation_succeeded` / `operation_failed`) from outside the retry
48+
and redirect wrappers, so the reported outcome reflects the final result of
49+
the whole call rather than a single attempt or hop. Sync-only, in line with
50+
the rest of the tracing stack; the async pipeline carries no tracing.
4051
- **HTTP tracer** (`instrumentation.http_tracer`). An adapter-style tracer base
4152
whose per-event methods default to no-ops, so a subclass overrides only the
4253
events it cares about. Wired through the tracing policy for span emission.
@@ -58,6 +69,15 @@ continues to work without modification.
5869
cancellation cleanly between attempts.
5970
- **Tracing and redirect policies** now emit tracer events and carry correlation
6071
through redirects, with credentials stripped on cross-origin redirects.
72+
- **Tracing lifecycle** (`pipeline.policies.tracing_policy`). The per-operation
73+
`HttpTracer` lifecycle moved out of `TracingPolicy` into the new
74+
`OperationTracingPolicy`; `TracingPolicy` now emits only its per-attempt span
75+
and the per-request events (`request_sent`, `response_headers_received`,
76+
`response_received`). `default_pipeline` wires both, so callers who use it are
77+
unaffected. A pipeline assembled by hand that wants the operation lifecycle
78+
must now add `OperationTracingPolicy` alongside `TracingPolicy` — a bare
79+
`TracingPolicy` no longer emits `operation_started` / `operation_succeeded` /
80+
`operation_failed`.
6181
- **Default pipelines** (`pipeline.defaults`). The standard sync/async stacks now
6282
assemble the new idempotency and client-identity policies alongside the
6383
existing retry, redirect, logging, and tracing policies.
@@ -96,6 +116,26 @@ continues to work without modification.
96116
`async_response_body`). Cancelling an in-flight read now releases the
97117
underlying resources instead of leaking them, and re-raises `CancelledError`
98118
after cleanup.
119+
- **Per-operation tracing outcome** (`pipeline.policies.tracing_policy`). A call
120+
retried after a failed first attempt no longer reports `operation_failed` for
121+
the discarded attempt (it reports the single `operation_succeeded` it ends on),
122+
and a redirect whose later hop fails no longer reports `operation_succeeded`
123+
for the earlier 3xx hop. The lifecycle now fires exactly once and reflects the
124+
final outcome. See *Tracing lifecycle* under Changed for the API shape.
125+
- **`Content-Length` under `Content-Encoding`** (`http.stdlib.urllib_http_client`).
126+
`UrllibHttpClient` no longer drops a valid `Content-Length` when
127+
`Content-Encoding` is present: `http.client` does not decode content codings,
128+
so the body it serves is the wire payload whose length the header describes,
129+
and the length is now surfaced as-is. (The decompressing requests/httpx/aiohttp
130+
adapters still drop it, since they hand back a decoded stream.)
131+
- **Chunked-framing detection** (`http.stdlib.asyncio_http_client`). The
132+
`Transfer-Encoding` check matches the `chunked` coding by token rather than
133+
substring, so a coding whose name merely contains `chunked` (e.g. `x-chunked`)
134+
is no longer mistaken for chunked framing.
135+
- **Out-of-range status reporting** (`http.stdlib.urllib_http_client`,
136+
`asyncio_http_client`). Both now raise a `ServiceResponseError` worded
137+
`Invalid status code: …` for a status outside 100–599, matching the other
138+
adapters.
99139

100140
### Verified
101141

@@ -113,6 +153,9 @@ The following were intentionally left out of this round and are **not** included
113153
errors themselves.
114154
- **`sendfile` fast-path** — file bodies are streamed via the existing
115155
`iter_bytes` path; no zero-copy `sendfile` transport optimisation was added.
156+
- **Async tracing / logging** — the tracing and logging policies (including the
157+
new `OperationTracingPolicy`) ship sync-only; `default_async_pipeline` carries
158+
no tracing, and async callers handle per-operation observability themselves.
116159
- **MCP support** — no Model Context Protocol integration is included.
117160
- **Java SDK items** — the Java counterpart lives in a separate repository and
118161
was out of scope here.

packages/dexpace-sdk-core/src/dexpace/sdk/core/pipeline/policies/tracing_policy.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
default tracer factory returns ``NOOP_HTTP_TRACER`` and the no-op span carries
3030
the sentinel trace ids. Disable either per-call by setting
3131
``ctx.options["tracing_enabled"] = False``.
32+
33+
Both policies are sync-only, in line with the rest of the tracing and logging
34+
stack; ``default_async_pipeline`` carries no tracing, so async callers own
35+
per-operation observability on their side.
3236
"""
3337

3438
from __future__ import annotations

packages/dexpace-sdk-core/src/dexpace/sdk/core/pipeline/staged_builder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
constructor. Policies declare their ``STAGE``; the builder slots them into
88
stage buckets and at ``build()`` time flattens to a list in stage order.
99
10-
Pillar stages (`REDIRECT`, `RETRY`, `AUTH`, `LOGGING`, `SERDE`) admit at
11-
most one policy. A second `append` of a pillar raises by default — use
10+
Pillar stages (`OPERATION`, `REDIRECT`, `RETRY`, `AUTH`, `LOGGING`, `SERDE`)
11+
admit at most one policy. A second `append` of a pillar raises by default — use
1212
``replace(target, new)`` for explicit swaps or ``append(p, force=True)``
1313
for the rare legitimate use case (test fixtures, runtime composition).
1414
"""

packages/dexpace-sdk-core/tests/pipeline/test_tracer_emission.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ def test_redirect_then_failure_reports_operation_failed(self) -> None:
517517
assert failed == [boom]
518518

519519

520-
# ----- request_sent fires for unknown-length bodies (L19) -----------------
520+
# ----- request_sent fires for unknown-length bodies -----------------
521521

522522

523523
class TestRequestSentUnknownLength:

packages/dexpace-sdk-http-stdlib/tests/test_asyncio_http_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def _header_value(head: list[str], name: str) -> str | None:
226226

227227

228228
async def test_host_header_includes_non_default_port() -> None:
229-
# M1: a non-default port must appear in the Host header (RFC 9112 §3.2).
229+
# A non-default port must appear in the Host header (RFC 9112 §3.2).
230230
sink: dict[str, list[str]] = {}
231231
gen = _serve(await _collect_head(sink))
232232
base = await anext(gen)
@@ -241,7 +241,7 @@ async def test_host_header_includes_non_default_port() -> None:
241241

242242

243243
async def test_empty_post_body_sends_content_length_zero() -> None:
244-
# L17: a body-bearing method with no payload must advertise
244+
# A body-bearing method with no payload must advertise
245245
# Content-Length: 0 (RFC 9110 §8.6).
246246
sink: dict[str, list[str]] = {}
247247
gen = _serve(await _collect_head(sink))
@@ -289,7 +289,7 @@ async def test_post_with_body_sets_content_length() -> None:
289289

290290

291291
async def test_plain_http_ignores_supplied_ssl_context() -> None:
292-
# M2: a caller-supplied ssl_context must not trigger a TLS handshake on
292+
# A caller-supplied ssl_context must not trigger a TLS handshake on
293293
# a plain http:// URL — the request must still succeed over plaintext.
294294
async def ok(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
295295
await _read_request_head(reader)
@@ -310,7 +310,7 @@ async def ok(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None
310310

311311

312312
async def test_chunked_response_raises_service_response_error() -> None:
313-
# M3: a chunked response cannot be dechunked by this reference client, so
313+
# A chunked response cannot be dechunked by this reference client, so
314314
# it must fail loudly rather than silently return an empty body.
315315
async def chunked(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
316316
await _read_request_head(reader)
@@ -333,7 +333,7 @@ async def chunked(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) ->
333333

334334

335335
async def test_multiline_transfer_encoding_with_chunked_not_first_raises() -> None:
336-
# M3: Transfer-Encoding may be split across lines with ``chunked`` NOT
336+
# Transfer-Encoding may be split across lines with ``chunked`` NOT
337337
# first (alongside a misleading Content-Length). The client must still
338338
# detect chunked framing and refuse to read the bytes as a fixed-length
339339
# body, rather than parsing chunk framing as the payload — the exact
@@ -377,7 +377,7 @@ def test_is_chunked_matches_coding_token_not_substring() -> None:
377377

378378

379379
async def test_connection_close_framed_body_read_to_eof() -> None:
380-
# M3: a response without Content-Length is connection-close framed; the
380+
# A response without Content-Length is connection-close framed; the
381381
# body must be read to EOF, not fabricated as empty.
382382
async def close_framed(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
383383
await _read_request_head(reader)

packages/dexpace-sdk-http-stdlib/tests/test_urllib_http_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def test_post_with_body(echo_server: str) -> None:
120120
class _RedirectHandler(socketserver.StreamRequestHandler):
121121
"""Replies with a 302 pointing at another origin and a small body.
122122
123-
Pins H1: the transport must NOT follow the redirect itself. If it did,
123+
Pins that the transport must NOT follow the redirect itself. If it did,
124124
the second hop would fail (the target host is unroutable) or the response
125125
would carry the followed target's status — either way not a 302.
126126
"""
@@ -158,7 +158,7 @@ def redirect_server() -> Iterator[str]:
158158

159159

160160
def test_redirect_is_not_followed(redirect_server: str) -> None:
161-
# H1: a 302 must surface to the pipeline as a 302 Response, not be
161+
# A 302 must surface to the pipeline as a 302 Response, not be
162162
# transparently followed by the transport (which would also leak the
163163
# Authorization header cross-origin).
164164
client = UrllibHttpClient(timeout=2.0)
@@ -340,7 +340,7 @@ def test_content_length_surfaced_under_content_encoding() -> None:
340340

341341

342342
def test_read_failure_maps_to_service_response_error() -> None:
343-
# M5: a read-phase failure on the raw HTTPResponse must surface as an
343+
# A read-phase failure on the raw HTTPResponse must surface as an
344344
# SdkError, not a bare OSError / IncompleteRead.
345345
class _BoomResponse(_TrackingResponse):
346346
def read(self, size: int = -1) -> bytes:

0 commit comments

Comments
 (0)