@typespec/http-client-python 0.33.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +25 -3
  2. package/dist/emitter/emitter.d.ts.map +1 -1
  3. package/dist/emitter/emitter.js +2 -11
  4. package/dist/emitter/emitter.js.map +1 -1
  5. package/dist/emitter/external-process.d.ts +14 -0
  6. package/dist/emitter/external-process.d.ts.map +1 -1
  7. package/dist/emitter/external-process.js +17 -1
  8. package/dist/emitter/external-process.js.map +1 -1
  9. package/dist/emitter/lib.js +1 -1
  10. package/dist/emitter/lib.js.map +1 -1
  11. package/dist/emitter/node-runner.d.ts.map +1 -1
  12. package/dist/emitter/node-runner.js +2 -1
  13. package/dist/emitter/node-runner.js.map +1 -1
  14. package/dist/emitter/types.d.ts.map +1 -1
  15. package/dist/emitter/types.js +2 -0
  16. package/dist/emitter/types.js.map +1 -1
  17. package/dist/emitter/utils.d.ts +10 -0
  18. package/dist/emitter/utils.d.ts.map +1 -1
  19. package/dist/emitter/utils.js +12 -0
  20. package/dist/emitter/utils.js.map +1 -1
  21. package/emitter/src/emitter.ts +2 -14
  22. package/emitter/src/external-process.ts +18 -1
  23. package/emitter/src/lib.ts +1 -1
  24. package/emitter/src/node-runner.ts +2 -1
  25. package/emitter/src/types.ts +2 -0
  26. package/emitter/src/utils.ts +13 -0
  27. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  28. package/emitter/test/external-process.test.ts +28 -0
  29. package/emitter/test/utils.test.ts +12 -1
  30. package/eng/scripts/ci/regenerate-common.ts +40 -15
  31. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  32. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  33. package/generator/build/lib/pygen/__init__.py +3 -3
  34. package/generator/build/lib/pygen/codegen/models/__init__.py +4 -2
  35. package/generator/build/lib/pygen/codegen/models/code_model.py +27 -5
  36. package/generator/build/lib/pygen/codegen/models/combined_type.py +3 -3
  37. package/generator/build/lib/pygen/codegen/models/enum_type.py +75 -25
  38. package/generator/build/lib/pygen/codegen/models/list_type.py +10 -4
  39. package/generator/build/lib/pygen/codegen/models/model_type.py +149 -4
  40. package/generator/build/lib/pygen/codegen/models/operation.py +1 -0
  41. package/generator/build/lib/pygen/codegen/models/paging_operation.py +5 -1
  42. package/generator/build/lib/pygen/codegen/models/parameter.py +2 -2
  43. package/generator/build/lib/pygen/codegen/models/primitive_types.py +30 -6
  44. package/generator/build/lib/pygen/codegen/models/property.py +21 -8
  45. package/generator/build/lib/pygen/codegen/models/response.py +16 -3
  46. package/generator/build/lib/pygen/codegen/models/utils.py +3 -6
  47. package/generator/build/lib/pygen/codegen/serializers/__init__.py +46 -11
  48. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +48 -15
  49. package/generator/build/lib/pygen/codegen/serializers/model_init_serializer.py +1 -1
  50. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +127 -3
  51. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +356 -12
  52. package/generator/build/lib/pygen/codegen/serializers/unions_serializer.py +44 -0
  53. package/generator/build/lib/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  54. package/generator/build/lib/pygen/codegen/templates/types.py.jinja2 +51 -2
  55. package/generator/build/lib/pygen/codegen/templates/unions.py.jinja2 +9 -0
  56. package/generator/build/lib/pygen/preprocess/__init__.py +175 -162
  57. package/generator/build/lib/pygen/utils.py +6 -24
  58. package/generator/component-detection-pip-report.json +9 -9
  59. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  60. package/generator/pygen/__init__.py +3 -3
  61. package/generator/pygen/codegen/models/__init__.py +4 -2
  62. package/generator/pygen/codegen/models/code_model.py +27 -5
  63. package/generator/pygen/codegen/models/combined_type.py +3 -3
  64. package/generator/pygen/codegen/models/enum_type.py +75 -25
  65. package/generator/pygen/codegen/models/list_type.py +10 -4
  66. package/generator/pygen/codegen/models/model_type.py +149 -4
  67. package/generator/pygen/codegen/models/operation.py +1 -0
  68. package/generator/pygen/codegen/models/paging_operation.py +5 -1
  69. package/generator/pygen/codegen/models/parameter.py +2 -2
  70. package/generator/pygen/codegen/models/primitive_types.py +30 -6
  71. package/generator/pygen/codegen/models/property.py +21 -8
  72. package/generator/pygen/codegen/models/response.py +16 -3
  73. package/generator/pygen/codegen/models/utils.py +3 -6
  74. package/generator/pygen/codegen/serializers/__init__.py +46 -11
  75. package/generator/pygen/codegen/serializers/builder_serializer.py +48 -15
  76. package/generator/pygen/codegen/serializers/model_init_serializer.py +1 -1
  77. package/generator/pygen/codegen/serializers/model_serializer.py +127 -3
  78. package/generator/pygen/codegen/serializers/types_serializer.py +356 -12
  79. package/generator/pygen/codegen/serializers/unions_serializer.py +44 -0
  80. package/generator/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  81. package/generator/pygen/codegen/templates/types.py.jinja2 +51 -2
  82. package/generator/pygen/codegen/templates/unions.py.jinja2 +9 -0
  83. package/generator/pygen/preprocess/__init__.py +175 -162
  84. package/generator/pygen/utils.py +6 -24
  85. package/generator/pygen.egg-info/SOURCES.txt +2 -0
  86. package/package.json +5 -5
@@ -180,7 +180,7 @@ class _ParameterBase(BaseModel, abc.ABC): # pylint: disable=too-many-instance-a
180
180
  if isinstance(self.type, CombinedType) and self.type.name:
181
181
  file_import.add_submodule_import(
182
182
  self.code_model.get_relative_import_path(serialize_namespace),
183
- "_types",
183
+ "_unions",
184
184
  ImportType.LOCAL,
185
185
  TypingSection.TYPING,
186
186
  )
@@ -338,7 +338,7 @@ 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"] == "dpg" and self.in_flattened_body:
341
+ if self.code_model.options["models-mode"] in ("dpg", "typeddict") and self.in_flattened_body:
342
342
  return ParameterMethodLocation.KEYWORD_ONLY
343
343
  if self.grouper:
344
344
  return ParameterMethodLocation.POSITIONAL
@@ -9,6 +9,7 @@ from typing import Any, Optional, Union, TYPE_CHECKING
9
9
 
10
10
  from .base import BaseType
11
11
  from .imports import FileImport, ImportType, TypingSection
12
+ from .utils import NamespaceType
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from .code_model import CodeModel
@@ -271,6 +272,8 @@ class DecimalType(NumberType):
271
272
  return "~" + self.type_annotation()
272
273
 
273
274
  def type_annotation(self, **kwargs: Any) -> str:
275
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
276
+ return "float"
274
277
  return "decimal.Decimal"
275
278
 
276
279
  def docstring_text(self, **kwargs: Any) -> str:
@@ -281,7 +284,8 @@ class DecimalType(NumberType):
281
284
 
282
285
  def imports(self, **kwargs: Any) -> FileImport:
283
286
  file_import = FileImport(self.code_model)
284
- file_import.add_import("decimal", ImportType.STDLIB)
287
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
288
+ file_import.add_import("decimal", ImportType.STDLIB)
285
289
  return file_import
286
290
 
287
291
  @property
@@ -357,6 +361,8 @@ class DatetimeType(PrimitiveType):
357
361
  return "~" + self.type_annotation()
358
362
 
359
363
  def type_annotation(self, **kwargs: Any) -> str:
364
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
365
+ return "str"
360
366
  return "datetime.datetime"
361
367
 
362
368
  def docstring_text(self, **kwargs: Any) -> str:
@@ -370,7 +376,8 @@ class DatetimeType(PrimitiveType):
370
376
 
371
377
  def imports(self, **kwargs: Any) -> FileImport:
372
378
  file_import = FileImport(self.code_model)
373
- file_import.add_import("datetime", ImportType.STDLIB)
379
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
380
+ file_import.add_import("datetime", ImportType.STDLIB)
374
381
  return file_import
375
382
 
376
383
  @property
@@ -399,6 +406,8 @@ class TimeType(PrimitiveType):
399
406
  return "~" + self.type_annotation()
400
407
 
401
408
  def type_annotation(self, **kwargs: Any) -> str:
409
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
410
+ return "str"
402
411
  return "datetime.time"
403
412
 
404
413
  def docstring_text(self, **kwargs: Any) -> str:
@@ -412,7 +421,8 @@ class TimeType(PrimitiveType):
412
421
 
413
422
  def imports(self, **kwargs: Any) -> FileImport:
414
423
  file_import = FileImport(self.code_model)
415
- file_import.add_import("datetime", ImportType.STDLIB)
424
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
425
+ file_import.add_import("datetime", ImportType.STDLIB)
416
426
  return file_import
417
427
 
418
428
  @property
@@ -445,6 +455,8 @@ class UnixTimeType(PrimitiveType):
445
455
  return "~" + self.type_annotation()
446
456
 
447
457
  def type_annotation(self, **kwargs: Any) -> str:
458
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
459
+ return "int"
448
460
  return "datetime.datetime"
449
461
 
450
462
  def docstring_text(self, **kwargs: Any) -> str:
@@ -458,7 +470,8 @@ class UnixTimeType(PrimitiveType):
458
470
 
459
471
  def imports(self, **kwargs: Any) -> FileImport:
460
472
  file_import = FileImport(self.code_model)
461
- file_import.add_import("datetime", ImportType.STDLIB)
473
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
474
+ file_import.add_import("datetime", ImportType.STDLIB)
462
475
  return file_import
463
476
 
464
477
  @property
@@ -487,6 +500,8 @@ class DateType(PrimitiveType):
487
500
  return "~" + self.type_annotation()
488
501
 
489
502
  def type_annotation(self, **kwargs: Any) -> str:
503
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
504
+ return "str"
490
505
  return "datetime.date"
491
506
 
492
507
  def docstring_text(self, **kwargs: Any) -> str:
@@ -500,7 +515,8 @@ class DateType(PrimitiveType):
500
515
 
501
516
  def imports(self, **kwargs: Any) -> FileImport:
502
517
  file_import = FileImport(self.code_model)
503
- file_import.add_import("datetime", ImportType.STDLIB)
518
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
519
+ file_import.add_import("datetime", ImportType.STDLIB)
504
520
  return file_import
505
521
 
506
522
  @property
@@ -543,6 +559,8 @@ class DurationType(PrimitiveType):
543
559
  return "~" + self.type_annotation()
544
560
 
545
561
  def type_annotation(self, **kwargs: Any) -> str:
562
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
563
+ return "str"
546
564
  return "datetime.timedelta"
547
565
 
548
566
  def docstring_text(self, **kwargs: Any) -> str:
@@ -556,7 +574,8 @@ class DurationType(PrimitiveType):
556
574
 
557
575
  def imports(self, **kwargs: Any) -> FileImport:
558
576
  file_import = FileImport(self.code_model)
559
- file_import.add_import("datetime", ImportType.STDLIB)
577
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
578
+ file_import.add_import("datetime", ImportType.STDLIB)
560
579
  return file_import
561
580
 
562
581
  @property
@@ -590,6 +609,11 @@ class ByteArraySchema(PrimitiveType):
590
609
  def docstring_type(self, **kwargs: Any) -> str:
591
610
  return "bytes"
592
611
 
612
+ def type_annotation(self, **kwargs: Any) -> str:
613
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
614
+ return "str"
615
+ return "bytes"
616
+
593
617
  def get_declaration(self, value: str) -> str:
594
618
  return f'bytes("{value}", encoding="utf-8")'
595
619
 
@@ -10,7 +10,7 @@ from .constant_type import ConstantType
10
10
  from .enum_type import EnumType
11
11
  from .base import BaseType
12
12
  from .imports import FileImport, ImportType
13
- from .utils import add_to_description, add_to_pylint_disable
13
+ from .utils import add_to_description, add_to_pylint_disable, NamespaceType
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from .code_model import CodeModel
@@ -29,6 +29,7 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
29
29
  self.client_name: str = self.yaml_data["clientName"]
30
30
  self.type = type
31
31
  self.optional: bool = self.yaml_data["optional"]
32
+ self.nullable: bool = self.yaml_data.get("nullable", False)
32
33
  self.readonly: bool = self.yaml_data.get("readonly", False)
33
34
  self.visibility: list[str] = self.yaml_data.get("visibility", [])
34
35
  self.is_polymorphic: bool = self.yaml_data.get("isPolymorphic", False)
@@ -110,6 +111,12 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
110
111
  if self.is_base_discriminator:
111
112
  return "str"
112
113
  types_type_annotation = self.type.type_annotation(is_operation_file=is_operation_file, **kwargs)
114
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
115
+ # In TypedDict types.py, Optional means nullable (not "not required" — that's handled by Required/total=False)
116
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
117
+ if self.nullable:
118
+ return f"Optional[{types_type_annotation}]"
119
+ return types_type_annotation
113
120
  if (self.optional and self.client_default_value is None) or self.readonly:
114
121
  return f"Optional[{types_type_annotation}]"
115
122
  return types_type_annotation
@@ -152,15 +159,21 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
152
159
  if self.is_discriminator and isinstance(self.type, EnumType):
153
160
  return file_import
154
161
  file_import.merge(self.type.imports(**kwargs))
155
- if (self.optional and self.client_default_value is None) or self.readonly:
162
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
163
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
164
+ # In TypedDict types.py, Optional means nullable
165
+ if self.nullable:
166
+ file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB)
167
+ elif (self.optional and self.client_default_value is None) or self.readonly:
156
168
  file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB)
157
169
  if self.code_model.options["models-mode"] == "dpg":
158
- serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
159
- file_import.add_submodule_import(
160
- self.code_model.get_relative_import_path(serialize_namespace, module_name="_utils.model_base"),
161
- "rest_discriminator" if self.is_discriminator else "rest_field",
162
- ImportType.LOCAL,
163
- )
170
+ if serialize_namespace_type != NamespaceType.TYPES_FILE:
171
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
172
+ file_import.add_submodule_import(
173
+ self.code_model.get_relative_import_path(serialize_namespace, module_name="_utils.model_base"),
174
+ "rest_discriminator" if self.is_discriminator else "rest_field",
175
+ ImportType.LOCAL,
176
+ )
164
177
  return file_import
165
178
 
166
179
  @classmethod
@@ -68,7 +68,12 @@ class Response(BaseModel):
68
68
 
69
69
  def get_polymorphic_subtypes(self, polymorphic_subtypes: list["ModelType"]) -> None:
70
70
  if self.type:
71
- self.type.get_polymorphic_subtypes(polymorphic_subtypes)
71
+ if isinstance(self.type, CombinedType):
72
+ target = self.type.target_model_subtype((ModelType,))
73
+ if target:
74
+ target.get_polymorphic_subtypes(polymorphic_subtypes)
75
+ else:
76
+ self.type.get_polymorphic_subtypes(polymorphic_subtypes)
72
77
 
73
78
  def get_json_template_representation(self) -> Any:
74
79
  if not self.type:
@@ -95,6 +100,7 @@ class Response(BaseModel):
95
100
  def type_annotation(self, **kwargs: Any) -> str:
96
101
  if self.type:
97
102
  kwargs["is_operation_file"] = True
103
+ kwargs["is_response"] = True
98
104
  type_annotation = self.type.type_annotation(**kwargs)
99
105
  if self.nullable:
100
106
  return f"Optional[{type_annotation}]"
@@ -102,11 +108,13 @@ class Response(BaseModel):
102
108
  return "None"
103
109
 
104
110
  def docstring_text(self, **kwargs: Any) -> str:
111
+ kwargs["is_response"] = True
105
112
  if self.nullable and self.type:
106
113
  return f"{self.type.docstring_text(**kwargs)} or None"
107
114
  return self.type.docstring_text(**kwargs) if self.type else "None"
108
115
 
109
116
  def docstring_type(self, **kwargs: Any) -> str:
117
+ kwargs["is_response"] = True
110
118
  if self.nullable and self.type:
111
119
  return f"{self.type.docstring_type(**kwargs)} or None"
112
120
  return self.type.docstring_type(**kwargs) if self.type else "None"
@@ -121,7 +129,7 @@ class Response(BaseModel):
121
129
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
122
130
  file_import.add_submodule_import(
123
131
  self.code_model.get_relative_import_path(serialize_namespace),
124
- "_types",
132
+ "_unions",
125
133
  ImportType.LOCAL,
126
134
  TypingSection.TYPING,
127
135
  )
@@ -165,7 +173,12 @@ class PagingResponse(Response):
165
173
  )
166
174
 
167
175
  def get_polymorphic_subtypes(self, polymorphic_subtypes: list["ModelType"]) -> None:
168
- return self.item_type.get_polymorphic_subtypes(polymorphic_subtypes)
176
+ if isinstance(self.item_type, CombinedType):
177
+ target = self.item_type.target_model_subtype((ModelType,))
178
+ if target:
179
+ target.get_polymorphic_subtypes(polymorphic_subtypes)
180
+ else:
181
+ self.item_type.get_polymorphic_subtypes(polymorphic_subtypes)
169
182
 
170
183
  def get_json_template_representation(self) -> Any:
171
184
  return self.item_type.get_json_template_representation()
@@ -41,14 +41,11 @@ class NamespaceType(str, Enum):
41
41
  OPERATION = "operation"
42
42
  CLIENT = "client"
43
43
  TYPES_FILE = "types_file"
44
+ UNIONS_FILE = "unions_file"
44
45
 
45
46
 
46
47
  LOCALS_LENGTH_LIMIT = 25
47
48
 
48
- REQUEST_BUILDER_BODY_VARIABLES_LENGTH = (
49
- 6 # how many body variables are present in a request builder
50
- )
49
+ REQUEST_BUILDER_BODY_VARIABLES_LENGTH = 6 # how many body variables are present in a request builder
51
50
 
52
- OPERATION_BODY_VARIABLES_LENGTH = (
53
- 14 # how many body variables are present in an operation
54
- )
51
+ OPERATION_BODY_VARIABLES_LENGTH = 14 # how many body variables are present in an operation
@@ -33,6 +33,7 @@ from .patch_serializer import PatchSerializer
33
33
  from .sample_serializer import SampleSerializer
34
34
  from .test_serializer import TestSerializer, TestGeneralSerializer
35
35
  from .types_serializer import TypesSerializer
36
+ from .unions_serializer import UnionsSerializer
36
37
  from ...utils import to_snake_case, VALID_PACKAGE_MODE
37
38
  from .utils import extract_sample_name, get_namespace_from_package_name, get_namespace_config, hash_file_import
38
39
 
@@ -120,7 +121,7 @@ class JinjaSerializer(ReaderAndWriter):
120
121
  # If parsing the version fails, we assume the version file is not valid and overwrite.
121
122
  return False
122
123
 
123
- # pylint: disable=too-many-branches
124
+ # pylint: disable=too-many-branches,too-many-statements
124
125
  def serialize(self) -> None:
125
126
  # remove existing folders when generate from tsp
126
127
  if self.code_model.is_tsp and self.code_model.options.get("clear-output-folder"):
@@ -200,12 +201,13 @@ class JinjaSerializer(ReaderAndWriter):
200
201
  general_serializer.serialize_pkgutil_init_file(),
201
202
  )
202
203
 
203
- # _utils/py.typed/_types.py/_validation.py
204
+ # _utils/py.typed/_unions.py/types.py/_validation.py
204
205
  # is always put in top level namespace
205
206
  if self.code_model.is_top_namespace(client_namespace):
206
207
  self._serialize_and_write_top_level_folder(env=env, namespace=client_namespace)
207
208
 
208
209
  # add models folder if there are models in this namespace
210
+ is_typeddict_mode = self.code_model.options["models-mode"] == "typeddict"
209
211
  if (
210
212
  self.code_model.has_non_json_models(client_namespace_type.models) or client_namespace_type.enums
211
213
  ) and self.code_model.options["models-mode"]:
@@ -213,7 +215,27 @@ class JinjaSerializer(ReaderAndWriter):
213
215
  env=env,
214
216
  namespace=client_namespace,
215
217
  models=client_namespace_type.models,
216
- enums=client_namespace_type.enums,
218
+ enums=[] if is_typeddict_mode else client_namespace_type.enums,
219
+ )
220
+
221
+ # write types.py per namespace (alongside models/)
222
+ # Only generate types.py if at least one model/enum would be imported via types
223
+ # in operations (the model itself volunteers this via is_used_in_operations_via_types)
224
+ has_types_models = any(
225
+ m.is_used_in_operations_via_types for m in client_namespace_type.models if m.base != "json"
226
+ )
227
+ has_types_enums = any(e.is_typeddict_mode for e in client_namespace_type.enums)
228
+ if has_types_models or has_types_enums:
229
+ generation_dir = self.code_model.get_generation_dir(client_namespace)
230
+ self.write_file(
231
+ generation_dir / Path("types.py"),
232
+ TypesSerializer(
233
+ code_model=self.code_model,
234
+ env=env,
235
+ client_namespace=client_namespace,
236
+ models=client_namespace_type.models,
237
+ enums=client_namespace_type.enums if is_typeddict_mode else None,
238
+ ).serialize(),
217
239
  )
218
240
 
219
241
  if not self.code_model.options["models-mode"]:
@@ -298,11 +320,19 @@ class JinjaSerializer(ReaderAndWriter):
298
320
  ) -> None:
299
321
  # Write the models folder
300
322
  models_path = self.code_model.get_generation_dir(namespace) / "models"
301
- serializer = DpgModelSerializer if self.code_model.options["models-mode"] == "dpg" else MsrestModelSerializer
302
- if self.code_model.has_non_json_models(models):
323
+ models_mode = self.code_model.options["models-mode"]
324
+ if models_mode in ("dpg", "typeddict"):
325
+ serializer = DpgModelSerializer
326
+ else:
327
+ serializer = MsrestModelSerializer
328
+ # Filter out typed-dict-only models and typeddict copies — they only appear in types.py, not as model classes
329
+ class_models = [m for m in models if not m.is_typed_dict_only and m.base != "typeddict"]
330
+ if self.code_model.has_non_json_models(class_models):
303
331
  self.write_file(
304
332
  models_path / Path(f"{self.code_model.models_filename}.py"),
305
- serializer(code_model=self.code_model, env=env, client_namespace=namespace, models=models).serialize(),
333
+ serializer(
334
+ code_model=self.code_model, env=env, client_namespace=namespace, models=class_models
335
+ ).serialize(),
306
336
  )
307
337
  if enums:
308
338
  self.write_file(
@@ -313,7 +343,7 @@ class JinjaSerializer(ReaderAndWriter):
313
343
  )
314
344
  self.write_file(
315
345
  models_path / Path("__init__.py"),
316
- ModelInitSerializer(code_model=self.code_model, env=env, models=models, enums=enums).serialize(),
346
+ ModelInitSerializer(code_model=self.code_model, env=env, models=class_models, enums=enums).serialize(),
317
347
  )
318
348
 
319
349
  self._keep_patch_file(models_path / Path("_patch.py"), env)
@@ -442,7 +472,9 @@ class JinjaSerializer(ReaderAndWriter):
442
472
  # when there is client.py, there must be __init__.py
443
473
  self.write_file(
444
474
  generation_path / Path(f"{async_path}__init__.py"),
445
- general_serializer.serialize_init_file([c for c in clients if c.has_operations]),
475
+ general_serializer.serialize_init_file(
476
+ [c for c in clients if c.has_operations],
477
+ ),
446
478
  )
447
479
 
448
480
  # if there was a patch file before, we keep it
@@ -519,11 +551,14 @@ class JinjaSerializer(ReaderAndWriter):
519
551
  general_serializer.serialize_validation_file(),
520
552
  )
521
553
 
522
- # write _types.py
554
+ # write _unions.py
523
555
  if self.code_model.named_unions:
524
556
  self.write_file(
525
- generation_dir / Path("_types.py"),
526
- TypesSerializer(code_model=self.code_model, env=env).serialize(),
557
+ generation_dir / Path("_unions.py"),
558
+ UnionsSerializer(
559
+ code_model=self.code_model,
560
+ env=env,
561
+ ).serialize(),
527
562
  )
528
563
 
529
564
  # pylint: disable=line-too-long
@@ -196,6 +196,16 @@ def is_json_model_type(parameters: ParameterListType) -> bool:
196
196
  )
197
197
 
198
198
 
199
+ def _is_dpg_or_typeddict_body(body_param: BodyParameter) -> bool:
200
+ """Check if a body parameter is a DPG model or a CombinedType wrapping one."""
201
+ body_type = body_param.type
202
+ if isinstance(body_type, DPGModelType):
203
+ return True
204
+ if isinstance(body_type, CombinedType):
205
+ return body_type.target_model_subtype((DPGModelType,)) is not None
206
+ return False
207
+
208
+
199
209
  class _BuilderBaseSerializer(Generic[BuilderType]):
200
210
  def __init__(self, code_model: CodeModel, async_mode: bool, client_namespace: str) -> None:
201
211
  self.code_model = code_model
@@ -712,8 +722,20 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
712
722
  f"_{body_kwarg_name} = self._serialize.body({body_param.client_name}, "
713
723
  f"'{serialization_type}'{is_xml_cmd}{serialization_ctxt_cmd})"
714
724
  )
725
+ elif self.code_model.options["models-mode"] == "typeddict":
726
+ # TypedDict-only models are plain dicts — no serialization needed
727
+ create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
715
728
  elif self.code_model.options["models-mode"] == "dpg":
716
- if json_serializable(body_param.default_content_type):
729
+ # Check if this is a typeddict-only model within dpg mode — skip serialization
730
+ body_model_type = body_param.type
731
+ if isinstance(body_model_type, CombinedType):
732
+ body_model_type = body_model_type.target_model_subtype((DPGModelType,))
733
+ is_typeddict_only_body = isinstance(body_model_type, DPGModelType) and getattr(
734
+ body_model_type, "is_typed_dict_only", False
735
+ )
736
+ if is_typeddict_only_body:
737
+ create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
738
+ elif json_serializable(body_param.default_content_type):
717
739
  if hasattr(body_param.type, "encode") and body_param.type.encode: # type: ignore
718
740
  create_body_call = (
719
741
  f"_{body_kwarg_name} = json.dumps({body_param.client_name}, "
@@ -791,9 +813,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
791
813
  overload.request_builder.parameters.body_parameter.client_name for overload in builder.overloads
792
814
  ]
793
815
  all_dpg_model_overloads = False
794
- if self.code_model.options["models-mode"] == "dpg" and builder.overloads:
816
+ if self.code_model.options["models-mode"] in ("dpg", "typeddict") and builder.overloads:
795
817
  all_dpg_model_overloads = all(
796
- isinstance(o.parameters.body_parameter.type, DPGModelType) for o in builder.overloads
818
+ _is_dpg_or_typeddict_body(o.parameters.body_parameter) for o in builder.overloads
797
819
  )
798
820
  if not all_dpg_model_overloads:
799
821
  for v in sorted(set(client_names), key=client_names.index):
@@ -1002,6 +1024,12 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1002
1024
  elif self.code_model.options["models-mode"] == "dpg":
1003
1025
  if builder.has_stream_response:
1004
1026
  deserialize_code.append("deserialized = response.content")
1027
+ elif isinstance(response.type, ModelType) and response.type.is_typed_dict_only:
1028
+ # Typed-dict-only models skip deserialization — return raw JSON
1029
+ deserialize_code.append("if response.content:")
1030
+ deserialize_code.append(" deserialized = response.json()")
1031
+ deserialize_code.append("else:")
1032
+ deserialize_code.append(" deserialized = None")
1005
1033
  else:
1006
1034
  format_filed = (
1007
1035
  f', format="{response.type.encode}"'
@@ -1437,18 +1465,23 @@ class _PagingOperationSerializer(_OperationSerializer[PagingOperationType]):
1437
1465
  )
1438
1466
  pylint_disable = ""
1439
1467
  if self.code_model.options["models-mode"] == "dpg":
1440
- item_type = builder.item_type.type_annotation(
1441
- is_operation_file=True, serialize_namespace=self.serialize_namespace
1442
- )
1443
- pylint_disable = (
1444
- " # pylint: disable=protected-access" if getattr(builder.item_type, "internal", False) else ""
1445
- )
1446
- list_of_elem_deserialized = [
1447
- "_deserialize(",
1448
- f"{item_type},{pylint_disable}",
1449
- f"deserialized{access},",
1450
- ")",
1451
- ]
1468
+ is_item_typed_dict_only = isinstance(builder.item_type, ModelType) and builder.item_type.is_typed_dict_only
1469
+ if is_item_typed_dict_only:
1470
+ # Typed-dict-only models skip deserialization — return raw JSON items
1471
+ list_of_elem_deserialized = [f"deserialized{access}"]
1472
+ else:
1473
+ item_type = builder.item_type.type_annotation(
1474
+ is_operation_file=True, serialize_namespace=self.serialize_namespace
1475
+ )
1476
+ pylint_disable = (
1477
+ " # pylint: disable=protected-access" if getattr(builder.item_type, "internal", False) else ""
1478
+ )
1479
+ list_of_elem_deserialized = [
1480
+ "_deserialize(",
1481
+ f"{item_type},{pylint_disable}",
1482
+ f"deserialized{access},",
1483
+ ")",
1484
+ ]
1452
1485
  else:
1453
1486
  list_of_elem_deserialized = [f"deserialized{access}"]
1454
1487
  list_of_elem_deserialized_str = "\n ".join(list_of_elem_deserialized)
@@ -32,7 +32,7 @@ class ModelInitSerializer:
32
32
  ", ".join(model_enum_name_intersection)
33
33
  )
34
34
  )
35
- has_models = self.models
35
+ has_models = self.code_model.has_non_json_models(self.models)
36
36
  has_enums = self.enums
37
37
  template = self.env.get_template("model_init.py.jinja2")
38
38
  return template.render(