@typespec/http-client-python 0.38.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/emitter/emitter.d.ts.map +1 -1
- package/dist/emitter/emitter.js +1 -0
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/lib.d.ts +1 -0
- package/dist/emitter/lib.d.ts.map +1 -1
- package/dist/emitter/lib.js +6 -0
- package/dist/emitter/lib.js.map +1 -1
- package/emitter/src/emitter.ts +1 -0
- package/emitter/src/lib.ts +8 -0
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/emitter/test/emitter.test.ts +35 -1
- package/emitter/test/test-host.ts +14 -3
- package/eng/scripts/ci/regenerate-common.ts +14 -6
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/generator/build/lib/pygen/__init__.py +4 -0
- package/generator/build/lib/pygen/codegen/models/response.py +6 -5
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +5 -3
- package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +70 -7
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/__init__.py +4 -0
- package/generator/pygen/codegen/models/response.py +6 -5
- package/generator/pygen/codegen/serializers/builder_serializer.py +5 -3
- package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +70 -7
- package/package.json +1 -1
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import { expectDiagnostics, t } from "@typespec/compiler/testing";
|
|
2
2
|
import { expect, it } from "vitest";
|
|
3
|
-
import { emitCodeModel } from "./test-host.js";
|
|
3
|
+
import { EmitterTester, emitCodeModel } from "./test-host.js";
|
|
4
|
+
|
|
5
|
+
it.each([undefined, false, true])(
|
|
6
|
+
"propagates enable-sse-reconnect=%s to the Python generator",
|
|
7
|
+
async (enabled) => {
|
|
8
|
+
const { commandArgs, diagnostics } = await emitCodeModel(
|
|
9
|
+
"@service namespace Test; op read(): string;",
|
|
10
|
+
enabled === undefined ? {} : { "enable-sse-reconnect": enabled },
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expectDiagnostics(diagnostics, []);
|
|
14
|
+
expect(commandArgs["enable-sse-reconnect"]).toBe(enabled ?? false);
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
it.each(["yes", "no", 2, {}, []])("rejects invalid enable-sse-reconnect=%s", async (value) => {
|
|
19
|
+
const [, diagnostics] = await EmitterTester.compileAndDiagnose(
|
|
20
|
+
"@service namespace Test; op read(): string;",
|
|
21
|
+
{
|
|
22
|
+
compilerOptions: {
|
|
23
|
+
options: {
|
|
24
|
+
"@typespec/http-client-python": {
|
|
25
|
+
"emit-yaml-only": true,
|
|
26
|
+
"enable-sse-reconnect": value,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expectDiagnostics(diagnostics, {
|
|
34
|
+
code: "invalid-schema",
|
|
35
|
+
message: /enable-sse-reconnect/,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
4
38
|
|
|
5
39
|
it("targets the service namespace when no SDK clients are found", async () => {
|
|
6
40
|
const { diagnostics } = await emitCodeModel(t.code`
|
|
@@ -6,6 +6,7 @@ import jsyaml from "js-yaml";
|
|
|
6
6
|
import { tmpdir } from "os";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
import { $onEmit } from "../src/emitter.js";
|
|
9
|
+
import { $lib, type PythonEmitterOptions } from "../src/lib.js";
|
|
9
10
|
|
|
10
11
|
const PythonTester = createTester(resolvePath(import.meta.dirname, "../.."), {
|
|
11
12
|
libraries: ["@azure-tools/typespec-client-generator-core"],
|
|
@@ -20,10 +21,13 @@ export const EmitterTester = PythonTester.files({
|
|
|
20
21
|
"node_modules/@typespec/http-client-python/package.json": JSON.stringify({
|
|
21
22
|
name: "@typespec/http-client-python",
|
|
22
23
|
version: "0.0.0",
|
|
24
|
+
tspMain: "./lib.tsp",
|
|
23
25
|
exports: { ".": "./index.js" },
|
|
24
26
|
}),
|
|
27
|
+
"node_modules/@typespec/http-client-python/lib.tsp": 'import "./index.js";',
|
|
25
28
|
"node_modules/@typespec/http-client-python/index.js": mockFile.js({
|
|
26
29
|
$onEmit,
|
|
30
|
+
$lib,
|
|
27
31
|
}),
|
|
28
32
|
}).emit("@typespec/http-client-python", {
|
|
29
33
|
"generate-packaging-files": false,
|
|
@@ -48,7 +52,12 @@ export interface CodeModel {
|
|
|
48
52
|
*/
|
|
49
53
|
export async function emitCodeModel(
|
|
50
54
|
code: string,
|
|
51
|
-
|
|
55
|
+
options: PythonEmitterOptions = {},
|
|
56
|
+
): Promise<{
|
|
57
|
+
codeModel: CodeModel;
|
|
58
|
+
commandArgs: Record<string, unknown>;
|
|
59
|
+
diagnostics: readonly Diagnostic[];
|
|
60
|
+
}> {
|
|
52
61
|
const outputDir = await mkdtemp(join(tmpdir(), "typespec-python-"));
|
|
53
62
|
let yamlPath: string | undefined;
|
|
54
63
|
try {
|
|
@@ -56,6 +65,7 @@ export async function emitCodeModel(
|
|
|
56
65
|
compilerOptions: {
|
|
57
66
|
options: {
|
|
58
67
|
"@typespec/http-client-python": {
|
|
68
|
+
...options,
|
|
59
69
|
"emit-yaml-only": true,
|
|
60
70
|
"emitter-output-dir": outputDir,
|
|
61
71
|
},
|
|
@@ -71,9 +81,10 @@ export async function emitCodeModel(
|
|
|
71
81
|
if (!pointerName) {
|
|
72
82
|
throw new Error("Emitter did not produce a code model.");
|
|
73
83
|
}
|
|
74
|
-
|
|
84
|
+
const pointer = JSON.parse(await readFile(join(outputDir, pointerName), "utf-8"));
|
|
85
|
+
({ yamlPath } = pointer);
|
|
75
86
|
const codeModel = jsyaml.load(await readFile(yamlPath!, "utf-8")) as CodeModel;
|
|
76
|
-
return { codeModel, diagnostics };
|
|
87
|
+
return { codeModel, commandArgs: pointer.commandArgs, diagnostics };
|
|
77
88
|
} finally {
|
|
78
89
|
await rm(outputDir, { recursive: true, force: true });
|
|
79
90
|
if (yamlPath) {
|
|
@@ -80,10 +80,9 @@ export const SpecialFlags: Record<string, Record<string, any>> = {
|
|
|
80
80
|
|
|
81
81
|
// ---- Spec-specific emitter option overrides ----
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
> = {
|
|
83
|
+
type EmitterOptions = Record<string, string | boolean>;
|
|
84
|
+
|
|
85
|
+
export const AZURE_EMITTER_OPTIONS: Record<string, EmitterOptions | EmitterOptions[]> = {
|
|
87
86
|
"azure/client-generator-core/access": {
|
|
88
87
|
namespace: "specs.azure.clientgenerator.core.access",
|
|
89
88
|
},
|
|
@@ -222,7 +221,16 @@ export const AZURE_EMITTER_OPTIONS: Record<
|
|
|
222
221
|
},
|
|
223
222
|
};
|
|
224
223
|
|
|
225
|
-
export const EMITTER_OPTIONS: Record<string,
|
|
224
|
+
export const EMITTER_OPTIONS: Record<string, EmitterOptions | EmitterOptions[]> = {
|
|
225
|
+
"streaming/sse": [
|
|
226
|
+
{
|
|
227
|
+
"enable-sse-reconnect": true,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"package-name": "streaming-sse-no-reconnect",
|
|
231
|
+
namespace: "streaming.ssenoreconnect",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
226
234
|
"resiliency/srv-driven/old.tsp": {
|
|
227
235
|
"package-name": "resiliency-srv-driven1",
|
|
228
236
|
namespace: "resiliency.srv.driven1",
|
|
@@ -419,7 +427,7 @@ export function getEmitterOptions(
|
|
|
419
427
|
spec: string,
|
|
420
428
|
flavor: string,
|
|
421
429
|
ctx: RegenerateContext,
|
|
422
|
-
):
|
|
430
|
+
): EmitterOptions[] {
|
|
423
431
|
const specDir = isAzureSpec(spec) ? ctx.azureHttpSpecs : ctx.httpSpecs;
|
|
424
432
|
const relativeSpec = toPosix(relative(specDir, spec));
|
|
425
433
|
const key = relativeSpec.includes("resiliency/srv-driven/old.tsp")
|
|
Binary file
|
|
Binary file
|
|
@@ -32,6 +32,7 @@ class OptionsDict(MutableMapping):
|
|
|
32
32
|
"keep-pyproject-fields": "",
|
|
33
33
|
"generate-test": False,
|
|
34
34
|
"head-as-boolean": True,
|
|
35
|
+
"enable-sse-reconnect": False,
|
|
35
36
|
"keep-version-file": False,
|
|
36
37
|
"low-level-client": False,
|
|
37
38
|
"no-async": False,
|
|
@@ -175,6 +176,9 @@ class OptionsDict(MutableMapping):
|
|
|
175
176
|
raise ValueError("Can only have tracing turned on for Azure SDKs.")
|
|
176
177
|
|
|
177
178
|
def _validate_and_transform(self, key: str, value: Any) -> Any:
|
|
179
|
+
if key == "enable-sse-reconnect" and not isinstance(value, bool):
|
|
180
|
+
raise ValueError("--enable-sse-reconnect must be a boolean")
|
|
181
|
+
|
|
178
182
|
if key == "builders-visibility" and value not in ["public", "hidden", "embedded"]:
|
|
179
183
|
raise ValueError("The value of --builders-visibility must be either 'public', 'hidden', or 'embedded'")
|
|
180
184
|
|
|
@@ -267,11 +267,12 @@ class Response(BaseModel):
|
|
|
267
267
|
file_import.add_submodule_import(relative_path, stream_class, ImportType.LOCAL)
|
|
268
268
|
if self.streaming_kind == "sse":
|
|
269
269
|
file_import.add_import("json", ImportType.STDLIB)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
270
|
+
if self.code_model.options["enable-sse-reconnect"]:
|
|
271
|
+
read_response_helper = (
|
|
272
|
+
"_read_sse_response_async" if kwargs.get("async_mode", False) else "_read_sse_response"
|
|
273
|
+
)
|
|
274
|
+
file_import.add_submodule_import(relative_path, read_response_helper, ImportType.LOCAL)
|
|
275
|
+
file_import.add_submodule_import(relative_path, "_update_sse_request_headers", ImportType.LOCAL)
|
|
275
276
|
for event in self.streaming_events:
|
|
276
277
|
file_import.merge(event.payload_type.imports(**kwargs))
|
|
277
278
|
return file_import
|
|
@@ -1043,8 +1043,10 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1043
1043
|
|
|
1044
1044
|
def call_request_builder(self, builder: OperationType, is_paging: bool = False) -> list[str]:
|
|
1045
1045
|
retval = self._call_request_builder_helper(builder, builder.request_builder, is_paging=is_paging)
|
|
1046
|
-
if
|
|
1047
|
-
|
|
1046
|
+
if (
|
|
1047
|
+
self.code_model.options["enable-sse-reconnect"]
|
|
1048
|
+
and builder.has_structured_stream_response
|
|
1049
|
+
and any(response.streaming_kind == "sse" for response in builder.responses)
|
|
1048
1050
|
):
|
|
1049
1051
|
retval.insert(0, '_last_event_id = kwargs.pop("last_event_id", None)')
|
|
1050
1052
|
retval.extend(
|
|
@@ -1396,7 +1398,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1396
1398
|
stream_kwargs.append(f"terminal_event={terminal_event!r}")
|
|
1397
1399
|
if terminal_event_names:
|
|
1398
1400
|
stream_kwargs.append(f"terminal_event_names={terminal_event_names!r}")
|
|
1399
|
-
if response.streaming_kind == "sse"
|
|
1401
|
+
if response.streaming_kind == "sse" and self.code_model.options["enable-sse-reconnect"]:
|
|
1400
1402
|
retval.append("")
|
|
1401
1403
|
retval.append(
|
|
1402
1404
|
"async def _reconnect(_last_event_id, _reconnect_delay):"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# coding=utf-8
|
|
2
|
+
{% set enable_sse_reconnect = code_model.options["enable-sse-reconnect"] %}
|
|
2
3
|
{% if code_model.license_header %}
|
|
3
4
|
{{ code_model.license_header }}
|
|
4
5
|
{% endif %}
|
|
@@ -33,8 +34,11 @@ from typing import (
|
|
|
33
34
|
|
|
34
35
|
from typing_extensions import Self
|
|
35
36
|
|
|
37
|
+
{% if enable_sse_reconnect %}
|
|
36
38
|
from {{ code_model.core_library }}.exceptions import StreamClosedError, StreamConsumedError
|
|
37
|
-
from {{ code_model.core_library }}.rest import
|
|
39
|
+
from {{ code_model.core_library }}.rest import HttpRequest
|
|
40
|
+
{% endif %}
|
|
41
|
+
from {{ code_model.core_library }}.rest import AsyncHttpResponse, HttpResponse
|
|
38
42
|
|
|
39
43
|
DecodedType = TypeVar("DecodedType")
|
|
40
44
|
ReturnType_co = TypeVar("ReturnType_co", covariant=True)
|
|
@@ -436,6 +440,7 @@ async def _aiter_sse_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncGenerator[s
|
|
|
436
440
|
yield line
|
|
437
441
|
|
|
438
442
|
|
|
443
|
+
{% if enable_sse_reconnect %}
|
|
439
444
|
def _read_sse_response(response: HttpResponse) -> None:
|
|
440
445
|
"""Read an SSE response body, ignoring responses that are already consumed or closed.
|
|
441
446
|
|
|
@@ -474,14 +479,22 @@ def _update_sse_request_headers(request: HttpRequest, last_event_id: Optional[st
|
|
|
474
479
|
request.headers.pop("Last-Event-ID", None)
|
|
475
480
|
|
|
476
481
|
|
|
482
|
+
{% endif %}
|
|
477
483
|
class _SSEEventBuilder:
|
|
478
484
|
"""Accumulates SSE field lines and builds :class:`ServerSentEvent` instances.
|
|
479
485
|
|
|
486
|
+
{% if enable_sse_reconnect %}
|
|
480
487
|
:param last_event_id: The initial value of the SSE last event ID buffer.
|
|
481
488
|
:type last_event_id: str
|
|
489
|
+
{% endif %}
|
|
482
490
|
"""
|
|
483
491
|
|
|
492
|
+
{% if enable_sse_reconnect %}
|
|
484
493
|
def __init__(self, last_event_id: str = "") -> None:
|
|
494
|
+
{% else %}
|
|
495
|
+
def __init__(self) -> None:
|
|
496
|
+
last_event_id = ""
|
|
497
|
+
{% endif %}
|
|
485
498
|
self._data: List[str] = []
|
|
486
499
|
self._event_type = ""
|
|
487
500
|
self._last_id = last_event_id
|
|
@@ -565,12 +578,19 @@ class SSEDecoder:
|
|
|
565
578
|
|
|
566
579
|
https://html.spec.whatwg.org/multipage/server-sent-events.html
|
|
567
580
|
|
|
581
|
+
{% if enable_sse_reconnect %}
|
|
568
582
|
:keyword last_event_id: The initial value of the SSE last event ID buffer.
|
|
569
583
|
:paramtype last_event_id: str or None
|
|
584
|
+
{% endif %}
|
|
570
585
|
"""
|
|
571
586
|
|
|
587
|
+
{% if enable_sse_reconnect %}
|
|
572
588
|
def __init__(self, *, last_event_id: Optional[str] = None) -> None:
|
|
573
589
|
self._builder = _SSEEventBuilder(last_event_id or "")
|
|
590
|
+
{% else %}
|
|
591
|
+
def __init__(self) -> None:
|
|
592
|
+
self._builder = _SSEEventBuilder()
|
|
593
|
+
{% endif %}
|
|
574
594
|
|
|
575
595
|
@property
|
|
576
596
|
def last_event_id(self) -> str:
|
|
@@ -609,12 +629,19 @@ class AsyncSSEDecoder:
|
|
|
609
629
|
|
|
610
630
|
https://html.spec.whatwg.org/multipage/server-sent-events.html
|
|
611
631
|
|
|
632
|
+
{% if enable_sse_reconnect %}
|
|
612
633
|
:keyword last_event_id: The initial value of the SSE last event ID buffer.
|
|
613
634
|
:paramtype last_event_id: str or None
|
|
635
|
+
{% endif %}
|
|
614
636
|
"""
|
|
615
637
|
|
|
638
|
+
{% if enable_sse_reconnect %}
|
|
616
639
|
def __init__(self, *, last_event_id: Optional[str] = None) -> None:
|
|
617
640
|
self._builder = _SSEEventBuilder(last_event_id or "")
|
|
641
|
+
{% else %}
|
|
642
|
+
def __init__(self) -> None:
|
|
643
|
+
self._builder = _SSEEventBuilder()
|
|
644
|
+
{% endif %}
|
|
618
645
|
|
|
619
646
|
@property
|
|
620
647
|
def last_event_id(self) -> str:
|
|
@@ -672,6 +699,7 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
672
699
|
:keyword terminal_event_predicate: Optional predicate that identifies a payload-bearing
|
|
673
700
|
terminal event. The event is yielded before iteration stops.
|
|
674
701
|
:paramtype terminal_event_predicate: Callable[[Any], bool] or None
|
|
702
|
+
{% if enable_sse_reconnect %}
|
|
675
703
|
:keyword last_event_id: Optional event ID from which this stream is resuming.
|
|
676
704
|
:paramtype last_event_id: str or None
|
|
677
705
|
:keyword reconnect_callback: Optional callback used to re-establish the underlying response
|
|
@@ -680,6 +708,7 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
680
708
|
called with the last-seen SSE event ID (or ``None``) and the reconnect delay in seconds,
|
|
681
709
|
and must return a new response.
|
|
682
710
|
:paramtype reconnect_callback: Callable[[str or None, float], ~{{ code_model.core_library }}.rest.HttpResponse] or None
|
|
711
|
+
{% endif %}
|
|
683
712
|
"""
|
|
684
713
|
|
|
685
714
|
def __init__(
|
|
@@ -691,8 +720,10 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
691
720
|
terminal_event: Optional[str] = None,
|
|
692
721
|
terminal_event_names: Optional[Sequence[str]] = None,
|
|
693
722
|
terminal_event_predicate: Optional[Callable[[DecodedType], bool]] = None,
|
|
723
|
+
{% if enable_sse_reconnect %}
|
|
694
724
|
last_event_id: Optional[str] = None,
|
|
695
725
|
reconnect_callback: Optional[Callable[[Optional[str], float], HttpResponse]] = None,
|
|
726
|
+
{% endif %}
|
|
696
727
|
) -> None:
|
|
697
728
|
self._response = response
|
|
698
729
|
content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
|
|
@@ -700,7 +731,7 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
700
731
|
decoder
|
|
701
732
|
if decoder is not None
|
|
702
733
|
else (
|
|
703
|
-
SSEDecoder(last_event_id=last_event_id)
|
|
734
|
+
SSEDecoder({% if enable_sse_reconnect %}last_event_id=last_event_id{% endif %})
|
|
704
735
|
if content_type == "text/event-stream"
|
|
705
736
|
else JSONLDecoder()
|
|
706
737
|
)
|
|
@@ -709,8 +740,12 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
709
740
|
self._terminal_event = terminal_event
|
|
710
741
|
self._terminal_event_names = frozenset(terminal_event_names or ())
|
|
711
742
|
self._terminal_event_predicate = terminal_event_predicate
|
|
743
|
+
{% if enable_sse_reconnect %}
|
|
712
744
|
self._reconnect_callback = reconnect_callback
|
|
713
745
|
self._last_event_id = last_event_id
|
|
746
|
+
{% else %}
|
|
747
|
+
self._last_event_id: Optional[str] = None
|
|
748
|
+
{% endif %}
|
|
714
749
|
self._retry: Optional[int] = None
|
|
715
750
|
self._iterator = self._iter_results()
|
|
716
751
|
|
|
@@ -731,7 +766,9 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
731
766
|
return self
|
|
732
767
|
|
|
733
768
|
def _iter_results(self) -> Generator[ReturnType_co, None, None]:
|
|
769
|
+
{% if enable_sse_reconnect %}
|
|
734
770
|
terminal = False
|
|
771
|
+
{% endif %}
|
|
735
772
|
try:
|
|
736
773
|
while True:
|
|
737
774
|
if getattr(self._response, "status_code", None) == 204:
|
|
@@ -744,7 +781,9 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
744
781
|
if event_retry is not None:
|
|
745
782
|
self._retry = event_retry
|
|
746
783
|
if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event:
|
|
784
|
+
{% if enable_sse_reconnect %}
|
|
747
785
|
terminal = True
|
|
786
|
+
{% endif %}
|
|
748
787
|
break
|
|
749
788
|
result = self._deserialization_callback(self._response, cast(Any, event))
|
|
750
789
|
yield result
|
|
@@ -755,14 +794,17 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
755
794
|
self._terminal_event_predicate is not None
|
|
756
795
|
and self._terminal_event_predicate(cast(Any, event))
|
|
757
796
|
):
|
|
797
|
+
{% if enable_sse_reconnect %}
|
|
758
798
|
terminal = True
|
|
799
|
+
{% endif %}
|
|
759
800
|
break
|
|
760
|
-
if terminal or self._reconnect_callback is None:
|
|
761
|
-
break
|
|
762
801
|
if isinstance(self._decoder, SSEDecoder):
|
|
763
802
|
self._last_event_id = self._decoder.last_event_id
|
|
764
803
|
if self._decoder.retry is not None:
|
|
765
804
|
self._retry = self._decoder.retry
|
|
805
|
+
{% if enable_sse_reconnect %}
|
|
806
|
+
if terminal or self._reconnect_callback is None:
|
|
807
|
+
break
|
|
766
808
|
self._response.close()
|
|
767
809
|
reconnect_delay = (self._retry if self._retry is not None else 3000) / 1000
|
|
768
810
|
self._response = self._reconnect_callback(self._last_event_id, reconnect_delay)
|
|
@@ -771,6 +813,9 @@ class Stream(Iterator[ReturnType_co]):
|
|
|
771
813
|
if isinstance(self._decoder, SSEDecoder)
|
|
772
814
|
else type(self._decoder)()
|
|
773
815
|
)
|
|
816
|
+
{% else %}
|
|
817
|
+
break
|
|
818
|
+
{% endif %}
|
|
774
819
|
finally:
|
|
775
820
|
self._response.close()
|
|
776
821
|
|
|
@@ -814,6 +859,7 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
814
859
|
:keyword terminal_event_predicate: Optional predicate that identifies a payload-bearing
|
|
815
860
|
terminal event. The event is yielded before iteration stops.
|
|
816
861
|
:paramtype terminal_event_predicate: Callable[[Any], bool] or None
|
|
862
|
+
{% if enable_sse_reconnect %}
|
|
817
863
|
:keyword last_event_id: Optional event ID from which this stream is resuming.
|
|
818
864
|
:paramtype last_event_id: str or None
|
|
819
865
|
:keyword reconnect_callback: Optional callback used to re-establish the underlying response
|
|
@@ -822,6 +868,7 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
822
868
|
called with the last-seen SSE event ID (or ``None``) and the reconnect delay in seconds,
|
|
823
869
|
and must resolve to a new response.
|
|
824
870
|
:paramtype reconnect_callback: Callable[[str or None, float], Any] or None
|
|
871
|
+
{% endif %}
|
|
825
872
|
"""
|
|
826
873
|
|
|
827
874
|
def __init__(
|
|
@@ -833,8 +880,10 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
833
880
|
terminal_event: Optional[str] = None,
|
|
834
881
|
terminal_event_names: Optional[Sequence[str]] = None,
|
|
835
882
|
terminal_event_predicate: Optional[Callable[[DecodedType], bool]] = None,
|
|
883
|
+
{% if enable_sse_reconnect %}
|
|
836
884
|
last_event_id: Optional[str] = None,
|
|
837
885
|
reconnect_callback: Optional[Callable[[Optional[str], float], Any]] = None,
|
|
886
|
+
{% endif %}
|
|
838
887
|
) -> None:
|
|
839
888
|
self._response = response
|
|
840
889
|
content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
|
|
@@ -842,7 +891,7 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
842
891
|
decoder
|
|
843
892
|
if decoder is not None
|
|
844
893
|
else (
|
|
845
|
-
AsyncSSEDecoder(last_event_id=last_event_id)
|
|
894
|
+
AsyncSSEDecoder({% if enable_sse_reconnect %}last_event_id=last_event_id{% endif %})
|
|
846
895
|
if content_type == "text/event-stream"
|
|
847
896
|
else AsyncJSONLDecoder()
|
|
848
897
|
)
|
|
@@ -851,8 +900,12 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
851
900
|
self._terminal_event = terminal_event
|
|
852
901
|
self._terminal_event_names = frozenset(terminal_event_names or ())
|
|
853
902
|
self._terminal_event_predicate = terminal_event_predicate
|
|
903
|
+
{% if enable_sse_reconnect %}
|
|
854
904
|
self._reconnect_callback = reconnect_callback
|
|
855
905
|
self._last_event_id = last_event_id
|
|
906
|
+
{% else %}
|
|
907
|
+
self._last_event_id: Optional[str] = None
|
|
908
|
+
{% endif %}
|
|
856
909
|
self._retry: Optional[int] = None
|
|
857
910
|
self._iterator = self._iter_results()
|
|
858
911
|
|
|
@@ -873,7 +926,9 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
873
926
|
return self
|
|
874
927
|
|
|
875
928
|
async def _iter_results(self) -> AsyncGenerator[ReturnType_co, None]:
|
|
929
|
+
{% if enable_sse_reconnect %}
|
|
876
930
|
terminal = False
|
|
931
|
+
{% endif %}
|
|
877
932
|
try:
|
|
878
933
|
while True:
|
|
879
934
|
if getattr(self._response, "status_code", None) == 204:
|
|
@@ -888,7 +943,9 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
888
943
|
if event_retry is not None:
|
|
889
944
|
self._retry = event_retry
|
|
890
945
|
if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event:
|
|
946
|
+
{% if enable_sse_reconnect %}
|
|
891
947
|
terminal = True
|
|
948
|
+
{% endif %}
|
|
892
949
|
break
|
|
893
950
|
result = self._deserialization_callback(self._response, cast(Any, event))
|
|
894
951
|
yield result
|
|
@@ -899,18 +956,21 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
899
956
|
self._terminal_event_predicate is not None
|
|
900
957
|
and self._terminal_event_predicate(cast(Any, event))
|
|
901
958
|
):
|
|
959
|
+
{% if enable_sse_reconnect %}
|
|
902
960
|
terminal = True
|
|
961
|
+
{% endif %}
|
|
903
962
|
break
|
|
904
963
|
finally:
|
|
905
964
|
aclose = getattr(events, "aclose", None)
|
|
906
965
|
if aclose is not None:
|
|
907
966
|
await aclose()
|
|
908
|
-
if terminal or self._reconnect_callback is None:
|
|
909
|
-
break
|
|
910
967
|
if isinstance(self._decoder, AsyncSSEDecoder):
|
|
911
968
|
self._last_event_id = self._decoder.last_event_id
|
|
912
969
|
if self._decoder.retry is not None:
|
|
913
970
|
self._retry = self._decoder.retry
|
|
971
|
+
{% if enable_sse_reconnect %}
|
|
972
|
+
if terminal or self._reconnect_callback is None:
|
|
973
|
+
break
|
|
914
974
|
await self._response.close()
|
|
915
975
|
reconnect_delay = (self._retry if self._retry is not None else 3000) / 1000
|
|
916
976
|
self._response = await self._reconnect_callback(self._last_event_id, reconnect_delay)
|
|
@@ -919,6 +979,9 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
|
|
|
919
979
|
if isinstance(self._decoder, AsyncSSEDecoder)
|
|
920
980
|
else type(self._decoder)()
|
|
921
981
|
)
|
|
982
|
+
{% else %}
|
|
983
|
+
break
|
|
984
|
+
{% endif %}
|
|
922
985
|
finally:
|
|
923
986
|
await self._response.close()
|
|
924
987
|
|
|
Binary file
|
|
@@ -32,6 +32,7 @@ class OptionsDict(MutableMapping):
|
|
|
32
32
|
"keep-pyproject-fields": "",
|
|
33
33
|
"generate-test": False,
|
|
34
34
|
"head-as-boolean": True,
|
|
35
|
+
"enable-sse-reconnect": False,
|
|
35
36
|
"keep-version-file": False,
|
|
36
37
|
"low-level-client": False,
|
|
37
38
|
"no-async": False,
|
|
@@ -175,6 +176,9 @@ class OptionsDict(MutableMapping):
|
|
|
175
176
|
raise ValueError("Can only have tracing turned on for Azure SDKs.")
|
|
176
177
|
|
|
177
178
|
def _validate_and_transform(self, key: str, value: Any) -> Any:
|
|
179
|
+
if key == "enable-sse-reconnect" and not isinstance(value, bool):
|
|
180
|
+
raise ValueError("--enable-sse-reconnect must be a boolean")
|
|
181
|
+
|
|
178
182
|
if key == "builders-visibility" and value not in ["public", "hidden", "embedded"]:
|
|
179
183
|
raise ValueError("The value of --builders-visibility must be either 'public', 'hidden', or 'embedded'")
|
|
180
184
|
|
|
@@ -267,11 +267,12 @@ class Response(BaseModel):
|
|
|
267
267
|
file_import.add_submodule_import(relative_path, stream_class, ImportType.LOCAL)
|
|
268
268
|
if self.streaming_kind == "sse":
|
|
269
269
|
file_import.add_import("json", ImportType.STDLIB)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
270
|
+
if self.code_model.options["enable-sse-reconnect"]:
|
|
271
|
+
read_response_helper = (
|
|
272
|
+
"_read_sse_response_async" if kwargs.get("async_mode", False) else "_read_sse_response"
|
|
273
|
+
)
|
|
274
|
+
file_import.add_submodule_import(relative_path, read_response_helper, ImportType.LOCAL)
|
|
275
|
+
file_import.add_submodule_import(relative_path, "_update_sse_request_headers", ImportType.LOCAL)
|
|
275
276
|
for event in self.streaming_events:
|
|
276
277
|
file_import.merge(event.payload_type.imports(**kwargs))
|
|
277
278
|
return file_import
|
|
@@ -1043,8 +1043,10 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1043
1043
|
|
|
1044
1044
|
def call_request_builder(self, builder: OperationType, is_paging: bool = False) -> list[str]:
|
|
1045
1045
|
retval = self._call_request_builder_helper(builder, builder.request_builder, is_paging=is_paging)
|
|
1046
|
-
if
|
|
1047
|
-
|
|
1046
|
+
if (
|
|
1047
|
+
self.code_model.options["enable-sse-reconnect"]
|
|
1048
|
+
and builder.has_structured_stream_response
|
|
1049
|
+
and any(response.streaming_kind == "sse" for response in builder.responses)
|
|
1048
1050
|
):
|
|
1049
1051
|
retval.insert(0, '_last_event_id = kwargs.pop("last_event_id", None)')
|
|
1050
1052
|
retval.extend(
|
|
@@ -1396,7 +1398,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1396
1398
|
stream_kwargs.append(f"terminal_event={terminal_event!r}")
|
|
1397
1399
|
if terminal_event_names:
|
|
1398
1400
|
stream_kwargs.append(f"terminal_event_names={terminal_event_names!r}")
|
|
1399
|
-
if response.streaming_kind == "sse"
|
|
1401
|
+
if response.streaming_kind == "sse" and self.code_model.options["enable-sse-reconnect"]:
|
|
1400
1402
|
retval.append("")
|
|
1401
1403
|
retval.append(
|
|
1402
1404
|
"async def _reconnect(_last_event_id, _reconnect_delay):"
|