-
Notifications
You must be signed in to change notification settings - Fork 392
feat(http-client-python): generate structured JSONL/SSE streaming #11594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
294b0c5
feat(http-client-python): generate structured JSONL and SSE streams
l0lawrence 1d5dddc
Merge branch 'main' into l0lawrence-jsonl-sse-streaming-codegen
l0lawrence 851b4a2
feat(http-client-python): sync vendored streaming_base with azure-cor…
l0lawrence f5db637
chore: add async streaming terms to cspell
l0lawrence bf35b41
fix(http-client-python): emit absolute import when namespaces share n…
l0lawrence d9f7aa2
Revert "fix(http-client-python): emit absolute import when namespaces…
l0lawrence 26d2448
Merge branch 'main' of https://github.com/microsoft/typespec into HEAD
l0lawrence dffe901
feat(http-client-python): structured JSONL/SSE streaming for both azu…
l0lawrence 8bb9994
docs(http-client-python): clarify streaming itemType fields and de-du…
l0lawrence 10d0a55
fix(http-client-python): make vendored streaming_base banner flavor-a…
l0lawrence 3ffe279
fix(http-client-python): keep Stream/AsyncStream internal, not a publ…
l0lawrence fd26ef2
test(http-client-python): add SSE streaming Spector mock API tests (s…
l0lawrence 0233370
test(http-client-python): assert streaming ops return Stream/AsyncStream
l0lawrence ece5a88
chore(http-client-python): address streaming PR review feedback
l0lawrence 268f135
test(http-client-python): remove streaming init unit test
l0lawrence 46cec7d
build(http-client-python): revert incidental package-lock.json churn
l0lawrence aa76c0d
feat(http-client-python): support named and model SSE terminal events
l0lawrence e60bca3
fix(http-client-python): avoid duplicate stream kwarg for structured …
l0lawrence c876d7f
refactor(http-client-python): use typed structured-stream response flag
l0lawrence c4e0aeb
fix(http-client-python): support structured stream callbacks in msres…
l0lawrence 45b423f
refactor(http-client-python): use has_structured_stream directly
l0lawrence d7d58fa
fix(http-client-python): deserialize msrest stream events correctly
l0lawrence 72e2747
fix(http-client-python): satisfy serializer pylint limit
l0lawrence 0e1b8a4
feat(http-client-python): support SSE id/retry, text payload envelope…
l0lawrence eb5282e
fix(http-client-python): resolve lint/type-check issues in streaming …
l0lawrence 4842043
fix(http-client-python): avoid double-closing response in Stream reco…
l0lawrence cb1a38b
fix(http-client-python): apply cls once to the Stream object, not per…
l0lawrence a169e35
chore(http-client-python): consolidate structured streaming changelog…
l0lawrence 393333b
docs(http-client-python): clarify Stream/AsyncStream are internal, no…
l0lawrence c8e66a2
fix(http-client-python): correct SSE payload typing and reconnect lint
l0lawrence 0e8af1d
fix(http-client-python): support updated SSE protocol scenarios
l0lawrence 852c92b
feat(http-client-python): support structured streaming events
l0lawrence 7225f6b
refactor(http-client-python): use TCGC SSE metadata
l0lawrence 32dc2dd
docs(http-client-python): remove streaming README section
l0lawrence 8acb481
fix(http-client-python): reject unknown SSE events
l0lawrence 30fc41c
Merge branch 'main' into l0lawrence-jsonl-sse-streaming-codegen
l0lawrence File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| --- | ||
| changeKind: feature | ||
| packages: | ||
| - "@typespec/http-client-python" | ||
| --- | ||
|
|
||
| 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. | ||
|
|
||
| The `Stream` / `AsyncStream` 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. These types are an internal implementation detail and are not part of the package's public API. | ||
|
|
||
| ```python | ||
| stream = client.receive() | ||
| for thing in stream: | ||
| ... | ||
| ``` | ||
|
|
||
| For SSE streams, the most recently received event `id` and `retry` value (if provided by the server) are exposed via `stream.last_event_id` / `stream.retry`. Event envelopes yield the `@Events.data` payload using its payload type and content type. JSON payload media types (`application/json` and `+json`) are decoded as JSON, while other SSE payload media types remain UTF-8 text for type-specific deserialization. Pass `last_event_id=` to an SSE operation to send `Last-Event-ID` when manually resuming a stream. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
packages/http-client-python/emitter/test/streaming.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| import { strictEqual } from "assert"; | ||
| import { describe, it } from "vitest"; | ||
| import { | ||
| getStructuredStreamKind, | ||
| isStructuredStreamType, | ||
| partitionSSEEvents, | ||
| } from "../src/http.js"; | ||
|
|
||
| describe("typespec-python: structured streaming", () => { | ||
| it("treats model and union payloads as structured", () => { | ||
| strictEqual(isStructuredStreamType({ kind: "model" } as any), true); | ||
| strictEqual(isStructuredStreamType({ kind: "union" } as any), true); | ||
| }); | ||
|
|
||
| it("unwraps nullable payloads", () => { | ||
| strictEqual(isStructuredStreamType({ kind: "nullable", type: { kind: "model" } } as any), true); | ||
| strictEqual( | ||
| isStructuredStreamType({ kind: "nullable", type: { kind: "bytes" } } as any), | ||
| false, | ||
| ); | ||
| }); | ||
|
|
||
| it("treats bare byte/string payloads as unstructured", () => { | ||
| strictEqual(isStructuredStreamType({ kind: "bytes" } as any), false); | ||
| strictEqual(isStructuredStreamType({ kind: "string" } as any), false); | ||
| }); | ||
|
|
||
| it("detects the stream protocol explicitly", () => { | ||
| strictEqual(getStructuredStreamKind({ sseMetadata: { events: [] } } as any), "sse"); | ||
| strictEqual( | ||
| getStructuredStreamKind({ | ||
| streamMetadata: { contentTypes: ["text/event-stream; charset=utf-8"] }, | ||
| } as any), | ||
| "sse", | ||
| ); | ||
| strictEqual( | ||
| getStructuredStreamKind({ | ||
| streamMetadata: { contentTypes: ["application/jsonl"] }, | ||
| } as any), | ||
| "jsonl", | ||
| ); | ||
| strictEqual( | ||
| getStructuredStreamKind({ | ||
| streamMetadata: { contentTypes: ["application/json"] }, | ||
| } as any), | ||
| undefined, | ||
| ); | ||
| }); | ||
|
|
||
| describe("terminal-event partitioning", () => { | ||
| const identity = (payloadType: any) => payloadType; | ||
| const model = (name: string) => ({ kind: "model", name }); | ||
| const constant = (value: string) => ({ kind: "constant", value }); | ||
|
|
||
| it("keeps a nameless string-constant `[DONE]` as a drop-and-stop sentinel", () => { | ||
| const created = model("ResponseCreated"); | ||
| const done = constant("[DONE]"); | ||
| const { events, terminalEvent } = partitionSSEEvents( | ||
| [ | ||
| { | ||
| eventType: "response.created", | ||
| isTerminalEvent: false, | ||
| type: created, | ||
| payloadType: created, | ||
| }, | ||
| { eventType: undefined, isTerminalEvent: true, type: done, payloadType: done }, | ||
| ] as any, | ||
| identity, | ||
| ); | ||
| // The sentinel is NOT a dispatch event; it only sets `terminalEvent`. | ||
| strictEqual(terminalEvent, "[DONE]"); | ||
| strictEqual(events.length, 1); | ||
| strictEqual(events[0].eventType, "response.created"); | ||
| strictEqual(events[0].isTerminal, undefined); | ||
| }); | ||
|
|
||
| it("keeps named / model `@terminalEvent`s in the dispatch table as yield-and-stop events", () => { | ||
| const created = model("ResponseCreated"); | ||
| const completed = model("ResponseCompleted"); | ||
| const errored = model("StreamError"); | ||
| const { events, terminalEvent } = partitionSSEEvents( | ||
| [ | ||
| { | ||
| eventType: "response.created", | ||
| isTerminalEvent: false, | ||
| type: created, | ||
| payloadType: created, | ||
| }, | ||
| { | ||
| eventType: "response.completed", | ||
| isTerminalEvent: true, | ||
| type: completed, | ||
| payloadType: completed, | ||
| }, | ||
| { eventType: "error", isTerminalEvent: true, type: errored, payloadType: errored }, | ||
| ] as any, | ||
| identity, | ||
| ); | ||
| // No bare sentinel: the two terminals carry payloads, so they stay in `events`. | ||
| strictEqual(terminalEvent, undefined); | ||
| strictEqual(events.length, 3); | ||
| strictEqual(events[0].isTerminal, undefined); | ||
| strictEqual(events[1].eventType, "response.completed"); | ||
| strictEqual(events[1].isTerminal, true); | ||
| strictEqual(events[1].payloadType, completed); | ||
| strictEqual(events[2].eventType, "error"); | ||
| strictEqual(events[2].isTerminal, true); | ||
| strictEqual(events[2].payloadType, errored); | ||
| }); | ||
|
|
||
| it("supports a sentinel and named terminals together", () => { | ||
| const delta = model("ResponseDelta"); | ||
| const completed = model("ResponseCompleted"); | ||
| const done = constant("[DONE]"); | ||
| const { events, terminalEvent } = partitionSSEEvents( | ||
| [ | ||
| { eventType: "response.delta", isTerminalEvent: false, type: delta, payloadType: delta }, | ||
| { | ||
| eventType: "response.completed", | ||
| isTerminalEvent: true, | ||
| type: completed, | ||
| payloadType: completed, | ||
| }, | ||
| { eventType: undefined, isTerminalEvent: true, type: done, payloadType: done }, | ||
| ] as any, | ||
| identity, | ||
| ); | ||
| strictEqual(terminalEvent, "[DONE]"); | ||
| strictEqual(events.length, 2); | ||
| strictEqual(events[0].isTerminal, undefined); | ||
| strictEqual(events[1].eventType, "response.completed"); | ||
| strictEqual(events[1].isTerminal, true); | ||
| }); | ||
|
|
||
| it("emits the payload metadata for event envelopes", () => { | ||
| const envelope = model("Envelope"); | ||
| const payload = { kind: "string" }; | ||
| const { events } = partitionSSEEvents( | ||
| [ | ||
| { | ||
| eventType: "withEnvelope", | ||
| isTerminalEvent: false, | ||
| type: envelope, | ||
| payloadType: payload, | ||
| isEventEnvelope: true, | ||
| payloadContentType: "text/plain", | ||
| }, | ||
| ] as any, | ||
| identity, | ||
| ); | ||
| strictEqual(events[0].payloadType, payload); | ||
| strictEqual(events[0].payloadContentType, "text/plain"); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.