From 294b0c57a7b2473422f1559d0298af5c15667406 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 7 Aug 2026 10:58:36 -0700 Subject: [PATCH 01/21] feat(http-client-python): generate structured JSONL and SSE streams Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e --- .../changes/structured-streaming-2026-0-0.md | 17 + cspell.yaml | 5 + packages/http-client-python/README.md | 21 + .../http-client-python/emitter/src/http.ts | 85 +++ .../emitter/test/streaming.test.ts | 45 ++ .../pygen/codegen/models/code_model.py | 17 + .../pygen/codegen/models/operation.py | 9 + .../pygen/codegen/models/response.py | 119 +++- .../pygen/codegen/serializers/__init__.py | 7 + .../codegen/serializers/builder_serializer.py | 91 ++- .../codegen/serializers/general_serializer.py | 17 + .../pygen/codegen/templates/init.py.jinja2 | 9 + .../templates/streaming_base.py.jinja2 | 562 ++++++++++++++++++ .../generator/pygen/preprocess/__init__.py | 2 + packages/http-client-python/package-lock.json | 482 ++++++++------- packages/http-client-python/package.json | 23 +- .../tests/unit/test_streaming_init.py | 57 ++ 17 files changed, 1315 insertions(+), 253 deletions(-) create mode 100644 .chronus/changes/structured-streaming-2026-0-0.md create mode 100644 packages/http-client-python/emitter/test/streaming.test.ts create mode 100644 packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 create mode 100644 packages/http-client-python/tests/unit/test_streaming_init.py diff --git a/.chronus/changes/structured-streaming-2026-0-0.md b/.chronus/changes/structured-streaming-2026-0-0.md new file mode 100644 index 00000000000..af07b4d5cb9 --- /dev/null +++ b/.chronus/changes/structured-streaming-2026-0-0.md @@ -0,0 +1,17 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-python" +--- + +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. + +`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`. + +```python +from your_sdk import Stream + +stream: Stream[Thing] = client.receive() +for thing in stream: + ... +``` diff --git a/cspell.yaml b/cspell.yaml index 4fdbe6d4b2f..6273e27f277 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -7,11 +7,15 @@ dictionaries: words: - Ablack - Adoptium + - aenter + - aexit - agentic - agentics - aiohttp + - aiter - alzimmer - amqp + - anext - AQID - Arize - arizeaiobservabilityeval @@ -120,6 +124,7 @@ words: - intrinsics - ints - IOHTTP + - isascii - isdigit - isinstance - issecret diff --git a/packages/http-client-python/README.md b/packages/http-client-python/README.md index 0c3335df5fa..6e5a6df1cd8 100644 --- a/packages/http-client-python/README.md +++ b/packages/http-client-python/README.md @@ -153,3 +153,24 @@ Whether to clear the output folder before generating the code. Defaults to `fals **Type:** `boolean` Emit YAML code model only, without running Python generator. For batch processing. + +## Structured streaming (JSONL / SSE) + +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]`). + +For an operation returning `JsonlStream`, the generated method returns `Stream[Thing]` (sync) / `AsyncStream[Thing]` (async), yielding deserialized `Thing` instances as each JSONL line arrives. Similarly, `SSEStream` produces a `Stream` / `AsyncStream` over the SSE event payloads. + +Generated packages re-export `Stream` and `AsyncStream` from their base namespace: + +```python +from your_sdk import Stream +from your_sdk.models import Thing + +stream: Stream[Thing] = client.receive() +for thing in stream: + ... +``` + +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. + +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. diff --git a/packages/http-client-python/emitter/src/http.ts b/packages/http-client-python/emitter/src/http.ts index 31d970d4b65..51743d17e58 100644 --- a/packages/http-client-python/emitter/src/http.ts +++ b/packages/http-client-python/emitter/src/http.ts @@ -42,6 +42,90 @@ export enum ReferredByOperationTypes { NonPagingOnly = 2, } +type StructuredStreamKind = "jsonl" | "sse"; +type EmittedType = ReturnType; + +interface StructuredStreamEvent { + eventType: string | undefined; + itemType: EmittedType; +} + +interface StructuredStreamingInfo { + kind: StructuredStreamKind; + itemType: EmittedType; + events?: StructuredStreamEvent[]; + terminalEvent?: string; +} + +/** Whether pygen can deserialize the stream item type. */ +export function isStructuredStreamType(type: SdkType): boolean { + switch (type.kind) { + case "model": + case "union": + return true; + case "nullable": + return isStructuredStreamType(type.type); + default: + return false; + } +} + +export function getStructuredStreamKind( + response: SdkHttpResponse | SdkHttpErrorResponse, +): StructuredStreamKind | undefined { + if (response.sseMetadata) return "sse"; + + const contentTypes = response.streamMetadata?.contentTypes ?? response.contentTypes ?? []; + for (const contentType of contentTypes) { + const mediaType = contentType.split(";", 1)[0].trim().toLowerCase(); + if (mediaType === "text/event-stream") return "sse"; + if (mediaType === "application/jsonl") return "jsonl"; + } + return undefined; +} + +function getStringConstantValue(type: SdkType): string | undefined { + if (type.kind === "nullable") return getStringConstantValue(type.type); + return type.kind === "constant" && typeof type.value === "string" ? type.value : undefined; +} + +function emitStructuredStreamingInfo( + context: PythonSdkContext, + response: SdkHttpResponse | SdkHttpErrorResponse, +): StructuredStreamingInfo | undefined { + const streamMetadata = response.streamMetadata; + if (!streamMetadata || !isStructuredStreamType(streamMetadata.streamType)) return undefined; + + const kind = getStructuredStreamKind(response); + if (!kind) return undefined; + + const streaming: StructuredStreamingInfo = { + kind, + itemType: getType(context, streamMetadata.streamType), + }; + + const sseMetadata = response.sseMetadata; + if (!sseMetadata) return streaming; + + const events = sseMetadata.events + .filter((event) => !event.isTerminalEvent) + .map((event) => ({ + eventType: event.eventType, + itemType: getType(context, event.payloadType), + })); + if (events.length > 0) streaming.events = events; + + const terminalEvent = sseMetadata.events.find((event) => event.isTerminalEvent); + if (terminalEvent) { + const terminalEventValue = + getStringConstantValue(terminalEvent.payloadType) ?? + getStringConstantValue(terminalEvent.type); + if (terminalEventValue !== undefined) streaming.terminalEvent = terminalEventValue; + } + + return streaming; +} + function isEtagType(type: SdkType): boolean { if (type.kind === "nullable") return isEtagType(type.type); const raw = type.__raw; @@ -682,6 +766,7 @@ function emitHttpResponse( "invalid-lro-result", method, ), + streaming: isException ? undefined : emitStructuredStreamingInfo(context, response), }; } diff --git a/packages/http-client-python/emitter/test/streaming.test.ts b/packages/http-client-python/emitter/test/streaming.test.ts new file mode 100644 index 00000000000..491bc04b098 --- /dev/null +++ b/packages/http-client-python/emitter/test/streaming.test.ts @@ -0,0 +1,45 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { getStructuredStreamKind, isStructuredStreamType } 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, + ); + }); +}); diff --git a/packages/http-client-python/generator/pygen/codegen/models/code_model.py b/packages/http-client-python/generator/pygen/codegen/models/code_model.py index 73cd410eb84..c1e0bb1ee6b 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/code_model.py +++ b/packages/http-client-python/generator/pygen/codegen/models/code_model.py @@ -279,12 +279,29 @@ def need_utils_folder(self, async_mode: bool, client_namespace: str) -> bool: self.need_utils_utils(async_mode, client_namespace) or self.need_utils_serialization or self.options["models-mode"] == "dpg" + or self.need_streaming_base ) @property def need_utils_serialization(self) -> bool: return not self.options["client-side-validation"] + @property + def has_structured_stream(self) -> bool: + return any( + op.has_structured_stream_response + for client in self.clients + for og in client.operation_groups + for op in og.operations + ) + + @property + def need_streaming_base(self) -> bool: + # Whether to emit the vendored ``_utils/streaming_base.py`` (Stream / AsyncStream + # + JSONL / SSE decoders). Only needed when at least one operation returns a + # structured stream. + return self.has_structured_stream + def need_utils_utils(self, async_mode: bool, client_namespace: str) -> bool: return ( self.need_utils_form_data(async_mode, client_namespace) diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index 3c6d525f122..b2fb243fef9 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -98,12 +98,21 @@ def exact_name_params(self) -> set[str]: @property def stream_value(self) -> Union[str, bool]: + # Structured streams (JSONL / SSE) must always run the pipeline with + # stream=True so the body can be consumed incrementally by Stream/AsyncStream. + if self.has_structured_stream_response: + return True return ( f'kwargs.pop("stream", {self.has_stream_response})' if self.expose_stream_keyword and self.has_response_body and "stream" not in self.exact_name_params else self.has_stream_response ) + @property + def has_structured_stream_response(self) -> bool: + """Whether any success response is a structured (JSONL / SSE) stream returning Stream[T].""" + return any(getattr(r, "is_structured_stream", False) for r in self.responses) + @property def has_form_data_body(self): return self.parameters.has_form_data_body diff --git a/packages/http-client-python/generator/pygen/codegen/models/response.py b/packages/http-client-python/generator/pygen/codegen/models/response.py index 99a90481319..c102ffd8802 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/response.py +++ b/packages/http-client-python/generator/pygen/codegen/models/response.py @@ -58,6 +58,15 @@ def __init__( self.type = type self.nullable = yaml_data.get("nullable") self.default_content_type = yaml_data.get("defaultContentType") + streaming = yaml_data.get("streaming") + self.streaming_kind: Optional[str] = streaming["kind"] if streaming else None + self.streaming_events: list[tuple[Optional[str], BaseType]] = [] + self._streaming_terminal_event: Optional[str] = streaming.get("terminalEvent") if streaming else None + if streaming: + self.streaming_events = [ + (event.get("eventType"), self.code_model.lookup_type(id(event["itemType"]))) + for event in streaming.get("events", []) + ] @property def result_property(self) -> str: @@ -92,12 +101,65 @@ def is_stream_response(self) -> bool: ) return retval + @property + def is_structured_stream(self) -> bool: + """Is the response a structured (JSONL / SSE) stream rendered as Stream[T] / AsyncStream[T].""" + return self.streaming_kind is not None + + @property + def terminal_event(self) -> Optional[str]: + """Terminal event marker for a heterogeneous SSE stream, if any. + + TCGC ``sseMetadata`` supplies this marker directly. For compatibility with + older metadata, a string-literal member of the item union is used as a fallback. + """ + if self.streaming_kind != "sse": + return None + if self._streaming_terminal_event is not None: + return self._streaming_terminal_event + if not isinstance(self.type, CombinedType): + return None + from .constant_type import ConstantType + + for member in self.type.types: + if isinstance(member, ConstantType) and isinstance(member.value, str): + return member.value + return None + + def stream_class_name(self, async_mode: bool) -> str: + return "AsyncStream" if async_mode else "Stream" + + @property + def stream_item_type(self) -> Optional[BaseType]: + if len(self.streaming_events) == 1: + return self.streaming_events[0][1] + return self.type + def serialization_type(self, **kwargs: Any) -> str: if self.type: return self.type.serialization_type(**kwargs) return "None" + def stream_item_annotation(self, **kwargs: Any) -> str: + """Valid type expression for a structured stream's item type. + + A named ``CombinedType`` (``@events`` union) renders its ``type_annotation`` as the + ``_unions.`` alias, which is a module-level variable and therefore rejected by + pyright/mypy inside ``Stream[...]`` ("Variable not allowed in type expression"). Expand + the union inline (``Union[Model, ...]`` / the single member) so the annotation is a + valid type expression. + """ + item_type = self.stream_item_type + if isinstance(item_type, CombinedType): + return item_type.type_definition(**kwargs) + return item_type.type_annotation(**kwargs) if item_type else "None" + def type_annotation(self, **kwargs: Any) -> str: + if self.is_structured_stream and self.type: + kwargs["is_operation_file"] = True + kwargs["is_response"] = True + stream_class = self.stream_class_name(kwargs.get("async_mode", False)) + return f"{stream_class}[{self.stream_item_annotation(**kwargs)}]" if self.type: kwargs["is_operation_file"] = True kwargs["is_response"] = True @@ -109,30 +171,63 @@ def type_annotation(self, **kwargs: Any) -> str: def docstring_text(self, **kwargs: Any) -> str: kwargs["is_response"] = True + if self.is_structured_stream and self.type: + stream_class = self.stream_class_name(kwargs.get("async_mode", False)) + item_type = self.stream_item_type or self.type + return f"An instance of {stream_class} that iterates over {item_type.docstring_text(**kwargs)}" if self.nullable and self.type: return f"{self.type.docstring_text(**kwargs)} or None" return self.type.docstring_text(**kwargs) if self.type else "None" def docstring_type(self, **kwargs: Any) -> str: kwargs["is_response"] = True + if self.is_structured_stream and self.type: + stream_class = self.stream_class_name(kwargs.get("async_mode", False)) + item_type = (self.stream_item_type or self.type).docstring_type(**kwargs) + return f"~{self.code_model.namespace}.{stream_class}[{item_type}]" if self.nullable and self.type: return f"{self.type.docstring_type(**kwargs)} or None" return self.type.docstring_type(**kwargs) if self.type else "None" def imports(self, **kwargs: Any) -> FileImport: file_import = FileImport(self.code_model) - if self.type: - file_import.merge(self.type.imports(**kwargs)) + item_type = self.stream_item_type if self.is_structured_stream else self.type + # For a structured stream whose item type is a named ``@events`` union, the annotation + # is expanded inline (see ``stream_item_annotation``), so import the union member types + # rather than the ``_unions`` alias. + if self.is_structured_stream and isinstance(item_type, CombinedType): + for member in item_type.types: + file_import.merge(member.imports(**kwargs)) + # ``Union`` is only needed when the inline expansion actually yields a union of + # 2+ distinct member types (a single member collapses to that member; a union of + # only literals collapses to a single ``Literal[...]``). + distinct = list(dict.fromkeys(m.type_annotation(**kwargs) for m in item_type.types)) + all_constant = all(t.type == "constant" for t in item_type.types) + if len(distinct) > 1 and not all_constant: + file_import.add_submodule_import("typing", "Union", ImportType.STDLIB) + elif item_type: + file_import.merge(item_type.imports(**kwargs)) + if not self.is_structured_stream and isinstance(item_type, CombinedType) and item_type.name: + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + file_import.add_submodule_import( + self.code_model.get_relative_import_path(serialize_namespace), + "_unions", + ImportType.LOCAL, + TypingSection.TYPING, + ) if self.nullable: file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB) - if isinstance(self.type, CombinedType) and self.type.name: + if self.is_structured_stream: + stream_class = self.stream_class_name(kwargs.get("async_mode", False)) serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) - file_import.add_submodule_import( - self.code_model.get_relative_import_path(serialize_namespace), - "_unions", - ImportType.LOCAL, - TypingSection.TYPING, + relative_path = self.code_model.get_relative_import_path( + serialize_namespace, module_name="_utils.streaming_base" ) + file_import.add_submodule_import(relative_path, stream_class, ImportType.LOCAL) + if self.streaming_kind == "sse": + file_import.add_import("json", ImportType.STDLIB) + for _event_type, event_item_type in self.streaming_events: + file_import.merge(event_item_type.imports(**kwargs)) return file_import def _get_import_type(self, input_path: str) -> ImportType: @@ -143,6 +238,14 @@ def _get_import_type(self, input_path: str) -> ImportType: @classmethod def from_yaml(cls, yaml_data: dict[str, Any], code_model: "CodeModel") -> "Response": + streaming = yaml_data.get("streaming") + if streaming: + return cls( + yaml_data=yaml_data, + code_model=code_model, + headers=[ResponseHeader.from_yaml(header, code_model) for header in yaml_data["headers"]], + type=code_model.lookup_type(id(streaming["itemType"])), + ) type = code_model.lookup_type(id(yaml_data["type"])) if yaml_data.get("type") else None # use ByteIteratorType if we are returning a binary type default_content_type = yaml_data.get("defaultContentType", "application/json") diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py index c5d786a4a6e..0321dc17d91 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py @@ -525,6 +525,13 @@ def _serialize_and_write_utils_folder(self, env: Environment, namespace: str): general_serializer.serialize_model_base_file(), ) + # write _utils/streaming_base.py (vendored Stream/AsyncStream + JSONL/SSE decoders) + if self.code_model.need_streaming_base: + self.write_file( + utils_folder_path / Path("streaming_base.py"), + general_serializer.serialize_streaming_base_file(), + ) + def _serialize_and_write_top_level_folder(self, env: Environment, namespace: str) -> None: root_dir = self.code_model.get_root_dir() generation_dir = self.code_model.get_generation_dir(namespace) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 152b840d260..04c3a266530 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1256,15 +1256,75 @@ def handle_error_response( # pylint: disable=too-many-statements, too-many-bran ) return retval - def handle_response(self, builder: OperationType) -> list[str]: - retval: list[str] = ["response = pipeline_response.http_response"] - retval.append("") - retval.extend(self.handle_error_response(builder)) + def handle_structured_stream_response(self, builder: OperationType) -> list[str]: + """Emit the body for an operation returning a structured (JSONL / SSE) stream. + + Produces a per-event deserialization callback and returns a ``Stream`` / + ``AsyncStream`` wrapping the streamed HTTP response. + """ + response = next(r for r in builder.responses if getattr(r, "is_structured_stream", False)) + item_annotation = response.type.type_annotation( # type: ignore[union-attr] + is_operation_file=True, serialize_namespace=self.serialize_namespace + ) + stream_class = response.stream_class_name(self.async_mode) # type: ignore[attr-defined] + terminal_event = getattr(response, "terminal_event", None) + streaming_events = getattr(response, "streaming_events", []) + retval: list[str] = [] + retval.append("def _callback(_http_response, _event):") + if response.streaming_kind == "sse": # type: ignore[attr-defined] + retval.append(" _event_json = json.loads(_event.data)") + named_events = [ + (event_type, event_item_type) + for event_type, event_item_type in streaming_events + if event_type is not None + ] + unnamed_events = [event_item_type for event_type, event_item_type in streaming_events if event_type is None] + if named_events: + for index, (event_type, event_item_type) in enumerate(named_events): + event_annotation = event_item_type.type_annotation( + is_operation_file=True, + serialize_namespace=self.serialize_namespace, + ) + keyword = "if" if index == 0 else "elif" + retval.append(f" {keyword} _event.event == {event_type!r}:") + retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") + retval.append(" else:") + if len(unnamed_events) == 1: + event_annotation = unnamed_events[0].type_annotation( + is_operation_file=True, + serialize_namespace=self.serialize_namespace, + ) + retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") + else: + retval.append(" deserialized = _event_json") + elif len(unnamed_events) == 1: + event_annotation = unnamed_events[0].type_annotation( + is_operation_file=True, + serialize_namespace=self.serialize_namespace, + ) + retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") + else: + retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)") + else: + retval.append(" _event_json = _event.json()") + 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") retval.append("") - if builder.has_optional_return_type: - retval.append("deserialized = None") - if builder.any_response_has_headers: - retval.append("response_headers = {}") + if terminal_event is not None: + retval.append( + f"return {stream_class}(response=response, deserialization_callback=_callback, " + f"terminal_event={terminal_event!r}) # type: ignore" + ) + else: + retval.append( + f"return {stream_class}(response=response, deserialization_callback=_callback) # type: ignore" + ) + return retval + + def _handle_response_body(self, builder: OperationType) -> list[str]: + retval: list[str] = [] if builder.has_response_body or builder.any_response_has_headers: # pylint: disable=too-many-nested-blocks if len(builder.responses) > 1: status_codes, res_headers, res_deserialization = [], [], [] @@ -1299,6 +1359,21 @@ def handle_response(self, builder: OperationType) -> list[str]: else: retval.extend(self.response_headers_and_deserialization(builder, builder.responses[0])) retval.append("") + return retval + + def handle_response(self, builder: OperationType) -> list[str]: + retval: list[str] = ["response = pipeline_response.http_response"] + retval.append("") + retval.extend(self.handle_error_response(builder)) + retval.append("") + if builder.has_structured_stream_response: + retval.extend(self.handle_structured_stream_response(builder)) + return retval + if builder.has_optional_return_type: + retval.append("deserialized = None") + if builder.any_response_has_headers: + retval.append("response_headers = {}") + retval.extend(self._handle_response_body(builder)) if ( builder.has_optional_return_type or self.code_model.options["models-mode"] diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index d44dbc8bc02..fd54fcc9260 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -200,11 +200,24 @@ def serialize_pkgutil_init_file(self) -> str: def serialize_init_file(self, clients: list[Client]) -> str: template = self.env.get_template("init.py.jinja2") + expose_streaming_types = ( + not self.async_mode + and self.code_model.need_streaming_base + and self.code_model.is_top_namespace(self.client_namespace) + ) return template.render( code_model=self.code_model, clients=clients, async_mode=self.async_mode, serialize_namespace=self.serialize_namespace, + streaming_import_path=( + self.code_model.get_relative_import_path( + self.serialize_namespace, + module_name="_utils.streaming_base", + ) + if expose_streaming_types + else None + ), ) def serialize_service_client_file(self, clients: list[Client]) -> str: @@ -318,6 +331,10 @@ def serialize_model_base_file(self) -> str: template = self.env.get_template("model_base.py.jinja2") return template.render(code_model=self.code_model, file_import=FileImport(self.code_model)) + def serialize_streaming_base_file(self) -> str: + template = self.env.get_template("streaming_base.py.jinja2") + return template.render(code_model=self.code_model, file_import=FileImport(self.code_model)) + def serialize_validation_file(self) -> str: template = self.env.get_template("validation.py.jinja2") return template.render( diff --git a/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 index 007009e4bc9..69176f27529 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 @@ -9,6 +9,9 @@ from .{{ client.filename }} import {{ client.name }} # type: ignore {% endfor %} {% endif %} +{% if streaming_import_path %} +from {{ streaming_import_path }} import AsyncStream, Stream +{% endif %} {% if not async_mode and code_model.options.get("package-version") %} from {{ code_model.get_relative_import_path(serialize_namespace, module_name="_version") }} import VERSION @@ -17,9 +20,15 @@ __version__ = VERSION {{ keywords.patch_imports(try_except=True) }} __all__ = [ + {% if streaming_import_path %} + "AsyncStream", + {% endif %} {% for client in clients %} {{ keywords.escape_str(client.name) }}, {% endfor %} + {% if streaming_import_path %} + "Stream", + {% endif %} ] {{ keywords.extend_all }} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 new file mode 100644 index 00000000000..1c422a7a391 --- /dev/null +++ b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 @@ -0,0 +1,562 @@ +# coding=utf-8 +{% if code_model.license_header %} +{{ code_model.license_header }} +{% endif %} +# pylint: disable=line-too-long,useless-suppression,unnecessary-ellipsis +# -------------------------------------------------------------------------- +# This file is vendored from azure-core (azure.core.streaming). It provides the +# Stream / AsyncStream helpers (plus the JSONL / SSE decoders and event types) +# used by generated structured-streaming operations, so the generated package +# does not take a hard dependency on an azure-core version that ships +# azure.core.streaming. Do not edit by hand. +# -------------------------------------------------------------------------- +import codecs +import json +from types import TracebackType +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + List, + Optional, + Protocol, + Tuple, + Type, + TypeVar, + cast, + runtime_checkable, +) + +from typing_extensions import Self + +from azure.core.rest import AsyncHttpResponse, HttpResponse + +DecodedType = TypeVar("DecodedType") +ReturnType_co = TypeVar("ReturnType_co", covariant=True) +T_co = TypeVar("T_co", covariant=True) + + +@runtime_checkable +class StreamDecoder(Protocol[T_co]): + """Protocol for stream decoders.""" + + def iter_events(self, iter_bytes: Iterator[bytes]) -> Iterator[T_co]: + """Iterate over events from a byte iterator. + + :param iter_bytes: An iterator of byte chunks. + :type iter_bytes: Iterator[bytes] + :return: An iterator of decoded data. + :rtype: Iterator[DecodedType_co] + """ + ... + + +@runtime_checkable +class AsyncStreamDecoder(Protocol[T_co]): + """Protocol for async stream decoders.""" + + # Why this isn't async def: https://mypy.readthedocs.io/en/stable/more_types.html#asynchronous-iterators + def aiter_events(self, iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[T_co]: + """Asynchronously iterate over events from a byte iterator. + + :param iter_bytes: An asynchronous iterator of byte chunks. + :type iter_bytes: AsyncIterator[bytes] + :return: An asynchronous iterator of decoded data. + :rtype: AsyncIterator[DecodedType_co] + """ + ... + + +class JSONLEvent: + """A single JSON Lines (JSONL) event. + + :ivar data: The raw JSONL record. + :vartype data: str or None + :keyword data: The raw JSONL record. + :paramtype data: str or None + """ + + def __init__( + self, + *, + data: Optional[str] = None, + ) -> None: + self.data = data + + def json(self) -> Any: + """Parse the event data as JSON. + + :return: The parsed JSON value. + :rtype: Any + """ + return json.loads(cast(str, self.data)) + + +def iter_lines(iter_bytes: Iterator[bytes]) -> Iterator[str]: + """Iterate over lines from a byte iterator. + + :param iter_bytes: An iterator of byte chunks. + :type iter_bytes: Iterator[bytes] + :rtype: Iterator[str] + :return: An iterator of lines. + """ + decoder = codecs.getincrementaldecoder("utf-8")() + + # Split only on "\n" (tolerating "\r\n") rather than using str.splitlines(), + # which would also break on other Unicode boundaries (\v, \f, \x1c-\x1e, \x85, + # \u2028, \u2029) that are valid inside a JSONL record's string value. + decoded = "" + for chunk in iter_bytes: + decoded += decoder.decode(chunk) + if decoded: + decoded_lines = [line[:-1] if line.endswith("\r") else line for line in decoded.split("\n")] + yield from decoded_lines[:-1] + decoded = decoded_lines[-1] + + decoded += decoder.decode(b"", final=True) + if decoded: + yield decoded[:-1] if decoded.endswith("\r") else decoded + + +async def aiter_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[str]: + """Iterate over lines from a byte iterator. + + :param iter_bytes: An iterator of byte chunks. + :type iter_bytes: Iterator[bytes] + :rtype: Iterator[str] + :return: An iterator of lines. + """ + decoder = codecs.getincrementaldecoder("utf-8")() + + # Split only on "\n" (tolerating "\r\n") rather than using str.splitlines(), + # which would also break on other Unicode boundaries (\v, \f, \x1c-\x1e, \x85, + # \u2028, \u2029) that are valid inside a JSONL record's string value. + decoded = "" + async for chunk in iter_bytes: + decoded += decoder.decode(chunk) + if decoded: + decoded_lines = [line[:-1] if line.endswith("\r") else line for line in decoded.split("\n")] + for line in decoded_lines[:-1]: + yield line + decoded = decoded_lines[-1] + + decoded += decoder.decode(b"", final=True) + if decoded: + yield decoded[:-1] if decoded.endswith("\r") else decoded + + +class JSONLDecoder: + """Decoder for JSON Lines (JSONL) format. https://jsonlines.org/""" + + def iter_events(self, iter_bytes: Iterator[bytes]) -> Iterator[JSONLEvent]: + """Iterate over JSONL events from a byte iterator. + + :param iter_bytes: An iterator of byte chunks. + :type iter_bytes: Iterator[bytes] + :rtype: Iterator[JSONLEvent] + :return: An iterator of JSONL events. + """ + + yield from (JSONLEvent(data=line) for line in iter_lines(iter_bytes)) + + +class AsyncJSONLDecoder: + """Asynchronous decoder for JSON Lines (JSONL) format. https://jsonlines.org/""" + + # pylint: disable=invalid-overridden-method + async def aiter_events(self, iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[JSONLEvent]: + """Asynchronously iterate over JSONL events from a byte iterator. + + :param iter_bytes: An asynchronous iterator of byte chunks. + :type iter_bytes: AsyncIterator[bytes] + :rtype: AsyncIterator[JSONLEvent] + :return: An asynchronous iterator of JSONL events. + """ + + async for line in aiter_lines(iter_bytes): + yield JSONLEvent(data=line) + + +class ServerSentEvent: + """A single Server-Sent Event (SSE). + + https://html.spec.whatwg.org/multipage/server-sent-events.html + + :ivar event: The event type. Defaults to ``"message"`` when the stream does not + specify one. + :vartype event: str + :ivar data: The event payload. Multiple ``data`` lines are joined with ``"\\n"``. + Left as a raw string; the caller is responsible for any further parsing. + :vartype data: str + :ivar id: The last event ID. Defaults to an empty string until the stream + provides one. + :vartype id: str + :ivar retry: The reconnection time in milliseconds, if the stream provided one. + :vartype retry: int or None + :keyword event: The event type. Defaults to ``"message"`` when the stream does not + specify one. + :paramtype event: str + :keyword data: The event payload. Multiple ``data`` lines are joined with ``"\\n"``. + Left as a raw string; the caller is responsible for any further parsing. + :paramtype data: str + :keyword id: The last event ID. Defaults to an empty string until the stream + provides one. + :paramtype id: str + :keyword retry: The reconnection time in milliseconds, if the stream provided one. + :paramtype retry: int or None + """ + + def __init__( + self, + *, + event: str = "message", + data: str = "", + id: str = "", # pylint: disable=redefined-builtin + retry: Optional[int] = None, + ) -> None: + self.event = event + self.data = data + self.id = id + self.retry = retry + + def __repr__(self) -> str: + return f"ServerSentEvent(event={self.event!r}, data={self.data!r}, " f"id={self.id!r}, retry={self.retry!r})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ServerSentEvent): + return NotImplemented + return (self.event, self.data, self.id, self.retry) == ( + other.event, + other.data, + other.id, + other.retry, + ) + + +def _split_sse_lines(buf: str) -> Tuple[List[str], str]: + """Split ``buf`` into complete SSE lines plus a trailing remainder. + + Per the SSE spec, lines may be separated by ``\\r\\n``, ``\\r`` or ``\\n``. A lone + trailing ``\\r`` is kept in the remainder because it may be the first half of a + ``\\r\\n`` that arrives in a later chunk. + + :param buf: The buffered, already UTF-8 decoded text. + :type buf: str + :return: A tuple of ``(complete_lines, remainder)`` where ``remainder`` is the + unterminated tail (never containing a line separator, except a single trailing + ``\\r`` awaiting a possible ``\\n``). + :rtype: tuple[list[str], str] + """ + lines: List[str] = [] + start = 0 + i = 0 + n = len(buf) + while i < n: + char = buf[i] + if char == "\n": + lines.append(buf[start:i]) + i += 1 + start = i + elif char == "\r": + if i + 1 < n: + lines.append(buf[start:i]) + i += 2 if buf[i + 1] == "\n" else 1 + start = i + else: + # Trailing lone "\r": ambiguous, defer until the next chunk. + break + else: + i += 1 + return lines, buf[start:] + + +def _iter_sse_lines(iter_bytes: Iterator[bytes]) -> Iterator[str]: + """Iterate over SSE lines (line separators stripped) from a byte iterator. + + :param iter_bytes: An iterator of byte chunks. + :type iter_bytes: Iterator[bytes] + :rtype: Iterator[str] + :return: An iterator of decoded lines. + """ + # SSE is always UTF-8 (WHATWG spec). Use utf-8-sig to drop one leading BOM and + # errors="replace" so invalid byte sequences become U+FFFD instead of crashing. + decoder = codecs.getincrementaldecoder("utf-8-sig")(errors="replace") + + buf = "" + for chunk in iter_bytes: + buf += decoder.decode(chunk) + lines, buf = _split_sse_lines(buf) + yield from lines + + buf += decoder.decode(b"", final=True) + lines, remainder = _split_sse_lines(buf) + yield from lines + if remainder: + yield remainder[:-1] if remainder.endswith("\r") else remainder + + +async def _aiter_sse_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[str]: + """Asynchronously iterate over SSE lines (separators stripped) from a byte iterator. + + :param iter_bytes: An asynchronous iterator of byte chunks. + :type iter_bytes: AsyncIterator[bytes] + :rtype: AsyncIterator[str] + :return: An asynchronous iterator of decoded lines. + """ + # SSE is always UTF-8 (WHATWG spec). Use utf-8-sig to drop one leading BOM and + # errors="replace" so invalid byte sequences become U+FFFD instead of crashing. + decoder = codecs.getincrementaldecoder("utf-8-sig")(errors="replace") + + buf = "" + async for chunk in iter_bytes: + buf += decoder.decode(chunk) + lines, buf = _split_sse_lines(buf) + for line in lines: + yield line + + buf += decoder.decode(b"", final=True) + lines, remainder = _split_sse_lines(buf) + for line in lines: + yield line + if remainder: + yield remainder[:-1] if remainder.endswith("\r") else remainder + + +class _SSEEventBuilder: + """Accumulates SSE field lines and builds :class:`ServerSentEvent` instances.""" + + def __init__(self) -> None: + self._data: List[str] = [] + self._event_type = "" + self._last_id = "" + self._retry: Optional[int] = None + + def add_line(self, line: str) -> Optional[ServerSentEvent]: + """Process a single SSE line, dispatching an event on a blank line. + + :param line: A single SSE line with its terminator already stripped. + :type line: str + :return: A :class:`ServerSentEvent` when ``line`` is blank and an event is + pending, otherwise ``None``. + :rtype: ServerSentEvent or None + """ + if line == "": + return self._dispatch() + if line.startswith(":"): + # Comment line, ignored. + return None + + field, _, value = line.partition(":") + if value.startswith(" "): + value = value[1:] + + if field == "event": + self._event_type = value + elif field == "data": + self._data.append(value) + elif field == "id": + if "\x00" not in value: + self._last_id = value + elif field == "retry": + if value.isascii() and value.isdigit(): + try: + self._retry = int(value) + except ValueError: + # All ASCII digits but too long for int() (CPython's int-string + # conversion limit). Ignore rather than crashing the stream. + pass + # Unknown fields are ignored per spec. + return None + + def _dispatch(self) -> Optional[ServerSentEvent]: + if not self._data: + # No data accumulated: reset and dispatch nothing. + self._event_type = "" + return None + event = ServerSentEvent( + event=self._event_type or "message", + data="\n".join(self._data), + id=self._last_id, + retry=self._retry, + ) + self._data = [] + self._event_type = "" + return event + + +class SSEDecoder: + """Decoder for Server-Sent Events (SSE). + + https://html.spec.whatwg.org/multipage/server-sent-events.html + """ + + def iter_events(self, iter_bytes: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Iterate over SSE events from a byte iterator. + + :param iter_bytes: An iterator of byte chunks. + :type iter_bytes: Iterator[bytes] + :rtype: Iterator[ServerSentEvent] + :return: An iterator of server-sent events. + """ + builder = _SSEEventBuilder() + for line in _iter_sse_lines(iter_bytes): + event = builder.add_line(line) + if event is not None: + yield event + + +class AsyncSSEDecoder: + """Asynchronous decoder for Server-Sent Events (SSE). + + https://html.spec.whatwg.org/multipage/server-sent-events.html + """ + + # pylint: disable=invalid-overridden-method + async def aiter_events(self, iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Asynchronously iterate over SSE events from a byte iterator. + + :param iter_bytes: An asynchronous iterator of byte chunks. + :type iter_bytes: AsyncIterator[bytes] + :rtype: AsyncIterator[ServerSentEvent] + :return: An asynchronous iterator of server-sent events. + """ + builder = _SSEEventBuilder() + async for line in _aiter_sse_lines(iter_bytes): + event = builder.add_line(line) + if event is not None: + yield event + + +class Stream(Iterator[ReturnType_co]): + """Stream class for consuming a decoded event stream (e.g. JSONL or SSE). + + :keyword response: The response object. + :paramtype response: ~azure.core.rest.HttpResponse + :keyword decoder: A decoder to use for the stream. If omitted, the decoder is + inferred from the response ``Content-Type`` header. + :paramtype decoder: StreamDecoder + :keyword deserialization_callback: A callback that takes the response and the decoded event and + returns a deserialized object. + :paramtype deserialization_callback: Callable[[~azure.core.rest.HttpResponse, Any], ReturnType] + :keyword terminal_event: Optional event ``data`` value that terminates the stream (e.g. + ``"[DONE]"``). When an event's ``data`` equals this value, iteration stops and the + event is not passed to ``deserialization_callback``. + :paramtype terminal_event: str or None + """ + + def __init__( + self, + *, + response: HttpResponse, + deserialization_callback: Callable[[HttpResponse, DecodedType], ReturnType_co], + decoder: Optional[StreamDecoder[DecodedType]] = None, + terminal_event: Optional[str] = None, + ) -> None: + self._response = response + content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() + self._decoder: StreamDecoder[Any] = ( + decoder if decoder is not None else (SSEDecoder() if content_type == "text/event-stream" else JSONLDecoder()) + ) + self._deserialization_callback = deserialization_callback + self._terminal_event = terminal_event + self._iterator = self._iter_results() + + def __next__(self) -> ReturnType_co: + return self._iterator.__next__() + + def __iter__(self) -> Self: + return self + + def _iter_results(self) -> Iterator[ReturnType_co]: + for event in self._decoder.iter_events(self._response.iter_bytes()): + if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event: + break + result = self._deserialization_callback(self._response, event) + yield result + + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + def __enter__(self) -> Self: + return self + + def close(self) -> None: + self._response.close() + + +class AsyncStream(AsyncIterator[ReturnType_co]): + """AsyncStream class for asynchronously consuming a decoded event stream (e.g. JSONL or SSE). + + :keyword response: The response object. + :paramtype response: ~azure.core.rest.AsyncHttpResponse + :keyword decoder: A decoder to use for the stream. If omitted, the decoder is + inferred from the response ``Content-Type`` header. + :paramtype decoder: AsyncStreamDecoder + :keyword deserialization_callback: A callback that takes the response and the decoded event and + returns a deserialized object. + :paramtype deserialization_callback: Callable[[~azure.core.rest.AsyncHttpResponse, Any], ReturnType] + :keyword terminal_event: Optional event ``data`` value that terminates the stream (e.g. + ``"[DONE]"``). When an event's ``data`` equals this value, iteration stops and the + event is not passed to ``deserialization_callback``. + :paramtype terminal_event: str or None + """ + + def __init__( + self, + *, + response: AsyncHttpResponse, + deserialization_callback: Callable[[AsyncHttpResponse, DecodedType], ReturnType_co], + decoder: Optional[AsyncStreamDecoder[DecodedType]] = None, + terminal_event: Optional[str] = None, + ) -> None: + self._response = response + content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() + self._decoder: AsyncStreamDecoder[Any] = ( + decoder + if decoder is not None + else (AsyncSSEDecoder() if content_type == "text/event-stream" else AsyncJSONLDecoder()) + ) + self._deserialization_callback = deserialization_callback + self._terminal_event = terminal_event + self._iterator = self._iter_results() + + async def __anext__(self) -> ReturnType_co: + return await self._iterator.__anext__() + + def __aiter__(self) -> Self: + return self + + async def _iter_results(self) -> AsyncIterator[ReturnType_co]: + async for event in self._decoder.aiter_events(self._response.iter_bytes()): + if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event: + break + result = self._deserialization_callback(self._response, event) + yield result + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + await self.close() + + async def __aenter__(self) -> Self: + return self + + async def close(self) -> None: + await self._response.close() + + +__all__ = [ + "Stream", + "AsyncStream", + "JSONLEvent", + "ServerSentEvent", +] diff --git a/packages/http-client-python/generator/pygen/preprocess/__init__.py b/packages/http-client-python/generator/pygen/preprocess/__init__.py index ff3d6f094e3..d79fcb42235 100644 --- a/packages/http-client-python/generator/pygen/preprocess/__init__.py +++ b/packages/http-client-python/generator/pygen/preprocess/__init__.py @@ -35,6 +35,8 @@ def update_overload_section( for overload_s, original_s in zip(overload[section], yaml_data[section]): if overload_s.get("type"): overload_s["type"] = original_s["type"] + if overload_s.get("streaming"): + overload_s["streaming"] = original_s["streaming"] if overload_s.get("headers"): for overload_h, original_h in zip(overload_s["headers"], original_s["headers"]): if overload_h.get("type"): diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index 41dd54e4b17..4533f7e1f3e 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -18,22 +18,22 @@ }, "devDependencies": { "@azure-tools/azure-http-specs": "0.1.0-alpha.43", - "@azure-tools/typespec-autorest": "~0.70.0", + "@azure-tools/typespec-autorest": "0.71.0-dev.4", "@azure-tools/typespec-azure-core": "~0.70.0", "@azure-tools/typespec-azure-resource-manager": "~0.70.0", - "@azure-tools/typespec-azure-rulesets": "~0.70.0", - "@azure-tools/typespec-client-generator-core": "~0.70.0", + "@azure-tools/typespec-azure-rulesets": "0.71.0-dev.5", + "@azure-tools/typespec-client-generator-core": "0.71.0-dev.11", "@types/js-yaml": "~4.0.5", "@types/node": "~25.0.2", "@types/semver": "7.5.8", - "@typespec/compiler": "^1.14.0", + "@typespec/compiler": "1.15.0-dev.17", "@typespec/events": "~0.84.0", "@typespec/http": "^1.14.0", - "@typespec/http-specs": "0.1.0-alpha.39", + "@typespec/http-specs": "0.1.0-alpha.40", "@typespec/openapi": "^1.14.0", "@typespec/rest": "~0.84.0", "@typespec/spec-api": "0.1.0-alpha.15", - "@typespec/spector": "0.1.0-alpha.26", + "@typespec/spector": "0.1.0-alpha.27", "@typespec/sse": "~0.84.0", "@typespec/streams": "~0.84.0", "@typespec/versioning": "~0.84.0", @@ -50,12 +50,12 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@azure-tools/typespec-autorest": ">=0.70.0 <1.0.0", + "@azure-tools/typespec-autorest": ">=0.70.0 <1.0.0 || >=0.71.0-dev.4 <0.71.0", "@azure-tools/typespec-azure-core": ">=0.70.0 <1.0.0", "@azure-tools/typespec-azure-resource-manager": ">=0.70.0 <1.0.0", - "@azure-tools/typespec-azure-rulesets": ">=0.70.0 <1.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.70.0 <1.0.0", - "@typespec/compiler": "^1.14.0", + "@azure-tools/typespec-azure-rulesets": ">=0.70.0 <1.0.0 || >=0.71.0-dev.5 <0.71.0", + "@azure-tools/typespec-client-generator-core": ">=0.70.0 <1.0.0 || >=0.71.0-dev.11 <0.71.0", + "@typespec/compiler": "^1.14.0 || >=1.15.0-dev.17 <1.15.0", "@typespec/events": ">=0.84.0 <1.0.0", "@typespec/http": "^1.14.0", "@typespec/openapi": "^1.14.0", @@ -89,9 +89,9 @@ } }, "node_modules/@azure-tools/typespec-autorest": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-autorest/-/typespec-autorest-0.70.0.tgz", - "integrity": "sha512-OaxLkgMcuOXAbaqTNpezmFF24jtkiIH1+2PBwAeRo3ZG7C1r7Hf8xZwCK6KVtBEgMbqnrd5eCqxsPl1zy3y9/Q==", + "version": "0.71.0-dev.4", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@azure-tools/typespec-autorest/-/typespec-autorest-0.71.0-dev.4.tgz", + "integrity": "sha1-p920uaoYRzfFVIeEUDW6+uUri1w=", "dev": true, "license": "MIT", "dependencies": { @@ -101,9 +101,9 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@azure-tools/typespec-azure-core": "^0.70.0", - "@azure-tools/typespec-azure-resource-manager": "^0.70.0", - "@azure-tools/typespec-client-generator-core": "^0.70.0", + "@azure-tools/typespec-azure-core": "^0.70.0 || >= 0.71.0-dev.3", + "@azure-tools/typespec-azure-resource-manager": "^0.70.0 || >= 0.71.0-dev.10", + "@azure-tools/typespec-client-generator-core": "^0.70.0 || >= 0.71.0-dev.11", "@typespec/compiler": "^1.14.0", "@typespec/http": "^1.14.0", "@typespec/openapi": "^1.14.0", @@ -119,8 +119,8 @@ }, "node_modules/@azure-tools/typespec-azure-core": { "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-core/-/typespec-azure-core-0.70.0.tgz", - "integrity": "sha512-8MojHWRtTLKycJJ98IMoXX/5b9tTo3F0d3Iu20OKoCsORnSDG2NfjOWHJVW63oxA2t8VTlqC6J8BDcnRihygQQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@azure-tools/typespec-azure-core/-/typespec-azure-core-0.70.0.tgz", + "integrity": "sha1-k3VYRkt7yNAA1kiBElz71YbZH7A=", "dev": true, "license": "MIT", "engines": { @@ -134,8 +134,8 @@ }, "node_modules/@azure-tools/typespec-azure-resource-manager": { "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-resource-manager/-/typespec-azure-resource-manager-0.70.0.tgz", - "integrity": "sha512-hVrbbsOhU3EQ2yQTppCqsGQwY/HcVZPOINtFkoUo+PUVBmCFXyqLkTO4jvUbsp/LvJEwoQ8aEA8Y35f7VWT5uw==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@azure-tools/typespec-azure-resource-manager/-/typespec-azure-resource-manager-0.70.0.tgz", + "integrity": "sha1-+Skz0cnW1LADE60g/JnqYk0nixg=", "dev": true, "license": "MIT", "dependencies": { @@ -155,25 +155,25 @@ } }, "node_modules/@azure-tools/typespec-azure-rulesets": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-azure-rulesets/-/typespec-azure-rulesets-0.70.0.tgz", - "integrity": "sha512-Uxxl/18oryDwk2S+aYx6cIqiyjmoMeFDGmjuQ72a+aw6u8mZjgahMxNsY0ShvGLSchjsDqsVGaUlazXGXakVrw==", + "version": "0.71.0-dev.5", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@azure-tools/typespec-azure-rulesets/-/typespec-azure-rulesets-0.71.0-dev.5.tgz", + "integrity": "sha1-bpzvEq9boKSAhUwBc+EKXvxNOwI=", "dev": true, "license": "MIT", "engines": { "node": ">=22.0.0" }, "peerDependencies": { - "@azure-tools/typespec-azure-core": "^0.70.0", - "@azure-tools/typespec-azure-resource-manager": "^0.70.0", - "@azure-tools/typespec-client-generator-core": "^0.70.0", + "@azure-tools/typespec-azure-core": "^0.70.0 || >= 0.71.0-dev.4", + "@azure-tools/typespec-azure-resource-manager": "^0.70.0 || >= 0.71.0-dev.11", + "@azure-tools/typespec-client-generator-core": "^0.70.0 || >= 0.71.0-dev.11", "@typespec/compiler": "^1.14.0" } }, "node_modules/@azure-tools/typespec-client-generator-core": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.70.0.tgz", - "integrity": "sha512-8yxOYJfID3wp3FLQYNIa3kbmR5YLWjYtpB+i4u66quHTTQWWANHV1/o9f8xymAf+8fO9jbLo5tw1JerumxISWg==", + "version": "0.71.0-dev.11", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.71.0-dev.11.tgz", + "integrity": "sha1-uBy0avXoMit1nkolVuXXLg2gbmw=", "dev": true, "license": "MIT", "dependencies": { @@ -185,7 +185,7 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@azure-tools/typespec-azure-core": "^0.70.0", + "@azure-tools/typespec-azure-core": "^0.70.0 || >= 0.71.0-dev.3", "@typespec/compiler": "^1.14.0", "@typespec/events": "^0.84.0", "@typespec/http": "^1.14.0", @@ -999,8 +999,8 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha1-8p4iBXrVMWzyODbO6aNMgf/8t+Y=", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1014,9 +1014,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha1-POdNiYhRNr4VNTQfjD1EJcKaXKs=", "dev": true, "license": "MIT", "peer": true, @@ -1027,8 +1027,8 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", "dev": true, "license": "ISC", "peer": true, @@ -1041,8 +1041,8 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha1-G9AGzut+LlWyt3OrMY0wDhpmrto=", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1055,8 +1055,8 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha1-dyJYIEE9lhdQnak0IZCiAZ54dhw=", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1068,9 +1068,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.6", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha1-0iv9azp9jh8sCy8ubeERtT7G4T4=", "dev": true, "license": "MIT", "peer": true, @@ -1081,7 +1081,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -1093,9 +1093,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", "dev": true, "license": "MIT", "peer": true, @@ -1111,9 +1111,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha1-POdNiYhRNr4VNTQfjD1EJcKaXKs=", "dev": true, "license": "MIT", "peer": true, @@ -1124,16 +1124,16 @@ }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", "dev": true, "license": "MIT", "peer": true }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", "dev": true, "license": "ISC", "peer": true, @@ -1145,9 +1145,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.39.5", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha1-by+8/3VQDSKdU14KlJrhNHLIR4c=", "dev": true, "license": "MIT", "peer": true, @@ -1160,8 +1160,8 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha1-biEmoTR+hqTe34cG7Gf/jhB+u60=", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1171,8 +1171,8 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha1-l3nj/Zt+4zVxpXQ1z0M1oXlKbLI=", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1185,48 +1185,52 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha1-qCcsoDsqz0kmcCIrIyC2xCG/3mA=", "dev": true, + "license": "Apache-2.0", "peer": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.8", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha1-j4AMzME/T4zTEW4tnAqUk52j4+0=", "dev": true, + "license": "Apache-2.0", "peer": true, "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha1-8qCfYgEjkLK/8/xvskjd7IwJoJA=", "dev": true, + "license": "Apache-2.0", "peer": true, "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha1-r1smkaIrRL6EewyoFkHF+2rQFyw=", "dev": true, + "license": "Apache-2.0", "peer": true, "engines": { "node": ">=12.22" @@ -1237,10 +1241,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha1-wrnS43TuYsWG062+qHGZsdenpro=", "dev": true, + "license": "Apache-2.0", "peer": true, "engines": { "node": ">=18.18" @@ -2061,8 +2066,8 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", "dev": true, "license": "MIT", "peer": true @@ -2327,9 +2332,9 @@ } }, "node_modules/@typespec/compiler": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.14.0.tgz", - "integrity": "sha512-RRN0LGVDlonG/IbB2b4mvRjdCo6LywwB9/J8lOp6UaH7vtaFnKe5FL+rpxhof4rXx/zI/4OWnQO6c01bTCz4/Q==", + "version": "1.15.0-dev.17", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/compiler/-/compiler-1.15.0-dev.17.tgz", + "integrity": "sha1-hrlL0q3kmaR0fWNddCfI7LA2BCY=", "dev": true, "license": "MIT", "dependencies": { @@ -2341,7 +2346,7 @@ "is-unicode-supported": "^2.1.0", "mustache": "^4.2.0", "picocolors": "^1.1.1", - "prettier": "^3.8.1", + "prettier": "^3.9.5", "semver": "^7.7.4", "tar": "^7.5.13", "temporal-polyfill": "^1.0.1", @@ -2441,8 +2446,8 @@ }, "node_modules/@typespec/events": { "version": "0.84.0", - "resolved": "https://registry.npmjs.org/@typespec/events/-/events-0.84.0.tgz", - "integrity": "sha512-UroDIu6t6Z+cOLyX8I+GJWhSFmYGrp1L93F7ZVt0Ypmj0ndmC9YYa4cpeEyS5PDDIC8u49WfCIwfGegxt4rPVQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/events/-/events-0.84.0.tgz", + "integrity": "sha1-U6JW0cqeb0+n5fCjTbaW3DlOYPk=", "dev": true, "license": "MIT", "engines": { @@ -2454,8 +2459,8 @@ }, "node_modules/@typespec/http": { "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@typespec/http/-/http-1.14.0.tgz", - "integrity": "sha512-W+heCzu8K63AVcoX8MachVWaRxSAMFWOI1yBTc2Kq8QHaJeDiLL5JbU8VfTZ4tL/6EoGSdKfIT5ZNRW7oVCzhg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/http/-/http-1.14.0.tgz", + "integrity": "sha1-La9yB2Ny8FhnXSBbst9wYQBiOq4=", "dev": true, "license": "MIT", "engines": { @@ -2472,30 +2477,32 @@ } }, "node_modules/@typespec/http-specs": { - "version": "0.1.0-alpha.39", - "resolved": "https://registry.npmjs.org/@typespec/http-specs/-/http-specs-0.1.0-alpha.39.tgz", - "integrity": "sha512-x3ORyF/qLSLt+QDyievKT90STB0tT9J4s0Yu/Isio8zK16U+eRbCFHi69T7FZ94ZrRpPDWJ2u9+dEZv/SCPj+w==", + "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, "license": "MIT", "dependencies": { "@typespec/spec-api": "^0.1.0-alpha.15", - "@typespec/spector": "^0.1.0-alpha.26" + "@typespec/spector": "^0.1.0-alpha.27" }, "engines": { "node": ">=22.0.0" }, "peerDependencies": { "@typespec/compiler": "^1.14.0", + "@typespec/events": "^0.84.0", "@typespec/http": "^1.14.0", "@typespec/rest": "^0.84.0", + "@typespec/sse": "^0.84.0", "@typespec/versioning": "^0.84.0", "@typespec/xml": "^0.84.0" } }, "node_modules/@typespec/openapi": { "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@typespec/openapi/-/openapi-1.14.0.tgz", - "integrity": "sha512-KL7kImPhCXRmxpHVt1k7TWaa4bb3NbSeUx2rxyxeq7lYZFllI6/NYRCTOI/5JOrbElWmmSxrajU9K9IAKI6PkQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/openapi/-/openapi-1.14.0.tgz", + "integrity": "sha1-uwjWOV3VwWP59UXlR2xVUuzyCGc=", "dev": true, "license": "MIT", "engines": { @@ -2508,8 +2515,8 @@ }, "node_modules/@typespec/rest": { "version": "0.84.0", - "resolved": "https://registry.npmjs.org/@typespec/rest/-/rest-0.84.0.tgz", - "integrity": "sha512-9s5dDfRoHRPdtbVvkBasUx/RnMvwWMTuXRieSQDEji4gWGgxVu4Zt4MiEEKSfQrkMr3Aw0QjRCSxBxjMCHIOmA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/rest/-/rest-0.84.0.tgz", + "integrity": "sha1-kMLB39G8geZbiA3EEdQBaqteuuA=", "dev": true, "license": "MIT", "engines": { @@ -2582,9 +2589,9 @@ "license": "MIT" }, "node_modules/@typespec/spector": { - "version": "0.1.0-alpha.26", - "resolved": "https://registry.npmjs.org/@typespec/spector/-/spector-0.1.0-alpha.26.tgz", - "integrity": "sha512-WtaWIJE+Xh80G11Bi/3zC1GIrK4EVYBSCnkmSvm9bEYcBMH8QuryPDbUeTXYUyozcCrMYXaKnBboHN91IEi8ug==", + "version": "0.1.0-alpha.27", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typespec/spector/-/spector-0.1.0-alpha.27.tgz", + "integrity": "sha1-aA4cucLEbAIR6ZLH0llZuaVOMMY=", "dev": true, "license": "MIT", "dependencies": { @@ -2682,8 +2689,8 @@ }, "node_modules/@typespec/sse": { "version": "0.84.0", - "resolved": "https://registry.npmjs.org/@typespec/sse/-/sse-0.84.0.tgz", - "integrity": "sha512-9joNgVisRCWDFfV1d79iTAuR1W/6r+AKJrKUfcjsaTrq5A8OWW3v5TTsfxbHAZArn7n2WxQkqhNGgNyc8LjEng==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/sse/-/sse-0.84.0.tgz", + "integrity": "sha1-dkP/3P+tvI4Q2SV3oRz/F2JMVXo=", "dev": true, "license": "MIT", "engines": { @@ -2698,8 +2705,8 @@ }, "node_modules/@typespec/streams": { "version": "0.84.0", - "resolved": "https://registry.npmjs.org/@typespec/streams/-/streams-0.84.0.tgz", - "integrity": "sha512-SDneR8+zY+ueOpzg9yJtttfDe/ikB99JgddZSXKPwiDPlAIEeEvI8auipcYfB58EEOB21h8Oq0tEm8HqiAAWdQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/streams/-/streams-0.84.0.tgz", + "integrity": "sha1-Bm3D76chBKlcou2jj913xFfTBI4=", "dev": true, "license": "MIT", "engines": { @@ -2726,8 +2733,8 @@ }, "node_modules/@typespec/versioning": { "version": "0.84.0", - "resolved": "https://registry.npmjs.org/@typespec/versioning/-/versioning-0.84.0.tgz", - "integrity": "sha512-ZoDasTDj4z0mgFK+0cJL2+7DduCaTjvICHL2nQ/RBWc7nLgObaIYCjvXLno8WneDXnpxCAr7larN4/nlHEv9fg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/versioning/-/versioning-0.84.0.tgz", + "integrity": "sha1-YS06C7uMMWXKp7vwuy8qV8kjr38=", "dev": true, "license": "MIT", "engines": { @@ -2739,8 +2746,8 @@ }, "node_modules/@typespec/xml": { "version": "0.84.0", - "resolved": "https://registry.npmjs.org/@typespec/xml/-/xml-0.84.0.tgz", - "integrity": "sha512-3x0spgIrr4u3azkYaOxrlumtjoqPiUnJ/G5RwGBmUCAeE5F413MHf/AeIkmZ2ULT1gY3myabfZp8bOijTbMk7A==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@typespec/xml/-/xml-0.84.0.tgz", + "integrity": "sha1-aP99jZ3+wHS7f9mMJbEUT4Py7+g=", "dev": true, "license": "MIT", "engines": { @@ -2878,9 +2885,9 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.18.0", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha1-T68BstbTJr/u2XrqH1IiC19MGUA=", "dev": true, "license": "MIT", "peer": true, @@ -2893,8 +2900,8 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=", "dev": true, "license": "MIT", "peer": true, @@ -3216,8 +3223,8 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=", "dev": true, "license": "MIT", "peer": true, @@ -3235,6 +3242,24 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha1-qsTit3NKdAhnrrFr8CqtVWoeegE=", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/change-case": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", @@ -3342,8 +3367,8 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "license": "MIT", "peer": true @@ -3448,9 +3473,10 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=", "dev": true, + "license": "MIT", "peer": true }, "node_modules/default-browser": { @@ -3685,9 +3711,10 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -3697,9 +3724,9 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "9.39.5", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha1-Kk48iw91MZbvrpQ8j/qocw/Go/o=", "dev": true, "license": "MIT", "peer": true, @@ -3709,8 +3736,8 @@ "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3759,8 +3786,8 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha1-iOZGogf61hQ2/6OetQUUcgBlXII=", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -3789,9 +3816,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", "dev": true, "license": "MIT", "peer": true, @@ -3807,9 +3834,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha1-POdNiYhRNr4VNTQfjD1EJcKaXKs=", "dev": true, "license": "MIT", "peer": true, @@ -3818,35 +3845,18 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", "dev": true, "license": "MIT", "peer": true }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", "dev": true, "license": "ISC", "peer": true, @@ -3859,8 +3869,8 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/espree/-/espree-10.4.0.tgz", + "integrity": "sha1-1U9JSdRikAWh+haNk3w/8ffiqDc=", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -3877,10 +3887,11 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha1-CNBI8mHw3e21uulfRoCUY9nJSW0=", "dev": true, + "license": "BSD-3-Clause", "peer": true, "dependencies": { "estraverse": "^5.1.0" @@ -3891,8 +3902,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha1-eteWTWeauyi+5yzsY3WLHF0smSE=", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -3905,9 +3916,10 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha1-LupSkHAvJquP5TcDcP+GyWXSESM=", "dev": true, + "license": "BSD-2-Clause", "peer": true, "engines": { "node": ">=4.0" @@ -3925,9 +3937,10 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=", "dev": true, + "license": "BSD-2-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -4015,17 +4028,18 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", "dev": true, "license": "MIT", "peer": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true, + "license": "MIT", "peer": true }, "node_modules/fast-string-truncated-width": { @@ -4115,9 +4129,10 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha1-d4e93PETG/+5JjbGlFe7wO3W2B8=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "flat-cache": "^4.0.0" @@ -4179,9 +4194,10 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha1-Ds45/LFO4BL0sEEL0z3ZwfAREnw=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "flatted": "^3.2.9", @@ -4192,9 +4208,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha1-ruyipQYwPwzuYcWebJ8qiNLyn8Y=", "dev": true, "license": "ISC", "peer": true @@ -4350,9 +4366,10 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "is-glob": "^4.0.3" @@ -4402,8 +4419,8 @@ }, "node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/globals/-/globals-14.0.0.tgz", + "integrity": "sha1-iY10E8Kbq89rr+Vvyt3thYrack4=", "dev": true, "license": "MIT", "peer": true, @@ -4536,8 +4553,8 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", "dev": true, "license": "MIT", "peer": true, @@ -4547,8 +4564,8 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha1-nOy1ZQPAraHydB271lRuSxO1fM8=", "dev": true, "license": "MIT", "peer": true, @@ -4565,8 +4582,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true, "license": "MIT", "peer": true, @@ -4609,9 +4626,10 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -4628,9 +4646,10 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "is-extglob": "^2.1.1" @@ -4779,9 +4798,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha1-0ZAFcqf3zwtfVAyDZz5gutNDZZI=", "funding": [ { "type": "github", @@ -4802,9 +4821,10 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", "dev": true, + "license": "MIT", "peer": true }, "node_modules/json-schema-traverse": { @@ -4816,9 +4836,10 @@ }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true, + "license": "MIT", "peer": true }, "node_modules/jsonwebtoken": { @@ -4869,9 +4890,10 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha1-qHmpnilFL5QkOfKkBeOvizHU3pM=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "json-buffer": "3.0.1" @@ -4879,9 +4901,10 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/levn/-/levn-0.4.1.tgz", + "integrity": "sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "prelude-ls": "^1.2.1", @@ -5211,9 +5234,10 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha1-VYqlO0O2YeGSWgr9+japoQhf5Xo=", "dev": true, + "license": "MIT", "peer": true }, "node_modules/lodash.once": { @@ -5625,9 +5649,10 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha1-fqHBpdkddk+yghOciP4R4YKjpzQ=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "deep-is": "^0.1.3", @@ -5679,8 +5704,8 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=", "dev": true, "license": "MIT", "peer": true, @@ -5829,9 +5854,10 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">= 0.8.0" @@ -5869,8 +5895,8 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", "dev": true, "license": "MIT", "peer": true, @@ -5972,8 +5998,8 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=", "dev": true, "license": "MIT", "peer": true, @@ -6452,8 +6478,8 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha1-MfEoGzgyYwQ0gxwxDAHMzajL4AY=", "dev": true, "license": "MIT", "peer": true, @@ -6744,9 +6770,10 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "prelude-ls": "^1.2.1" @@ -6852,8 +6879,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -7158,9 +7185,10 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha1-0sRcbdT7zmIaZvE2y+Mor9BBCzQ=", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index d3d837fcad5..1074cb3e58d 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -66,12 +66,12 @@ "emitter" ], "peerDependencies": { - "@azure-tools/typespec-autorest": ">=0.70.0 <1.0.0", + "@azure-tools/typespec-autorest": ">=0.70.0 <1.0.0 || >=0.71.0-dev.4 <0.71.0", "@azure-tools/typespec-azure-core": ">=0.70.0 <1.0.0", "@azure-tools/typespec-azure-resource-manager": ">=0.70.0 <1.0.0", - "@azure-tools/typespec-azure-rulesets": ">=0.70.0 <1.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.70.0 <1.0.0", - "@typespec/compiler": "^1.14.0", + "@azure-tools/typespec-azure-rulesets": ">=0.70.0 <1.0.0 || >=0.71.0-dev.5 <0.71.0", + "@azure-tools/typespec-client-generator-core": ">=0.70.0 <1.0.0 || >=0.71.0-dev.11 <0.71.0", + "@typespec/compiler": "^1.14.0 || >=1.15.0-dev.17 <1.15.0", "@typespec/http": "^1.14.0", "@typespec/openapi": "^1.14.0", "@typespec/rest": ">=0.84.0 <1.0.0", @@ -104,24 +104,24 @@ "tsx": "^4.21.0" }, "devDependencies": { - "@azure-tools/typespec-autorest": "~0.70.0", + "@azure-tools/typespec-autorest": "0.71.0-dev.4", "@azure-tools/typespec-azure-core": "~0.70.0", "@azure-tools/typespec-azure-resource-manager": "~0.70.0", - "@azure-tools/typespec-azure-rulesets": "~0.70.0", - "@azure-tools/typespec-client-generator-core": "~0.70.0", + "@azure-tools/typespec-azure-rulesets": "0.71.0-dev.5", + "@azure-tools/typespec-client-generator-core": "0.71.0-dev.11", "@azure-tools/azure-http-specs": "0.1.0-alpha.43", - "@typespec/compiler": "^1.14.0", + "@typespec/compiler": "1.15.0-dev.17", "@typespec/http": "^1.14.0", "@typespec/openapi": "^1.14.0", "@typespec/rest": "~0.84.0", "@typespec/versioning": "~0.84.0", "@typespec/events": "~0.84.0", - "@typespec/spector": "0.1.0-alpha.26", + "@typespec/spector": "0.1.0-alpha.27", "@typespec/spec-api": "0.1.0-alpha.15", "@typespec/sse": "~0.84.0", "@typespec/streams": "~0.84.0", "@typespec/xml": "~0.84.0", - "@typespec/http-specs": "0.1.0-alpha.39", + "@typespec/http-specs": "0.1.0-alpha.40", "@types/js-yaml": "~4.0.5", "@types/node": "~25.0.2", "@types/semver": "7.5.8", @@ -132,5 +132,8 @@ "typescript-eslint": "^8.49.0", "vitest": "^4.0.15", "prettier": "^3.9.5" + }, + "overrides": { + "@typespec/compiler": "$@typespec/compiler" } } diff --git a/packages/http-client-python/tests/unit/test_streaming_init.py b/packages/http-client-python/tests/unit/test_streaming_init.py new file mode 100644 index 00000000000..1d83e492c5a --- /dev/null +++ b/packages/http-client-python/tests/unit/test_streaming_init.py @@ -0,0 +1,57 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from types import SimpleNamespace + +import pytest +from jinja2 import Environment, PackageLoader + +from pygen.codegen.serializers.general_serializer import GeneralSerializer + + +def _env() -> Environment: + return Environment( + loader=PackageLoader("pygen.codegen", "templates"), + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, + ) + + +def _render_init(*, has_structured_stream: bool, async_mode: bool = False) -> str: + code_model = SimpleNamespace( + namespace="sample", + license_header="", + options={"package-version": None}, + need_streaming_base=has_structured_stream, + get_serialize_namespace=lambda namespace, async_mode: f"{namespace}.aio" if async_mode else namespace, + get_relative_import_path=lambda _namespace, module_name: f".{module_name}", + is_top_namespace=lambda namespace: namespace == "sample", + ) + client = SimpleNamespace(filename="_client", name="SampleClient") + return GeneralSerializer(code_model=code_model, env=_env(), async_mode=async_mode).serialize_init_file([client]) + + +def test_exports_stream_types_from_base_namespace(): + init_file = _render_init(has_structured_stream=True) + + assert "from ._utils.streaming_base import AsyncStream, Stream" in init_file + assert '"AsyncStream",' in init_file + assert '"Stream",' in init_file + + +@pytest.mark.parametrize( + "has_structured_stream,async_mode", + [ + (False, False), + (True, True), + ], +) +def test_does_not_export_stream_types_outside_structured_base_namespace(has_structured_stream, async_mode): + init_file = _render_init(has_structured_stream=has_structured_stream, async_mode=async_mode) + + assert "streaming_base import" not in init_file + assert '"AsyncStream",' not in init_file + assert '"Stream",' not in init_file From 851b4a2c106bcd9af6a45d36141f541395516141 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 11 Aug 2026 12:52:22 -0700 Subject: [PATCH 02/21] feat(http-client-python): sync vendored streaming_base with azure-core PR #48077 Adopt linear-time JSONL/SSE line framers (_JSONLLineFramer, _SSELineFramer) and aclosing/try-finally lifecycle from the refactored azure.core.streaming runtime. Preserve the emitter's terminal_event extension on Stream/AsyncStream (generated builder_serializer.py call site unchanged). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2a68a869-b075-46b2-87b4-2bbe62948e69 --- .../templates/streaming_base.py.jinja2 | 322 ++++++++++++------ 1 file changed, 225 insertions(+), 97 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 index 1c422a7a391..d89a051d1eb 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 @@ -12,16 +12,18 @@ # -------------------------------------------------------------------------- import codecs import json +from contextlib import aclosing from types import TracebackType from typing import ( Any, + AsyncGenerator, AsyncIterator, Callable, + Generator, Iterator, List, Optional, Protocol, - Tuple, Type, TypeVar, cast, @@ -93,6 +95,64 @@ class JSONLEvent: return json.loads(cast(str, self.data)) +class _JSONLLineFramer: + """Incremental JSONL line framer with linear-time behavior. + + JSONL records are separated only by ``"\\n"`` (tolerating ``"\\r\\n"``). Unlike + ``str.splitlines()``, other Unicode boundaries (``\\v``, ``\\f``, ``\\x1c``-``\\x1e``, + ``\\x85``, ``\\u2028``, ``\\u2029``) are preserved because they are valid inside a JSONL + record's string value. + + Rather than re-concatenating and re-splitting the whole pending record on every network chunk + (which is O(n^2) for a single long record fragmented across many chunks), the unfinished record + is held as a list of fragments and joined only when a terminator arrives (or at EOF). Only the + newly decoded text is scanned per chunk, giving O(total) behavior. + """ + + def __init__(self) -> None: + # Fragments of the current, not-yet-terminated record. Never contains a "\n". + self._parts: List[str] = [] + + def push(self, text: str) -> List[str]: + """Feed newly decoded text and return any completed records. + + :param text: Newly decoded text from a single chunk. + :type text: str + :return: Completed records produced by this chunk (may be empty). + :rtype: list[str] + """ + if not text: + return [] + + segments = text.split("\n") + # Fast path: no line terminator, so this is a continuation of the current record. Stash the + # fragment without joining or rescanning the accumulated tail. + if len(segments) == 1: + self._parts.append(text) + return [] + + first = "".join(self._parts) + segments[0] + # All but the final segment are complete records (terminated by "\n"). Strip a trailing "\r" + # to tolerate "\r\n" line endings. + completed = [line[:-1] if line.endswith("\r") else line for line in [first, *segments[1:-1]]] + self._parts = [segments[-1]] + return completed + + def flush(self, extra: str = "") -> List[str]: + """Return the final unterminated record, if any, at end of stream. + + :param extra: Trailing text from finalizing the incremental decoder. + :type extra: str + :return: The final record, if non-empty. + :rtype: list[str] + """ + tail = "".join(self._parts) + extra + self._parts = [] + if not tail: + return [] + return [tail[:-1] if tail.endswith("\r") else tail] + + def iter_lines(iter_bytes: Iterator[bytes]) -> Iterator[str]: """Iterate over lines from a byte iterator. @@ -102,24 +162,15 @@ def iter_lines(iter_bytes: Iterator[bytes]) -> Iterator[str]: :return: An iterator of lines. """ decoder = codecs.getincrementaldecoder("utf-8")() + framer = _JSONLLineFramer() - # Split only on "\n" (tolerating "\r\n") rather than using str.splitlines(), - # which would also break on other Unicode boundaries (\v, \f, \x1c-\x1e, \x85, - # \u2028, \u2029) that are valid inside a JSONL record's string value. - decoded = "" for chunk in iter_bytes: - decoded += decoder.decode(chunk) - if decoded: - decoded_lines = [line[:-1] if line.endswith("\r") else line for line in decoded.split("\n")] - yield from decoded_lines[:-1] - decoded = decoded_lines[-1] + yield from framer.push(decoder.decode(chunk)) - decoded += decoder.decode(b"", final=True) - if decoded: - yield decoded[:-1] if decoded.endswith("\r") else decoded + yield from framer.flush(decoder.decode(b"", final=True)) -async def aiter_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[str]: +async def aiter_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncGenerator[str, None]: """Iterate over lines from a byte iterator. :param iter_bytes: An iterator of byte chunks. @@ -128,22 +179,19 @@ async def aiter_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[str]: :return: An iterator of lines. """ decoder = codecs.getincrementaldecoder("utf-8")() + framer = _JSONLLineFramer() - # Split only on "\n" (tolerating "\r\n") rather than using str.splitlines(), - # which would also break on other Unicode boundaries (\v, \f, \x1c-\x1e, \x85, - # \u2028, \u2029) that are valid inside a JSONL record's string value. - decoded = "" - async for chunk in iter_bytes: - decoded += decoder.decode(chunk) - if decoded: - decoded_lines = [line[:-1] if line.endswith("\r") else line for line in decoded.split("\n")] - for line in decoded_lines[:-1]: + try: + async for chunk in iter_bytes: + for line in framer.push(decoder.decode(chunk)): yield line - decoded = decoded_lines[-1] + finally: + aclose = getattr(iter_bytes, "aclose", None) + if aclose is not None: + await aclose() - decoded += decoder.decode(b"", final=True) - if decoded: - yield decoded[:-1] if decoded.endswith("\r") else decoded + for line in framer.flush(decoder.decode(b"", final=True)): + yield line class JSONLDecoder: @@ -174,8 +222,9 @@ class AsyncJSONLDecoder: :return: An asynchronous iterator of JSONL events. """ - async for line in aiter_lines(iter_bytes): - yield JSONLEvent(data=line) + async with aclosing(aiter_lines(iter_bytes)) as lines: + async for line in lines: + yield JSONLEvent(data=line) class ServerSentEvent: @@ -234,41 +283,110 @@ class ServerSentEvent: ) -def _split_sse_lines(buf: str) -> Tuple[List[str], str]: - """Split ``buf`` into complete SSE lines plus a trailing remainder. +class _SSELineFramer: + """Incremental SSE line framer with linear-time behavior. - Per the SSE spec, lines may be separated by ``\\r\\n``, ``\\r`` or ``\\n``. A lone - trailing ``\\r`` is kept in the remainder because it may be the first half of a - ``\\r\\n`` that arrives in a later chunk. + Per the SSE spec, lines may be separated by ``"\\r\\n"``, ``"\\r"`` or ``"\\n"``. A lone trailing + ``"\\r"`` at a chunk boundary is ambiguous (it may be the first half of a ``"\\r\\n"``) and its + resolution is deferred until the next chunk (or EOF). - :param buf: The buffered, already UTF-8 decoded text. - :type buf: str - :return: A tuple of ``(complete_lines, remainder)`` where ``remainder`` is the - unterminated tail (never containing a line separator, except a single trailing - ``\\r`` awaiting a possible ``\\n``). - :rtype: tuple[list[str], str] + Rather than re-concatenating and re-scanning the whole pending line on every network chunk + (which is O(n^2) for a single long line fragmented across many chunks), the unfinished line is + held as a list of fragments and joined only when a terminator arrives (or at EOF). Only the + newly decoded text is scanned per chunk, giving O(total) behavior. """ - lines: List[str] = [] - start = 0 - i = 0 - n = len(buf) - while i < n: - char = buf[i] - if char == "\n": - lines.append(buf[start:i]) - i += 1 - start = i - elif char == "\r": - if i + 1 < n: - lines.append(buf[start:i]) - i += 2 if buf[i + 1] == "\n" else 1 + + def __init__(self) -> None: + # Fragments of the current, not-yet-terminated line. Never contains a separator. + self._parts: List[str] = [] + # True when the previous chunk ended with a lone "\r" whose "\r\n" status is still unknown. + self._pending_cr = False + + def _emit_current(self) -> str: + line = "".join(self._parts) + self._parts = [] + return line + + def push(self, text: str) -> List[str]: + """Feed newly decoded text and return any completed lines. + + :param text: Newly decoded text from a single chunk. + :type text: str + :return: Completed lines (separators stripped) produced by this chunk. + :rtype: list[str] + """ + if not text: + return [] + + out: List[str] = [] + if self._pending_cr: + # The deferred "\r" terminates the current line now that more data is available. + out.append(self._emit_current()) + self._pending_cr = False + # A leading "\n" is the second half of that "\r\n" pair: consume it. + if text[:1] == "\n": + text = text[1:] + + n = len(text) + start = 0 + i = 0 + while i < n: + char = text[i] + if char == "\n": + self._parts.append(text[start:i]) + out.append(self._emit_current()) + i += 1 start = i + elif char == "\r": + if i + 1 < n: + self._parts.append(text[start:i]) + out.append(self._emit_current()) + i += 2 if text[i + 1] == "\n" else 1 + start = i + else: + # Trailing lone "\r": defer resolution until the next chunk. + self._parts.append(text[start:i]) + self._pending_cr = True + start = n + break else: - # Trailing lone "\r": ambiguous, defer until the next chunk. - break + i += 1 + + if start < n: + self._parts.append(text[start:n]) + return out + + def flush(self, extra: str = "") -> List[str]: + """Return any remaining lines at end of stream. + + A lone trailing ``"\\r"`` is treated as a terminator (its ``"\\r\\n"`` half never arrives), + and a non-empty final unterminated line is emitted; an empty tail is not, so no blank line + (and therefore no spurious event) is invented at EOF. + + :param extra: Trailing text from finalizing the incremental decoder. + :type extra: str + :return: The remaining lines, if any. + :rtype: list[str] + """ + out: List[str] = [] + if self._pending_cr: + out.append(self._emit_current()) + self._pending_cr = False + if extra[:1] == "\n": + extra = extra[1:] + + if extra: + out.extend(self.push(extra)) + + if self._pending_cr: + # 'extra' ended in a lone "\r": at EOF it is a terminator; emit the preceding content. + out.append(self._emit_current()) + self._pending_cr = False else: - i += 1 - return lines, buf[start:] + tail = self._emit_current() + if tail: + out.append(tail) + return out def _iter_sse_lines(iter_bytes: Iterator[bytes]) -> Iterator[str]: @@ -282,21 +400,15 @@ def _iter_sse_lines(iter_bytes: Iterator[bytes]) -> Iterator[str]: # SSE is always UTF-8 (WHATWG spec). Use utf-8-sig to drop one leading BOM and # errors="replace" so invalid byte sequences become U+FFFD instead of crashing. decoder = codecs.getincrementaldecoder("utf-8-sig")(errors="replace") + framer = _SSELineFramer() - buf = "" for chunk in iter_bytes: - buf += decoder.decode(chunk) - lines, buf = _split_sse_lines(buf) - yield from lines + yield from framer.push(decoder.decode(chunk)) - buf += decoder.decode(b"", final=True) - lines, remainder = _split_sse_lines(buf) - yield from lines - if remainder: - yield remainder[:-1] if remainder.endswith("\r") else remainder + yield from framer.flush(decoder.decode(b"", final=True)) -async def _aiter_sse_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[str]: +async def _aiter_sse_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncGenerator[str, None]: """Asynchronously iterate over SSE lines (separators stripped) from a byte iterator. :param iter_bytes: An asynchronous iterator of byte chunks. @@ -307,20 +419,19 @@ async def _aiter_sse_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[st # SSE is always UTF-8 (WHATWG spec). Use utf-8-sig to drop one leading BOM and # errors="replace" so invalid byte sequences become U+FFFD instead of crashing. decoder = codecs.getincrementaldecoder("utf-8-sig")(errors="replace") + framer = _SSELineFramer() - buf = "" - async for chunk in iter_bytes: - buf += decoder.decode(chunk) - lines, buf = _split_sse_lines(buf) - for line in lines: - yield line + try: + async for chunk in iter_bytes: + for line in framer.push(decoder.decode(chunk)): + yield line + finally: + aclose = getattr(iter_bytes, "aclose", None) + if aclose is not None: + await aclose() - buf += decoder.decode(b"", final=True) - lines, remainder = _split_sse_lines(buf) - for line in lines: + for line in framer.flush(decoder.decode(b"", final=True)): yield line - if remainder: - yield remainder[:-1] if remainder.endswith("\r") else remainder class _SSEEventBuilder: @@ -422,10 +533,11 @@ class AsyncSSEDecoder: :return: An asynchronous iterator of server-sent events. """ builder = _SSEEventBuilder() - async for line in _aiter_sse_lines(iter_bytes): - event = builder.add_line(line) - if event is not None: - yield event + async with aclosing(_aiter_sse_lines(iter_bytes)) as lines: + async for line in lines: + event = builder.add_line(line) + if event is not None: + yield event class Stream(Iterator[ReturnType_co]): @@ -468,12 +580,15 @@ class Stream(Iterator[ReturnType_co]): def __iter__(self) -> Self: return self - def _iter_results(self) -> Iterator[ReturnType_co]: - for event in self._decoder.iter_events(self._response.iter_bytes()): - if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event: - break - result = self._deserialization_callback(self._response, event) - yield result + def _iter_results(self) -> Generator[ReturnType_co, None, None]: + try: + for event in self._decoder.iter_events(self._response.iter_bytes()): + if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event: + break + result = self._deserialization_callback(self._response, event) + yield result + finally: + self._response.close() def __exit__( self, @@ -487,7 +602,10 @@ class Stream(Iterator[ReturnType_co]): return self def close(self) -> None: - self._response.close() + try: + self._iterator.close() + finally: + self._response.close() class AsyncStream(AsyncIterator[ReturnType_co]): @@ -532,12 +650,19 @@ class AsyncStream(AsyncIterator[ReturnType_co]): def __aiter__(self) -> Self: return self - async def _iter_results(self) -> AsyncIterator[ReturnType_co]: - async for event in self._decoder.aiter_events(self._response.iter_bytes()): - if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event: - break - result = self._deserialization_callback(self._response, event) - yield result + async def _iter_results(self) -> AsyncGenerator[ReturnType_co, None]: + events = self._decoder.aiter_events(self._response.iter_bytes()) + try: + async for event in events: + if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event: + break + result = self._deserialization_callback(self._response, event) + yield result + finally: + aclose = getattr(events, "aclose", None) + if aclose is not None: + await aclose() + await self._response.close() async def __aexit__( self, @@ -551,7 +676,10 @@ class AsyncStream(AsyncIterator[ReturnType_co]): return self async def close(self) -> None: - await self._response.close() + try: + await self._iterator.aclose() + finally: + await self._response.close() __all__ = [ From f5db6370e74af5ae6b160791da04bd525cc6bf28 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 11 Aug 2026 12:57:10 -0700 Subject: [PATCH 03/21] chore: add async streaming terms to cspell Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e --- cspell.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cspell.yaml b/cspell.yaml index 6273e27f277..0140694c651 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -5,6 +5,8 @@ dictionaries: - node - typescript words: + - aclose + - aclosing - Ablack - Adoptium - aenter From bf35b418d50d4cf82e492a176ecfac08a5ec64b1 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 12 Aug 2026 11:01:33 -0700 Subject: [PATCH 04/21] fix(http-client-python): emit absolute import when namespaces share no package root Structured JSONL/SSE streaming (and any cross-root model reference) could emit a package-escaping relative import such as `from .......search import models` when a payload model's client_namespace shares no top-level package component with the generated module's namespace (e.g. a `search` model referenced from an `azure.search.documents` package). Python rejects that at runtime with "attempted relative import beyond top-level package". CodeModel.get_relative_import_path now falls back to a valid absolute import when the common-prefix length is zero (idx == 0); in-package imports (which always share the package root, idx >= 1) are byte-identical to before. Stream/AsyncStream continue to be imported locally from `_utils.streaming_base`. Adds spec-agnostic regression tests for the helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e --- .../pygen/codegen/models/code_model.py | 13 ++- .../tests/unit/test_relative_import_path.py | 95 +++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 packages/http-client-python/tests/unit/test_relative_import_path.py diff --git a/packages/http-client-python/generator/pygen/codegen/models/code_model.py b/packages/http-client-python/generator/pygen/codegen/models/code_model.py index c1e0bb1ee6b..4ce1d86bbc8 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/code_model.py +++ b/packages/http-client-python/generator/pygen/codegen/models/code_model.py @@ -145,9 +145,16 @@ def get_relative_import_path( if serialize_namespace_split[idx] != imported_namespace_split[idx]: break idx += 1 - self._relative_import_path[key] = "." * (len(serialize_namespace_split[idx:]) + 1) + ".".join( - imported_namespace_split[idx:] - ) + if idx == 0: + # The two namespaces share no top-level package, so a relative import would + # climb above the package root (e.g. ``from .......search import models``), + # which Python rejects at runtime ("attempted relative import beyond top-level + # package"). Emit a valid absolute import of the foreign namespace instead. + self._relative_import_path[key] = ".".join(imported_namespace_split) + else: + self._relative_import_path[key] = "." * (len(serialize_namespace_split[idx:]) + 1) + ".".join( + imported_namespace_split[idx:] + ) result = self._relative_import_path[key] if module_name is None: return result diff --git a/packages/http-client-python/tests/unit/test_relative_import_path.py b/packages/http-client-python/tests/unit/test_relative_import_path.py new file mode 100644 index 00000000000..13d95ccfada --- /dev/null +++ b/packages/http-client-python/tests/unit/test_relative_import_path.py @@ -0,0 +1,95 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +"""Tests for ``CodeModel.get_relative_import_path``. + +A structured (JSONL / SSE) streaming operation can reference a payload model +whose ``client_namespace`` shares no top-level package with the operation being +generated (for example an SSE event payload that TCGC reports under a ``search`` +namespace while the package root is ``azure.search.documents``). Emitting a +relative import in that case climbs above the package root +(``from .......search import models``), which Python rejects at runtime with +``attempted relative import beyond top-level package``. The generator must fall +back to a valid absolute import instead. In-package imports must stay relative +(byte-identical to before). +""" + +import pytest + +from pygen.codegen.models import CodeModel + + +def _code_model() -> CodeModel: + return CodeModel( + { + "clients": [ + { + "name": "client", + "namespace": "azure.search.documents", + "moduleName": "azure.search.documents", + "parameters": [], + "url": "", + "operationGroups": [], + } + ], + "namespace": "azure.search.documents", + }, + options={ + "show-send-request": True, + "builders-visibility": "public", + "show-operations": True, + "models-mode": "dpg", + "only-path-and-body-params-positional": True, + }, + ) + + +@pytest.mark.parametrize( + "serialize_namespace,imported_namespace,expected", + [ + # In-package: shares a top-level package -> relative import (unchanged). + ("azure.test.operations", "azure.test", ".."), + ("azure.test.operations", "azure", "..."), + ("azure.test.subtest.aio.operations", "azure.test", "...."), + ("azure.search.documents.operations", "azure.search.documents", ".."), + ], +) +def test_in_package_stays_relative(serialize_namespace, imported_namespace, expected): + assert _code_model().get_relative_import_path(serialize_namespace, imported_namespace) == expected + + +@pytest.mark.parametrize( + "serialize_namespace,imported_namespace,expected", + [ + # No shared top-level package -> valid absolute import (no leading dots). + ("azure.search.documents.grp.aio.operations", "search", "search"), + ("azure.search.documents.aio.operations", "search", "search"), + ("search.aio.operations", "azure.search.documents", "azure.search.documents"), + ], +) +def test_cross_root_uses_absolute_import(serialize_namespace, imported_namespace, expected): + result = _code_model().get_relative_import_path(serialize_namespace, imported_namespace) + assert result == expected + assert not result.startswith("."), "cross-root import must be absolute, not a package-escaping relative import" + + +def test_cross_root_absolute_import_with_module_name(): + # The Stream / AsyncStream import points at ``._utils.streaming_base``. When an + # operation lives outside the package root, that import must also be absolute-valid. + result = _code_model().get_relative_import_path( + "search.aio.operations", + "azure.search.documents", + module_name="_utils.streaming_base", + ) + assert result == "azure.search.documents._utils.streaming_base" + + +def test_in_package_module_name_stays_relative(): + result = _code_model().get_relative_import_path( + "azure.search.documents.grp.aio.operations", + "azure.search.documents", + module_name="_utils.streaming_base", + ) + assert result == "...._utils.streaming_base" From d9f7aa257b35ff5026d44998e673d55878321f91 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 12 Aug 2026 12:55:28 -0700 Subject: [PATCH 05/21] Revert "fix(http-client-python): emit absolute import when namespaces share no package root" This reverts commit bf35b418d50d4cf82e492a176ecfac08a5ec64b1. --- .../pygen/codegen/models/code_model.py | 13 +-- .../tests/unit/test_relative_import_path.py | 95 ------------------- 2 files changed, 3 insertions(+), 105 deletions(-) delete mode 100644 packages/http-client-python/tests/unit/test_relative_import_path.py diff --git a/packages/http-client-python/generator/pygen/codegen/models/code_model.py b/packages/http-client-python/generator/pygen/codegen/models/code_model.py index 4ce1d86bbc8..c1e0bb1ee6b 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/code_model.py +++ b/packages/http-client-python/generator/pygen/codegen/models/code_model.py @@ -145,16 +145,9 @@ def get_relative_import_path( if serialize_namespace_split[idx] != imported_namespace_split[idx]: break idx += 1 - if idx == 0: - # The two namespaces share no top-level package, so a relative import would - # climb above the package root (e.g. ``from .......search import models``), - # which Python rejects at runtime ("attempted relative import beyond top-level - # package"). Emit a valid absolute import of the foreign namespace instead. - self._relative_import_path[key] = ".".join(imported_namespace_split) - else: - self._relative_import_path[key] = "." * (len(serialize_namespace_split[idx:]) + 1) + ".".join( - imported_namespace_split[idx:] - ) + self._relative_import_path[key] = "." * (len(serialize_namespace_split[idx:]) + 1) + ".".join( + imported_namespace_split[idx:] + ) result = self._relative_import_path[key] if module_name is None: return result diff --git a/packages/http-client-python/tests/unit/test_relative_import_path.py b/packages/http-client-python/tests/unit/test_relative_import_path.py deleted file mode 100644 index 13d95ccfada..00000000000 --- a/packages/http-client-python/tests/unit/test_relative_import_path.py +++ /dev/null @@ -1,95 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -"""Tests for ``CodeModel.get_relative_import_path``. - -A structured (JSONL / SSE) streaming operation can reference a payload model -whose ``client_namespace`` shares no top-level package with the operation being -generated (for example an SSE event payload that TCGC reports under a ``search`` -namespace while the package root is ``azure.search.documents``). Emitting a -relative import in that case climbs above the package root -(``from .......search import models``), which Python rejects at runtime with -``attempted relative import beyond top-level package``. The generator must fall -back to a valid absolute import instead. In-package imports must stay relative -(byte-identical to before). -""" - -import pytest - -from pygen.codegen.models import CodeModel - - -def _code_model() -> CodeModel: - return CodeModel( - { - "clients": [ - { - "name": "client", - "namespace": "azure.search.documents", - "moduleName": "azure.search.documents", - "parameters": [], - "url": "", - "operationGroups": [], - } - ], - "namespace": "azure.search.documents", - }, - options={ - "show-send-request": True, - "builders-visibility": "public", - "show-operations": True, - "models-mode": "dpg", - "only-path-and-body-params-positional": True, - }, - ) - - -@pytest.mark.parametrize( - "serialize_namespace,imported_namespace,expected", - [ - # In-package: shares a top-level package -> relative import (unchanged). - ("azure.test.operations", "azure.test", ".."), - ("azure.test.operations", "azure", "..."), - ("azure.test.subtest.aio.operations", "azure.test", "...."), - ("azure.search.documents.operations", "azure.search.documents", ".."), - ], -) -def test_in_package_stays_relative(serialize_namespace, imported_namespace, expected): - assert _code_model().get_relative_import_path(serialize_namespace, imported_namespace) == expected - - -@pytest.mark.parametrize( - "serialize_namespace,imported_namespace,expected", - [ - # No shared top-level package -> valid absolute import (no leading dots). - ("azure.search.documents.grp.aio.operations", "search", "search"), - ("azure.search.documents.aio.operations", "search", "search"), - ("search.aio.operations", "azure.search.documents", "azure.search.documents"), - ], -) -def test_cross_root_uses_absolute_import(serialize_namespace, imported_namespace, expected): - result = _code_model().get_relative_import_path(serialize_namespace, imported_namespace) - assert result == expected - assert not result.startswith("."), "cross-root import must be absolute, not a package-escaping relative import" - - -def test_cross_root_absolute_import_with_module_name(): - # The Stream / AsyncStream import points at ``._utils.streaming_base``. When an - # operation lives outside the package root, that import must also be absolute-valid. - result = _code_model().get_relative_import_path( - "search.aio.operations", - "azure.search.documents", - module_name="_utils.streaming_base", - ) - assert result == "azure.search.documents._utils.streaming_base" - - -def test_in_package_module_name_stays_relative(): - result = _code_model().get_relative_import_path( - "azure.search.documents.grp.aio.operations", - "azure.search.documents", - module_name="_utils.streaming_base", - ) - assert result == "...._utils.streaming_base" From dffe9014d8c6d5a37a9fadef987e985be5e95952 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Mon, 17 Aug 2026 12:24:50 -0700 Subject: [PATCH 06/21] 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 --- .chronus/changes/structured-streaming-2026-0-0.md | 12 ++++++++++-- packages/http-client-python/README.md | 4 ++-- .../codegen/templates/streaming_base.py.jinja2 | 14 +++++++------- .../asynctests/test_streaming_jsonl_async.py | 3 ++- .../tests/mock_api/shared/test_streaming_jsonl.py | 2 +- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.chronus/changes/structured-streaming-2026-0-0.md b/.chronus/changes/structured-streaming-2026-0-0.md index af07b4d5cb9..877bc896b98 100644 --- a/.chronus/changes/structured-streaming-2026-0-0.md +++ b/.chronus/changes/structured-streaming-2026-0-0.md @@ -4,9 +4,17 @@ packages: - "@typespec/http-client-python" --- -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. +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. -`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`. +`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. + +```python +from your_sdk import Stream + +stream: Stream[Thing] = client.receive() +for thing in stream: + ... +``` ```python from your_sdk import Stream diff --git a/packages/http-client-python/README.md b/packages/http-client-python/README.md index 6e5a6df1cd8..c896aa6a990 100644 --- a/packages/http-client-python/README.md +++ b/packages/http-client-python/README.md @@ -156,7 +156,7 @@ Emit YAML code model only, without running Python generator. For batch processin ## Structured streaming (JSONL / SSE) -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]`). +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. For an operation returning `JsonlStream`, the generated method returns `Stream[Thing]` (sync) / `AsyncStream[Thing]` (async), yielding deserialized `Thing` instances as each JSONL line arrives. Similarly, `SSEStream` produces a `Stream` / `AsyncStream` over the SSE event payloads. @@ -171,6 +171,6 @@ for thing in stream: ... ``` -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. +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. 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. diff --git a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 index d89a051d1eb..d94b376effa 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 @@ -7,8 +7,8 @@ # This file is vendored from azure-core (azure.core.streaming). It provides the # Stream / AsyncStream helpers (plus the JSONL / SSE decoders and event types) # used by generated structured-streaming operations, so the generated package -# does not take a hard dependency on an azure-core version that ships -# azure.core.streaming. Do not edit by hand. +# does not take a hard dependency on a core runtime that ships the streaming +# helpers. Do not edit by hand. # -------------------------------------------------------------------------- import codecs import json @@ -32,7 +32,7 @@ from typing import ( from typing_extensions import Self -from azure.core.rest import AsyncHttpResponse, HttpResponse +from {{ code_model.core_library }}.rest import AsyncHttpResponse, HttpResponse DecodedType = TypeVar("DecodedType") ReturnType_co = TypeVar("ReturnType_co", covariant=True) @@ -544,13 +544,13 @@ class Stream(Iterator[ReturnType_co]): """Stream class for consuming a decoded event stream (e.g. JSONL or SSE). :keyword response: The response object. - :paramtype response: ~azure.core.rest.HttpResponse + :paramtype response: ~{{ code_model.core_library }}.rest.HttpResponse :keyword decoder: A decoder to use for the stream. If omitted, the decoder is inferred from the response ``Content-Type`` header. :paramtype decoder: StreamDecoder :keyword deserialization_callback: A callback that takes the response and the decoded event and returns a deserialized object. - :paramtype deserialization_callback: Callable[[~azure.core.rest.HttpResponse, Any], ReturnType] + :paramtype deserialization_callback: Callable[[~{{ code_model.core_library }}.rest.HttpResponse, Any], ReturnType] :keyword terminal_event: Optional event ``data`` value that terminates the stream (e.g. ``"[DONE]"``). When an event's ``data`` equals this value, iteration stops and the event is not passed to ``deserialization_callback``. @@ -612,13 +612,13 @@ class AsyncStream(AsyncIterator[ReturnType_co]): """AsyncStream class for asynchronously consuming a decoded event stream (e.g. JSONL or SSE). :keyword response: The response object. - :paramtype response: ~azure.core.rest.AsyncHttpResponse + :paramtype response: ~{{ code_model.core_library }}.rest.AsyncHttpResponse :keyword decoder: A decoder to use for the stream. If omitted, the decoder is inferred from the response ``Content-Type`` header. :paramtype decoder: AsyncStreamDecoder :keyword deserialization_callback: A callback that takes the response and the decoded event and returns a deserialized object. - :paramtype deserialization_callback: Callable[[~azure.core.rest.AsyncHttpResponse, Any], ReturnType] + :paramtype deserialization_callback: Callable[[~{{ code_model.core_library }}.rest.AsyncHttpResponse, Any], ReturnType] :keyword terminal_event: Optional event ``data`` value that terminates the stream (e.g. ``"[DONE]"``). When an event's ``data`` equals this value, iteration stops and the event is not passed to ``deserialization_callback``. diff --git a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py index 74e05cebd14..2893e7de8cc 100644 --- a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py +++ b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py @@ -25,4 +25,5 @@ async def test_basic_send(client: JsonlClient): @pytest.mark.asyncio async def test_basic_recv(client: JsonlClient): - assert b"".join([d async for d in (await client.basic.receive())]) == JSONL + stream = await client.basic.receive() + assert [item.desc async for item in stream] == ["one", "two", "three"] diff --git a/packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py b/packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py index 494c17a3493..3515d83613f 100644 --- a/packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py +++ b/packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py @@ -22,4 +22,4 @@ def test_basic_send(client: JsonlClient): def test_basic_recv(client: JsonlClient): - assert b"".join(client.basic.receive()) == JSONL + assert [item.desc for item in client.basic.receive()] == ["one", "two", "three"] From 8bb99948dc5ccec109933aea52aea9a3f5ef1241 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Mon, 17 Aug 2026 13:16:31 -0700 Subject: [PATCH 07/21] docs(http-client-python): clarify streaming itemType fields and de-dupe changelog Add JSDoc to StructuredStreamingInfo.itemType (aggregate stream element type used for the Stream[T]/AsyncStream[T] annotation) and StructuredStreamEvent.itemType (per-event payload type forming the runtime dispatch table), documenting the deliberate overlap for heterogeneous SSE. Remove a duplicated code block in the structured streaming changelog entry. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .chronus/changes/structured-streaming-2026-0-0.md | 8 -------- packages/http-client-python/emitter/src/http.ts | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.chronus/changes/structured-streaming-2026-0-0.md b/.chronus/changes/structured-streaming-2026-0-0.md index 877bc896b98..26098acbef7 100644 --- a/.chronus/changes/structured-streaming-2026-0-0.md +++ b/.chronus/changes/structured-streaming-2026-0-0.md @@ -15,11 +15,3 @@ stream: Stream[Thing] = client.receive() for thing in stream: ... ``` - -```python -from your_sdk import Stream - -stream: Stream[Thing] = client.receive() -for thing in stream: - ... -``` diff --git a/packages/http-client-python/emitter/src/http.ts b/packages/http-client-python/emitter/src/http.ts index 51743d17e58..8bc9ce50692 100644 --- a/packages/http-client-python/emitter/src/http.ts +++ b/packages/http-client-python/emitter/src/http.ts @@ -47,11 +47,26 @@ type EmittedType = ReturnType; interface StructuredStreamEvent { eventType: string | undefined; + /** + * Payload type for this one SSE event. Together with {@link eventType} these form the + * runtime dispatch table (wire event name -> model to deserialize) inside the generated + * `_callback`. This is a narrower type than {@link StructuredStreamingInfo.itemType}. + */ itemType: EmittedType; } interface StructuredStreamingInfo { kind: StructuredStreamKind; + /** + * The aggregate stream element type used for the `Stream[T]` / `AsyncStream[T]` return + * annotation (a single type expression). For homogeneous JSONL this is the one model; for + * heterogeneous SSE this is the union of every event payload. + * + * Note the deliberate overlap with the per-event {@link StructuredStreamEvent.itemType}: for + * heterogeneous SSE this union is exactly the sum of the `events[]` payload types. Both are + * carried because the union alone cannot recover the wire-name -> member mapping needed for + * dispatch, and the events list alone is not a single valid type expression for the annotation. + */ itemType: EmittedType; events?: StructuredStreamEvent[]; terminalEvent?: string; From 10d0a551da7d061e7718f97d2de07ef72c6a091e Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Mon, 17 Aug 2026 14:14:14 -0700 Subject: [PATCH 08/21] fix(http-client-python): make vendored streaming_base banner flavor-aware The vendored _utils/streaming_base.py header hardcoded "vendored from azure-core (azure.core.streaming)". For the unbranded flavor this leaked the word "azure" into generated output, failing the unbranded test_sensitive_word check. Reword the banner to reference {{ code_model.core_library }}.streaming (azure.core.streaming for azure, corehttp.streaming for unbranded), consistent with the flavor-aware import just below it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../pygen/codegen/templates/streaming_base.py.jinja2 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 index d94b376effa..daac525a0be 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 @@ -4,11 +4,11 @@ {% endif %} # pylint: disable=line-too-long,useless-suppression,unnecessary-ellipsis # -------------------------------------------------------------------------- -# This file is vendored from azure-core (azure.core.streaming). It provides the -# Stream / AsyncStream helpers (plus the JSONL / SSE decoders and event types) -# used by generated structured-streaming operations, so the generated package -# does not take a hard dependency on a core runtime that ships the streaming -# helpers. Do not edit by hand. +# This file is vendored from the core streaming runtime +# ({{ code_model.core_library }}.streaming). It provides the Stream / AsyncStream +# helpers (plus the JSONL / SSE decoders and event types) used by generated +# structured-streaming operations, so the generated package does not take a hard +# dependency on a core runtime that ships the streaming helpers. Do not edit by hand. # -------------------------------------------------------------------------- import codecs import json From 3ffe279b3bec66520195f8d22a87c86ed237220d Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 18 Aug 2026 08:57:44 -0700 Subject: [PATCH 09/21] fix(http-client-python): keep Stream/AsyncStream internal, not a public export The vendored Stream/AsyncStream runtime is planned to move to azure-core and be removed from generated SDKs; exporting it from the package's base namespace now would make that removal a breaking change for anyone importing it. Keep it as an internal implementation detail - operations still return Stream[T]/AsyncStream[T] and import it from _utils.streaming_base. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .chronus/changes/structured-streaming-2026-0-0.md | 6 ++---- .../pygen/codegen/serializers/general_serializer.py | 13 ------------- .../pygen/codegen/templates/init.py.jinja2 | 9 --------- 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/.chronus/changes/structured-streaming-2026-0-0.md b/.chronus/changes/structured-streaming-2026-0-0.md index 26098acbef7..5fa79ea8015 100644 --- a/.chronus/changes/structured-streaming-2026-0-0.md +++ b/.chronus/changes/structured-streaming-2026-0-0.md @@ -6,12 +6,10 @@ packages: 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. -`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. +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 -from your_sdk import Stream - -stream: Stream[Thing] = client.receive() +stream = client.receive() for thing in stream: ... ``` diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index fd54fcc9260..4700b527667 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -200,24 +200,11 @@ def serialize_pkgutil_init_file(self) -> str: def serialize_init_file(self, clients: list[Client]) -> str: template = self.env.get_template("init.py.jinja2") - expose_streaming_types = ( - not self.async_mode - and self.code_model.need_streaming_base - and self.code_model.is_top_namespace(self.client_namespace) - ) return template.render( code_model=self.code_model, clients=clients, async_mode=self.async_mode, serialize_namespace=self.serialize_namespace, - streaming_import_path=( - self.code_model.get_relative_import_path( - self.serialize_namespace, - module_name="_utils.streaming_base", - ) - if expose_streaming_types - else None - ), ) def serialize_service_client_file(self, clients: list[Client]) -> str: diff --git a/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 index 69176f27529..007009e4bc9 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 @@ -9,9 +9,6 @@ from .{{ client.filename }} import {{ client.name }} # type: ignore {% endfor %} {% endif %} -{% if streaming_import_path %} -from {{ streaming_import_path }} import AsyncStream, Stream -{% endif %} {% if not async_mode and code_model.options.get("package-version") %} from {{ code_model.get_relative_import_path(serialize_namespace, module_name="_version") }} import VERSION @@ -20,15 +17,9 @@ __version__ = VERSION {{ keywords.patch_imports(try_except=True) }} __all__ = [ - {% if streaming_import_path %} - "AsyncStream", - {% endif %} {% for client in clients %} {{ keywords.escape_str(client.name) }}, {% endfor %} - {% if streaming_import_path %} - "Stream", - {% endif %} ] {{ keywords.extend_all }} From fd26ef27787ebe85e59ffa868e940c252ff88bce Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 18 Aug 2026 08:57:57 -0700 Subject: [PATCH 10/21] test(http-client-python): add SSE streaming Spector mock API tests (sync + async, shared) Shared mock API tests for the SSE streaming spec covering homogeneous (unnamed), heterogeneous named events with a [DONE] terminal event, and a POST-body retrieve stream. Asserts deserialized model instances and terminal-event termination for both sync (for) and async (async for) iteration; runs in both azure and unbranded tox envs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../streaming-sse-tests-2026-8-18-8-40-0.md | 7 +++ .../asynctests/test_streaming_sse_async.py | 48 +++++++++++++++++++ .../mock_api/shared/test_streaming_sse.py | 41 ++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 .chronus/changes/streaming-sse-tests-2026-8-18-8-40-0.md create mode 100644 packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py create mode 100644 packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py diff --git a/.chronus/changes/streaming-sse-tests-2026-8-18-8-40-0.md b/.chronus/changes/streaming-sse-tests-2026-8-18-8-40-0.md new file mode 100644 index 00000000000..3e276589545 --- /dev/null +++ b/.chronus/changes/streaming-sse-tests-2026-8-18-8-40-0.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-python" +--- + +Add Spector mock API tests (sync + async, shared across the Azure and unbranded flavors) for SSE structured streaming: unnamed homogeneous events, named heterogeneous events with a `[DONE]` terminal event, and a POST request whose response is an SSE stream. diff --git a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py new file mode 100644 index 00000000000..9f122dee185 --- /dev/null +++ b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py @@ -0,0 +1,48 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import pytest +import pytest_asyncio + +from streaming.sse.aio import SseClient +from streaming.sse.named.models import ResponseCreated, ResponseDelta +from streaming.sse.retrieve.models import FinalResult, PartialResult, RetrievalRequest +from streaming.sse.unnamed.models import Info + + +@pytest_asyncio.fixture +async def client(): + async with SseClient(endpoint="http://localhost:3000") as client: + yield client + + +@pytest.mark.asyncio +async def test_unnamed_receive(client: SseClient): + stream = await client.unnamed.receive() + items = [item async for item in stream] + assert all(isinstance(item, Info) for item in items) + assert [item.desc for item in items] == ["one", "two", "three"] + + +@pytest.mark.asyncio +async def test_named_receive(client: SseClient): + stream = await client.named.receive() + items = [item async for item in stream] + # The terminal "[DONE]" event stops iteration and is not yielded. + assert len(items) == 3 + assert isinstance(items[0], ResponseCreated) and items[0].id == "resp_1" + assert isinstance(items[1], ResponseDelta) and items[1].delta == "Hello" + assert isinstance(items[2], ResponseDelta) and items[2].delta == " world" + + +@pytest.mark.asyncio +async def test_retrieve_stream(client: SseClient): + stream = await client.retrieve.stream(RetrievalRequest(query="what is typespec?")) + items = [item async for item in stream] + # The terminal "[DONE]" event stops iteration and is not yielded. + assert len(items) == 3 + assert isinstance(items[0], PartialResult) and items[0].text == "partial one" + assert isinstance(items[1], PartialResult) and items[1].text == "partial two" + assert isinstance(items[2], FinalResult) and items[2].references == ["doc1", "doc2"] diff --git a/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py b/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py new file mode 100644 index 00000000000..235172ae24e --- /dev/null +++ b/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py @@ -0,0 +1,41 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import pytest + +from streaming.sse import SseClient +from streaming.sse.named.models import ResponseCreated, ResponseDelta +from streaming.sse.retrieve.models import FinalResult, PartialResult, RetrievalRequest +from streaming.sse.unnamed.models import Info + + +@pytest.fixture +def client(): + with SseClient(endpoint="http://localhost:3000") as client: + yield client + + +def test_unnamed_receive(client: SseClient): + items = list(client.unnamed.receive()) + assert all(isinstance(item, Info) for item in items) + assert [item.desc for item in items] == ["one", "two", "three"] + + +def test_named_receive(client: SseClient): + items = list(client.named.receive()) + # The terminal "[DONE]" event stops iteration and is not yielded. + assert len(items) == 3 + assert isinstance(items[0], ResponseCreated) and items[0].id == "resp_1" + assert isinstance(items[1], ResponseDelta) and items[1].delta == "Hello" + assert isinstance(items[2], ResponseDelta) and items[2].delta == " world" + + +def test_retrieve_stream(client: SseClient): + items = list(client.retrieve.stream(RetrievalRequest(query="what is typespec?"))) + # The terminal "[DONE]" event stops iteration and is not yielded. + assert len(items) == 3 + assert isinstance(items[0], PartialResult) and items[0].text == "partial one" + assert isinstance(items[1], PartialResult) and items[1].text == "partial two" + assert isinstance(items[2], FinalResult) and items[2].references == ["doc1", "doc2"] From 0233370e7fc4d85127e449c43db4a8fe2ddb917e Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 18 Aug 2026 12:35:28 -0700 Subject: [PATCH 11/21] test(http-client-python): assert streaming ops return Stream/AsyncStream The JSONL/SSE streaming mock API tests now capture the returned object and assert isinstance(stream, Stream) / isinstance(stream, AsyncStream) before iterating, verifying the generated method returns the streaming wrapper rather than a plain iterator or list. The concrete classes are imported from the internal _utils.streaming_base module (they are intentionally not part of the public API). JSONL element checks are strengthened with isinstance(item, Info) for parity with the SSE tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../shared/asynctests/test_streaming_jsonl_async.py | 7 ++++++- .../shared/asynctests/test_streaming_sse_async.py | 4 ++++ .../tests/mock_api/shared/test_streaming_jsonl.py | 8 +++++++- .../tests/mock_api/shared/test_streaming_sse.py | 13 ++++++++++--- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py index 2893e7de8cc..d46dd1c41b9 100644 --- a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py +++ b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_jsonl_async.py @@ -7,6 +7,8 @@ import pytest_asyncio from streaming.jsonl.aio import JsonlClient +from streaming.jsonl._utils.streaming_base import AsyncStream +from streaming.jsonl.basic.models import Info @pytest_asyncio.fixture @@ -26,4 +28,7 @@ async def test_basic_send(client: JsonlClient): @pytest.mark.asyncio async def test_basic_recv(client: JsonlClient): stream = await client.basic.receive() - assert [item.desc async for item in stream] == ["one", "two", "three"] + assert isinstance(stream, AsyncStream) + items = [item async for item in stream] + assert all(isinstance(item, Info) for item in items) + assert [item.desc for item in items] == ["one", "two", "three"] diff --git a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py index 9f122dee185..8991eb984ab 100644 --- a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py +++ b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py @@ -7,6 +7,7 @@ import pytest_asyncio from streaming.sse.aio import SseClient +from streaming.sse._utils.streaming_base import AsyncStream from streaming.sse.named.models import ResponseCreated, ResponseDelta from streaming.sse.retrieve.models import FinalResult, PartialResult, RetrievalRequest from streaming.sse.unnamed.models import Info @@ -21,6 +22,7 @@ async def client(): @pytest.mark.asyncio async def test_unnamed_receive(client: SseClient): stream = await client.unnamed.receive() + assert isinstance(stream, AsyncStream) items = [item async for item in stream] assert all(isinstance(item, Info) for item in items) assert [item.desc for item in items] == ["one", "two", "three"] @@ -29,6 +31,7 @@ async def test_unnamed_receive(client: SseClient): @pytest.mark.asyncio async def test_named_receive(client: SseClient): stream = await client.named.receive() + assert isinstance(stream, AsyncStream) items = [item async for item in stream] # The terminal "[DONE]" event stops iteration and is not yielded. assert len(items) == 3 @@ -40,6 +43,7 @@ async def test_named_receive(client: SseClient): @pytest.mark.asyncio async def test_retrieve_stream(client: SseClient): stream = await client.retrieve.stream(RetrievalRequest(query="what is typespec?")) + assert isinstance(stream, AsyncStream) items = [item async for item in stream] # The terminal "[DONE]" event stops iteration and is not yielded. assert len(items) == 3 diff --git a/packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py b/packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py index 3515d83613f..bc2febf91d2 100644 --- a/packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py +++ b/packages/http-client-python/tests/mock_api/shared/test_streaming_jsonl.py @@ -6,6 +6,8 @@ import pytest from streaming.jsonl import JsonlClient +from streaming.jsonl._utils.streaming_base import Stream +from streaming.jsonl.basic.models import Info @pytest.fixture @@ -22,4 +24,8 @@ def test_basic_send(client: JsonlClient): def test_basic_recv(client: JsonlClient): - assert [item.desc for item in client.basic.receive()] == ["one", "two", "three"] + stream = client.basic.receive() + assert isinstance(stream, Stream) + items = list(stream) + assert all(isinstance(item, Info) for item in items) + assert [item.desc for item in items] == ["one", "two", "three"] diff --git a/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py b/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py index 235172ae24e..60c0de9e53a 100644 --- a/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py +++ b/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py @@ -6,6 +6,7 @@ import pytest from streaming.sse import SseClient +from streaming.sse._utils.streaming_base import Stream from streaming.sse.named.models import ResponseCreated, ResponseDelta from streaming.sse.retrieve.models import FinalResult, PartialResult, RetrievalRequest from streaming.sse.unnamed.models import Info @@ -18,13 +19,17 @@ def client(): def test_unnamed_receive(client: SseClient): - items = list(client.unnamed.receive()) + stream = client.unnamed.receive() + assert isinstance(stream, Stream) + items = list(stream) assert all(isinstance(item, Info) for item in items) assert [item.desc for item in items] == ["one", "two", "three"] def test_named_receive(client: SseClient): - items = list(client.named.receive()) + stream = client.named.receive() + assert isinstance(stream, Stream) + items = list(stream) # The terminal "[DONE]" event stops iteration and is not yielded. assert len(items) == 3 assert isinstance(items[0], ResponseCreated) and items[0].id == "resp_1" @@ -33,7 +38,9 @@ def test_named_receive(client: SseClient): def test_retrieve_stream(client: SseClient): - items = list(client.retrieve.stream(RetrievalRequest(query="what is typespec?"))) + stream = client.retrieve.stream(RetrievalRequest(query="what is typespec?")) + assert isinstance(stream, Stream) + items = list(stream) # The terminal "[DONE]" event stops iteration and is not yielded. assert len(items) == 3 assert isinstance(items[0], PartialResult) and items[0].text == "partial one" From ece5a8846265fb29ccd25c600e4e0cb4eddc9f85 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 19 Aug 2026 09:42:31 -0700 Subject: [PATCH 12/21] chore(http-client-python): address streaming PR review feedback - Remove redundant internal changeset for the SSE tests; the feature changelog already covers them. - Update test_streaming_init unit test to assert Stream/AsyncStream are never publicly exported (all sync/async + structured combos). - Remove the now-unnecessary @typespec/compiler package.json override. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../streaming-sse-tests-2026-8-18-8-40-0.md | 7 ----- packages/http-client-python/package.json | 3 --- .../tests/unit/test_streaming_init.py | 27 ++++++++++--------- 3 files changed, 15 insertions(+), 22 deletions(-) delete mode 100644 .chronus/changes/streaming-sse-tests-2026-8-18-8-40-0.md diff --git a/.chronus/changes/streaming-sse-tests-2026-8-18-8-40-0.md b/.chronus/changes/streaming-sse-tests-2026-8-18-8-40-0.md deleted file mode 100644 index 3e276589545..00000000000 --- a/.chronus/changes/streaming-sse-tests-2026-8-18-8-40-0.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: internal -packages: - - "@typespec/http-client-python" ---- - -Add Spector mock API tests (sync + async, shared across the Azure and unbranded flavors) for SSE structured streaming: unnamed homogeneous events, named heterogeneous events with a `[DONE]` terminal event, and a POST request whose response is an SSE stream. diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 7a33b7749ba..b530962f347 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -132,8 +132,5 @@ "typescript-eslint": "^8.49.0", "vitest": "^4.0.15", "prettier": "^3.9.5" - }, - "overrides": { - "@typespec/compiler": "$@typespec/compiler" } } diff --git a/packages/http-client-python/tests/unit/test_streaming_init.py b/packages/http-client-python/tests/unit/test_streaming_init.py index 1d83e492c5a..6454c991da4 100644 --- a/packages/http-client-python/tests/unit/test_streaming_init.py +++ b/packages/http-client-python/tests/unit/test_streaming_init.py @@ -34,24 +34,27 @@ def _render_init(*, has_structured_stream: bool, async_mode: bool = False) -> st return GeneralSerializer(code_model=code_model, env=_env(), async_mode=async_mode).serialize_init_file([client]) -def test_exports_stream_types_from_base_namespace(): - init_file = _render_init(has_structured_stream=True) - - assert "from ._utils.streaming_base import AsyncStream, Stream" in init_file - assert '"AsyncStream",' in init_file - assert '"Stream",' in init_file - - @pytest.mark.parametrize( "has_structured_stream,async_mode", [ (False, False), + (False, True), + (True, False), (True, True), ], ) -def test_does_not_export_stream_types_outside_structured_base_namespace(has_structured_stream, async_mode): +def test_does_not_export_stream_types(has_structured_stream, async_mode): + # Stream / AsyncStream are an internal implementation detail (vendored in + # _utils/streaming_base.py) and must never be part of the generated package's + # public API -- even when a structured stream is present in the sync base + # namespace. This guards against reintroducing a public export that would + # become a breaking change once the runtime moves to the core library. init_file = _render_init(has_structured_stream=has_structured_stream, async_mode=async_mode) - assert "streaming_base import" not in init_file - assert '"AsyncStream",' not in init_file - assert '"Stream",' not in init_file + # No public export of the streaming types -- neither an import line nor an + # __all__ entry, regardless of quote style. + assert "streaming_base" not in init_file + assert "AsyncStream" not in init_file + assert "Stream" not in init_file + # Sanity check: the client itself is still imported/exported as normal. + assert "from ._client import SampleClient" in init_file From 268f135ad0b6c76b83411abb799ab8f1fdc06d1c Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 19 Aug 2026 09:51:08 -0700 Subject: [PATCH 13/21] test(http-client-python): remove streaming init unit test The test guarded the generated package __init__; with Stream/AsyncStream no longer publicly exported it no longer covers a supported behavior, so remove it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../tests/unit/test_streaming_init.py | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 packages/http-client-python/tests/unit/test_streaming_init.py diff --git a/packages/http-client-python/tests/unit/test_streaming_init.py b/packages/http-client-python/tests/unit/test_streaming_init.py deleted file mode 100644 index 6454c991da4..00000000000 --- a/packages/http-client-python/tests/unit/test_streaming_init.py +++ /dev/null @@ -1,60 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from types import SimpleNamespace - -import pytest -from jinja2 import Environment, PackageLoader - -from pygen.codegen.serializers.general_serializer import GeneralSerializer - - -def _env() -> Environment: - return Environment( - loader=PackageLoader("pygen.codegen", "templates"), - keep_trailing_newline=True, - trim_blocks=True, - lstrip_blocks=True, - ) - - -def _render_init(*, has_structured_stream: bool, async_mode: bool = False) -> str: - code_model = SimpleNamespace( - namespace="sample", - license_header="", - options={"package-version": None}, - need_streaming_base=has_structured_stream, - get_serialize_namespace=lambda namespace, async_mode: f"{namespace}.aio" if async_mode else namespace, - get_relative_import_path=lambda _namespace, module_name: f".{module_name}", - is_top_namespace=lambda namespace: namespace == "sample", - ) - client = SimpleNamespace(filename="_client", name="SampleClient") - return GeneralSerializer(code_model=code_model, env=_env(), async_mode=async_mode).serialize_init_file([client]) - - -@pytest.mark.parametrize( - "has_structured_stream,async_mode", - [ - (False, False), - (False, True), - (True, False), - (True, True), - ], -) -def test_does_not_export_stream_types(has_structured_stream, async_mode): - # Stream / AsyncStream are an internal implementation detail (vendored in - # _utils/streaming_base.py) and must never be part of the generated package's - # public API -- even when a structured stream is present in the sync base - # namespace. This guards against reintroducing a public export that would - # become a breaking change once the runtime moves to the core library. - init_file = _render_init(has_structured_stream=has_structured_stream, async_mode=async_mode) - - # No public export of the streaming types -- neither an import line nor an - # __all__ entry, regardless of quote style. - assert "streaming_base" not in init_file - assert "AsyncStream" not in init_file - assert "Stream" not in init_file - # Sanity check: the client itself is still imported/exported as normal. - assert "from ._client import SampleClient" in init_file From 46cec7dd398bcb8043123f19f7cd872961e225f5 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Thu, 20 Aug 2026 08:39:49 -0700 Subject: [PATCH 14/21] build(http-client-python): revert incidental package-lock.json churn The lock had been regenerated against the internal Azure SDK npm feed, rewriting all resolved URLs (registry.npmjs.org -> pkgs.dev.azure.com), downgrading integrity hashes from sha512 to sha1, and bumping a few unrelated dev-tooling transitive versions. package.json is byte-identical to main and no spec/core dependency changed, so restore the lock to main to keep the PR free of unnecessary churn. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- packages/http-client-python/package-lock.json | 366 ++++++++---------- 1 file changed, 170 insertions(+), 196 deletions(-) diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index 1b84a448322..64e2b328755 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -999,8 +999,8 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.2", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha1-8p4iBXrVMWzyODbO6aNMgf/8t+Y=", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1014,9 +1014,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha1-POdNiYhRNr4VNTQfjD1EJcKaXKs=", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "peer": true, @@ -1027,8 +1027,8 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -1041,8 +1041,8 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha1-G9AGzut+LlWyt3OrMY0wDhpmrto=", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1055,8 +1055,8 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha1-dyJYIEE9lhdQnak0IZCiAZ54dhw=", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1068,9 +1068,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.6", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", - "integrity": "sha1-0iv9azp9jh8sCy8ubeERtT7G4T4=", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "peer": true, @@ -1081,7 +1081,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.3.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -1093,9 +1093,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "peer": true, @@ -1111,9 +1111,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha1-POdNiYhRNr4VNTQfjD1EJcKaXKs=", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "peer": true, @@ -1124,16 +1124,16 @@ }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -1145,9 +1145,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.5", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/js/-/js-9.39.5.tgz", - "integrity": "sha1-by+8/3VQDSKdU14KlJrhNHLIR4c=", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "peer": true, @@ -1160,8 +1160,8 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha1-biEmoTR+hqTe34cG7Gf/jhB+u60=", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1171,8 +1171,8 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha1-l3nj/Zt+4zVxpXQ1z0M1oXlKbLI=", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -1185,52 +1185,48 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha1-qCcsoDsqz0kmcCIrIyC2xCG/3mA=", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, - "license": "Apache-2.0", "peer": true, - "dependencies": { - "@humanfs/types": "^0.15.0" - }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha1-j4AMzME/T4zTEW4tnAqUk52j4+0=", + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, - "license": "Apache-2.0", "peer": true, "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha1-8qCfYgEjkLK/8/xvskjd7IwJoJA=", + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, - "license": "Apache-2.0", "peer": true, "engines": { - "node": ">=18.18.0" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha1-r1smkaIrRL6EewyoFkHF+2rQFyw=", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "Apache-2.0", "peer": true, "engines": { "node": ">=12.22" @@ -1241,11 +1237,10 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha1-wrnS43TuYsWG062+qHGZsdenpro=", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, - "license": "Apache-2.0", "peer": true, "engines": { "node": ">=18.18" @@ -2066,8 +2061,8 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT", "peer": true @@ -2901,9 +2896,9 @@ } }, "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha1-T68BstbTJr/u2XrqH1IiC19MGUA=", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "peer": true, @@ -2916,8 +2911,8 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peer": true, @@ -3240,8 +3235,8 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "peer": true, @@ -3259,24 +3254,6 @@ "node": ">=18" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha1-qsTit3NKdAhnrrFr8CqtVWoeegE=", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/change-case": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", @@ -3384,8 +3361,8 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT", "peer": true @@ -3490,10 +3467,9 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/default-browser": { @@ -3728,10 +3704,9 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -3741,9 +3716,9 @@ } }, "node_modules/eslint": { - "version": "9.39.5", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/eslint/-/eslint-9.39.5.tgz", - "integrity": "sha1-Kk48iw91MZbvrpQ8j/qocw/Go/o=", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "peer": true, @@ -3753,8 +3728,8 @@ "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.6", - "@eslint/js": "9.39.5", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3803,8 +3778,8 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha1-iOZGogf61hQ2/6OetQUUcgBlXII=", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -3833,9 +3808,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "peer": true, @@ -3851,9 +3826,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha1-POdNiYhRNr4VNTQfjD1EJcKaXKs=", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "peer": true, @@ -3862,18 +3837,35 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT", "peer": true }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -3886,8 +3878,8 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/espree/-/espree-10.4.0.tgz", - "integrity": "sha1-1U9JSdRikAWh+haNk3w/8ffiqDc=", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -3904,11 +3896,10 @@ } }, "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha1-CNBI8mHw3e21uulfRoCUY9nJSW0=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, - "license": "BSD-3-Clause", "peer": true, "dependencies": { "estraverse": "^5.1.0" @@ -3919,8 +3910,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha1-eteWTWeauyi+5yzsY3WLHF0smSE=", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -3933,10 +3924,9 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha1-LupSkHAvJquP5TcDcP+GyWXSESM=", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause", "peer": true, "engines": { "node": ">=4.0" @@ -3954,10 +3944,9 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "BSD-2-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -4045,18 +4034,17 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/fast-string-truncated-width": { @@ -4146,10 +4134,9 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha1-d4e93PETG/+5JjbGlFe7wO3W2B8=", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "flat-cache": "^4.0.0" @@ -4211,10 +4198,9 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha1-Ds45/LFO4BL0sEEL0z3ZwfAREnw=", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "flatted": "^3.2.9", @@ -4225,9 +4211,9 @@ } }, "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha1-ruyipQYwPwzuYcWebJ8qiNLyn8Y=", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC", "peer": true @@ -4383,10 +4369,9 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "ISC", "peer": true, "dependencies": { "is-glob": "^4.0.3" @@ -4436,8 +4421,8 @@ }, "node_modules/globals": { "version": "14.0.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/globals/-/globals-14.0.0.tgz", - "integrity": "sha1-iY10E8Kbq89rr+Vvyt3thYrack4=", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "peer": true, @@ -4570,8 +4555,8 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "peer": true, @@ -4581,8 +4566,8 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha1-nOy1ZQPAraHydB271lRuSxO1fM8=", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "peer": true, @@ -4599,8 +4584,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "peer": true, @@ -4643,10 +4628,9 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -4663,10 +4647,9 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "is-extglob": "^2.1.1" @@ -4815,9 +4798,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha1-0ZAFcqf3zwtfVAyDZz5gutNDZZI=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "funding": [ { "type": "github", @@ -4838,10 +4821,9 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/json-schema-traverse": { @@ -4853,10 +4835,9 @@ }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/jsonwebtoken": { @@ -4907,10 +4888,9 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha1-qHmpnilFL5QkOfKkBeOvizHU3pM=", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "json-buffer": "3.0.1" @@ -4918,10 +4898,9 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/levn/-/levn-0.4.1.tgz", - "integrity": "sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "prelude-ls": "^1.2.1", @@ -5251,10 +5230,9 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha1-VYqlO0O2YeGSWgr9+japoQhf5Xo=", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/lodash.once": { @@ -5670,10 +5648,9 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha1-fqHBpdkddk+yghOciP4R4YKjpzQ=", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "deep-is": "^0.1.3", @@ -5725,8 +5702,8 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "peer": true, @@ -5875,10 +5852,9 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "license": "MIT", "peer": true, "engines": { "node": ">= 0.8.0" @@ -5916,8 +5892,8 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "peer": true, @@ -6019,8 +5995,8 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "peer": true, @@ -6479,8 +6455,8 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha1-MfEoGzgyYwQ0gxwxDAHMzajL4AY=", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "peer": true, @@ -6771,10 +6747,9 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "prelude-ls": "^1.2.1" @@ -6880,8 +6855,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -7186,10 +7161,9 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha1-0sRcbdT7zmIaZvE2y+Mor9BBCzQ=", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, - "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" From aa76c0d2f02202e3742f9092b5ab1124deb6603f Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Thu, 20 Aug 2026 13:13:38 -0700 Subject: [PATCH 15/21] feat(http-client-python): support named and model SSE terminal events Structured SSE streaming previously recognized only a nameless string-constant `@terminalEvent` (e.g. `[DONE]`), which stops iteration without being yielded. Named / model `@terminalEvent`s (e.g. `error`, `response.completed`) are now deserialized and yielded like any other event, with iteration stopping immediately afterwards. Multiple terminal events are supported. The emitter partitions SSE events via a new pure `partitionSseEvents` helper: nameless string-constant terminals become the drop-and-stop sentinel, while named/model terminals stay in the dispatch table flagged `isTerminal`. The generator threads those names through to the vendored `Stream` / `AsyncStream` runtime, which breaks after yielding a matching event. Behavior for existing specs is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../http-client-python/emitter/src/http.ts | 80 +++++++++++++--- .../emitter/test/streaming.test.ts | 92 +++++++++++++++++- .../pygen/codegen/models/response.py | 9 ++ .../codegen/serializers/builder_serializer.py | 14 ++- .../templates/streaming_base.py.jinja2 | 17 ++++ .../asynctests/test_streaming_sse_async.py | 80 ++++++++++++++++ .../mock_api/shared/test_streaming_sse.py | 95 +++++++++++++++++++ 7 files changed, 364 insertions(+), 23 deletions(-) diff --git a/packages/http-client-python/emitter/src/http.ts b/packages/http-client-python/emitter/src/http.ts index 8bc9ce50692..5526dfbb47a 100644 --- a/packages/http-client-python/emitter/src/http.ts +++ b/packages/http-client-python/emitter/src/http.ts @@ -53,6 +53,13 @@ interface StructuredStreamEvent { * `_callback`. This is a narrower type than {@link StructuredStreamingInfo.itemType}. */ itemType: EmittedType; + /** + * True when this event is a `@terminalEvent` that carries a payload (a named / model event, + * not a bare string-constant sentinel). Such events are deserialized and yielded like any + * other event, and iteration stops immediately after one is yielded. Contrast with + * {@link StructuredStreamingInfo.terminalEvent}, the sentinel that stops without yielding. + */ + isTerminal?: boolean; } interface StructuredStreamingInfo { @@ -69,6 +76,11 @@ interface StructuredStreamingInfo { */ itemType: EmittedType; events?: StructuredStreamEvent[]; + /** + * A bare string-constant `@terminalEvent` with no event name (e.g. `"[DONE]"`). Iteration + * stops when an event's `data` equals this value, and the sentinel is NOT yielded. Named / + * model terminal events are carried in {@link events} with `isTerminal: true` instead. + */ terminalEvent?: string; } @@ -104,6 +116,56 @@ function getStringConstantValue(type: SdkType): string | undefined { return type.kind === "constant" && typeof type.value === "string" ? type.value : undefined; } +/** The subset of an {@link SdkSseMetadata} event that terminal-event partitioning depends on. */ +interface SseEventLike { + eventType?: string | undefined; + isTerminalEvent: boolean; + type: SdkType; + payloadType: SdkType; +} + +/** + * Split the SSE events into the runtime dispatch table and a bare string-constant sentinel. + * + * A `@terminalEvent` comes in two shapes: + * * a nameless string constant (e.g. `"[DONE]"`) -> a pure sentinel: iteration stops when an + * event's `data` equals this value and the event is NOT yielded. Returned as `terminalEvent`. + * * a named / model event (e.g. `error`, `response.completed`) -> carries a payload the consumer + * needs, so it is deserialized and yielded like any other event, then iteration stops. Returned + * in `events` with `isTerminal: true`. + * + * `toItemType` maps an event payload to its emitted type; it is injected so this partitioning stays + * a pure function that can be unit-tested without a full emitter context. + */ +export function partitionSseEvents( + events: readonly SseEventLike[], + toItemType: (payloadType: SdkType) => EmittedType, +): { events: StructuredStreamEvent[]; terminalEvent?: string } { + const dispatch: StructuredStreamEvent[] = []; + let terminalEvent: string | undefined; + for (const event of events) { + if (event.isTerminalEvent) { + const sentinelValue = + event.eventType === undefined + ? (getStringConstantValue(event.payloadType) ?? getStringConstantValue(event.type)) + : undefined; + if (sentinelValue !== undefined) { + // Keep the first sentinel; no current spec defines more than one. + terminalEvent ??= sentinelValue; + continue; + } + dispatch.push({ + eventType: event.eventType, + itemType: toItemType(event.payloadType), + isTerminal: true, + }); + continue; + } + dispatch.push({ eventType: event.eventType, itemType: toItemType(event.payloadType) }); + } + return terminalEvent !== undefined ? { events: dispatch, terminalEvent } : { events: dispatch }; +} + function emitStructuredStreamingInfo( context: PythonSdkContext, response: SdkHttpResponse | SdkHttpErrorResponse, @@ -122,21 +184,11 @@ function emitStructuredStreamingInfo( const sseMetadata = response.sseMetadata; if (!sseMetadata) return streaming; - const events = sseMetadata.events - .filter((event) => !event.isTerminalEvent) - .map((event) => ({ - eventType: event.eventType, - itemType: getType(context, event.payloadType), - })); + const { events, terminalEvent } = partitionSseEvents(sseMetadata.events, (payloadType) => + getType(context, payloadType), + ); if (events.length > 0) streaming.events = events; - - const terminalEvent = sseMetadata.events.find((event) => event.isTerminalEvent); - if (terminalEvent) { - const terminalEventValue = - getStringConstantValue(terminalEvent.payloadType) ?? - getStringConstantValue(terminalEvent.type); - if (terminalEventValue !== undefined) streaming.terminalEvent = terminalEventValue; - } + if (terminalEvent !== undefined) streaming.terminalEvent = terminalEvent; return streaming; } diff --git a/packages/http-client-python/emitter/test/streaming.test.ts b/packages/http-client-python/emitter/test/streaming.test.ts index 491bc04b098..f0c03f4ac52 100644 --- a/packages/http-client-python/emitter/test/streaming.test.ts +++ b/packages/http-client-python/emitter/test/streaming.test.ts @@ -1,6 +1,10 @@ import { strictEqual } from "assert"; import { describe, it } from "vitest"; -import { getStructuredStreamKind, isStructuredStreamType } from "../src/http.js"; +import { + getStructuredStreamKind, + isStructuredStreamType, + partitionSseEvents, +} from "../src/http.js"; describe("typespec-python: structured streaming", () => { it("treats model and union payloads as structured", () => { @@ -42,4 +46,90 @@ describe("typespec-python: structured streaming", () => { 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].itemType, completed); + strictEqual(events[2].eventType, "error"); + strictEqual(events[2].isTerminal, true); + strictEqual(events[2].itemType, 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); + }); + }); }); diff --git a/packages/http-client-python/generator/pygen/codegen/models/response.py b/packages/http-client-python/generator/pygen/codegen/models/response.py index c102ffd8802..724db0db50a 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/response.py +++ b/packages/http-client-python/generator/pygen/codegen/models/response.py @@ -62,11 +62,20 @@ def __init__( self.streaming_kind: Optional[str] = streaming["kind"] if streaming else None self.streaming_events: list[tuple[Optional[str], BaseType]] = [] self._streaming_terminal_event: Optional[str] = streaming.get("terminalEvent") if streaming else None + # Named / model ``@terminalEvent`` events: deserialized and yielded like any other event, + # then iteration stops. The bare string-constant sentinel (``_streaming_terminal_event``) + # stops WITHOUT yielding and is matched on event ``data`` instead of the event name. + self.terminal_event_names: list[str] = [] if streaming: self.streaming_events = [ (event.get("eventType"), self.code_model.lookup_type(id(event["itemType"]))) for event in streaming.get("events", []) ] + self.terminal_event_names = [ + event["eventType"] + for event in streaming.get("events", []) + if event.get("isTerminal") and event.get("eventType") is not None + ] @property def result_property(self) -> str: diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index dd12919cf07..c449cab366f 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1267,6 +1267,7 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] ) stream_class = response.stream_class_name(self.async_mode) # type: ignore[attr-defined] terminal_event = getattr(response, "terminal_event", None) + terminal_event_names = getattr(response, "terminal_event_names", []) streaming_events = getattr(response, "streaming_events", []) retval: list[str] = [] retval.append("def _callback(_http_response, _event):") @@ -1311,15 +1312,12 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] retval.append(" return cls(pipeline_response, deserialized, {}) # type: ignore") retval.append(" return deserialized") retval.append("") + stream_kwargs = ["response=response", "deserialization_callback=_callback"] if terminal_event is not None: - retval.append( - f"return {stream_class}(response=response, deserialization_callback=_callback, " - f"terminal_event={terminal_event!r}) # type: ignore" - ) - else: - retval.append( - f"return {stream_class}(response=response, deserialization_callback=_callback) # type: ignore" - ) + stream_kwargs.append(f"terminal_event={terminal_event!r}") + if terminal_event_names: + stream_kwargs.append(f"terminal_event_names={terminal_event_names!r}") + retval.append(f"return {stream_class}({', '.join(stream_kwargs)}) # type: ignore") return retval def _handle_response_body(self, builder: OperationType) -> list[str]: diff --git a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 index daac525a0be..5246167751e 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 @@ -24,6 +24,7 @@ from typing import ( List, Optional, Protocol, + Sequence, Type, TypeVar, cast, @@ -555,6 +556,10 @@ class Stream(Iterator[ReturnType_co]): ``"[DONE]"``). When an event's ``data`` equals this value, iteration stops and the event is not passed to ``deserialization_callback``. :paramtype terminal_event: str or None + :keyword terminal_event_names: Optional event names (the SSE ``event`` field) that terminate + the stream. Unlike ``terminal_event``, such an event carries a payload: it is passed to + ``deserialization_callback`` and yielded, and iteration stops immediately afterwards. + :paramtype terminal_event_names: ~typing.Sequence[str] or None """ def __init__( @@ -564,6 +569,7 @@ class Stream(Iterator[ReturnType_co]): deserialization_callback: Callable[[HttpResponse, DecodedType], ReturnType_co], decoder: Optional[StreamDecoder[DecodedType]] = None, terminal_event: Optional[str] = None, + terminal_event_names: Optional[Sequence[str]] = None, ) -> None: self._response = response content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() @@ -572,6 +578,7 @@ class Stream(Iterator[ReturnType_co]): ) self._deserialization_callback = deserialization_callback self._terminal_event = terminal_event + self._terminal_event_names = frozenset(terminal_event_names or ()) self._iterator = self._iter_results() def __next__(self) -> ReturnType_co: @@ -587,6 +594,8 @@ class Stream(Iterator[ReturnType_co]): break result = self._deserialization_callback(self._response, event) yield result + if self._terminal_event_names and getattr(event, "event", None) in self._terminal_event_names: + break finally: self._response.close() @@ -623,6 +632,10 @@ class AsyncStream(AsyncIterator[ReturnType_co]): ``"[DONE]"``). When an event's ``data`` equals this value, iteration stops and the event is not passed to ``deserialization_callback``. :paramtype terminal_event: str or None + :keyword terminal_event_names: Optional event names (the SSE ``event`` field) that terminate + the stream. Unlike ``terminal_event``, such an event carries a payload: it is passed to + ``deserialization_callback`` and yielded, and iteration stops immediately afterwards. + :paramtype terminal_event_names: ~typing.Sequence[str] or None """ def __init__( @@ -632,6 +645,7 @@ class AsyncStream(AsyncIterator[ReturnType_co]): deserialization_callback: Callable[[AsyncHttpResponse, DecodedType], ReturnType_co], decoder: Optional[AsyncStreamDecoder[DecodedType]] = None, terminal_event: Optional[str] = None, + terminal_event_names: Optional[Sequence[str]] = None, ) -> None: self._response = response content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() @@ -642,6 +656,7 @@ class AsyncStream(AsyncIterator[ReturnType_co]): ) self._deserialization_callback = deserialization_callback self._terminal_event = terminal_event + self._terminal_event_names = frozenset(terminal_event_names or ()) self._iterator = self._iter_results() async def __anext__(self) -> ReturnType_co: @@ -658,6 +673,8 @@ class AsyncStream(AsyncIterator[ReturnType_co]): break result = self._deserialization_callback(self._response, event) yield result + if self._terminal_event_names and getattr(event, "event", None) in self._terminal_event_names: + break finally: aclose = getattr(events, "aclose", None) if aclose is not None: diff --git a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py index 8991eb984ab..e8eff299908 100644 --- a/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py +++ b/packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +import json + import pytest import pytest_asyncio @@ -50,3 +52,81 @@ async def test_retrieve_stream(client: SseClient): assert isinstance(items[0], PartialResult) and items[0].text == "partial one" assert isinstance(items[1], PartialResult) and items[1].text == "partial two" assert isinstance(items[2], FinalResult) and items[2].references == ["doc1", "doc2"] + + +# --------------------------------------------------------------------------- +# Named / model terminal events (yield-then-stop) -- see the sync test module +# for the rationale. Driven through the generated ``AsyncStream`` with a fake +# response because no published Spector spec produces named-model terminals. +# --------------------------------------------------------------------------- + + +class _FakeAsyncResponse: + """A minimal AsyncHttpResponse-shaped stand-in that replays SSE bytes.""" + + def __init__(self, body: bytes): + self.headers = {"Content-Type": "text/event-stream"} + self._body = body + self.closed = False + + def iter_bytes(self): + async def gen(): + for index in range(0, len(self._body), 8): + yield self._body[index : index + 8] + + return gen() + + async def close(self): + self.closed = True + + +def _event_kind(_response, event): + return (event.event, json.loads(event.data)) + + +_NAMED_TERMINAL_SSE = ( + b'event: response.partial\ndata: {"text": "one"}\n\n' + b'event: response.delta\ndata: {"delta": "hi"}\n\n' + b'event: response.completed\ndata: {"references": []}\n\n' + b'event: response.delta\ndata: {"delta": "AFTER-TERMINAL"}\n\n' +) + +_TERMINAL_EVENT_NAMES = ["response.completed", "error"] + + +@pytest.mark.asyncio +async def test_named_terminal_event_yields_then_stops(): + response = _FakeAsyncResponse(_NAMED_TERMINAL_SSE) + stream = AsyncStream( + response=response, + deserialization_callback=_event_kind, + terminal_event_names=_TERMINAL_EVENT_NAMES, + ) + items = [item async for item in stream] + # The named terminal `response.completed` IS yielded, then iteration stops. + assert items == [ + ("response.partial", {"text": "one"}), + ("response.delta", {"delta": "hi"}), + ("response.completed", {"references": []}), + ] + assert response.closed + + +@pytest.mark.asyncio +async def test_sentinel_and_named_terminal_coexist(): + body = ( + b'event: response.delta\ndata: {"delta": "a"}\n\n' + b"data: [DONE]\n\n" + b'event: response.delta\ndata: {"delta": "AFTER-DONE"}\n\n' + ) + response = _FakeAsyncResponse(body) + stream = AsyncStream( + response=response, + deserialization_callback=_event_kind, + terminal_event="[DONE]", + terminal_event_names=_TERMINAL_EVENT_NAMES, + ) + items = [item async for item in stream] + # The bare `[DONE]` sentinel stops iteration WITHOUT being yielded. + assert items == [("response.delta", {"delta": "a"})] + assert response.closed diff --git a/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py b/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py index 60c0de9e53a..5be85663b1f 100644 --- a/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py +++ b/packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +import json + import pytest from streaming.sse import SseClient @@ -46,3 +48,96 @@ def test_retrieve_stream(client: SseClient): assert isinstance(items[0], PartialResult) and items[0].text == "partial one" assert isinstance(items[1], PartialResult) and items[1].text == "partial two" assert isinstance(items[2], FinalResult) and items[2].references == ["doc1", "doc2"] + + +# --------------------------------------------------------------------------- +# Named / model terminal events (yield-then-stop). +# +# The published Spector SSE specs only cover the bare string-constant `[DONE]` +# sentinel (drop-and-stop, exercised above). A ``@terminalEvent`` can also be a +# *named* event carrying a model payload (e.g. `response.completed`, `error`): +# such an event IS deserialized and yielded, and iteration stops immediately +# afterwards. No published spec produces that shape, so we drive the generated +# ``Stream`` runtime directly with a fake response instead of the mock server. +# --------------------------------------------------------------------------- + + +class _FakeResponse: + """A minimal HttpResponse-shaped stand-in that replays SSE bytes.""" + + def __init__(self, body: bytes): + self.headers = {"Content-Type": "text/event-stream"} + self._body = body + self.closed = False + + def iter_bytes(self): + # Emit in small chunks so incremental SSE framing is exercised. + for index in range(0, len(self._body), 8): + yield self._body[index : index + 8] + + def close(self): + self.closed = True + + +def _event_kind(_response, event): + return (event.event, json.loads(event.data)) + + +_NAMED_TERMINAL_SSE = ( + b'event: response.partial\ndata: {"text": "one"}\n\n' + b'event: response.delta\ndata: {"delta": "hi"}\n\n' + b'event: response.completed\ndata: {"references": []}\n\n' + b'event: response.delta\ndata: {"delta": "AFTER-TERMINAL"}\n\n' +) + +_TERMINAL_EVENT_NAMES = ["response.completed", "error"] + + +def test_named_terminal_event_yields_then_stops(): + response = _FakeResponse(_NAMED_TERMINAL_SSE) + stream = Stream( + response=response, + deserialization_callback=_event_kind, + terminal_event_names=_TERMINAL_EVENT_NAMES, + ) + items = list(stream) + # The named terminal `response.completed` IS yielded (it carries a payload), + # then iteration stops -- the trailing `response.delta` must not appear. + assert items == [ + ("response.partial", {"text": "one"}), + ("response.delta", {"delta": "hi"}), + ("response.completed", {"references": []}), + ] + assert response.closed + + +def test_named_terminal_event_first_stops_immediately(): + body = b'event: error\ndata: {"code": "boom"}\n\n' b'event: response.delta\ndata: {"delta": "AFTER-ERROR"}\n\n' + response = _FakeResponse(body) + stream = Stream( + response=response, + deserialization_callback=_event_kind, + terminal_event_names=_TERMINAL_EVENT_NAMES, + ) + items = list(stream) + assert items == [("error", {"code": "boom"})] + assert response.closed + + +def test_sentinel_and_named_terminal_coexist(): + body = ( + b'event: response.delta\ndata: {"delta": "a"}\n\n' + b"data: [DONE]\n\n" + b'event: response.delta\ndata: {"delta": "AFTER-DONE"}\n\n' + ) + response = _FakeResponse(body) + stream = Stream( + response=response, + deserialization_callback=_event_kind, + terminal_event="[DONE]", + terminal_event_names=_TERMINAL_EVENT_NAMES, + ) + items = list(stream) + # The bare `[DONE]` sentinel stops iteration WITHOUT being yielded. + assert items == [("response.delta", {"delta": "a"})] + assert response.closed From e60bca32f83a33b812584e9d2082390a39aa730e Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Thu, 20 Aug 2026 15:00:00 -0700 Subject: [PATCH 16/21] fix(http-client-python): avoid duplicate stream kwarg for structured streams Treat structured streams as stream responses in has_stream_response so stream_value uses kwargs.pop("stream", True) when exposed. This preserves behavior and prevents duplicate stream forwarding. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../generator/pygen/codegen/models/operation.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index b2fb243fef9..c9bd18fa1d0 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -98,10 +98,6 @@ def exact_name_params(self) -> set[str]: @property def stream_value(self) -> Union[str, bool]: - # Structured streams (JSONL / SSE) must always run the pipeline with - # stream=True so the body can be consumed incrementally by Stream/AsyncStream. - if self.has_structured_stream_response: - return True return ( f'kwargs.pop("stream", {self.has_stream_response})' if self.expose_stream_keyword and self.has_response_body and "stream" not in self.exact_name_params @@ -515,7 +511,7 @@ def filename(self) -> str: @property def has_stream_response(self) -> bool: - return any(r.is_stream_response for r in self.responses) + return any(r.is_stream_response or getattr(r, "is_structured_stream", False) for r in self.responses) @classmethod def get_request_builder(cls, yaml_data: dict[str, Any], client: "Client"): From c876d7fae806850599a151ac35deda7482a69da7 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Thu, 20 Aug 2026 15:34:52 -0700 Subject: [PATCH 17/21] refactor(http-client-python): use typed structured-stream response flag Replace getattr fallback in Operation.has_stream_response with direct Response.is_structured_stream access to keep stream response checks explicit and typed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../generator/pygen/codegen/models/operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index c9bd18fa1d0..d1cac714727 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -511,7 +511,7 @@ def filename(self) -> str: @property def has_stream_response(self) -> bool: - return any(r.is_stream_response or getattr(r, "is_structured_stream", False) for r in self.responses) + return any(r.is_stream_response or r.is_structured_stream for r in self.responses) @classmethod def get_request_builder(cls, yaml_data: dict[str, Any], client: "Client"): From c4e0aebc03f2f5768b902eed3c4140a89dee3088 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Thu, 20 Aug 2026 15:41:15 -0700 Subject: [PATCH 18/21] fix(http-client-python): support structured stream callbacks in msrest mode Branch structured stream per-event deserialization by models-mode, using self._deserialize(...) for msrest and _deserialize(...) for dpg, so generated msrest clients do not raise NameError. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../codegen/serializers/builder_serializer.py | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index c449cab366f..db3c8cad12f 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1261,7 +1261,7 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] Produces a per-event deserialization callback and returns a ``Stream`` / ``AsyncStream`` wrapping the streamed HTTP response. """ - response = next(r for r in builder.responses if getattr(r, "is_structured_stream", False)) + response = next(r for r in builder.responses if r.is_structured_stream) item_annotation = response.type.type_annotation( # type: ignore[union-attr] is_operation_file=True, serialize_namespace=self.serialize_namespace ) @@ -1287,14 +1287,26 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] ) keyword = "if" if index == 0 else "elif" retval.append(f" {keyword} _event.event == {event_type!r}:") - retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") + if self.code_model.options["models-mode"] == "msrest": + retval.append(" deserialized = self._deserialize(") + retval.append(f" '{event_annotation}',") + retval.append(" _http_response") + retval.append(" )") + else: + retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") retval.append(" else:") if len(unnamed_events) == 1: event_annotation = unnamed_events[0].type_annotation( is_operation_file=True, serialize_namespace=self.serialize_namespace, ) - retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") + if self.code_model.options["models-mode"] == "msrest": + retval.append(" deserialized = self._deserialize(") + retval.append(f" '{event_annotation}',") + retval.append(" _http_response") + retval.append(" )") + else: + retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") else: retval.append(" deserialized = _event_json") elif len(unnamed_events) == 1: @@ -1302,12 +1314,30 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] is_operation_file=True, serialize_namespace=self.serialize_namespace, ) - retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") + if self.code_model.options["models-mode"] == "msrest": + retval.append(" deserialized = self._deserialize(") + retval.append(f" '{event_annotation}',") + retval.append(" _http_response") + retval.append(" )") + else: + retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") else: - retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)") + if self.code_model.options["models-mode"] == "msrest": + retval.append(" deserialized = self._deserialize(") + retval.append(f" '{item_annotation}',") + retval.append(" _http_response") + retval.append(" )") + else: + retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)") else: retval.append(" _event_json = _event.json()") - retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)") + if self.code_model.options["models-mode"] == "msrest": + retval.append(" deserialized = self._deserialize(") + retval.append(f" '{item_annotation}',") + retval.append(" _http_response") + retval.append(" )") + else: + 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") From 45b423f687d2621619a859bf241b853c3199f2ba Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Thu, 20 Aug 2026 16:01:28 -0700 Subject: [PATCH 19/21] refactor(http-client-python): use has_structured_stream directly Remove need_streaming_base alias and reference has_structured_stream directly when deciding utils folder contents. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../generator/pygen/codegen/models/code_model.py | 9 +-------- .../generator/pygen/codegen/serializers/__init__.py | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/models/code_model.py b/packages/http-client-python/generator/pygen/codegen/models/code_model.py index c1e0bb1ee6b..5db0e8aed85 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/code_model.py +++ b/packages/http-client-python/generator/pygen/codegen/models/code_model.py @@ -279,7 +279,7 @@ def need_utils_folder(self, async_mode: bool, client_namespace: str) -> bool: self.need_utils_utils(async_mode, client_namespace) or self.need_utils_serialization or self.options["models-mode"] == "dpg" - or self.need_streaming_base + or self.has_structured_stream ) @property @@ -295,13 +295,6 @@ def has_structured_stream(self) -> bool: for op in og.operations ) - @property - def need_streaming_base(self) -> bool: - # Whether to emit the vendored ``_utils/streaming_base.py`` (Stream / AsyncStream - # + JSONL / SSE decoders). Only needed when at least one operation returns a - # structured stream. - return self.has_structured_stream - def need_utils_utils(self, async_mode: bool, client_namespace: str) -> bool: return ( self.need_utils_form_data(async_mode, client_namespace) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py index 0321dc17d91..e73e3866970 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py @@ -526,7 +526,7 @@ def _serialize_and_write_utils_folder(self, env: Environment, namespace: str): ) # write _utils/streaming_base.py (vendored Stream/AsyncStream + JSONL/SSE decoders) - if self.code_model.need_streaming_base: + if self.code_model.has_structured_stream: self.write_file( utils_folder_path / Path("streaming_base.py"), general_serializer.serialize_streaming_base_file(), From d7d58faf01054e0235331244dfd0c30917f226ed Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 21 Aug 2026 08:54:02 -0700 Subject: [PATCH 20/21] fix(http-client-python): deserialize msrest stream events correctly Use inline stream item annotations for union payloads and deserialize msrest structured-stream events from each event JSON payload using serialization types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../codegen/serializers/builder_serializer.py | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index db3c8cad12f..a3930344d57 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1262,7 +1262,7 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] ``AsyncStream`` wrapping the streamed HTTP response. """ response = next(r for r in builder.responses if r.is_structured_stream) - item_annotation = response.type.type_annotation( # type: ignore[union-attr] + item_annotation = response.stream_item_annotation( is_operation_file=True, serialize_namespace=self.serialize_namespace ) stream_class = response.stream_class_name(self.async_mode) # type: ignore[attr-defined] @@ -1288,9 +1288,12 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] keyword = "if" if index == 0 else "elif" retval.append(f" {keyword} _event.event == {event_type!r}:") if self.code_model.options["models-mode"] == "msrest": + serialization_type = event_item_type.serialization_type( + serialize_namespace=self.serialize_namespace + ) retval.append(" deserialized = self._deserialize(") - retval.append(f" '{event_annotation}',") - retval.append(" _http_response") + retval.append(f" '{serialization_type}',") + retval.append(" _event_json") retval.append(" )") else: retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") @@ -1301,9 +1304,12 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] serialize_namespace=self.serialize_namespace, ) if self.code_model.options["models-mode"] == "msrest": + serialization_type = unnamed_events[0].serialization_type( + serialize_namespace=self.serialize_namespace + ) retval.append(" deserialized = self._deserialize(") - retval.append(f" '{event_annotation}',") - retval.append(" _http_response") + retval.append(f" '{serialization_type}',") + retval.append(" _event_json") retval.append(" )") else: retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") @@ -1315,26 +1321,35 @@ def handle_structured_stream_response(self, builder: OperationType) -> list[str] serialize_namespace=self.serialize_namespace, ) if self.code_model.options["models-mode"] == "msrest": + serialization_type = unnamed_events[0].serialization_type( + serialize_namespace=self.serialize_namespace + ) retval.append(" deserialized = self._deserialize(") - retval.append(f" '{event_annotation}',") - retval.append(" _http_response") + retval.append(f" '{serialization_type}',") + retval.append(" _event_json") retval.append(" )") else: retval.append(f" deserialized = _deserialize({event_annotation}, _event_json)") else: if self.code_model.options["models-mode"] == "msrest": + serialization_type = response.stream_item_type.serialization_type( + serialize_namespace=self.serialize_namespace + ) retval.append(" deserialized = self._deserialize(") - retval.append(f" '{item_annotation}',") - retval.append(" _http_response") + retval.append(f" '{serialization_type}',") + retval.append(" _event_json") retval.append(" )") else: retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)") else: retval.append(" _event_json = _event.json()") if self.code_model.options["models-mode"] == "msrest": + serialization_type = response.stream_item_type.serialization_type( + serialize_namespace=self.serialize_namespace + ) retval.append(" deserialized = self._deserialize(") - retval.append(f" '{item_annotation}',") - retval.append(" _http_response") + retval.append(f" '{serialization_type}',") + retval.append(" _event_json") retval.append(" )") else: retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)") From 72e27478d59af4dc4305a65a866953639709b88e Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 21 Aug 2026 09:34:49 -0700 Subject: [PATCH 21/21] fix(http-client-python): satisfy serializer pylint limit Suppress the statement-count warning for the structured stream response serializer, matching the existing generated-code style for complex response handlers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51359cbf-bc53-429e-b947-8284a91d7d46 --- .../generator/pygen/codegen/serializers/builder_serializer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index a3930344d57..a09bc2e96a9 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1255,6 +1255,7 @@ def handle_error_response( # pylint: disable=too-many-statements, too-many-bran ) return retval + # pylint: disable=too-many-statements def handle_structured_stream_response(self, builder: OperationType) -> list[str]: """Emit the body for an operation returning a structured (JSONL / SSE) stream.