@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
|
@@ -35,6 +35,8 @@ def update_overload_section(
|
|
|
35
35
|
for overload_s, original_s in zip(overload[section], yaml_data[section]):
|
|
36
36
|
if overload_s.get("type"):
|
|
37
37
|
overload_s["type"] = original_s["type"]
|
|
38
|
+
if overload_s.get("streaming"):
|
|
39
|
+
overload_s["streaming"] = original_s["streaming"]
|
|
38
40
|
if overload_s.get("headers"):
|
|
39
41
|
for overload_h, original_h in zip(overload_s["headers"], original_s["headers"]):
|
|
40
42
|
if overload_h.get("type"):
|
|
@@ -81,8 +83,17 @@ def add_overload(yaml_data: dict[str, Any], body_type: dict[str, Any], for_flatt
|
|
|
81
83
|
return overload
|
|
82
84
|
|
|
83
85
|
|
|
84
|
-
def add_overloads_for_body_param(yaml_data: dict[str, Any]) -> None:
|
|
85
|
-
"""If we added a body parameter type, add overloads for that type
|
|
86
|
+
def add_overloads_for_body_param(yaml_data: dict[str, Any], skip_single_body_json: bool = False) -> None:
|
|
87
|
+
"""If we added a body parameter type, add overloads for that type.
|
|
88
|
+
|
|
89
|
+
``skip_single_body_json`` is the authoritative signal, computed by
|
|
90
|
+
``add_body_param_type``, for whether a TypedDict-style overload was inserted
|
|
91
|
+
to replace the single-body raw-JSON overload on the spread (``base: json``)
|
|
92
|
+
path. It is True for both models-mode: dpg (generate-typeddict on) and
|
|
93
|
+
the TypedDict-only mode (models-mode: none with generate-typeddict on), and
|
|
94
|
+
False when TypedDict generation is disabled (in which case the single-body
|
|
95
|
+
raw-JSON overload is kept, matching pre-TypedDict behavior).
|
|
96
|
+
"""
|
|
86
97
|
body_parameter = yaml_data["bodyParameter"]
|
|
87
98
|
if not (
|
|
88
99
|
body_parameter["type"]["type"] == "combined"
|
|
@@ -94,8 +105,8 @@ def add_overloads_for_body_param(yaml_data: dict[str, Any]) -> None:
|
|
|
94
105
|
continue
|
|
95
106
|
if body_type.get("type") == "model" and body_type.get("base") == "json":
|
|
96
107
|
yaml_data["overloads"].append(add_overload(yaml_data, body_type, for_flatten_params=True))
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
if skip_single_body_json:
|
|
109
|
+
continue
|
|
99
110
|
yaml_data["overloads"].append(add_overload(yaml_data, body_type))
|
|
100
111
|
content_type_param = next(p for p in yaml_data["parameters"] if p["wireName"].lower() == "content-type")
|
|
101
112
|
content_type_param["inOverload"] = False
|
|
@@ -306,6 +317,10 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
306
317
|
def is_tsp(self) -> bool:
|
|
307
318
|
return self.options.get("tsp_file", False)
|
|
308
319
|
|
|
320
|
+
@property
|
|
321
|
+
def generate_typeddict(self) -> bool:
|
|
322
|
+
return self.options.get("generate-typeddict", True)
|
|
323
|
+
|
|
309
324
|
@staticmethod
|
|
310
325
|
def _find_existing_typeddict(
|
|
311
326
|
code_model: dict[str, Any],
|
|
@@ -394,11 +409,39 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
394
409
|
if not existing_td:
|
|
395
410
|
code_model["types"].append(td_elem)
|
|
396
411
|
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _insert_json_overload(
|
|
414
|
+
body_parameter: dict[str, Any],
|
|
415
|
+
origin_type: str,
|
|
416
|
+
) -> None:
|
|
417
|
+
"""Insert a raw-JSON (any-object) type into the body parameter's combined types.
|
|
418
|
+
|
|
419
|
+
This restores the pre-TypedDict dict-body overload used when TypedDict
|
|
420
|
+
generation is disabled.
|
|
421
|
+
"""
|
|
422
|
+
if origin_type == "model":
|
|
423
|
+
body_parameter["type"]["types"].insert(1, KNOWN_TYPES["any-object"])
|
|
424
|
+
else:
|
|
425
|
+
# dict or list: copy the original container type and swap its element
|
|
426
|
+
# type for the raw-JSON any-object.
|
|
427
|
+
any_obj_list_or_dict = copy.deepcopy(body_parameter["type"]["types"][0])
|
|
428
|
+
any_obj_list_or_dict["elementType"] = KNOWN_TYPES["any-object"]
|
|
429
|
+
body_parameter["type"]["types"].insert(1, any_obj_list_or_dict)
|
|
430
|
+
|
|
397
431
|
def add_body_param_type(
|
|
398
432
|
self,
|
|
399
433
|
code_model: dict[str, Any],
|
|
400
434
|
body_parameter: dict[str, Any],
|
|
401
|
-
):
|
|
435
|
+
) -> bool:
|
|
436
|
+
"""Build the combined body-parameter type and its overload variants.
|
|
437
|
+
|
|
438
|
+
Returns whether a TypedDict-style overload was inserted in place of the
|
|
439
|
+
single-body raw-JSON overload on the spread (``base: json``) path. The
|
|
440
|
+
caller passes this to ``add_overloads_for_body_param`` as
|
|
441
|
+
``skip_single_body_json``. It is False when TypedDict generation is
|
|
442
|
+
disabled, so the pre-TypedDict single-body raw-JSON overload is kept.
|
|
443
|
+
"""
|
|
444
|
+
skip_single_body_json = False
|
|
402
445
|
# For a binary `bytes` body (e.g. content type application/octet-stream or a custom
|
|
403
446
|
# binary media type), add an IO overload alongside the `bytes` one. This keeps backward
|
|
404
447
|
# compatibility for services migrating from swagger, whose binary bodies were typed as IO.
|
|
@@ -408,7 +451,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
408
451
|
body_parameter
|
|
409
452
|
and body_parameter["type"]["type"] == "bytes"
|
|
410
453
|
and is_binary_body
|
|
411
|
-
and self.options
|
|
454
|
+
and not self.options.generate_typeddict_only
|
|
412
455
|
and not any(t for t in ["flattened", "groupedBy"] if body_parameter.get(t))
|
|
413
456
|
):
|
|
414
457
|
body_parameter["type"] = {
|
|
@@ -416,7 +459,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
416
459
|
"types": [body_parameter["type"], KNOWN_TYPES["binary"]],
|
|
417
460
|
}
|
|
418
461
|
code_model["types"].append(body_parameter["type"])
|
|
419
|
-
return
|
|
462
|
+
return skip_single_body_json
|
|
420
463
|
|
|
421
464
|
# only add overload for special content type
|
|
422
465
|
if ( # pylint: disable=too-many-boolean-expressions
|
|
@@ -432,7 +475,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
432
475
|
)
|
|
433
476
|
is_dpg_model = model_type.get("base") == "dpg"
|
|
434
477
|
is_json_model = model_type.get("base") == "json"
|
|
435
|
-
is_typeddict_only = self.options
|
|
478
|
+
is_typeddict_only = self.options.generate_typeddict_only
|
|
436
479
|
|
|
437
480
|
body_parameter["type"] = {
|
|
438
481
|
"type": "combined",
|
|
@@ -442,11 +485,16 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
442
485
|
if not (self.is_tsp and has_multi_part_content_type(body_parameter)) and not is_typeddict_only:
|
|
443
486
|
body_parameter["type"]["types"].append(KNOWN_TYPES["binary"])
|
|
444
487
|
|
|
445
|
-
# Add
|
|
488
|
+
# Add the dict-body overload for non-spread dpg models: a TypedDict when
|
|
489
|
+
# enabled, otherwise the raw-JSON overload (pre-TypedDict behavior).
|
|
446
490
|
if self.options["models-mode"] == "dpg" and is_dpg_model:
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
491
|
+
if self.generate_typeddict:
|
|
492
|
+
cross_lang_id = model_type.get("crossLanguageDefinitionId")
|
|
493
|
+
existing_td = self._find_existing_typeddict(code_model, cross_lang_id, model_type.get("name"))
|
|
494
|
+
self._insert_typeddict_overload(code_model, body_parameter, model_type, origin_type, existing_td)
|
|
495
|
+
skip_single_body_json = True
|
|
496
|
+
else:
|
|
497
|
+
self._insert_json_overload(body_parameter, origin_type)
|
|
450
498
|
|
|
451
499
|
# For spread bodies (json base), add a typeddict overload that references
|
|
452
500
|
# the original model. This replaces the JSON single-body overload.
|
|
@@ -456,17 +504,20 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
456
504
|
|
|
457
505
|
if is_typeddict_only and original:
|
|
458
506
|
# In typeddict-only mode, the original dpg model already renders
|
|
459
|
-
# as a TypedDict — reference it directly, no copy needed.
|
|
507
|
+
# as a TypedDict — reference it directly, no copy needed. It also
|
|
508
|
+
# replaces the single-body JSON overload.
|
|
509
|
+
skip_single_body_json = True
|
|
460
510
|
if origin_type == "model":
|
|
461
511
|
body_parameter["type"]["types"].insert(1, original)
|
|
462
512
|
else:
|
|
463
513
|
td_list_or_dict = copy.deepcopy(body_parameter["type"]["types"][0])
|
|
464
514
|
td_list_or_dict["elementType"] = original
|
|
465
515
|
body_parameter["type"]["types"].insert(1, td_list_or_dict)
|
|
466
|
-
|
|
516
|
+
elif self.generate_typeddict:
|
|
467
517
|
source = original or model_type
|
|
468
518
|
existing_td = self._find_existing_typeddict(code_model, cross_lang_id, source.get("name"))
|
|
469
519
|
self._insert_typeddict_overload(code_model, body_parameter, source, origin_type, existing_td)
|
|
520
|
+
skip_single_body_json = True
|
|
470
521
|
|
|
471
522
|
if len(body_parameter["type"]["types"]) == 1:
|
|
472
523
|
# Only one body variant remains (e.g. typeddict-only mode where the
|
|
@@ -474,10 +525,12 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
474
525
|
# wrapper back to the single type so we don't emit a lone
|
|
475
526
|
# ``@overload`` (mypy rejects a single overload definition).
|
|
476
527
|
body_parameter["type"] = body_parameter["type"]["types"][0]
|
|
477
|
-
return
|
|
528
|
+
return skip_single_body_json
|
|
478
529
|
|
|
479
530
|
code_model["types"].append(body_parameter["type"])
|
|
480
531
|
|
|
532
|
+
return skip_single_body_json
|
|
533
|
+
|
|
481
534
|
def pad_reserved_words(self, name: str, pad_type: PadType, yaml_type: dict[str, Any]) -> str:
|
|
482
535
|
# we want to pad hidden variables as well
|
|
483
536
|
if not name:
|
|
@@ -649,8 +702,8 @@ class PreProcessPlugin(YamlUpdatePlugin):
|
|
|
649
702
|
response["discriminator"] = "operation"
|
|
650
703
|
if body_parameter and not is_overload:
|
|
651
704
|
# if we have a JSON body, we add a binary overload
|
|
652
|
-
self.add_body_param_type(code_model, body_parameter)
|
|
653
|
-
add_overloads_for_body_param(yaml_data)
|
|
705
|
+
skip_single_body_json = self.add_body_param_type(code_model, body_parameter)
|
|
706
|
+
add_overloads_for_body_param(yaml_data, skip_single_body_json=skip_single_body_json)
|
|
654
707
|
|
|
655
708
|
def _update_lro_operation_helper(self, yaml_data: dict[str, Any]) -> None:
|
|
656
709
|
for response in yaml_data.get("responses", []):
|
|
@@ -28,6 +28,21 @@ def description_ends_with_code_block(description: str) -> bool:
|
|
|
28
28
|
return all(not line.strip() or line.startswith((" ", "\t")) for line in lines[directives[-1] + 1 :])
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def is_typeddict_only(options: Any) -> bool:
|
|
32
|
+
"""Whether generation is TypedDict-only.
|
|
33
|
+
|
|
34
|
+
True for TypeSpec input where no concrete models mode is selected (``models-mode: none``,
|
|
35
|
+
normalized to a falsy value by :class:`OptionsDict`) and ``generate-typeddict`` is enabled.
|
|
36
|
+
Swagger ``models-mode: none`` is left as a plain no-models mode because it never sets
|
|
37
|
+
``tsp_file``.
|
|
38
|
+
"""
|
|
39
|
+
return (
|
|
40
|
+
bool(options.get("tsp_file"))
|
|
41
|
+
and not options.get("models-mode")
|
|
42
|
+
and bool(options.get("generate-typeddict", True))
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
31
46
|
def update_enum_value(name: str, value: Any, description: str, enum_type: dict[str, Any]) -> dict[str, Any]:
|
|
32
47
|
return {
|
|
33
48
|
"name": name,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1",
|
|
3
|
-
"pip_version": "26.2",
|
|
3
|
+
"pip_version": "26.2.1",
|
|
4
4
|
"install": [
|
|
5
5
|
{
|
|
6
6
|
"download_info": {
|
|
7
|
-
"url": "https://
|
|
7
|
+
"url": "https://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/setuptools/84/setuptools-84.0.0-py3-none-any.whl",
|
|
8
8
|
"archive_info": {
|
|
9
9
|
"hashes": {
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670"
|
|
11
11
|
},
|
|
12
|
-
"hash": "sha256=
|
|
12
|
+
"hash": "sha256=51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"is_direct": false,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"metadata": {
|
|
19
19
|
"metadata_version": "2.4",
|
|
20
20
|
"name": "setuptools",
|
|
21
|
-
"version": "
|
|
21
|
+
"version": "84.0.0",
|
|
22
22
|
"dynamic": [
|
|
23
23
|
"license-file"
|
|
24
24
|
],
|
|
@@ -122,13 +122,13 @@
|
|
|
122
122
|
],
|
|
123
123
|
"environment": {
|
|
124
124
|
"implementation_name": "cpython",
|
|
125
|
-
"implementation_version": "3.11.
|
|
125
|
+
"implementation_version": "3.11.16",
|
|
126
126
|
"os_name": "posix",
|
|
127
127
|
"platform_machine": "x86_64",
|
|
128
|
-
"platform_release": "6.17.0-
|
|
128
|
+
"platform_release": "6.17.0-1022-azure",
|
|
129
129
|
"platform_system": "Linux",
|
|
130
|
-
"platform_version": "#
|
|
131
|
-
"python_full_version": "3.11.
|
|
130
|
+
"platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026",
|
|
131
|
+
"python_full_version": "3.11.16",
|
|
132
132
|
"platform_python_implementation": "CPython",
|
|
133
133
|
"python_version": "3.11",
|
|
134
134
|
"sys_platform": "linux"
|
|
Binary file
|
|
@@ -12,7 +12,7 @@ from abc import ABC, abstractmethod
|
|
|
12
12
|
from typing import Any, Iterator, Optional, Union
|
|
13
13
|
|
|
14
14
|
import yaml
|
|
15
|
-
from .utils import TYPESPEC_PACKAGE_MODE, VALID_PACKAGE_MODE
|
|
15
|
+
from .utils import TYPESPEC_PACKAGE_MODE, VALID_PACKAGE_MODE, is_typeddict_only
|
|
16
16
|
|
|
17
17
|
from ._version import VERSION
|
|
18
18
|
|
|
@@ -36,6 +36,7 @@ class OptionsDict(MutableMapping):
|
|
|
36
36
|
"low-level-client": False,
|
|
37
37
|
"no-async": False,
|
|
38
38
|
"no-namespace-folders": False,
|
|
39
|
+
"generate-typeddict": True,
|
|
39
40
|
"polymorphic-examples": 5,
|
|
40
41
|
"validate-versioning": True,
|
|
41
42
|
"version-tolerant": True,
|
|
@@ -45,10 +46,20 @@ class OptionsDict(MutableMapping):
|
|
|
45
46
|
|
|
46
47
|
def __init__(self, options: Optional[dict[str, Any]] = None) -> None:
|
|
47
48
|
self._data = options.copy() if options else {}
|
|
49
|
+
# 'models-mode: typeddict' is deprecated. Represent it internally as 'none' models-mode with
|
|
50
|
+
# TypedDict generation enabled, so the rest of the codebase reasons in terms of models-mode
|
|
51
|
+
# plus the generate-typeddict flag.
|
|
52
|
+
if self._data.get("models-mode") == "typeddict":
|
|
53
|
+
self._data["generate-typeddict"] = True
|
|
48
54
|
for key in list(self._data):
|
|
49
55
|
self._data[key] = self._validate_and_transform(key, self._data[key])
|
|
50
56
|
self._validate_combinations()
|
|
51
57
|
|
|
58
|
+
@property
|
|
59
|
+
def generate_typeddict_only(self) -> bool:
|
|
60
|
+
"""Whether this is TypedDict-only generation ('models-mode: none' + TypedDicts)."""
|
|
61
|
+
return is_typeddict_only(self)
|
|
62
|
+
|
|
52
63
|
def __getitem__(self, key: str) -> Any: # pylint: disable=too-many-return-statements
|
|
53
64
|
if key == "head-as-boolean" and self.get("azure-arm"):
|
|
54
65
|
# override to always true if azure-arm is set
|
|
@@ -167,14 +178,22 @@ class OptionsDict(MutableMapping):
|
|
|
167
178
|
if key == "builders-visibility" and value not in ["public", "hidden", "embedded"]:
|
|
168
179
|
raise ValueError("The value of --builders-visibility must be either 'public', 'hidden', or 'embedded'")
|
|
169
180
|
|
|
181
|
+
if key == "models-mode" and value == "typeddict":
|
|
182
|
+
# Deprecated: keep accepting it for back-compat but store it as 'none' (falsy) with
|
|
183
|
+
# TypedDict generation enabled (see OptionsDict.__init__ and generate_typeddict_only).
|
|
184
|
+
_LOGGER.warning(
|
|
185
|
+
"'models-mode: typeddict' is deprecated. Use 'models-mode: none' with "
|
|
186
|
+
"'generate-typeddict: true' (the default) instead."
|
|
187
|
+
)
|
|
188
|
+
value = False
|
|
189
|
+
|
|
170
190
|
if key == "models-mode" and value == "none":
|
|
171
191
|
value = False # switch to falsy value for easier code writing
|
|
172
192
|
|
|
173
|
-
if key == "models-mode" and value not in ["msrest", "dpg",
|
|
193
|
+
if key == "models-mode" and value not in ["msrest", "dpg", False]:
|
|
174
194
|
raise ValueError(
|
|
175
|
-
"--models-mode can only be '
|
|
176
|
-
"Pass in '
|
|
177
|
-
"'none' if you don't want any."
|
|
195
|
+
"--models-mode can only be 'dpg' or 'none'. "
|
|
196
|
+
"Pass in 'dpg' for DPG models, or 'none' if you don't want any."
|
|
178
197
|
)
|
|
179
198
|
if key == "package-mode":
|
|
180
199
|
if (
|
|
@@ -169,7 +169,9 @@ def build_type(yaml_data: dict[str, Any], code_model: CodeModel) -> BaseType:
|
|
|
169
169
|
# need to special case model to avoid recursion
|
|
170
170
|
if yaml_data["base"] == "typeddict":
|
|
171
171
|
model_type = TypedDictModelType # type: ignore
|
|
172
|
-
elif yaml_data["base"] == "json" or
|
|
172
|
+
elif yaml_data["base"] == "json" or (
|
|
173
|
+
not code_model.options["models-mode"] and not code_model.generate_typeddict_only
|
|
174
|
+
):
|
|
173
175
|
model_type = JSONModelType
|
|
174
176
|
elif yaml_data["base"] == "dpg":
|
|
175
177
|
model_type = DPGModelType # type: ignore
|
|
@@ -17,6 +17,7 @@ from .operation_group import OperationGroup
|
|
|
17
17
|
from .utils import NamespaceType
|
|
18
18
|
from .._utils import DEFAULT_HEADER_TEXT, DEFAULT_LICENSE_DESCRIPTION
|
|
19
19
|
from ... import OptionsDict
|
|
20
|
+
from ...utils import is_typeddict_only
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _is_legacy(options) -> bool:
|
|
@@ -89,7 +90,7 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
89
90
|
self.clients: list[Client] = [
|
|
90
91
|
Client.from_yaml(client_yaml_data, self) for client_yaml_data in yaml_data["clients"]
|
|
91
92
|
]
|
|
92
|
-
if self.options["models-mode"] and self.model_types:
|
|
93
|
+
if (self.options["models-mode"] or self.generate_typeddict_only) and self.model_types:
|
|
93
94
|
self.sort_model_types()
|
|
94
95
|
self.named_unions: list[CombinedType] = [
|
|
95
96
|
t for t in self.types_map.values() if isinstance(t, CombinedType) and t.name
|
|
@@ -174,6 +175,11 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
174
175
|
def get_unique_types_alias(self, serialize_namespace: str, imported_namespace: str) -> str:
|
|
175
176
|
return self._get_unique_import_alias(serialize_namespace, imported_namespace, "types")
|
|
176
177
|
|
|
178
|
+
@property
|
|
179
|
+
def generate_typeddict_only(self) -> bool:
|
|
180
|
+
"""Whether this is TypedDict-only generation ('models-mode: none' + TypedDicts)."""
|
|
181
|
+
return is_typeddict_only(self.options)
|
|
182
|
+
|
|
177
183
|
@property
|
|
178
184
|
def client_namespace_types(self) -> dict[str, ClientNamespaceType]:
|
|
179
185
|
if not self._client_namespace_types:
|
|
@@ -273,12 +279,22 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
273
279
|
self.need_utils_utils(async_mode, client_namespace)
|
|
274
280
|
or self.need_utils_serialization
|
|
275
281
|
or self.options["models-mode"] == "dpg"
|
|
282
|
+
or self.has_structured_stream
|
|
276
283
|
)
|
|
277
284
|
|
|
278
285
|
@property
|
|
279
286
|
def need_utils_serialization(self) -> bool:
|
|
280
287
|
return not self.options["client-side-validation"]
|
|
281
288
|
|
|
289
|
+
@property
|
|
290
|
+
def has_structured_stream(self) -> bool:
|
|
291
|
+
return any(
|
|
292
|
+
op.has_structured_stream_response
|
|
293
|
+
for client in self.clients
|
|
294
|
+
for og in client.operation_groups
|
|
295
|
+
for op in og.operations
|
|
296
|
+
)
|
|
297
|
+
|
|
282
298
|
def need_utils_utils(self, async_mode: bool, client_namespace: str) -> bool:
|
|
283
299
|
return (
|
|
284
300
|
self.need_utils_form_data(async_mode, client_namespace)
|
|
@@ -389,22 +405,26 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
389
405
|
def _sort_model_types_helper(
|
|
390
406
|
self,
|
|
391
407
|
current: ModelType,
|
|
392
|
-
|
|
408
|
+
seen_schema_keys: set[tuple[str, str]],
|
|
393
409
|
seen_schema_yaml_ids: set[int],
|
|
394
410
|
):
|
|
395
411
|
if current.id in seen_schema_yaml_ids:
|
|
396
412
|
return []
|
|
397
|
-
|
|
398
|
-
|
|
413
|
+
current_schema_key = (current.client_namespace, current.name)
|
|
414
|
+
if current_schema_key in seen_schema_keys:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
f"We have already generated a schema with name {current.name} "
|
|
417
|
+
f"in namespace {current.client_namespace}"
|
|
418
|
+
)
|
|
399
419
|
ancestors = [current]
|
|
400
420
|
if current.parents:
|
|
401
421
|
for parent in current.parents:
|
|
402
422
|
if parent.id in seen_schema_yaml_ids:
|
|
403
423
|
continue
|
|
404
|
-
|
|
424
|
+
seen_schema_keys.add(current_schema_key)
|
|
405
425
|
seen_schema_yaml_ids.add(current.id)
|
|
406
|
-
ancestors = self._sort_model_types_helper(parent,
|
|
407
|
-
|
|
426
|
+
ancestors = self._sort_model_types_helper(parent, seen_schema_keys, seen_schema_yaml_ids) + ancestors
|
|
427
|
+
seen_schema_keys.add(current_schema_key)
|
|
408
428
|
seen_schema_yaml_ids.add(current.id)
|
|
409
429
|
return ancestors
|
|
410
430
|
|
|
@@ -414,11 +434,11 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
414
434
|
:return: None
|
|
415
435
|
:rtype: None
|
|
416
436
|
"""
|
|
417
|
-
|
|
437
|
+
seen_schema_keys: set[tuple[str, str]] = set()
|
|
418
438
|
seen_schema_yaml_ids: set[int] = set()
|
|
419
439
|
sorted_object_schemas: list[ModelType] = []
|
|
420
440
|
for schema in sorted(self.model_types, key=lambda x: x.name.lower()):
|
|
421
|
-
sorted_object_schemas.extend(self._sort_model_types_helper(schema,
|
|
441
|
+
sorted_object_schemas.extend(self._sort_model_types_helper(schema, seen_schema_keys, seen_schema_yaml_ids))
|
|
422
442
|
self.model_types = sorted_object_schemas
|
|
423
443
|
|
|
424
444
|
@property
|
|
@@ -40,7 +40,7 @@ class EnumValue(BaseType):
|
|
|
40
40
|
|
|
41
41
|
def type_annotation(self, **kwargs: Any) -> str:
|
|
42
42
|
"""The python type used for type annotation"""
|
|
43
|
-
if self.code_model.
|
|
43
|
+
if self.code_model.generate_typeddict_only:
|
|
44
44
|
# A single constant enum value must be
|
|
45
45
|
# annotated with its literal value directly (e.g. ``Literal["red"]``).
|
|
46
46
|
return f"Literal[{self.value_type.get_declaration(self.value)}]"
|
|
@@ -83,7 +83,7 @@ class EnumValue(BaseType):
|
|
|
83
83
|
file_import = FileImport(self.code_model)
|
|
84
84
|
file_import.merge(self.value_type.imports(**kwargs))
|
|
85
85
|
file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB, TypingSection.REGULAR)
|
|
86
|
-
if self.code_model.
|
|
86
|
+
if self.code_model.generate_typeddict_only:
|
|
87
87
|
# In typeddict mode the enums module (``_enums.py``) is never generated
|
|
88
88
|
return file_import
|
|
89
89
|
serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
|
|
@@ -176,7 +176,7 @@ class EnumType(BaseType):
|
|
|
176
176
|
|
|
177
177
|
@property
|
|
178
178
|
def is_typeddict_mode(self) -> bool:
|
|
179
|
-
return self.code_model.
|
|
179
|
+
return self.code_model.generate_typeddict_only
|
|
180
180
|
|
|
181
181
|
def type_annotation(self, **kwargs: Any) -> str:
|
|
182
182
|
"""The python type used for type annotation
|
|
@@ -184,7 +184,7 @@ class EnumType(BaseType):
|
|
|
184
184
|
:return: The type annotation for this schema
|
|
185
185
|
:rtype: str
|
|
186
186
|
"""
|
|
187
|
-
if self.code_model.options["models-mode"]:
|
|
187
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
188
188
|
if self.is_typeddict_mode:
|
|
189
189
|
# In typeddict mode, enums are Literal aliases defined in types.py
|
|
190
190
|
serialize_namespace_type = kwargs.get("serialize_namespace_type")
|
|
@@ -203,6 +203,10 @@ class EnumType(BaseType):
|
|
|
203
203
|
model_alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
|
|
204
204
|
module_name = f"{model_alias}."
|
|
205
205
|
file_name = f"{self.code_model.enums_filename}." if self.internal else ""
|
|
206
|
+
if serialize_namespace_type == NamespaceType.TYPES_FILE:
|
|
207
|
+
# In types.py the enum symbol is imported directly (bare name), so no ``_enums.``
|
|
208
|
+
# module prefix even for internal enums — the prefix would be an undefined name.
|
|
209
|
+
file_name = ""
|
|
206
210
|
model_name = module_name + file_name + self.name
|
|
207
211
|
# we don't need quoted annotation in operation files, and need it in model folder files.
|
|
208
212
|
if not kwargs.get("is_operation_file", False):
|
|
@@ -221,13 +225,13 @@ class EnumType(BaseType):
|
|
|
221
225
|
return self.value_type.get_declaration(value)
|
|
222
226
|
|
|
223
227
|
def docstring_text(self, **kwargs: Any) -> str:
|
|
224
|
-
if self.code_model.options["models-mode"]:
|
|
228
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
225
229
|
return self.name
|
|
226
230
|
return self.value_type.type_annotation(**kwargs)
|
|
227
231
|
|
|
228
232
|
def docstring_type(self, **kwargs: Any) -> str:
|
|
229
233
|
"""The python type used for RST syntax input and type annotation."""
|
|
230
|
-
if self.code_model.options["models-mode"]:
|
|
234
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
231
235
|
type_annotation = self.value_type.type_annotation(**kwargs)
|
|
232
236
|
enum_type_annotation = f"{self.client_namespace}.models.{self.name}"
|
|
233
237
|
return f"{type_annotation} or ~{enum_type_annotation}"
|
|
@@ -261,7 +265,7 @@ class EnumType(BaseType):
|
|
|
261
265
|
def imports(self, **kwargs: Any) -> FileImport:
|
|
262
266
|
file_import = FileImport(self.code_model)
|
|
263
267
|
file_import.merge(self.value_type.imports(**kwargs))
|
|
264
|
-
if self.code_model.options["models-mode"]:
|
|
268
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
265
269
|
if self.is_typeddict_mode:
|
|
266
270
|
# In typeddict mode, enums are Literal aliases in types.py — no Union needed
|
|
267
271
|
serialize_namespace_type = kwargs.get("serialize_namespace_type")
|
|
@@ -305,9 +309,17 @@ class EnumType(BaseType):
|
|
|
305
309
|
typing_section=TypingSection.REGULAR,
|
|
306
310
|
)
|
|
307
311
|
elif serialize_namespace_type == NamespaceType.TYPES_FILE:
|
|
308
|
-
# Import enum
|
|
312
|
+
# Import the enum symbol directly to avoid dotted forward refs in TypedDict
|
|
313
|
+
# annotations. Internal enums are not re-exported from the public ``models``
|
|
314
|
+
# package, so import them from the private ``_enums`` submodule instead — the
|
|
315
|
+
# bare-symbol import (rather than the ``_enums`` module) also avoids name
|
|
316
|
+
# collisions when internal enums come from several sibling namespaces.
|
|
317
|
+
module_name = f"models.{self.code_model.enums_filename}" if self.internal else "models"
|
|
318
|
+
enums_module = self.code_model.get_relative_import_path(
|
|
319
|
+
serialize_namespace, self.client_namespace, module_name=module_name
|
|
320
|
+
)
|
|
309
321
|
file_import.add_submodule_import(
|
|
310
|
-
|
|
322
|
+
enums_module,
|
|
311
323
|
self.name,
|
|
312
324
|
ImportType.LOCAL,
|
|
313
325
|
typing_section=TypingSection.TYPING,
|
|
@@ -38,6 +38,7 @@ class ListType(BaseType):
|
|
|
38
38
|
self.code_model.options["version-tolerant"]
|
|
39
39
|
and self.element_type.is_xml
|
|
40
40
|
and not self.code_model.options["models-mode"]
|
|
41
|
+
and not self.code_model.generate_typeddict_only
|
|
41
42
|
):
|
|
42
43
|
# this means we're version tolerant XML, we just return the XML element
|
|
43
44
|
return self.element_type.type_annotation(**kwargs)
|
|
@@ -77,9 +77,7 @@ class ModelType(BaseType): # pylint: disable=too-many-instance-attributes, too-
|
|
|
77
77
|
self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId")
|
|
78
78
|
self.usage: int = self.yaml_data.get("usage", UsageFlags.Input.value | UsageFlags.Output.value)
|
|
79
79
|
self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace)
|
|
80
|
-
self.is_typed_dict_only: bool = (
|
|
81
|
-
self.yaml_data.get("typedDictOnly", False) or code_model.options["models-mode"] == "typeddict"
|
|
82
|
-
)
|
|
80
|
+
self.is_typed_dict_only: bool = self.yaml_data.get("typedDictOnly", False) or code_model.generate_typeddict_only
|
|
83
81
|
|
|
84
82
|
@property
|
|
85
83
|
def is_usage_output(self) -> bool:
|
|
@@ -104,6 +104,11 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
|
|
|
104
104
|
else self.has_stream_response
|
|
105
105
|
)
|
|
106
106
|
|
|
107
|
+
@property
|
|
108
|
+
def has_structured_stream_response(self) -> bool:
|
|
109
|
+
"""Whether any success response is a structured (JSONL / SSE) stream returning Stream[T]."""
|
|
110
|
+
return any(getattr(r, "is_structured_stream", False) for r in self.responses)
|
|
111
|
+
|
|
107
112
|
@property
|
|
108
113
|
def has_form_data_body(self):
|
|
109
114
|
return self.parameters.has_form_data_body
|
|
@@ -334,7 +339,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
|
|
|
334
339
|
file_import.merge(
|
|
335
340
|
response.imports(async_mode=async_mode, need_import_iobase=self.need_import_iobase, **kwargs)
|
|
336
341
|
)
|
|
337
|
-
if self.code_model.options["models-mode"]:
|
|
342
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
338
343
|
for exception in self.exceptions:
|
|
339
344
|
file_import.merge(exception.imports(async_mode=async_mode, **kwargs))
|
|
340
345
|
|
|
@@ -506,7 +511,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
|
|
|
506
511
|
|
|
507
512
|
@property
|
|
508
513
|
def has_stream_response(self) -> bool:
|
|
509
|
-
return any(r.is_stream_response for r in self.responses)
|
|
514
|
+
return any(r.is_stream_response or r.is_structured_stream for r in self.responses)
|
|
510
515
|
|
|
511
516
|
@classmethod
|
|
512
517
|
def get_request_builder(cls, yaml_data: dict[str, Any], client: "Client"):
|
|
@@ -552,7 +557,12 @@ class Operation(OperationBase[Response]):
|
|
|
552
557
|
"distributed_trace_async",
|
|
553
558
|
ImportType.SDKCORE,
|
|
554
559
|
)
|
|
555
|
-
if
|
|
560
|
+
if (
|
|
561
|
+
self.has_response_body
|
|
562
|
+
and not self.has_optional_return_type
|
|
563
|
+
and not self.code_model.options["models-mode"]
|
|
564
|
+
and not self.code_model.generate_typeddict_only
|
|
565
|
+
):
|
|
556
566
|
file_import.add_submodule_import("typing", "cast", ImportType.STDLIB)
|
|
557
567
|
|
|
558
568
|
return file_import
|
|
@@ -338,7 +338,9 @@ class Parameter(_ParameterBase):
|
|
|
338
338
|
) -> ParameterMethodLocation:
|
|
339
339
|
if not self.in_method_signature:
|
|
340
340
|
raise ValueError(f"Parameter '{self.client_name}' is not in the method.")
|
|
341
|
-
if
|
|
341
|
+
if (
|
|
342
|
+
self.code_model.options["models-mode"] == "dpg" or self.code_model.generate_typeddict_only
|
|
343
|
+
) and self.in_flattened_body:
|
|
342
344
|
return ParameterMethodLocation.KEYWORD_ONLY
|
|
343
345
|
if self.grouper:
|
|
344
346
|
return ParameterMethodLocation.POSITIONAL
|