@typespec/http-client-python 0.37.3 → 0.38.1

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 (24) hide show
  1. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  2. package/eng/scripts/ci/regenerate-common.ts +3 -0
  3. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  4. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  5. package/generator/build/lib/pygen/codegen/models/response.py +5 -0
  6. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +76 -23
  7. package/generator/build/lib/pygen/codegen/serializers/unions_serializer.py +14 -3
  8. package/generator/build/lib/pygen/codegen/templates/enum.py.jinja2 +2 -2
  9. package/generator/build/lib/pygen/codegen/templates/operation_tools.jinja2 +5 -0
  10. package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +235 -54
  11. package/generator/build/lib/pygen/codegen/templates/types.py.jinja2 +1 -1
  12. package/generator/build/lib/pygen/codegen/templates/unions.py.jinja2 +1 -1
  13. package/generator/build/lib/pygen/preprocess/__init__.py +4 -3
  14. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  15. package/generator/pygen/codegen/models/response.py +5 -0
  16. package/generator/pygen/codegen/serializers/builder_serializer.py +76 -23
  17. package/generator/pygen/codegen/serializers/unions_serializer.py +14 -3
  18. package/generator/pygen/codegen/templates/enum.py.jinja2 +2 -2
  19. package/generator/pygen/codegen/templates/operation_tools.jinja2 +5 -0
  20. package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +235 -54
  21. package/generator/pygen/codegen/templates/types.py.jinja2 +1 -1
  22. package/generator/pygen/codegen/templates/unions.py.jinja2 +1 -1
  23. package/generator/pygen/preprocess/__init__.py +4 -3
  24. package/package.json +32 -32
@@ -120,6 +120,9 @@ export const AZURE_EMITTER_OPTIONS: Record<
120
120
  "azure/client-generator-core/override": {
121
121
  namespace: "specs.azure.clientgenerator.core.override",
122
122
  },
123
+ "azure/client-generator-core/response-replacement": {
124
+ namespace: "specs.azure.clientgenerator.core.responsereplacement",
125
+ },
123
126
  "azure/client-generator-core/hierarchy-building": {
124
127
  namespace: "specs.azure.clientgenerator.core.hierarchybuilding",
125
128
  },
@@ -267,6 +267,11 @@ 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
+ read_response_helper = (
271
+ "_read_sse_response_async" if kwargs.get("async_mode", False) else "_read_sse_response"
272
+ )
273
+ file_import.add_submodule_import(relative_path, read_response_helper, ImportType.LOCAL)
274
+ file_import.add_submodule_import(relative_path, "_update_sse_request_headers", ImportType.LOCAL)
270
275
  for event in self.streaming_events:
271
276
  file_import.merge(event.payload_type.imports(**kwargs))
272
277
  return file_import
@@ -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,9 +18,19 @@ 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:
24
34
  file_import.add_submodule_import(
25
35
  "typing",
26
36
  "TypeAlias",
@@ -31,7 +41,7 @@ class UnionsSerializer(BaseSerializer):
31
41
  "Union",
32
42
  ImportType.STDLIB,
33
43
  )
34
- for nu in self.code_model.named_unions:
44
+ for nu in self.named_unions:
35
45
  file_import.merge(
36
46
  nu.imports(
37
47
  serialize_namespace=self.serialize_namespace,
@@ -44,6 +54,7 @@ class UnionsSerializer(BaseSerializer):
44
54
  template = self.env.get_template("unions.py.jinja2")
45
55
  return template.render(
46
56
  code_model=self.code_model,
57
+ named_unions=self.named_unions,
47
58
  imports=FileImportSerializer(self.imports()),
48
59
  serializer=self,
49
60
  )
@@ -3,13 +3,13 @@ class {{ enum.name }}({{enum.pylint_disable()}}
3
3
  {{ enum.value_type.type_annotation(is_operation_file=False) }}, Enum, metaclass=CaseInsensitiveEnumMeta
4
4
  ):
5
5
  {% if enum.yaml_data.get("description") %}
6
- """{{ op_tools.wrap_string(enum.yaml_data["description"], "\n ") }}
6
+ """{{ op_tools.wrap_docstring(enum.yaml_data["description"], "\n ") }}
7
7
  """
8
8
  {% endif %}
9
9
 
10
10
  {% for value in enum.values %}
11
11
  {{ value.name }} = {{ enum.value_type.get_declaration(value.value) }}
12
12
  {% if value.description(is_operation_file=False) %}
13
- """{{ op_tools.wrap_string(value.description(is_operation_file=False), "\n ") }}"""
13
+ """{{ op_tools.wrap_docstring(value.description(is_operation_file=False), "\n ") }}"""
14
14
  {% endif %}
15
15
  {% endfor %}
@@ -31,6 +31,11 @@
31
31
  {%- endif -%}
32
32
  {{ normalized_string | replace("\\", "\\\\") | wordwrap(width=width, break_long_words=False, break_on_hyphens=False, wrapstring=wrapstring)}}{% endmacro %}
33
33
 
34
+ {# Escape quotes after wrapping so wrapping cannot split an escape sequence. #}
35
+ {% macro wrap_docstring(string, wrapstring, width=95) -%}
36
+ {{ wrap_string(string, wrapstring, width) | replace('"', '\\"') }}
37
+ {%- endmacro %}
38
+
34
39
  {% macro description(builder, serializer) %}
35
40
  {% set example_template = serializer.example_template(builder) %}
36
41
  {% set param_description_and_response_docstring = serializer.param_description_and_response_docstring(builder) %}