@typespec/http-client-python 0.35.1 → 0.37.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.
Files changed (115) hide show
  1. package/README.md +6 -0
  2. package/dist/emitter/code-model.d.ts +1 -1
  3. package/dist/emitter/code-model.d.ts.map +1 -1
  4. package/dist/emitter/code-model.js.map +1 -1
  5. package/dist/emitter/emitter.d.ts +2 -2
  6. package/dist/emitter/emitter.d.ts.map +1 -1
  7. package/dist/emitter/emitter.js +24 -3
  8. package/dist/emitter/emitter.js.map +1 -1
  9. package/dist/emitter/external-process.d.ts +1 -15
  10. package/dist/emitter/external-process.d.ts.map +1 -1
  11. package/dist/emitter/external-process.js +1 -17
  12. package/dist/emitter/external-process.js.map +1 -1
  13. package/dist/emitter/http.d.ts +45 -2
  14. package/dist/emitter/http.d.ts.map +1 -1
  15. package/dist/emitter/http.js +97 -1
  16. package/dist/emitter/http.js.map +1 -1
  17. package/dist/emitter/index.d.ts +2 -1
  18. package/dist/emitter/index.d.ts.map +1 -1
  19. package/dist/emitter/index.js.map +1 -1
  20. package/dist/emitter/lib.d.ts +3 -2
  21. package/dist/emitter/lib.d.ts.map +1 -1
  22. package/dist/emitter/lib.js +6 -1
  23. package/dist/emitter/lib.js.map +1 -1
  24. package/dist/emitter/node-runner.browser.d.ts +1 -1
  25. package/dist/emitter/node-runner.browser.d.ts.map +1 -1
  26. package/dist/emitter/node-runner.browser.js.map +1 -1
  27. package/dist/emitter/node-runner.d.ts +3 -3
  28. package/dist/emitter/node-runner.d.ts.map +1 -1
  29. package/dist/emitter/node-runner.js.map +1 -1
  30. package/dist/emitter/system-requirements.d.ts.map +1 -1
  31. package/dist/emitter/system-requirements.js.map +1 -1
  32. package/dist/emitter/types.d.ts +4 -4
  33. package/dist/emitter/types.d.ts.map +1 -1
  34. package/dist/emitter/types.js +15 -3
  35. package/dist/emitter/types.js.map +1 -1
  36. package/dist/emitter/utils.d.ts +2 -2
  37. package/dist/emitter/utils.d.ts.map +1 -1
  38. package/dist/emitter/utils.js +1 -1
  39. package/dist/emitter/utils.js.map +1 -1
  40. package/dist/emitter/yaml-utils.d.ts +19 -0
  41. package/dist/emitter/yaml-utils.d.ts.map +1 -0
  42. package/dist/emitter/yaml-utils.js +22 -0
  43. package/dist/emitter/yaml-utils.js.map +1 -0
  44. package/emitter/src/code-model.ts +4 -2
  45. package/emitter/src/emitter.ts +31 -7
  46. package/emitter/src/external-process.ts +3 -19
  47. package/emitter/src/http.ts +159 -5
  48. package/emitter/src/index.ts +2 -1
  49. package/emitter/src/lib.ts +11 -6
  50. package/emitter/src/node-runner.browser.ts +2 -1
  51. package/emitter/src/node-runner.ts +6 -3
  52. package/emitter/src/system-requirements.ts +2 -1
  53. package/emitter/src/types.ts +23 -8
  54. package/emitter/src/utils.ts +5 -4
  55. package/emitter/src/yaml-utils.ts +22 -0
  56. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  57. package/emitter/test/browser-pyodide.test.ts +25 -0
  58. package/emitter/test/streaming.test.ts +155 -0
  59. package/emitter/test/types.test.ts +72 -0
  60. package/emitter/test/{external-process.test.ts → yaml-utils.test.ts} +2 -2
  61. package/eng/scripts/ci/dev_requirements.txt +1 -1
  62. package/eng/scripts/ci/regenerate-common.ts +1 -1
  63. package/eng/scripts/ci/regenerate.ts +1 -2
  64. package/eng/scripts/ci/run-tests.ts +2 -1
  65. package/eng/scripts/ci/run_pylint.py +1 -0
  66. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  67. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  68. package/eng/scripts/setup/system-requirements.ts +2 -1
  69. package/generator/build/lib/pygen/__init__.py +24 -5
  70. package/generator/build/lib/pygen/codegen/models/__init__.py +3 -1
  71. package/generator/build/lib/pygen/codegen/models/code_model.py +29 -9
  72. package/generator/build/lib/pygen/codegen/models/enum_type.py +21 -9
  73. package/generator/build/lib/pygen/codegen/models/list_type.py +1 -0
  74. package/generator/build/lib/pygen/codegen/models/model_type.py +1 -3
  75. package/generator/build/lib/pygen/codegen/models/operation.py +13 -3
  76. package/generator/build/lib/pygen/codegen/models/parameter.py +3 -1
  77. package/generator/build/lib/pygen/codegen/models/response.py +152 -8
  78. package/generator/build/lib/pygen/codegen/models/utils.py +14 -0
  79. package/generator/build/lib/pygen/codegen/serializers/__init__.py +13 -6
  80. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +219 -69
  81. package/generator/build/lib/pygen/codegen/serializers/client_serializer.py +13 -6
  82. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +4 -0
  83. package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +27 -0
  84. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +4 -1
  85. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +44 -12
  86. package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
  87. package/generator/build/lib/pygen/preprocess/__init__.py +70 -17
  88. package/generator/build/lib/pygen/utils.py +15 -0
  89. package/generator/component-detection-pip-report.json +9 -9
  90. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  91. package/generator/pygen/__init__.py +24 -5
  92. package/generator/pygen/codegen/models/__init__.py +3 -1
  93. package/generator/pygen/codegen/models/code_model.py +29 -9
  94. package/generator/pygen/codegen/models/enum_type.py +21 -9
  95. package/generator/pygen/codegen/models/list_type.py +1 -0
  96. package/generator/pygen/codegen/models/model_type.py +1 -3
  97. package/generator/pygen/codegen/models/operation.py +13 -3
  98. package/generator/pygen/codegen/models/parameter.py +3 -1
  99. package/generator/pygen/codegen/models/response.py +152 -8
  100. package/generator/pygen/codegen/models/utils.py +14 -0
  101. package/generator/pygen/codegen/serializers/__init__.py +13 -6
  102. package/generator/pygen/codegen/serializers/builder_serializer.py +219 -69
  103. package/generator/pygen/codegen/serializers/client_serializer.py +13 -6
  104. package/generator/pygen/codegen/serializers/general_serializer.py +4 -0
  105. package/generator/pygen/codegen/serializers/import_serializer.py +27 -0
  106. package/generator/pygen/codegen/serializers/model_serializer.py +4 -1
  107. package/generator/pygen/codegen/serializers/types_serializer.py +44 -12
  108. package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
  109. package/generator/pygen/preprocess/__init__.py +70 -17
  110. package/generator/pygen/utils.py +15 -0
  111. package/generator/pygen.egg-info/PKG-INFO +1 -1
  112. package/generator/pygen.egg-info/SOURCES.txt +1 -0
  113. package/generator/pygen.egg-info/requires.txt +1 -1
  114. package/generator/setup.py +1 -1
  115. package/package.json +33 -33
@@ -32,15 +32,36 @@ from ..models import (
32
32
  ParameterListType,
33
33
  ByteArraySchema,
34
34
  )
35
- from ..models.utils import NamespaceType
35
+ from ..models.utils import NamespaceType, escape_sphinx_field_name
36
36
  from .parameter_serializer import ParameterSerializer, PopKwargType, check_body_optional
37
37
  from ..models.parameter_list import ParameterType
38
+ from ..models.response import (
39
+ DEFAULT_SSE_EVENT_TYPE,
40
+ StreamingEvent,
41
+ get_streaming_event_discriminator,
42
+ )
38
43
  from . import utils
39
44
  from ...utils import xml_serializable, json_serializable
40
45
 
41
46
  T = TypeVar("T")
42
47
  OrderedSet = dict[T, None]
43
48
 
49
+
50
+ def _sse_event_data_expression(payload_content_type: Optional[str]) -> str:
51
+ """Return the generated expression that decodes an SSE event's UTF-8 data field."""
52
+ media_type = payload_content_type.split(";", 1)[0].strip().lower() if payload_content_type else None
53
+ is_json = media_type is None or media_type == "application/json" or media_type.endswith("+json")
54
+ return "json.loads(_event.data)" if is_json else "_event.data"
55
+
56
+
57
+ def _sse_fallback_data_expression(events: list[StreamingEvent]) -> str:
58
+ """Decode fallback events only when every candidate payload is JSON."""
59
+ if not events:
60
+ return "_event.data"
61
+ expressions = {_sse_event_data_expression(event.payload_content_type) for event in events}
62
+ return "json.loads(_event.data)" if expressions == {"json.loads(_event.data)"} else "_event.data"
63
+
64
+
44
65
  BuilderType = TypeVar(
45
66
  "BuilderType",
46
67
  bound=Union[
@@ -311,16 +332,15 @@ class _BuilderBaseSerializer(Generic[BuilderType]):
311
332
  or param.method_location == ParameterMethodLocation.KWARG
312
333
  ):
313
334
  continue
335
+ escaped_name = escape_sphinx_field_name(param.client_name)
314
336
  description_list.extend(
315
- f":{param.description_keyword} {param.client_name}: {param.description}".replace("\n", "\n ").split(
316
- "\n"
317
- )
337
+ f":{param.description_keyword} {escaped_name}: {param.description}".replace("\n", "\n ").split("\n")
318
338
  )
319
339
  docstring_type = param.docstring_type(
320
340
  async_mode=self.async_mode,
321
341
  serialize_namespace=self.serialize_namespace,
322
342
  )
323
- description_list.append(f":{param.docstring_type_keyword} {param.client_name}: {docstring_type}")
343
+ description_list.append(f":{param.docstring_type_keyword} {escaped_name}: {docstring_type}")
324
344
  return description_list
325
345
 
326
346
  def param_description_and_response_docstring(self, builder: BuilderType) -> list[str]:
@@ -715,7 +735,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
715
735
  send_xml = builder.parameters.body_parameter.type.is_xml
716
736
  xml_serialization_ctxt = body_param.type.xml_serialization_ctxt if send_xml else None
717
737
  ser_ctxt_name = "serialization_ctxt"
718
- if xml_serialization_ctxt and self.code_model.options["models-mode"]:
738
+ if xml_serialization_ctxt and (
739
+ self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
740
+ ):
719
741
  retval.append(f'{ser_ctxt_name} = {{"xml": {{{xml_serialization_ctxt}}}}}')
720
742
  if self.code_model.options["models-mode"] == "msrest":
721
743
  is_xml_cmd = _xml_config(send_xml, builder.parameters.body_parameter.content_types)
@@ -725,7 +747,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
725
747
  f"_{body_kwarg_name} = self._serialize.body({body_param.client_name}, "
726
748
  f"'{serialization_type}'{is_xml_cmd}{serialization_ctxt_cmd})"
727
749
  )
728
- elif self.code_model.options["models-mode"] == "typeddict":
750
+ elif self.code_model.generate_typeddict_only:
729
751
  # TypedDict-only models are plain dicts — no serialization needed
730
752
  create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
731
753
  elif self.code_model.options["models-mode"] == "dpg":
@@ -844,7 +866,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
844
866
  overload.request_builder.parameters.body_parameter.client_name for overload in builder.overloads
845
867
  ]
846
868
  all_dpg_model_overloads = False
847
- if self.code_model.options["models-mode"] in ("dpg", "typeddict") and builder.overloads:
869
+ if (
870
+ self.code_model.options["models-mode"] == "dpg" or self.code_model.generate_typeddict_only
871
+ ) and builder.overloads:
848
872
  all_dpg_model_overloads = all(
849
873
  _is_dpg_or_typeddict_body(o.parameters.body_parameter) for o in builder.overloads
850
874
  )
@@ -1018,7 +1042,19 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1018
1042
  return retval
1019
1043
 
1020
1044
  def call_request_builder(self, builder: OperationType, is_paging: bool = False) -> list[str]:
1021
- return self._call_request_builder_helper(builder, builder.request_builder, is_paging=is_paging)
1045
+ retval = self._call_request_builder_helper(builder, builder.request_builder, is_paging=is_paging)
1046
+ if builder.has_structured_stream_response and any(
1047
+ response.streaming_kind == "sse" for response in builder.responses
1048
+ ):
1049
+ retval.insert(0, '_last_event_id = kwargs.pop("last_event_id", None)')
1050
+ retval.extend(
1051
+ [
1052
+ "",
1053
+ "if _last_event_id is not None:",
1054
+ ' _request.headers["Last-Event-ID"] = _last_event_id',
1055
+ ]
1056
+ )
1057
+ return retval
1022
1058
 
1023
1059
  def response_headers_and_deserialization(
1024
1060
  self,
@@ -1131,8 +1167,8 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1131
1167
  retval.extend([f" {l}" for l in response_read])
1132
1168
  retval.append(" map_error(status_code=response.status_code, response=response, error_map=error_map)")
1133
1169
  error_model = ""
1134
- if ( # pylint: disable=too-many-nested-blocks
1135
- builder.non_default_errors and self.code_model.options["models-mode"]
1170
+ if builder.non_default_errors and ( # pylint: disable=too-many-nested-blocks
1171
+ self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
1136
1172
  ):
1137
1173
  error_model = ", model=error"
1138
1174
  condition = "if"
@@ -1164,25 +1200,12 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1164
1200
  " )",
1165
1201
  ]
1166
1202
  )
1167
- # add build-in error type
1168
- # TODO: we should decide whether need to this wrapper for customized error type
1169
- status_code_error_map = {
1170
- 401: "ClientAuthenticationError",
1171
- 404: "ResourceNotFoundError",
1172
- 409: "ResourceExistsError",
1173
- 304: "ResourceNotModifiedError",
1174
- }
1175
- if status_code in status_code_error_map:
1176
- retval.append(
1177
- " raise {}(response=response{}{})".format(
1178
- status_code_error_map[cast(int, status_code)],
1179
- error_model,
1180
- (", error_format=ARMErrorFormat" if self.code_model.options["azure-arm"] else ""),
1181
- )
1182
- )
1183
- condition = "if"
1184
- else:
1185
- condition = "elif"
1203
+ # The dedicated azure-core error type for a standard status
1204
+ # code is raised by ``map_error`` via the error map, so here
1205
+ # we only deserialize the customized error body (used by the
1206
+ # generic ``HttpResponseError`` fallback for non-standard
1207
+ # status codes within the response).
1208
+ condition = "elif"
1186
1209
  # ranged status code only exist in typespec and will not have multiple status codes
1187
1210
  else:
1188
1211
  retval.append(
@@ -1214,7 +1237,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1214
1237
  condition = "elif"
1215
1238
  # default error handling
1216
1239
  default_error_deserialization = builder.default_error_deserialization(self.serialize_namespace)
1217
- if default_error_deserialization and self.code_model.options["models-mode"]:
1240
+ if default_error_deserialization and (
1241
+ self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
1242
+ ):
1218
1243
  error_model = ", model=error"
1219
1244
  indent = " " if builder.non_default_errors else " "
1220
1245
  if builder.non_default_errors:
@@ -1250,15 +1275,140 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1250
1275
  )
1251
1276
  return retval
1252
1277
 
1253
- def handle_response(self, builder: OperationType) -> list[str]:
1254
- retval: list[str] = ["response = pipeline_response.http_response"]
1255
- retval.append("")
1256
- retval.extend(self.handle_error_response(builder))
1278
+ # pylint: disable=too-many-statements
1279
+ def handle_structured_stream_response(self, builder: OperationType) -> list[str]:
1280
+ """Emit the body for an operation returning a structured (JSONL / SSE) stream.
1281
+
1282
+ Produces a per-event deserialization callback and returns a ``Stream`` /
1283
+ ``AsyncStream`` wrapping the streamed HTTP response.
1284
+ """
1285
+ response = next(r for r in builder.responses if r.is_structured_stream)
1286
+ item_annotation = response.stream_item_annotation(
1287
+ is_operation_file=True, serialize_namespace=self.serialize_namespace
1288
+ )
1289
+ stream_class = response.stream_class_name(self.async_mode) # type: ignore[attr-defined]
1290
+ terminal_event = getattr(response, "terminal_event", None)
1291
+ terminal_event_names = getattr(response, "terminal_event_names", [])
1292
+ streaming_events = getattr(response, "streaming_events", [])
1293
+ unnamed_events = [event for event in streaming_events if event.event_type is None]
1294
+ unnamed_discriminator = get_streaming_event_discriminator(unnamed_events)
1295
+ retval: list[str] = []
1296
+ retval.append("def _callback(_http_response, _event):")
1297
+ if response.streaming_kind == "sse": # type: ignore[attr-defined]
1298
+ named_events = [event for event in streaming_events if event.event_type is not None]
1299
+
1300
+ def emit_deserialized_event(event: StreamingEvent, indent: str) -> None:
1301
+ event_item_type = event.payload_type
1302
+ event_annotation = event_item_type.type_annotation(
1303
+ is_operation_file=True,
1304
+ serialize_namespace=self.serialize_namespace,
1305
+ )
1306
+ if self.code_model.options["models-mode"] == "msrest":
1307
+ serialization_type = event_item_type.serialization_type(
1308
+ serialize_namespace=self.serialize_namespace
1309
+ )
1310
+ retval.append(f"{indent}deserialized = self._deserialize(")
1311
+ retval.append(f"{indent} '{serialization_type}',")
1312
+ retval.append(f"{indent} _event_json")
1313
+ retval.append(f"{indent})")
1314
+ else:
1315
+ retval.append(f"{indent}deserialized = _deserialize({event_annotation}, _event_json)")
1316
+
1317
+ def emit_typed_event(event: StreamingEvent, indent: str) -> None:
1318
+ event_json = _sse_event_data_expression(event.payload_content_type)
1319
+ retval.append(f"{indent}_event_json = {event_json}")
1320
+ emit_deserialized_event(event, indent)
1321
+
1322
+ def emit_multiple_unnamed_events(events: list[StreamingEvent], indent: str) -> None:
1323
+ event_json = _sse_fallback_data_expression(events)
1324
+ retval.append(f"{indent}_event_json = {event_json}")
1325
+ if unnamed_discriminator is None or event_json != "json.loads(_event.data)":
1326
+ retval.append(f"{indent}deserialized = _event_json")
1327
+ return
1328
+ discriminator_name, variants = unnamed_discriminator
1329
+ for index, (discriminator_value, event) in enumerate(variants):
1330
+ keyword = "if" if index == 0 else "elif"
1331
+ retval.append(
1332
+ f"{indent}{keyword} isinstance(_event_json, dict) "
1333
+ f"and _event_json.get({discriminator_name!r}) == {discriminator_value!r}:"
1334
+ )
1335
+ emit_deserialized_event(event, indent + " ")
1336
+ retval.append(f"{indent}else:")
1337
+ retval.append(f"{indent} deserialized = _event_json")
1338
+
1339
+ def emit_message_branch(keyword: str) -> None:
1340
+ retval.append(f" {keyword} _event.event == {DEFAULT_SSE_EVENT_TYPE!r}:")
1341
+ if len(unnamed_events) == 1:
1342
+ emit_typed_event(unnamed_events[0], " ")
1343
+ else:
1344
+ emit_multiple_unnamed_events(unnamed_events, " ")
1345
+
1346
+ if named_events:
1347
+ for index, event in enumerate(named_events):
1348
+ event_type = event.event_type
1349
+ keyword = "if" if index == 0 else "elif"
1350
+ retval.append(f" {keyword} _event.event == {event_type!r}:")
1351
+ emit_typed_event(event, " ")
1352
+ if unnamed_events:
1353
+ emit_message_branch("elif")
1354
+ elif unnamed_events:
1355
+ emit_message_branch("if")
1356
+
1357
+ if named_events or unnamed_events:
1358
+ retval.append(" else:")
1359
+ retval.append(' raise ValueError(f"Unknown SSE event type: {_event.event!r}")')
1360
+ else:
1361
+ retval.append(' raise ValueError(f"Unknown SSE event type: {_event.event!r}")')
1362
+ else:
1363
+ retval.append(" _event_json = _event.json()")
1364
+ if self.code_model.options["models-mode"] == "msrest":
1365
+ serialization_type = response.stream_item_type.serialization_type(
1366
+ serialize_namespace=self.serialize_namespace
1367
+ )
1368
+ retval.append(" deserialized = self._deserialize(")
1369
+ retval.append(f" '{serialization_type}',")
1370
+ retval.append(" _event_json")
1371
+ retval.append(" )")
1372
+ else:
1373
+ retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)")
1374
+ retval.append(" return deserialized")
1257
1375
  retval.append("")
1258
- if builder.has_optional_return_type:
1259
- retval.append("deserialized = None")
1260
- if builder.any_response_has_headers:
1261
- retval.append("response_headers = {}")
1376
+ stream_kwargs = ["response=response", "deserialization_callback=_callback"]
1377
+ if terminal_event is not None:
1378
+ stream_kwargs.append(f"terminal_event={terminal_event!r}")
1379
+ if terminal_event_names:
1380
+ stream_kwargs.append(f"terminal_event_names={terminal_event_names!r}")
1381
+ unnamed_terminal_values = (
1382
+ [discriminator_value for discriminator_value, event in unnamed_discriminator[1] if event.is_terminal]
1383
+ if unnamed_discriminator is not None
1384
+ else []
1385
+ )
1386
+ if unnamed_terminal_values:
1387
+ discriminator_name = unnamed_discriminator[0] # type: ignore[index]
1388
+ retval.append("")
1389
+ retval.append("def _is_terminal_event(_event):")
1390
+ retval.append(f" if _event.event != {DEFAULT_SSE_EVENT_TYPE!r}:")
1391
+ retval.append(" return False")
1392
+ retval.append(" try:")
1393
+ retval.append(" _event_json = json.loads(_event.data)")
1394
+ retval.append(" except (TypeError, ValueError):")
1395
+ retval.append(" return False")
1396
+ retval.append(
1397
+ f" return isinstance(_event_json, dict) "
1398
+ f"and _event_json.get({discriminator_name!r}) in {unnamed_terminal_values!r}"
1399
+ )
1400
+ stream_kwargs.append("terminal_event_predicate=_is_terminal_event")
1401
+ retval.append(
1402
+ f"deserialized: {stream_class}[{item_annotation}] = "
1403
+ f"{stream_class}({', '.join(stream_kwargs)}) # type: ignore"
1404
+ )
1405
+ retval.append("if cls:")
1406
+ retval.append(" return cls(pipeline_response, deserialized, {}) # type: ignore")
1407
+ retval.append("return deserialized")
1408
+ return retval
1409
+
1410
+ def _handle_response_body(self, builder: OperationType) -> list[str]:
1411
+ retval: list[str] = []
1262
1412
  if builder.has_response_body or builder.any_response_has_headers: # pylint: disable=too-many-nested-blocks
1263
1413
  if len(builder.responses) > 1:
1264
1414
  status_codes, res_headers, res_deserialization = [], [], []
@@ -1293,7 +1443,26 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1293
1443
  else:
1294
1444
  retval.extend(self.response_headers_and_deserialization(builder, builder.responses[0]))
1295
1445
  retval.append("")
1296
- if builder.has_optional_return_type or self.code_model.options["models-mode"]:
1446
+ return retval
1447
+
1448
+ def handle_response(self, builder: OperationType) -> list[str]:
1449
+ retval: list[str] = ["response = pipeline_response.http_response"]
1450
+ retval.append("")
1451
+ retval.extend(self.handle_error_response(builder))
1452
+ retval.append("")
1453
+ if builder.has_structured_stream_response:
1454
+ retval.extend(self.handle_structured_stream_response(builder))
1455
+ return retval
1456
+ if builder.has_optional_return_type:
1457
+ retval.append("deserialized = None")
1458
+ if builder.any_response_has_headers:
1459
+ retval.append("response_headers = {}")
1460
+ retval.extend(self._handle_response_body(builder))
1461
+ if (
1462
+ builder.has_optional_return_type
1463
+ or self.code_model.options["models-mode"]
1464
+ or self.code_model.generate_typeddict_only
1465
+ ):
1297
1466
  deserialized = "deserialized"
1298
1467
  else:
1299
1468
  deserialized = f"cast({builder.response_type_annotation(async_mode=self.async_mode)}, deserialized)"
@@ -1314,36 +1483,17 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1314
1483
  retval.append("return 200 <= response.status_code <= 299")
1315
1484
  return retval
1316
1485
 
1317
- def _need_specific_error_map(self, code: int, builder: OperationType) -> bool:
1318
- for non_default_error in builder.non_default_errors:
1319
- # single status code
1320
- if code in non_default_error.status_codes:
1321
- return False
1322
- # ranged status code
1323
- if (
1324
- isinstance(non_default_error.status_codes[0], list)
1325
- and non_default_error.status_codes[0][0] <= code <= non_default_error.status_codes[0][1]
1326
- ):
1327
- return False
1328
- return True
1329
-
1330
1486
  def error_map(self, builder: OperationType) -> list[str]:
1331
1487
  retval = ["error_map: MutableMapping = {"]
1332
- if builder.non_default_errors and self.code_model.options["models-mode"]:
1333
- # TODO: we should decide whether to add the build-in error map when there is a customized default error type
1334
- if self._need_specific_error_map(401, builder):
1335
- retval.append(" 401: ClientAuthenticationError,")
1336
- if self._need_specific_error_map(404, builder):
1337
- retval.append(" 404: ResourceNotFoundError,")
1338
- if self._need_specific_error_map(409, builder):
1339
- retval.append(" 409: ResourceExistsError,")
1340
- if self._need_specific_error_map(304, builder):
1341
- retval.append(" 304: ResourceNotModifiedError,")
1342
- else:
1343
- retval.append(
1344
- " 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, "
1345
- "304: ResourceNotModifiedError"
1346
- )
1488
+ # Always map the standard status codes to their dedicated azure-core error
1489
+ # type so ``map_error`` raises the correct semantic error, even when a
1490
+ # customized error model (single, ranged, or default) covers them. The
1491
+ # customized error body is still deserialized in ``handle_error_response``
1492
+ # for the generic ``HttpResponseError`` fallback.
1493
+ retval.append(
1494
+ " 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, "
1495
+ "304: ResourceNotModifiedError"
1496
+ )
1347
1497
  retval.append("}")
1348
1498
  if builder.has_etag:
1349
1499
  retval.extend(
@@ -1693,7 +1843,7 @@ class _LROOperationSerializer(_OperationSerializer[LROOperationType]):
1693
1843
  if builder.lro_response.headers:
1694
1844
  retval.append(" response_headers = {}")
1695
1845
  if (
1696
- not self.code_model.options["models-mode"]
1846
+ (not self.code_model.options["models-mode"] and not self.code_model.generate_typeddict_only)
1697
1847
  or self.code_model.options["models-mode"] == "dpg"
1698
1848
  or builder.lro_response.headers
1699
1849
  ):
@@ -8,6 +8,7 @@ from typing import cast
8
8
  from . import utils
9
9
  from ..models import Client, ParameterMethodLocation, Parameter, ParameterLocation
10
10
  from .parameter_serializer import ParameterSerializer, PopKwargType
11
+ from ..models.utils import escape_sphinx_field_name
11
12
  from ...utils import build_policies
12
13
 
13
14
 
@@ -61,13 +62,16 @@ class ClientSerializer:
61
62
  retval: list[str] = []
62
63
  operations_folder = ".aio.operations." if async_mode else ".operations."
63
64
  for og in [og for og in self.client.operation_groups if not og.is_mixin]:
64
- retval.append(f":ivar {og.property_name}: {og.class_name} operations")
65
+ retval.append(f":ivar {escape_sphinx_field_name(og.property_name)}: {og.class_name} operations")
65
66
  property_type = f"{self.client.code_model.namespace}{operations_folder}{og.class_name}"
66
- retval.append(f":vartype {og.property_name}: {property_type}")
67
+ retval.append(f":vartype {escape_sphinx_field_name(og.property_name)}: {property_type}")
67
68
  for param in self.client.parameters.method:
68
- retval.append(f":{param.description_keyword} {param.client_name}: {param.description}")
69
69
  retval.append(
70
- f":{param.docstring_type_keyword} {param.client_name}: {param.docstring_type(async_mode=async_mode)}"
70
+ f":{param.description_keyword} {escape_sphinx_field_name(param.client_name)}: {param.description}"
71
+ )
72
+ retval.append(
73
+ f":{param.docstring_type_keyword} {escape_sphinx_field_name(param.client_name)}: "
74
+ f"{param.docstring_type(async_mode=async_mode)}"
71
75
  )
72
76
  if self.client.has_public_lro_operations:
73
77
  retval.append(
@@ -315,7 +319,10 @@ class ConfigSerializer:
315
319
  def property_descriptions(self, async_mode: bool) -> list[str]:
316
320
  retval: list[str] = []
317
321
  for p in self.client.config.parameters.method:
318
- retval.append(f":{p.description_keyword} {p.client_name}: {p.description}")
319
- retval.append(f":{p.docstring_type_keyword} {p.client_name}: {p.docstring_type(async_mode=async_mode)}")
322
+ retval.append(f":{p.description_keyword} {escape_sphinx_field_name(p.client_name)}: {p.description}")
323
+ retval.append(
324
+ f":{p.docstring_type_keyword} {escape_sphinx_field_name(p.client_name)}: "
325
+ f"{p.docstring_type(async_mode=async_mode)}"
326
+ )
320
327
  retval.append('"""')
321
328
  return retval
@@ -318,6 +318,10 @@ class GeneralSerializer(BaseSerializer):
318
318
  template = self.env.get_template("model_base.py.jinja2")
319
319
  return template.render(code_model=self.code_model, file_import=FileImport(self.code_model))
320
320
 
321
+ def serialize_streaming_base_file(self) -> str:
322
+ template = self.env.get_template("streaming_base.py.jinja2")
323
+ return template.render(code_model=self.code_model, file_import=FileImport(self.code_model))
324
+
321
325
  def serialize_validation_file(self) -> str:
322
326
  template = self.env.get_template("validation.py.jinja2")
323
327
  return template.render(
@@ -84,6 +84,32 @@ class FileImportSerializer:
84
84
  if any(self.file_import.get_imports_from_section(TypingSection.TYPING)):
85
85
  self.file_import.add_submodule_import("typing", "TYPE_CHECKING", ImportType.STDLIB)
86
86
 
87
+ def _dedupe_typing_imports(self):
88
+ """Drop TYPE_CHECKING imports whose bound name is already imported at runtime.
89
+
90
+ A name imported in the regular (runtime) section is also available during type checking, so a
91
+ duplicate import under ``if TYPE_CHECKING:`` is redundant and triggers mypy's ``no-redef``
92
+ error. This can happen when the same symbol is imported from two different modules — e.g. a
93
+ cross-namespace enum imported at runtime from its ``_enums`` submodule and again for its
94
+ annotation from the public ``models`` package. The runtime import is sufficient.
95
+ """
96
+ regular_bound_names = {
97
+ (i.alias or i.submodule_name)
98
+ for i in self.file_import.get_imports_from_section(TypingSection.REGULAR)
99
+ if i.submodule_name
100
+ }
101
+ if not regular_bound_names:
102
+ return
103
+ self.file_import.imports = [
104
+ i
105
+ for i in self.file_import.imports
106
+ if not (
107
+ i.typing_section == TypingSection.TYPING
108
+ and i.submodule_name
109
+ and (i.alias or i.submodule_name) in regular_bound_names
110
+ )
111
+ ]
112
+
87
113
  def _add_sys_import_if_needed(self):
88
114
  all_imports = list(self.file_import.get_imports_from_section(TypingSection.REGULAR)) + list(
89
115
  self.file_import.get_imports_from_section(TypingSection.TYPING)
@@ -106,6 +132,7 @@ class FileImportSerializer:
106
132
  return "\n".join(declarations)
107
133
 
108
134
  def __str__(self) -> str:
135
+ self._dedupe_typing_imports()
109
136
  self._add_type_checking_import()
110
137
  self._add_sys_import_if_needed()
111
138
  regular_imports = ""
@@ -23,7 +23,7 @@ from ..models.primitive_types import (
23
23
  )
24
24
  from .import_serializer import FileImportSerializer
25
25
  from .base_serializer import BaseSerializer
26
- from ..models.utils import NamespaceType, add_to_pylint_disable
26
+ from ..models.utils import NamespaceType, add_to_pylint_disable, escape_sphinx_field_name
27
27
 
28
28
 
29
29
  def _get_xml_deserializer_name(prop: Property) -> Optional[str]: # pylint: disable=too-many-return-statements
@@ -80,6 +80,9 @@ def _documentation_string(
80
80
  doc_name = (
81
81
  prop.wire_name if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE else prop.client_name
82
82
  )
83
+ # Escape names that Sphinx would otherwise misinterpret in the info field target
84
+ # (e.g. a wire name with a leading "@" such as "@search.facets").
85
+ doc_name = escape_sphinx_field_name(doc_name)
83
86
  sphinx_prefix = f":{description_keyword} {doc_name}:"
84
87
  description = prop.description(is_operation_file=False).replace("\\", "\\\\")
85
88
  retval.append(f"{sphinx_prefix} {description}" if description else sphinx_prefix)
@@ -103,21 +103,16 @@ class TypesSerializer(BaseSerializer):
103
103
 
104
104
  TypedDicts (and discriminated-base union aliases) in ``types.py`` describe request-body
105
105
  (*input*) shapes. Output-only models already render as classes in ``models/`` and are
106
- referenced via ``_models.*``, so a response-only model that nothing input references would be
107
- dead code. A model seeds the input surface when it is:
108
-
109
- * a typeddict copy (``base == "typeddict"``) — these only exist as input body overloads
110
- (their ``usage`` may carry ``Spread``/``Json`` rather than ``Input``), or
111
- * ``is_typed_dict_only`` — includes every model in full ``typeddict`` mode (responses too),
112
- and input-only anonymous bodies, or
113
- * used as input (``is_usage_input``) — e.g. a model shared between request and response.
106
+ referenced via ``_models.*``, so a model that no operation references through ``types.py``
107
+ would be dead code. TypedDict copies and models in full TypedDict mode volunteer through
108
+ ``is_used_in_operations_via_types``.
114
109
 
115
110
  The full set rendered in types.py is the transitive closure of these seeds over base
116
111
  classes, discriminated subtypes and property types (see :attr:`_types_file_model_names`), so
117
112
  an output-only model that *is* referenced by an input model (e.g. ARM ``SystemData`` on
118
113
  ``Resource``) is still rendered.
119
114
  """
120
- return m.base == "typeddict" or m.is_typed_dict_only or m.is_usage_input
115
+ return m.is_used_in_operations_via_types
121
116
 
122
117
  @staticmethod
123
118
  def _iter_referenced_models(base_type: Any):
@@ -248,6 +243,39 @@ class TypesSerializer(BaseSerializer):
248
243
  """Whether any property wire_name is a Python keyword or requires functional TypedDict form."""
249
244
  return any(keyword.iskeyword(p.wire_name) or not p.wire_name.isidentifier() for p in model.properties)
250
245
 
246
+ @staticmethod
247
+ def needs_flat_typeddict(model: ModelType) -> bool:
248
+ """Whether a TypedDict must be emitted in flat (non-inheriting) form.
249
+
250
+ PEP 589 forbids changing an inherited TypedDict field's requiredness in a subclass. When a
251
+ child redeclares an inherited field with a different requiredness (e.g. the parent renders it
252
+ as optional via ``total=False`` while the child needs ``Required[...]``), subclassing would
253
+ emit an illegal ``Overwriting TypedDict field ... while extending`` construct. Such models are
254
+ instead rendered as a flat TypedDict that lists every field (inherited + own) directly.
255
+
256
+ Models with keyword wire_names already flatten all fields via the functional form, so they
257
+ never hit this path.
258
+ """
259
+ if TypesSerializer.has_keyword_wire_names(model):
260
+ return False
261
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
262
+ if not non_discriminated_parents:
263
+ return False
264
+ for parent in non_discriminated_parents:
265
+ for parent_prop in parent.properties:
266
+ child_prop = next(
267
+ (p for p in model.properties if p.client_name == parent_prop.client_name),
268
+ None,
269
+ )
270
+ if child_prop is None or child_prop is parent_prop:
271
+ # Not overridden by the child (same object is reused when inherited unchanged).
272
+ continue
273
+ parent_required = not (parent_prop.optional or parent_prop.client_default_value is not None)
274
+ child_required = not (child_prop.optional or child_prop.client_default_value is not None)
275
+ if parent_required != child_required:
276
+ return True
277
+ return False
278
+
251
279
  def get_shadowed_builtins(self, model: ModelType) -> frozenset[str]:
252
280
  """Return the set of builtin type names shadowed by property wire_names in this model.
253
281
 
@@ -308,7 +336,11 @@ class TypesSerializer(BaseSerializer):
308
336
  if self.get_shadowed_builtins(model):
309
337
  needs_builtins = True
310
338
  for parent in model.parents:
311
- if parent.client_namespace != model.client_namespace and not parent.discriminated_subtypes:
339
+ if (
340
+ parent.client_namespace != model.client_namespace
341
+ and not parent.discriminated_subtypes
342
+ and not self.needs_flat_typeddict(model)
343
+ ):
312
344
  # Import parent class from sibling namespace's types module
313
345
  file_import.add_submodule_import(
314
346
  self.code_model.get_relative_import_path(
@@ -334,7 +366,7 @@ class TypesSerializer(BaseSerializer):
334
366
  if self.has_keyword_wire_names(model):
335
367
  return "" # functional form is rendered separately
336
368
  non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
337
- if non_discriminated_parents:
369
+ if non_discriminated_parents and not self.needs_flat_typeddict(model):
338
370
  basename = ", ".join([m.name for m in non_discriminated_parents])
339
371
  return f"class {model.name}({basename}):{model.pylint_disable()}"
340
372
  return f"class {model.name}(TypedDict, total=False):{model.pylint_disable()}"
@@ -367,7 +399,7 @@ class TypesSerializer(BaseSerializer):
367
399
  if TypesSerializer.has_keyword_wire_names(model):
368
400
  return [] # functional form handles all properties
369
401
  non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
370
- if non_discriminated_parents:
402
+ if non_discriminated_parents and not TypesSerializer.needs_flat_typeddict(model):
371
403
  parent_properties = [p for bm in non_discriminated_parents for p in bm.properties]
372
404
  return [
373
405
  p