Skip to content

Commit dffe901

Browse files
l0lawrenceCopilot
andcommitted
feat(http-client-python): structured JSONL/SSE streaming for both azure and unbranded flavors
Make the vendored Stream/AsyncStream runtime flavor-aware: the functional import and the docstrings in streaming_base.py now use {{ code_model.core_library }}.rest, so the unbranded flavor targets corehttp.rest instead of a hardcoded azure.core.rest (azure flavor output is unchanged). Update the shared JSONL mock tests to consume Stream[Info] (sync + async), and refresh the README and changelog to state that both flavors emit structured streaming. The terminal_event extension and the builder_serializer generated call site are preserved. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46
1 parent 26d2448 commit dffe901

5 files changed

Lines changed: 22 additions & 13 deletions

File tree

.chronus/changes/structured-streaming-2026-0-0.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@ packages:
44
- "@typespec/http-client-python"
55
---
66

7-
Generate structured streaming client methods for the **Azure flavor**: operations whose HTTP response is a JSONL (`application/jsonl`) or SSE (`text/event-stream`) stream now return `Stream[T]` / `AsyncStream[T]`, yielding deserialized model instances instead of raw bytes.
7+
Generate structured streaming client methods: operations whose HTTP response is a JSONL (`application/jsonl`) or SSE (`text/event-stream`) stream now return `Stream[T]` / `AsyncStream[T]`, yielding deserialized model instances instead of raw bytes.
88

9-
`Stream` and `AsyncStream` are available from the generated package's base namespace. Their runtime (plus the JSONL / SSE decoders) is vendored at `_utils/streaming_base.py`, so it depends only on the released `azure.core.rest`.
9+
`Stream` and `AsyncStream` are available from the generated package's base namespace. Their runtime (plus the JSONL / SSE decoders) is vendored at `_utils/streaming_base.py` and depends only on the released core runtime for the flavor — `azure.core.rest` for the Azure flavor and `corehttp.rest` for the unbranded flavor.
10+
11+
```python
12+
from your_sdk import Stream
13+
14+
stream: Stream[Thing] = client.receive()
15+
for thing in stream:
16+
...
17+
```
1018

1119
```python
1220
from your_sdk import Stream

packages/http-client-python/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ Emit YAML code model only, without running Python generator. For batch processin
156156

157157
## Structured streaming (JSONL / SSE)
158158

159-
For the **Azure flavor**, operations whose HTTP response is a JSONL (`application/jsonl`) or SSE (`text/event-stream`) stream generate client methods that return `Stream[T]` (sync) / `AsyncStream[T]` (async), yielding deserialized model instances instead of raw bytes. This is driven by the TCGC response stream metadata (the response stream type) — there is no opt-in emitter option. For the unbranded flavor, streaming responses keep the existing raw byte-iterator behavior (`Iterator[bytes]` / `AsyncIterator[bytes]`).
159+
Operations whose HTTP response is a JSONL (`application/jsonl`) or SSE (`text/event-stream`) stream generate client methods that return `Stream[T]` (sync) / `AsyncStream[T]` (async), yielding deserialized model instances instead of raw bytes. This is driven by the TCGC response stream metadata (the response stream type) — there is no opt-in emitter option. This applies to both the Azure and unbranded flavors.
160160

161161
For an operation returning `JsonlStream<Thing>`, the generated method returns `Stream[Thing]` (sync) / `AsyncStream[Thing]` (async), yielding deserialized `Thing` instances as each JSONL line arrives. Similarly, `SSEStream<Events>` produces a `Stream` / `AsyncStream` over the SSE event payloads.
162162

@@ -171,6 +171,6 @@ for thing in stream:
171171
...
172172
```
173173

174-
The `Stream` / `AsyncStream` runtime (plus the JSONL and SSE decoders) is **vendored** into the generated package at `_utils/streaming_base.py` (alongside `_utils/model_base.py`). It depends only on the released `azure.core.rest`, so no unreleased `azure.core.streaming` dependency is required at runtime.
174+
The `Stream` / `AsyncStream` runtime (plus the JSONL and SSE decoders) is **vendored** into the generated package at `_utils/streaming_base.py` (alongside `_utils/model_base.py`). It depends only on the released core runtime for the flavor — `azure.core.rest` for the Azure flavor and `corehttp.rest` for the unbranded flavor — so no unreleased `azure.core.streaming` dependency is required at runtime.
175175

176176
SSE `@events` unions use TCGC event metadata to deserialize each named event into its corresponding generated model. Events marked with `@terminalEvent` stop iteration without being yielded.

packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# This file is vendored from azure-core (azure.core.streaming). It provides the
88
# Stream / AsyncStream helpers (plus the JSONL / SSE decoders and event types)
99
# used by generated structured-streaming operations, so the generated package
10-
# does not take a hard dependency on an azure-core version that ships
11-
# azure.core.streaming. Do not edit by hand.
10+
# does not take a hard dependency on a core runtime that ships the streaming
11+
# helpers. Do not edit by hand.
1212
# --------------------------------------------------------------------------
1313
import codecs
1414
import json
@@ -32,7 +32,7 @@ from typing import (
3232

3333
from typing_extensions import Self
3434

35-
from azure.core.rest import AsyncHttpResponse, HttpResponse
35+
from {{ code_model.core_library }}.rest import AsyncHttpResponse, HttpResponse
3636

3737
DecodedType = TypeVar("DecodedType")
3838
ReturnType_co = TypeVar("ReturnType_co", covariant=True)
@@ -544,13 +544,13 @@ class Stream(Iterator[ReturnType_co]):
544544
"""Stream class for consuming a decoded event stream (e.g. JSONL or SSE).
545545

546546
:keyword response: The response object.
547-
:paramtype response: ~azure.core.rest.HttpResponse
547+
:paramtype response: ~{{ code_model.core_library }}.rest.HttpResponse
548548
:keyword decoder: A decoder to use for the stream. If omitted, the decoder is
549549
inferred from the response ``Content-Type`` header.
550550
:paramtype decoder: StreamDecoder
551551
:keyword deserialization_callback: A callback that takes the response and the decoded event and
552552
returns a deserialized object.
553-
:paramtype deserialization_callback: Callable[[~azure.core.rest.HttpResponse, Any], ReturnType]
553+
:paramtype deserialization_callback: Callable[[~{{ code_model.core_library }}.rest.HttpResponse, Any], ReturnType]
554554
:keyword terminal_event: Optional event ``data`` value that terminates the stream (e.g.
555555
``"[DONE]"``). When an event's ``data`` equals this value, iteration stops and the
556556
event is not passed to ``deserialization_callback``.
@@ -612,13 +612,13 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
612612
"""AsyncStream class for asynchronously consuming a decoded event stream (e.g. JSONL or SSE).
613613

614614
:keyword response: The response object.
615-
:paramtype response: ~azure.core.rest.AsyncHttpResponse
615+
:paramtype response: ~{{ code_model.core_library }}.rest.AsyncHttpResponse
616616
:keyword decoder: A decoder to use for the stream. If omitted, the decoder is
617617
inferred from the response ``Content-Type`` header.
618618
:paramtype decoder: AsyncStreamDecoder
619619
:keyword deserialization_callback: A callback that takes the response and the decoded event and
620620
returns a deserialized object.
621-
:paramtype deserialization_callback: Callable[[~azure.core.rest.AsyncHttpResponse, Any], ReturnType]
621+
:paramtype deserialization_callback: Callable[[~{{ code_model.core_library }}.rest.AsyncHttpResponse, Any], ReturnType]
622622
:keyword terminal_event: Optional event ``data`` value that terminates the stream (e.g.
623623
``"[DONE]"``). When an event's ``data`` equals this value, iteration stops and the
624624
event is not passed to ``deserialization_callback``.

packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ async def test_basic_send(client: JsonlClient):
2525

2626
@pytest.mark.asyncio
2727
async def test_basic_recv(client: JsonlClient):
28-
assert b"".join([d async for d in (await client.basic.receive())]) == JSONL
28+
stream = await client.basic.receive()
29+
assert [item.desc async for item in stream] == ["one", "two", "three"]

packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ def test_basic_send(client: JsonlClient):
2222

2323

2424
def test_basic_recv(client: JsonlClient):
25-
assert b"".join(client.basic.receive()) == JSONL
25+
assert [item.desc for item in client.basic.receive()] == ["one", "two", "three"]

0 commit comments

Comments
 (0)