@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.
- package/README.md +6 -0
- package/dist/emitter/code-model.d.ts +1 -1
- package/dist/emitter/code-model.d.ts.map +1 -1
- package/dist/emitter/code-model.js.map +1 -1
- package/dist/emitter/emitter.d.ts +2 -2
- package/dist/emitter/emitter.d.ts.map +1 -1
- package/dist/emitter/emitter.js +24 -3
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/external-process.d.ts +1 -15
- package/dist/emitter/external-process.d.ts.map +1 -1
- package/dist/emitter/external-process.js +1 -17
- package/dist/emitter/external-process.js.map +1 -1
- package/dist/emitter/http.d.ts +45 -2
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +97 -1
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/index.d.ts +2 -1
- package/dist/emitter/index.d.ts.map +1 -1
- package/dist/emitter/index.js.map +1 -1
- package/dist/emitter/lib.d.ts +3 -2
- package/dist/emitter/lib.d.ts.map +1 -1
- package/dist/emitter/lib.js +6 -1
- package/dist/emitter/lib.js.map +1 -1
- package/dist/emitter/node-runner.browser.d.ts +1 -1
- package/dist/emitter/node-runner.browser.d.ts.map +1 -1
- package/dist/emitter/node-runner.browser.js.map +1 -1
- package/dist/emitter/node-runner.d.ts +3 -3
- package/dist/emitter/node-runner.d.ts.map +1 -1
- package/dist/emitter/node-runner.js.map +1 -1
- package/dist/emitter/system-requirements.d.ts.map +1 -1
- package/dist/emitter/system-requirements.js.map +1 -1
- package/dist/emitter/types.d.ts +4 -4
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +15 -3
- package/dist/emitter/types.js.map +1 -1
- package/dist/emitter/utils.d.ts +2 -2
- package/dist/emitter/utils.d.ts.map +1 -1
- package/dist/emitter/utils.js +1 -1
- package/dist/emitter/utils.js.map +1 -1
- package/dist/emitter/yaml-utils.d.ts +19 -0
- package/dist/emitter/yaml-utils.d.ts.map +1 -0
- package/dist/emitter/yaml-utils.js +22 -0
- package/dist/emitter/yaml-utils.js.map +1 -0
- package/emitter/src/code-model.ts +4 -2
- package/emitter/src/emitter.ts +31 -7
- package/emitter/src/external-process.ts +3 -19
- package/emitter/src/http.ts +159 -5
- package/emitter/src/index.ts +2 -1
- package/emitter/src/lib.ts +11 -6
- package/emitter/src/node-runner.browser.ts +2 -1
- package/emitter/src/node-runner.ts +6 -3
- package/emitter/src/system-requirements.ts +2 -1
- package/emitter/src/types.ts +23 -8
- package/emitter/src/utils.ts +5 -4
- package/emitter/src/yaml-utils.ts +22 -0
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/emitter/test/browser-pyodide.test.ts +25 -0
- package/emitter/test/streaming.test.ts +155 -0
- package/emitter/test/types.test.ts +72 -0
- package/emitter/test/{external-process.test.ts → yaml-utils.test.ts} +2 -2
- package/eng/scripts/ci/dev_requirements.txt +1 -1
- package/eng/scripts/ci/regenerate-common.ts +1 -1
- package/eng/scripts/ci/regenerate.ts +1 -2
- package/eng/scripts/ci/run-tests.ts +2 -1
- package/eng/scripts/ci/run_pylint.py +1 -0
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/eng/scripts/setup/system-requirements.ts +2 -1
- package/generator/build/lib/pygen/__init__.py +24 -5
- package/generator/build/lib/pygen/codegen/models/__init__.py +3 -1
- package/generator/build/lib/pygen/codegen/models/code_model.py +29 -9
- package/generator/build/lib/pygen/codegen/models/enum_type.py +21 -9
- package/generator/build/lib/pygen/codegen/models/list_type.py +1 -0
- package/generator/build/lib/pygen/codegen/models/model_type.py +1 -3
- package/generator/build/lib/pygen/codegen/models/operation.py +13 -3
- package/generator/build/lib/pygen/codegen/models/parameter.py +3 -1
- package/generator/build/lib/pygen/codegen/models/response.py +152 -8
- package/generator/build/lib/pygen/codegen/models/utils.py +14 -0
- package/generator/build/lib/pygen/codegen/serializers/__init__.py +13 -6
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +219 -69
- package/generator/build/lib/pygen/codegen/serializers/client_serializer.py +13 -6
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +4 -0
- package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +27 -0
- package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +4 -1
- package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +44 -12
- package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
- package/generator/build/lib/pygen/preprocess/__init__.py +70 -17
- package/generator/build/lib/pygen/utils.py +15 -0
- package/generator/component-detection-pip-report.json +9 -9
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/__init__.py +24 -5
- package/generator/pygen/codegen/models/__init__.py +3 -1
- package/generator/pygen/codegen/models/code_model.py +29 -9
- package/generator/pygen/codegen/models/enum_type.py +21 -9
- package/generator/pygen/codegen/models/list_type.py +1 -0
- package/generator/pygen/codegen/models/model_type.py +1 -3
- package/generator/pygen/codegen/models/operation.py +13 -3
- package/generator/pygen/codegen/models/parameter.py +3 -1
- package/generator/pygen/codegen/models/response.py +152 -8
- package/generator/pygen/codegen/models/utils.py +14 -0
- package/generator/pygen/codegen/serializers/__init__.py +13 -6
- package/generator/pygen/codegen/serializers/builder_serializer.py +219 -69
- package/generator/pygen/codegen/serializers/client_serializer.py +13 -6
- package/generator/pygen/codegen/serializers/general_serializer.py +4 -0
- package/generator/pygen/codegen/serializers/import_serializer.py +27 -0
- package/generator/pygen/codegen/serializers/model_serializer.py +4 -1
- package/generator/pygen/codegen/serializers/types_serializer.py +44 -12
- package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
- package/generator/pygen/preprocess/__init__.py +70 -17
- package/generator/pygen/utils.py +15 -0
- package/generator/pygen.egg-info/PKG-INFO +1 -1
- package/generator/pygen.egg-info/SOURCES.txt +1 -0
- package/generator/pygen.egg-info/requires.txt +1 -1
- package/generator/setup.py +1 -1
- 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} {
|
|
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} {
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1168
|
-
#
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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.
|
|
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(
|
|
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
|
|
107
|
-
dead code.
|
|
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.
|
|
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
|
|
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
|