@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
@@ -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
- # Skip single-body JSON overload; the TypedDict overload replaces it
98
- continue
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["models-mode"] != "typeddict"
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["models-mode"] == "typeddict"
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 typeddict overload for non-spread dpg models
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
- cross_lang_id = model_type.get("crossLanguageDefinitionId")
448
- existing_td = self._find_existing_typeddict(code_model, cross_lang_id, model_type.get("name"))
449
- self._insert_typeddict_overload(code_model, body_parameter, model_type, origin_type, existing_td)
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
- else:
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://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl",
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": "29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"
10
+ "sha256": "51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670"
11
11
  },
12
- "hash": "sha256=29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"
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": "83.0.0",
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.15",
125
+ "implementation_version": "3.11.16",
126
126
  "os_name": "posix",
127
127
  "platform_machine": "x86_64",
128
- "platform_release": "6.17.0-1020-azure",
128
+ "platform_release": "6.17.0-1022-azure",
129
129
  "platform_system": "Linux",
130
- "platform_version": "#20~24.04.1-Ubuntu SMP Fri Jun 19 20:09:14 UTC 2026",
131
- "python_full_version": "3.11.15",
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"
@@ -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", "typeddict", False]:
193
+ if key == "models-mode" and value not in ["msrest", "dpg", False]:
174
194
  raise ValueError(
175
- "--models-mode can only be 'msrest', 'dpg', 'typeddict', or 'none'. "
176
- "Pass in 'msrest' if you want msrest models, 'typeddict' for TypedDict models, or "
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 not code_model.options["models-mode"]:
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
- seen_schema_names: set[str],
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
- if current.name in seen_schema_names:
398
- raise ValueError(f"We have already generated a schema with name {current.name}")
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
- seen_schema_names.add(current.name)
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, seen_schema_names, seen_schema_yaml_ids) + ancestors
407
- seen_schema_names.add(current.name)
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
- seen_schema_names: set[str] = set()
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, seen_schema_names, seen_schema_yaml_ids))
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.options["models-mode"] == "typeddict":
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.options["models-mode"] == "typeddict":
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.options["models-mode"] == "typeddict"
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 name directly to avoid dotted forward refs in TypedDict annotations
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
- f"{relative_path}models" if relative_path != "." else ".models",
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 self.has_response_body and not self.has_optional_return_type and not self.code_model.options["models-mode"]:
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 self.code_model.options["models-mode"] in ("dpg", "typeddict") and self.in_flattened_body:
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