Skip to content

feat(http-client-python): generate structured JSONL/SSE streaming (Azure flavor) - #11579

Closed
Libba Lawrence (l0lawrence) wants to merge 16 commits into
microsoft:mainfrom
l0lawrence:l0lawrence-jsonl-sse-streaming-codegen
Closed

feat(http-client-python): generate structured JSONL/SSE streaming (Azure flavor)#11579
Libba Lawrence (l0lawrence) wants to merge 16 commits into
microsoft:mainfrom
l0lawrence:l0lawrence-jsonl-sse-streaming-codegen

Conversation

@l0lawrence

@l0lawrence Libba Lawrence (l0lawrence) commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

Operations whose HTTP response is a JSONL (application/jsonl) or SSE (text/event-stream) stream now generate client methods that return Stream[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]).

# For an operation returning JsonlStream<Thing> (Azure flavor):
stream = client.receive()  # -> Stream[Thing]
for thing in stream:       # deserialized model instances
    ...

The Stream / AsyncStream runtime (plus the JSONL and SSE decoders) is vendored into the generated package at _utils/streaming_base.py, so it depends only on the released azure.core.rest.

Coverage

  • Emitter unit testsemitter/test/streaming.test.ts (YAML output: JSONL, homogeneous SSE, heterogeneous SSE).
  • pygen unit teststests/unit/test_structured_streaming_response.py.
  • Azure mock_api teststests/mock_api/azure/test_streaming_structured.py: JSONL homogeneous, SSE homogeneous (unnamed/receive), and SSE heterogeneous (named/receive, terminating at [DONE]), sync + async.
  • Unbranded mock_api tests — byte-iterator behavior preserved.

Notes

  • SSE union item deserialization — SSE item types are @events unions; each event is yielded as the parsed JSON value (dict) rather than a model instance (same root cause as paging item deserialization — the shared _deserialize needs a module argument). 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 as terminal_event.
  • The Mock API Tests integration check runs Azure/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.

…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
@microsoft-github-policy-service microsoft-github-policy-service Bot added the emitter:client:python Issue for the Python client emitter: @typespec/http-client-python label Aug 6, 2026
@azure-sdk-automation

azure-sdk-automation Bot commented Aug 6, 2026

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

…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
@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-python@11579

commit: c72247a

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/http-client-python
Show changes

@typespec/http-client-python - feature ✏️

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.,> ,> ,> The Stream / AsyncStream runtime (plus the JSONL / SSE decoders) is vendored into the generated package at _utils/streaming_base.py (like _utils/model_base.py), so it depends only on the released azure.core.rest — not on an unreleased azure.core.streaming.,> ,> python,> # For an operation returning JsonlStream<Thing> (Azure flavor):,> stream = client.receive() # -> Stream[Thing],> for thing in stream: # deserialized model instances,> ...,>

- 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
…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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py runtime (Stream/AsyncStream + JSONL/SSE decoders) and wire generator response typing/imports to return Stream[T] for structured stream responses.
  • Extend the TypeSpec emitter YAML to include a streaming block (kind + itemType) for Azure flavor structured streaming responses and add unit coverage.
  • Update docs/changelog and bump @typespec/spector + @typespec/http-specs test 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/spector is also resolved from an ms-feed-*.pkgs.visualstudio.com URL with sha1-... integrity. Like the @typespec/http-specs entry, this may break npm ci/npm install in 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.

Comment on lines +1286 to +1289
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]
Comment on lines +2475 to 2478
"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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:python Issue for the Python client emitter: @typespec/http-client-python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants