feat(http-client-python): generate structured JSONL/SSE streaming (Azure flavor) - #11579
Conversation
…ure flavor) Operations whose HTTP response is a JSONL (application/jsonl) or SSE (text/event-stream) stream now generate client methods returning Stream[T] / AsyncStream[T] that yield deserialized payloads instead of raw bytes, driven by the TCGC response stream metadata (no opt-in option). The unbranded flavor keeps the raw byte-iterator behavior. The Stream/AsyncStream runtime plus JSONL/SSE decoders are vendored into the generated package at _utils/streaming_base.py, so they depend only on the released azure.core.rest (no unreleased azure.core.streaming). Heterogeneous SSE terminal-event termination is supported without TCGC sseMetadata: the terminal marker (e.g. "[DONE]") is a string-literal member of the item union, detected structurally and wired into the runtime as terminal_event so iteration stops before parsing it. Coverage: emitter unit tests (streaming.test.ts), pygen unit tests (test_structured_streaming_response.py), and azure mock_api tests (JSONL + SSE homogeneous + SSE heterogeneous, sync + async) plus unbranded byte-iterator tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
|
You can try these changes here
|
…tadata Consume TCGC `SdkSseMetadata` (0.71.0-dev.11) to route each SSE `event:` name to its concrete payload model, so structured SSE streams yield distinct model instances instead of parsed JSON. Homogeneous SSE returns `Stream[Model]`; heterogeneous `@events` SSE dispatches per event name and wires the `@terminalEvent` marker (e.g. `[DONE]`) into the vendored runtime. - emitter: emit `streaming.events[]` (eventType + payload itemType) and `terminalEvent` for SSE responses. - pygen: parse events, dispatch on `_event.event` in the generated callback, and annotate homogeneous streams with the concrete payload model (fixes the single-member union-alias mypy error). - deps: bump @typespec 1.15-dev / @Azure-Tools 0.71-dev prerelease stack (from the azure-sdk-for-js feed) for sseMetadata; `.npmrc` pins the scoped registries and `legacy-peer-deps` for reproducible install. - tests/docs: SSE mock_api tests assert model instances; README/changelog updated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
Extract the multi-response body handling out of `handle_response` into a `_handle_response_body` helper so `handle_response` stays under the too-many-statements limit after the structured-stream branch was added. Output is byte-identical (verified via azure streaming regen). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
commit: |
|
All changed packages have been documented.
Show changes
|
- cspell: add aenter/aexit/aiter/anext/isascii used by the vendored streaming runtime template - prettier: reformat package-lock.json - pylint: add matching :keyword:/:paramtype: docstrings to JSONLEvent/ServerSentEvent in streaming_base.py.jinja2 to satisfy azure guidelines checker C4758 (docstring-keyword-should-match-keyword-only) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
package-lock.json uses prettier's json-stringify parser (like package.json); reformat streaming.test.ts to printWidth 100. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
… on stable deps The 0.71.0-dev / 1.15-dev prerelease stack needed for per-event SSE model dispatch has no coherent published wave, so the ADO 'Python - Build' gate (npm ls -a) fails on peer-incoherent deps and legacy-peer-deps drops peers like eslint. Revert the dep bump (and the sseMetadata-driven model dispatch) back to the stable ~0.70/^1.14 line: SSE items deserialize via the structural union scan (dicts) with terminal-event handling, which is fully green in CI. Per-event model dispatch is deferred until a coherent TCGC release ships SdkSseMetadata. Keeps the C4758 docstring fix in streaming_base.py.jinja2 and the cspell additions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
Wrap the long Stream docstring_type line in response.py and extract _handle_response_body from handle_response to drop below the statement limit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
… valid type annotation The Stream[T]/AsyncStream[T] annotation for a structured SSE stream referenced the _unions.<Name> alias (a module-level variable), which pyright/mypy reject inside a type expression (reportInvalidTypeForm). Expand the @events union inline (Union[Model, ...] / the single member) so the stream item annotation is a valid type expression, and import the member models instead of the _unions alias. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
…streaming-codegen
…m item The inline stream item expansion collapses a single union member to that member (no Union) and a union of only literals to a single Literal, so importing Union unconditionally left it unused (pylint W0611) in the homogeneous SSE op. Import Union only when the expansion yields 2+ distinct member types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
…rge ref) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
d7783bc to
77eada8
Compare
There was a problem hiding this comment.
Pull request overview
Adds type-driven structured streaming support to the http-client-python Azure flavor so JSONL (application/jsonl) and SSE (text/event-stream) responses generate client methods returning Stream[T] / AsyncStream[T] that yield deserialized items, while preserving the existing raw byte-iterator behavior for the unbranded flavor.
Changes:
- Emit a vendored
_utils/streaming_base.pyruntime (Stream/AsyncStream + JSONL/SSE decoders) and wire generator response typing/imports to returnStream[T]for structured stream responses. - Extend the TypeSpec emitter YAML to include a
streamingblock (kind + itemType) for Azure flavor structured streaming responses and add unit coverage. - Update docs/changelog and bump
@typespec/spector+@typespec/http-specstest dependencies.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/http-client-python/README.md | Documents structured streaming behavior, limitations, and follow-ups. |
| packages/http-client-python/package.json | Bumps @typespec/spector and @typespec/http-specs versions. |
| packages/http-client-python/package-lock.json | Updates lockfile for the bumped dependencies. |
| packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 | Adds the vendored streaming runtime template (Stream/AsyncStream + JSONL/SSE parsing). |
| packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py | Adds serializer for the new streaming runtime template. |
| packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py | Generates operation bodies that return Stream/AsyncStream for structured streams. |
| packages/http-client-python/generator/pygen/codegen/serializers/init.py | Writes _utils/streaming_base.py when the code model needs it. |
| packages/http-client-python/generator/pygen/codegen/models/response.py | Introduces structured-stream response typing, imports, and SSE terminal-event detection. |
| packages/http-client-python/generator/pygen/codegen/models/operation.py | Forces stream=True pipeline behavior when returning structured streams. |
| packages/http-client-python/generator/pygen/codegen/models/code_model.py | Adds need_streaming_base / has_structured_stream gating for emitting the streaming runtime. |
| packages/http-client-python/emitter/test/streaming.test.ts | Adds emitter-level unit tests for structured stream type detection. |
| packages/http-client-python/emitter/src/http.ts | Emits streaming metadata for Azure flavor structured JSONL/SSE responses. |
| cspell.yaml | Adds dictionary words for new async iterator/callback identifiers. |
| .chronus/changes/structured-streaming-2026-0-0.md | Adds changelog entry describing the feature and example usage. |
Files not reviewed (1)
- packages/http-client-python/package-lock.json: Generated file
Suppressed comments (2)
packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2:66
- The AsyncStreamDecoder docstring references
DecodedType_co, but that TypeVar isn't defined in this file. It should match the protocol type parameter (T_co).
:rtype: AsyncIterator[DecodedType_co]
packages/http-client-python/package-lock.json:2589
@typespec/spectoris also resolved from anms-feed-*.pkgs.visualstudio.comURL withsha1-...integrity. Like the@typespec/http-specsentry, this may breaknpm ci/npm installin environments without access to that feed and is inconsistent with the registry.npmjs.org + sha512 entries elsewhere. Regenerate the lockfile against the public npm registry.
| retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)") | ||
| retval.append(" if cls:") | ||
| retval.append(" return cls(pipeline_response, deserialized, {}) # type: ignore") | ||
| retval.append(" return deserialized") |
| :param iter_bytes: An iterator of byte chunks. | ||
| :type iter_bytes: Iterator[bytes] | ||
| :return: An iterator of decoded data. | ||
| :rtype: Iterator[DecodedType_co] |
| "version": "0.1.0-alpha.40", | ||
| "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typespec/http-specs/-/http-specs-0.1.0-alpha.40.tgz", | ||
| "integrity": "sha1-Jbgrft+poBvuGMqqGR+shaf07Kk=", | ||
| "dev": true, |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
Summary
Operations whose HTTP response is a JSONL (
application/jsonl) or SSE (text/event-stream) stream now generate client methods that returnStream[T]/AsyncStream[T]and yield deserialized payloads instead of raw bytes, for the Azure flavor. This is driven by the response stream type — there is no opt-in emitter option. The unbranded flavor keeps the raw byte-iterator behavior (Iterator[bytes]/AsyncIterator[bytes]).The
Stream/AsyncStreamruntime (plus the JSONL and SSE decoders) is vendored into the generated package at_utils/streaming_base.py, so it depends only on the releasedazure.core.rest.Coverage
emitter/test/streaming.test.ts(YAML output: JSONL, homogeneous SSE, heterogeneous SSE).tests/unit/test_structured_streaming_response.py.tests/mock_api/azure/test_streaming_structured.py: JSONL homogeneous, SSE homogeneous (unnamed/receive), and SSE heterogeneous (named/receive, terminating at[DONE]), sync + async.Notes
@eventsunions; each event is yielded as the parsed JSON value (dict) rather than a model instance (same root cause as paging item deserialization — the shared_deserializeneeds amoduleargument). JSONL (single model item type) fully deserializes. Heterogeneous SSE terminal-event termination works today: the terminal marker (e.g.Literal["[DONE]"]) is detected structurally and passed to the runtime asterminal_event.Mock API Testsintegration check runsAzure/typespec-azure's committed streaming test (which asserts the old byte contract) against this PR's core, so it fails until that repo's test is aligned with the new type-driven behavior — tracked separately.