@typespec/http-client-python 0.37.2 → 0.38.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.
@@ -1150,22 +1150,36 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1150
1150
  return retval
1151
1151
 
1152
1152
  def handle_error_response( # pylint: disable=too-many-statements, too-many-branches
1153
- self, builder: OperationType
1153
+ self,
1154
+ builder: OperationType,
1155
+ *,
1156
+ response_name: str = "response",
1157
+ pipeline_response_name: str = "pipeline_response",
1158
+ success_status_codes: Optional[list[int]] = None,
1159
+ stream_value: Optional[Union[str, bool]] = None,
1160
+ response_read_name: Optional[str] = None,
1154
1161
  ) -> list[str]:
1155
1162
  async_await = "await " if self.async_mode else ""
1156
- retval = [f"if response.status_code not in {str(builder.success_status_codes)}:"]
1157
- response_read = [
1158
- " try:",
1159
- f" {async_await}response.read() # Load the body in memory and close the socket",
1160
- " except (StreamConsumedError, StreamClosedError):",
1161
- " pass",
1162
- ]
1163
- if builder.stream_value is True: # _stream is True so no need to judge it
1163
+ success_status_codes = builder.success_status_codes if success_status_codes is None else success_status_codes
1164
+ stream_value = builder.stream_value if stream_value is None else stream_value
1165
+ retval = [f"if {response_name}.status_code not in {str(success_status_codes)}:"]
1166
+ if response_read_name:
1167
+ response_read = [f" {async_await}{response_read_name}({response_name})"]
1168
+ else:
1169
+ response_read = [
1170
+ " try:",
1171
+ f" {async_await}{response_name}.read() # Load the body in memory and close the socket",
1172
+ " except (StreamConsumedError, StreamClosedError):",
1173
+ " pass",
1174
+ ]
1175
+ if stream_value is True: # _stream is True so no need to judge it
1164
1176
  retval.extend(response_read)
1165
- elif isinstance(builder.stream_value, str): # _stream is not sure, so we need to judge it
1177
+ elif isinstance(stream_value, str): # _stream is not sure, so we need to judge it
1166
1178
  retval.append(" if _stream:")
1167
1179
  retval.extend([f" {l}" for l in response_read])
1168
- retval.append(" map_error(status_code=response.status_code, response=response, error_map=error_map)")
1180
+ retval.append(
1181
+ f" map_error(status_code={response_name}.status_code, " f"response={response_name}, error_map=error_map)"
1182
+ )
1169
1183
  error_model = ""
1170
1184
  if builder.non_default_errors and ( # pylint: disable=too-many-nested-blocks
1171
1185
  self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
@@ -1181,7 +1195,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1181
1195
  pylint_disable = ""
1182
1196
  if isinstance(e.status_codes[0], int):
1183
1197
  for status_code in e.status_codes:
1184
- retval.append(f" {condition} response.status_code == {status_code}:")
1198
+ retval.append(f" {condition} {response_name}.status_code == {status_code}:")
1185
1199
  type_annotation = e.type.type_annotation( # type: ignore
1186
1200
  is_operation_file=True, skip_quote=True, serialize_namespace=self.serialize_namespace
1187
1201
  )
@@ -1190,13 +1204,15 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1190
1204
  fn = "_failsafe_deserialize_xml"
1191
1205
  else:
1192
1206
  fn = "_failsafe_deserialize"
1193
- retval.append(f" error = {fn}({type_annotation},{pylint_disable}\n response)")
1207
+ retval.append(
1208
+ f" error = {fn}({type_annotation},{pylint_disable}\n {response_name})"
1209
+ )
1194
1210
  else:
1195
1211
  retval.extend(
1196
1212
  [
1197
1213
  " error = self._deserialize.failsafe_deserialize(",
1198
1214
  f" {type_annotation},{pylint_disable}",
1199
- " pipeline_response,",
1215
+ f" {pipeline_response_name},",
1200
1216
  " )",
1201
1217
  ]
1202
1218
  )
@@ -1209,7 +1225,8 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1209
1225
  # ranged status code only exist in typespec and will not have multiple status codes
1210
1226
  else:
1211
1227
  retval.append(
1212
- f" {condition} {e.status_codes[0][0]} <= response.status_code <= {e.status_codes[0][1]}:"
1228
+ f" {condition} {e.status_codes[0][0]} <= "
1229
+ f"{response_name}.status_code <= {e.status_codes[0][1]}:"
1213
1230
  )
1214
1231
  type_annotation = e.type.type_annotation( # type: ignore
1215
1232
  is_operation_file=True, skip_quote=True, serialize_namespace=self.serialize_namespace
@@ -1218,19 +1235,19 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1218
1235
  if xml_serializable(str(e.default_content_type)):
1219
1236
  retval.append(
1220
1237
  " error = _failsafe_deserialize_xml("
1221
- f"{type_annotation},{pylint_disable}\n response)"
1238
+ f"{type_annotation},{pylint_disable}\n {response_name})"
1222
1239
  )
1223
1240
  else:
1224
1241
  retval.append(
1225
- " error = _failsafe_deserialize("
1226
- f"{type_annotation},{pylint_disable}\n response)"
1242
+ f" error = _failsafe_deserialize("
1243
+ f"{type_annotation},{pylint_disable}\n {response_name})"
1227
1244
  )
1228
1245
  else:
1229
1246
  retval.extend(
1230
1247
  [
1231
1248
  " error = self._deserialize.failsafe_deserialize(",
1232
1249
  f" {type_annotation},{pylint_disable}",
1233
- " pipeline_response,",
1250
+ f" {pipeline_response_name},",
1234
1251
  " )",
1235
1252
  ]
1236
1253
  )
@@ -1254,7 +1271,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1254
1271
  [
1255
1272
  f"{indent}error = {fn}(",
1256
1273
  f"{indent} {default_error_deserialization}",
1257
- f"{indent} response,",
1274
+ f"{indent} {response_name},",
1258
1275
  f"{indent})",
1259
1276
  ]
1260
1277
  )
@@ -1263,12 +1280,13 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1263
1280
  [
1264
1281
  f"{indent}error = self._deserialize.failsafe_deserialize(",
1265
1282
  f"{indent} {default_error_deserialization}",
1266
- f"{indent} pipeline_response,",
1283
+ f"{indent} {pipeline_response_name},",
1267
1284
  f"{indent})",
1268
1285
  ]
1269
1286
  )
1270
1287
  retval.append(
1271
- " raise HttpResponseError(response=response{}{})".format(
1288
+ " raise HttpResponseError(response={}{}{})".format(
1289
+ response_name,
1272
1290
  error_model,
1273
1291
  (", error_format=ARMErrorFormat" if self.code_model.options["azure-arm"] else ""),
1274
1292
  )
@@ -1378,6 +1396,41 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1378
1396
  stream_kwargs.append(f"terminal_event={terminal_event!r}")
1379
1397
  if terminal_event_names:
1380
1398
  stream_kwargs.append(f"terminal_event_names={terminal_event_names!r}")
1399
+ if response.streaming_kind == "sse": # type: ignore[attr-defined]
1400
+ retval.append("")
1401
+ retval.append(
1402
+ "async def _reconnect(_last_event_id, _reconnect_delay):"
1403
+ if self.async_mode
1404
+ else "def _reconnect(_last_event_id, _reconnect_delay):"
1405
+ )
1406
+ retval.append(" _transport: Any = pipeline_response.context.transport")
1407
+ retval.append(f" {'await ' if self.async_mode else ''}_transport.sleep(_reconnect_delay)")
1408
+ retval.append(" _update_sse_request_headers(_request, _last_event_id)")
1409
+ retval.append(
1410
+ f" _reconnect_response = {self._call_method}self._client.send_request("
1411
+ "_request, stream=True, **kwargs)"
1412
+ )
1413
+ reconnect_status_codes = list(builder.success_status_codes)
1414
+ if 204 not in reconnect_status_codes:
1415
+ reconnect_status_codes.append(204)
1416
+ retval.extend(
1417
+ [
1418
+ f" {line}"
1419
+ for line in self.handle_error_response(
1420
+ builder,
1421
+ response_name="_reconnect_response",
1422
+ pipeline_response_name=(
1423
+ "PipelineResponse(_request, _reconnect_response, pipeline_response.context)"
1424
+ ),
1425
+ success_status_codes=reconnect_status_codes,
1426
+ stream_value=True,
1427
+ response_read_name=("_read_sse_response_async" if self.async_mode else "_read_sse_response"),
1428
+ )
1429
+ ]
1430
+ )
1431
+ retval.append(" return _reconnect_response")
1432
+ stream_kwargs.append("last_event_id=_last_event_id")
1433
+ stream_kwargs.append("reconnect_callback=_reconnect")
1381
1434
  unnamed_terminal_values = (
1382
1435
  [discriminator_value for discriminator_value, event in unnamed_discriminator[1] if event.is_terminal]
1383
1436
  if unnamed_discriminator is not None
@@ -1843,7 +1896,7 @@ class _LROOperationSerializer(_OperationSerializer[LROOperationType]):
1843
1896
  if builder.lro_response.headers:
1844
1897
  retval.append(" response_headers = {}")
1845
1898
  if (
1846
- (not self.code_model.options["models-mode"] and not self.code_model.generate_typeddict_only)
1899
+ not self.code_model.options["models-mode"]
1847
1900
  or self.code_model.options["models-mode"] == "dpg"
1848
1901
  or builder.lro_response.headers
1849
1902
  ):
@@ -3,7 +3,7 @@
3
3
  # Licensed under the MIT License. See License.txt in the project root for
4
4
  # license information.
5
5
  # --------------------------------------------------------------------------
6
- from ..models import CodeModel
6
+ from ..models import CodeModel, CombinedType
7
7
  from ..models.imports import FileImport, ImportType
8
8
  from ..models.utils import NamespaceType
9
9
  from .import_serializer import FileImportSerializer
@@ -18,15 +18,30 @@ class UnionsSerializer(BaseSerializer):
18
18
  ):
19
19
  super().__init__(code_model=code_model, env=env)
20
20
 
21
+ @property
22
+ def named_unions(self) -> list[CombinedType]:
23
+ # The same named union can reach codegen as multiple objects, so collapse
24
+ # by emitted alias name (keeping insertion order) to avoid duplicate aliases.
25
+ deduped: dict[str, CombinedType] = {}
26
+ for union in self.code_model.named_unions:
27
+ if union.name:
28
+ deduped.setdefault(union.name, union)
29
+ return list(deduped.values())
30
+
21
31
  def imports(self) -> FileImport:
22
32
  file_import = FileImport(self.code_model)
23
- if self.code_model.named_unions:
33
+ if self.named_unions:
34
+ file_import.add_submodule_import(
35
+ "typing",
36
+ "TypeAlias",
37
+ ImportType.STDLIB,
38
+ )
24
39
  file_import.add_submodule_import(
25
40
  "typing",
26
41
  "Union",
27
42
  ImportType.STDLIB,
28
43
  )
29
- for nu in self.code_model.named_unions:
44
+ for nu in self.named_unions:
30
45
  file_import.merge(
31
46
  nu.imports(
32
47
  serialize_namespace=self.serialize_namespace,
@@ -39,6 +54,7 @@ class UnionsSerializer(BaseSerializer):
39
54
  template = self.env.get_template("unions.py.jinja2")
40
55
  return template.render(
41
56
  code_model=self.code_model,
57
+ named_unions=self.named_unions,
42
58
  imports=FileImportSerializer(self.imports()),
43
59
  serializer=self,
44
60
  )
@@ -33,7 +33,8 @@ from typing import (
33
33
 
34
34
  from typing_extensions import Self
35
35
 
36
- from {{ code_model.core_library }}.rest import AsyncHttpResponse, HttpResponse
36
+ from {{ code_model.core_library }}.exceptions import StreamClosedError, StreamConsumedError
37
+ from {{ code_model.core_library }}.rest import AsyncHttpResponse, HttpRequest, HttpResponse
37
38
 
38
39
  DecodedType = TypeVar("DecodedType")
39
40
  ReturnType_co = TypeVar("ReturnType_co", covariant=True)
@@ -435,13 +436,56 @@ async def _aiter_sse_lines(iter_bytes: AsyncIterator[bytes]) -> AsyncGenerator[s
435
436
  yield line
436
437
 
437
438
 
439
+ def _read_sse_response(response: HttpResponse) -> None:
440
+ """Read an SSE response body, ignoring responses that are already consumed or closed.
441
+
442
+ :param response: The response to read.
443
+ :type response: HttpResponse
444
+ """
445
+ try:
446
+ response.read()
447
+ except (StreamConsumedError, StreamClosedError):
448
+ pass
449
+
450
+
451
+ async def _read_sse_response_async(response: AsyncHttpResponse) -> None:
452
+ """Read an SSE response body, ignoring responses that are already consumed or closed.
453
+
454
+ :param response: The response to read.
455
+ :type response: AsyncHttpResponse
456
+ """
457
+ try:
458
+ await response.read()
459
+ except (StreamConsumedError, StreamClosedError):
460
+ pass
461
+
462
+
463
+ def _update_sse_request_headers(request: HttpRequest, last_event_id: Optional[str]) -> None:
464
+ """Update the last event ID header for an SSE reconnection request.
465
+
466
+ :param request: The reconnection request to update.
467
+ :type request: HttpRequest
468
+ :param last_event_id: The current value of the SSE last event ID buffer.
469
+ :type last_event_id: str or None
470
+ """
471
+ if last_event_id:
472
+ request.headers["Last-Event-ID"] = last_event_id
473
+ else:
474
+ request.headers.pop("Last-Event-ID", None)
475
+
476
+
438
477
  class _SSEEventBuilder:
439
- """Accumulates SSE field lines and builds :class:`ServerSentEvent` instances."""
478
+ """Accumulates SSE field lines and builds :class:`ServerSentEvent` instances.
440
479
 
441
- def __init__(self) -> None:
480
+ :param last_event_id: The initial value of the SSE last event ID buffer.
481
+ :type last_event_id: str
482
+ """
483
+
484
+ def __init__(self, last_event_id: str = "") -> None:
442
485
  self._data: List[str] = []
443
486
  self._event_type = ""
444
- self._last_id = ""
487
+ self._last_id = last_event_id
488
+ self._pending_last_id = last_event_id
445
489
  self._retry: Optional[int] = None
446
490
 
447
491
  def add_line(self, line: str) -> Optional[ServerSentEvent]:
@@ -469,7 +513,7 @@ class _SSEEventBuilder:
469
513
  self._data.append(value)
470
514
  elif field == "id":
471
515
  if "\x00" not in value:
472
- self._last_id = value
516
+ self._pending_last_id = value
473
517
  elif field == "retry":
474
518
  if value.isascii() and value.isdigit():
475
519
  try:
@@ -482,6 +526,7 @@ class _SSEEventBuilder:
482
526
  return None
483
527
 
484
528
  def _dispatch(self) -> Optional[ServerSentEvent]:
529
+ self._last_id = self._pending_last_id
485
530
  if not self._data:
486
531
  # No data accumulated: reset and dispatch nothing.
487
532
  self._event_type = ""
@@ -496,13 +541,55 @@ class _SSEEventBuilder:
496
541
  self._event_type = ""
497
542
  return event
498
543
 
544
+ @property
545
+ def last_event_id(self) -> str:
546
+ """Return the current value of the last event ID buffer.
547
+
548
+ :return: The current value of the last event ID buffer.
549
+ :rtype: str
550
+ """
551
+ return self._last_id
552
+
553
+ @property
554
+ def retry(self) -> Optional[int]:
555
+ """Return the most recently received valid reconnection time.
556
+
557
+ :return: The reconnection time in milliseconds, if one was provided.
558
+ :rtype: int or None
559
+ """
560
+ return self._retry
561
+
499
562
 
500
563
  class SSEDecoder:
501
564
  """Decoder for Server-Sent Events (SSE).
502
565
 
503
566
  https://html.spec.whatwg.org/multipage/server-sent-events.html
567
+
568
+ :keyword last_event_id: The initial value of the SSE last event ID buffer.
569
+ :paramtype last_event_id: str or None
504
570
  """
505
571
 
572
+ def __init__(self, *, last_event_id: Optional[str] = None) -> None:
573
+ self._builder = _SSEEventBuilder(last_event_id or "")
574
+
575
+ @property
576
+ def last_event_id(self) -> str:
577
+ """Return the current value of the last event ID buffer.
578
+
579
+ :return: The current value of the last event ID buffer.
580
+ :rtype: str
581
+ """
582
+ return self._builder.last_event_id
583
+
584
+ @property
585
+ def retry(self) -> Optional[int]:
586
+ """Return the most recently received valid reconnection time.
587
+
588
+ :return: The reconnection time in milliseconds, if one was provided.
589
+ :rtype: int or None
590
+ """
591
+ return self._builder.retry
592
+
506
593
  def iter_events(self, iter_bytes: Iterator[bytes]) -> Iterator[ServerSentEvent]:
507
594
  """Iterate over SSE events from a byte iterator.
508
595
 
@@ -511,9 +598,8 @@ class SSEDecoder:
511
598
  :rtype: Iterator[ServerSentEvent]
512
599
  :return: An iterator of server-sent events.
513
600
  """
514
- builder = _SSEEventBuilder()
515
601
  for line in _iter_sse_lines(iter_bytes):
516
- event = builder.add_line(line)
602
+ event = self._builder.add_line(line)
517
603
  if event is not None:
518
604
  yield event
519
605
 
@@ -522,8 +608,32 @@ class AsyncSSEDecoder:
522
608
  """Asynchronous decoder for Server-Sent Events (SSE).
523
609
 
524
610
  https://html.spec.whatwg.org/multipage/server-sent-events.html
611
+
612
+ :keyword last_event_id: The initial value of the SSE last event ID buffer.
613
+ :paramtype last_event_id: str or None
525
614
  """
526
615
 
616
+ def __init__(self, *, last_event_id: Optional[str] = None) -> None:
617
+ self._builder = _SSEEventBuilder(last_event_id or "")
618
+
619
+ @property
620
+ def last_event_id(self) -> str:
621
+ """Return the current value of the last event ID buffer.
622
+
623
+ :return: The current value of the last event ID buffer.
624
+ :rtype: str
625
+ """
626
+ return self._builder.last_event_id
627
+
628
+ @property
629
+ def retry(self) -> Optional[int]:
630
+ """Return the most recently received valid reconnection time.
631
+
632
+ :return: The reconnection time in milliseconds, if one was provided.
633
+ :rtype: int or None
634
+ """
635
+ return self._builder.retry
636
+
527
637
  # pylint: disable=invalid-overridden-method
528
638
  async def aiter_events(self, iter_bytes: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
529
639
  """Asynchronously iterate over SSE events from a byte iterator.
@@ -533,10 +643,9 @@ class AsyncSSEDecoder:
533
643
  :rtype: AsyncIterator[ServerSentEvent]
534
644
  :return: An asynchronous iterator of server-sent events.
535
645
  """
536
- builder = _SSEEventBuilder()
537
646
  async with aclosing(_aiter_sse_lines(iter_bytes)) as lines:
538
647
  async for line in lines:
539
- event = builder.add_line(line)
648
+ event = self._builder.add_line(line)
540
649
  if event is not None:
541
650
  yield event
542
651
 
@@ -563,6 +672,14 @@ class Stream(Iterator[ReturnType_co]):
563
672
  :keyword terminal_event_predicate: Optional predicate that identifies a payload-bearing
564
673
  terminal event. The event is yielded before iteration stops.
565
674
  :paramtype terminal_event_predicate: Callable[[Any], bool] or None
675
+ :keyword last_event_id: Optional event ID from which this stream is resuming.
676
+ :paramtype last_event_id: str or None
677
+ :keyword reconnect_callback: Optional callback used to re-establish the underlying response
678
+ when the stream disconnects before a terminal event is seen. Reconnection uses a
679
+ three-second delay unless the server provides an SSE ``retry`` value. The callback is
680
+ called with the last-seen SSE event ID (or ``None``) and the reconnect delay in seconds,
681
+ and must return a new response.
682
+ :paramtype reconnect_callback: Callable[[str or None, float], ~{{ code_model.core_library }}.rest.HttpResponse] or None
566
683
  """
567
684
 
568
685
  def __init__(
@@ -574,17 +691,26 @@ class Stream(Iterator[ReturnType_co]):
574
691
  terminal_event: Optional[str] = None,
575
692
  terminal_event_names: Optional[Sequence[str]] = None,
576
693
  terminal_event_predicate: Optional[Callable[[DecodedType], bool]] = None,
694
+ last_event_id: Optional[str] = None,
695
+ reconnect_callback: Optional[Callable[[Optional[str], float], HttpResponse]] = None,
577
696
  ) -> None:
578
697
  self._response = response
579
698
  content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
580
699
  self._decoder: StreamDecoder[Any] = (
581
- decoder if decoder is not None else (SSEDecoder() if content_type == "text/event-stream" else JSONLDecoder())
700
+ decoder
701
+ if decoder is not None
702
+ else (
703
+ SSEDecoder(last_event_id=last_event_id)
704
+ if content_type == "text/event-stream"
705
+ else JSONLDecoder()
706
+ )
582
707
  )
583
708
  self._deserialization_callback = deserialization_callback
584
709
  self._terminal_event = terminal_event
585
710
  self._terminal_event_names = frozenset(terminal_event_names or ())
586
711
  self._terminal_event_predicate = terminal_event_predicate
587
- self._last_event_id: Optional[str] = None
712
+ self._reconnect_callback = reconnect_callback
713
+ self._last_event_id = last_event_id
588
714
  self._retry: Optional[int] = None
589
715
  self._iterator = self._iter_results()
590
716
 
@@ -605,26 +731,46 @@ class Stream(Iterator[ReturnType_co]):
605
731
  return self
606
732
 
607
733
  def _iter_results(self) -> Generator[ReturnType_co, None, None]:
734
+ terminal = False
608
735
  try:
609
- for event in self._decoder.iter_events(self._response.iter_bytes()):
610
- event_id = getattr(event, "id", None)
611
- if event_id is not None:
612
- self._last_event_id = event_id
613
- event_retry = getattr(event, "retry", None)
614
- if event_retry is not None:
615
- self._retry = event_retry
616
- if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event:
736
+ while True:
737
+ if getattr(self._response, "status_code", None) == 204:
617
738
  break
618
- result = self._deserialization_callback(self._response, event)
619
- yield result
620
- if (
621
- self._terminal_event_names
622
- and getattr(event, "event", None) in self._terminal_event_names
623
- ) or (
624
- self._terminal_event_predicate is not None
625
- and self._terminal_event_predicate(event)
626
- ):
739
+ for event in self._decoder.iter_events(self._response.iter_bytes()):
740
+ event_id = getattr(event, "id", None)
741
+ if event_id is not None:
742
+ self._last_event_id = event_id
743
+ event_retry = getattr(event, "retry", None)
744
+ if event_retry is not None:
745
+ self._retry = event_retry
746
+ if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event:
747
+ terminal = True
748
+ break
749
+ result = self._deserialization_callback(self._response, cast(Any, event))
750
+ yield result
751
+ if (
752
+ self._terminal_event_names
753
+ and getattr(event, "event", None) in self._terminal_event_names
754
+ ) or (
755
+ self._terminal_event_predicate is not None
756
+ and self._terminal_event_predicate(cast(Any, event))
757
+ ):
758
+ terminal = True
759
+ break
760
+ if terminal or self._reconnect_callback is None:
627
761
  break
762
+ if isinstance(self._decoder, SSEDecoder):
763
+ self._last_event_id = self._decoder.last_event_id
764
+ if self._decoder.retry is not None:
765
+ self._retry = self._decoder.retry
766
+ self._response.close()
767
+ reconnect_delay = (self._retry if self._retry is not None else 3000) / 1000
768
+ self._response = self._reconnect_callback(self._last_event_id, reconnect_delay)
769
+ self._decoder = (
770
+ SSEDecoder(last_event_id=self._last_event_id)
771
+ if isinstance(self._decoder, SSEDecoder)
772
+ else type(self._decoder)()
773
+ )
628
774
  finally:
629
775
  self._response.close()
630
776
 
@@ -668,6 +814,14 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
668
814
  :keyword terminal_event_predicate: Optional predicate that identifies a payload-bearing
669
815
  terminal event. The event is yielded before iteration stops.
670
816
  :paramtype terminal_event_predicate: Callable[[Any], bool] or None
817
+ :keyword last_event_id: Optional event ID from which this stream is resuming.
818
+ :paramtype last_event_id: str or None
819
+ :keyword reconnect_callback: Optional callback used to re-establish the underlying response
820
+ when the stream disconnects before a terminal event is seen. Reconnection uses a
821
+ three-second delay unless the server provides an SSE ``retry`` value. The callback is
822
+ called with the last-seen SSE event ID (or ``None``) and the reconnect delay in seconds,
823
+ and must resolve to a new response.
824
+ :paramtype reconnect_callback: Callable[[str or None, float], Any] or None
671
825
  """
672
826
 
673
827
  def __init__(
@@ -679,19 +833,26 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
679
833
  terminal_event: Optional[str] = None,
680
834
  terminal_event_names: Optional[Sequence[str]] = None,
681
835
  terminal_event_predicate: Optional[Callable[[DecodedType], bool]] = None,
836
+ last_event_id: Optional[str] = None,
837
+ reconnect_callback: Optional[Callable[[Optional[str], float], Any]] = None,
682
838
  ) -> None:
683
839
  self._response = response
684
840
  content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
685
841
  self._decoder: AsyncStreamDecoder[Any] = (
686
842
  decoder
687
843
  if decoder is not None
688
- else (AsyncSSEDecoder() if content_type == "text/event-stream" else AsyncJSONLDecoder())
844
+ else (
845
+ AsyncSSEDecoder(last_event_id=last_event_id)
846
+ if content_type == "text/event-stream"
847
+ else AsyncJSONLDecoder()
848
+ )
689
849
  )
690
850
  self._deserialization_callback = deserialization_callback
691
851
  self._terminal_event = terminal_event
692
852
  self._terminal_event_names = frozenset(terminal_event_names or ())
693
853
  self._terminal_event_predicate = terminal_event_predicate
694
- self._last_event_id: Optional[str] = None
854
+ self._reconnect_callback = reconnect_callback
855
+ self._last_event_id = last_event_id
695
856
  self._retry: Optional[int] = None
696
857
  self._iterator = self._iter_results()
697
858
 
@@ -712,34 +873,54 @@ class AsyncStream(AsyncIterator[ReturnType_co]):
712
873
  return self
713
874
 
714
875
  async def _iter_results(self) -> AsyncGenerator[ReturnType_co, None]:
715
- events = self._decoder.aiter_events(self._response.iter_bytes())
876
+ terminal = False
716
877
  try:
717
- async for event in events:
718
- event_id = getattr(event, "id", None)
719
- if event_id is not None:
720
- self._last_event_id = event_id
721
- event_retry = getattr(event, "retry", None)
722
- if event_retry is not None:
723
- self._retry = event_retry
724
- if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event:
878
+ while True:
879
+ if getattr(self._response, "status_code", None) == 204:
725
880
  break
726
- result = self._deserialization_callback(self._response, event)
727
- yield result
728
- if (
729
- self._terminal_event_names
730
- and getattr(event, "event", None) in self._terminal_event_names
731
- ) or (
732
- self._terminal_event_predicate is not None
733
- and self._terminal_event_predicate(event)
734
- ):
881
+ events = self._decoder.aiter_events(self._response.iter_bytes())
882
+ try:
883
+ async for event in events:
884
+ event_id = getattr(event, "id", None)
885
+ if event_id is not None:
886
+ self._last_event_id = event_id
887
+ event_retry = getattr(event, "retry", None)
888
+ if event_retry is not None:
889
+ self._retry = event_retry
890
+ if self._terminal_event is not None and getattr(event, "data", None) == self._terminal_event:
891
+ terminal = True
892
+ break
893
+ result = self._deserialization_callback(self._response, cast(Any, event))
894
+ yield result
895
+ if (
896
+ self._terminal_event_names
897
+ and getattr(event, "event", None) in self._terminal_event_names
898
+ ) or (
899
+ self._terminal_event_predicate is not None
900
+ and self._terminal_event_predicate(cast(Any, event))
901
+ ):
902
+ terminal = True
903
+ break
904
+ finally:
905
+ aclose = getattr(events, "aclose", None)
906
+ if aclose is not None:
907
+ await aclose()
908
+ if terminal or self._reconnect_callback is None:
735
909
  break
736
- finally:
737
- try:
738
- aclose = getattr(events, "aclose", None)
739
- if aclose is not None:
740
- await aclose()
741
- finally:
910
+ if isinstance(self._decoder, AsyncSSEDecoder):
911
+ self._last_event_id = self._decoder.last_event_id
912
+ if self._decoder.retry is not None:
913
+ self._retry = self._decoder.retry
742
914
  await self._response.close()
915
+ reconnect_delay = (self._retry if self._retry is not None else 3000) / 1000
916
+ self._response = await self._reconnect_callback(self._last_event_id, reconnect_delay)
917
+ self._decoder = (
918
+ AsyncSSEDecoder(last_event_id=self._last_event_id)
919
+ if isinstance(self._decoder, AsyncSSEDecoder)
920
+ else type(self._decoder)()
921
+ )
922
+ finally:
923
+ await self._response.close()
743
924
 
744
925
  async def __aexit__(
745
926
  self,
@@ -4,6 +4,6 @@
4
4
  {% endif %}
5
5
 
6
6
  {{ imports }}
7
- {% for nu in code_model.named_unions %}
8
- {{nu.name}} = {{nu.type_definition()}}
7
+ {% for nu in named_unions %}
8
+ {{nu.name}}: TypeAlias = {{nu.type_definition()}}
9
9
  {% endfor %}