pydantic-openapi-generator 2.2.8__py3-none-any.whl

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 (31) hide show
  1. pydantic_openapi_generator/__init__.py +17 -0
  2. pydantic_openapi_generator/__main__.py +85 -0
  3. pydantic_openapi_generator/common.py +58 -0
  4. pydantic_openapi_generator/generate_data.py +235 -0
  5. pydantic_openapi_generator/language_converters/__init__.py +0 -0
  6. pydantic_openapi_generator/language_converters/python/__init__.py +0 -0
  7. pydantic_openapi_generator/language_converters/python/client_generator.py +495 -0
  8. pydantic_openapi_generator/language_converters/python/common.py +58 -0
  9. pydantic_openapi_generator/language_converters/python/exception_generator.py +23 -0
  10. pydantic_openapi_generator/language_converters/python/generator.py +61 -0
  11. pydantic_openapi_generator/language_converters/python/jinja_config.py +38 -0
  12. pydantic_openapi_generator/language_converters/python/model_generator.py +1024 -0
  13. pydantic_openapi_generator/language_converters/python/templates/alias_union.jinja2 +17 -0
  14. pydantic_openapi_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +151 -0
  15. pydantic_openapi_generator/language_converters/python/templates/discriminator_enum.jinja2 +7 -0
  16. pydantic_openapi_generator/language_converters/python/templates/enum.jinja2 +11 -0
  17. pydantic_openapi_generator/language_converters/python/templates/http_exception.jinja2 +8 -0
  18. pydantic_openapi_generator/language_converters/python/templates/models.jinja2 +24 -0
  19. pydantic_openapi_generator/language_converters/python/templates/models_pydantic_2.jinja2 +28 -0
  20. pydantic_openapi_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +150 -0
  21. pydantic_openapi_generator/models.py +86 -0
  22. pydantic_openapi_generator/parsers/__init__.py +13 -0
  23. pydantic_openapi_generator/parsers/openapi_30.py +65 -0
  24. pydantic_openapi_generator/parsers/openapi_31.py +65 -0
  25. pydantic_openapi_generator/py.typed +0 -0
  26. pydantic_openapi_generator/version_detector.py +67 -0
  27. pydantic_openapi_generator-2.2.8.dist-info/METADATA +141 -0
  28. pydantic_openapi_generator-2.2.8.dist-info/RECORD +31 -0
  29. pydantic_openapi_generator-2.2.8.dist-info/WHEEL +4 -0
  30. pydantic_openapi_generator-2.2.8.dist-info/entry_points.txt +2 -0
  31. pydantic_openapi_generator-2.2.8.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1024 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import re
5
+ from dataclasses import dataclass
6
+ from typing import Dict, List, Optional, Set, Tuple, Union
7
+
8
+ import click
9
+ from openapi_pydantic.v3 import Operation, PathItem
10
+ from openapi_pydantic.v3.v3_0 import (
11
+ Components as Components30,
12
+ )
13
+ from openapi_pydantic.v3.v3_0 import (
14
+ Reference as Reference30,
15
+ )
16
+ from openapi_pydantic.v3.v3_0 import Response as Response30
17
+ from openapi_pydantic.v3.v3_0 import (
18
+ Schema as Schema30,
19
+ )
20
+ from openapi_pydantic.v3.v3_1 import (
21
+ Components as Components31,
22
+ )
23
+ from openapi_pydantic.v3.v3_1 import (
24
+ Reference as Reference31,
25
+ )
26
+ from openapi_pydantic.v3.v3_1 import Response as Response31
27
+ from openapi_pydantic.v3.v3_1 import (
28
+ Schema as Schema31,
29
+ )
30
+
31
+ from pydantic_openapi_generator.common import PydanticVersion
32
+ from pydantic_openapi_generator.language_converters.python import common
33
+ from pydantic_openapi_generator.language_converters.python.jinja_config import (
34
+ ALIAS_UNION_TEMPLATE,
35
+ DISCRIMINATOR_ENUM_TEMPLATE,
36
+ ENUM_TEMPLATE,
37
+ MODELS_TEMPLATE,
38
+ MODELS_TEMPLATE_PYDANTIC_V2,
39
+ create_jinja_env,
40
+ )
41
+ from pydantic_openapi_generator.models import Model, Property, TypeConversion
42
+
43
+ # Type aliases for compatibility
44
+ Schema = Union[Schema30, Schema31]
45
+ Reference = Union[Reference30, Reference31]
46
+ Components = Union[Components30, Components31]
47
+
48
+
49
+ # Map of wrapper component name -> TypeConversion to use instead of generating wrapper module
50
+ _REFERENCE_TYPE_OVERRIDES: dict[str, TypeConversion] = {}
51
+
52
+
53
+ def _is_null_schema(s: object) -> bool:
54
+ t = getattr(s, "type", None)
55
+ return t == "null" or str(t) == "DataType.NULL"
56
+
57
+
58
+ def _build_nullable_wrapper_overrides(components: Components) -> dict[str, TypeConversion]:
59
+ """
60
+ Collapse component schemas shaped like:
61
+ X = anyOf/oneOf: [ $ref: Y, {type: null} ]
62
+ into an override so refs to X become Optional[Y] without generating X.py.
63
+ """
64
+ overrides: dict[str, TypeConversion] = {}
65
+ schemas = getattr(components, "schemas", None)
66
+ if not schemas:
67
+ return overrides
68
+
69
+ for schema_name, schema in schemas.items():
70
+ # Only non-discriminator wrappers
71
+ if getattr(schema, "discriminator", None) is not None:
72
+ continue
73
+
74
+ variants = getattr(schema, "anyOf", None) or getattr(schema, "oneOf", None)
75
+ if not variants or len(variants) != 2:
76
+ continue
77
+
78
+ ref = next((v for v in variants if isinstance(v, (Reference30, Reference31))), None)
79
+ nul = next((v for v in variants if isinstance(v, (Schema30, Schema31)) and _is_null_schema(v)), None)
80
+ if ref is None or nul is None:
81
+ continue
82
+
83
+ wrapper_name = common.normalize_symbol(schema_name)
84
+ target_model = common.normalize_symbol(ref.ref.split("/")[-1])
85
+
86
+ overrides[wrapper_name] = TypeConversion(
87
+ original_type=ref.ref,
88
+ converted_type=f"Optional[{target_model}]",
89
+ import_types=[f"from .{target_model} import {target_model}"],
90
+ )
91
+
92
+ return overrides
93
+
94
+
95
+ def _get_discriminator_key(schema: Schema) -> Optional[str]:
96
+ """
97
+ Return discriminator property name if present on the schema.
98
+ openapi-pydantic v3.x uses `schema.discriminator.propertyName` (common),
99
+ but we defensively check a couple of variants.
100
+ """
101
+ disc = getattr(schema, "discriminator", None)
102
+ if disc is None:
103
+ return None
104
+
105
+ # Most common: propertyName
106
+ key = getattr(disc, "propertyName", None)
107
+ if key:
108
+ return str(key)
109
+
110
+ # Defensive fallbacks
111
+ key = getattr(disc, "property_name", None)
112
+ if key:
113
+ return str(key)
114
+
115
+ return None
116
+
117
+
118
+ def _schema_is_union(schema: Schema) -> bool:
119
+ used = schema.oneOf if schema.oneOf is not None else schema.anyOf
120
+ return used is not None and len(used) > 0
121
+
122
+
123
+ def _alias_name_for_property(prop_name: str) -> str:
124
+ # token_issuer -> TokenIssuer, foo-bar -> FooBar, etc.
125
+ parts = re.split(r"[^a-zA-Z0-9]+", prop_name.strip())
126
+ parts = [p for p in parts if p]
127
+ return "".join(p[:1].upper() + p[1:] for p in parts)
128
+
129
+
130
+ def _dedupe_imports(imports: Optional[List[str]]) -> List[str]:
131
+ if not imports:
132
+ return []
133
+ seen: Set[str] = set()
134
+ out: List[str] = []
135
+ for imp in imports:
136
+ if imp and imp not in seen:
137
+ seen.add(imp)
138
+ out.append(imp)
139
+ return out
140
+
141
+
142
+ def _render_union_alias_module(
143
+ *,
144
+ jinja_env,
145
+ alias_name: str,
146
+ union_type: str,
147
+ discriminator_key: Optional[str],
148
+ member_imports: List[str],
149
+ ) -> str:
150
+ return jinja_env.get_template(ALIAS_UNION_TEMPLATE).render(
151
+ alias_name=alias_name,
152
+ union_type=union_type,
153
+ discriminator_key=discriminator_key,
154
+ member_imports=_dedupe_imports(member_imports),
155
+ )
156
+
157
+
158
+ @dataclass(frozen=True)
159
+ class DiscriminatorBinding:
160
+ enum_name: str
161
+ enum_member: str
162
+ discriminator_key: str
163
+
164
+
165
+ def _enum_member_name(value: str) -> str:
166
+ # Make a safe enum member name, e.g. "pkce" -> "PKCE", "oauth2" -> "OAUTH2"
167
+ return common.normalize_symbol(str(value)).upper()
168
+
169
+
170
+ def _pascal_discriminator(discriminator_key: str) -> str:
171
+ """
172
+ Convert a discriminator property name (e.g. "type", "kind", "event_type")
173
+ into a PascalCase suffix ("Type", "Kind", "EventType").
174
+ """
175
+ sym = common.normalize_symbol(discriminator_key)
176
+ parts = [p for p in sym.replace("-", "_").split("_") if p]
177
+ return "".join(p[:1].upper() + p[1:] for p in parts) or "Discriminator"
178
+
179
+
180
+ def _pascal_schema_name(schema_name: str) -> str:
181
+ """
182
+ Convert a schema name like "token_issuer" into "TokenIssuer" for generated type/enum names.
183
+ (We don't want enum class names like `token_issuerType`.)
184
+ """
185
+ sym = common.normalize_symbol(schema_name)
186
+ parts = [p for p in sym.replace("-", "_").split("_") if p]
187
+ return "".join(p[:1].upper() + p[1:] for p in parts) or "Schema"
188
+
189
+
190
+ def _invert_discriminator_mapping(mapping: Optional[dict]) -> Dict[str, str]:
191
+ """
192
+ discriminator.mapping is usually { "<disc_value>": "<$ref>" }
193
+ Return { "<$ref>": "<disc_value>" }
194
+ """
195
+ if not mapping:
196
+ return {}
197
+ inv: Dict[str, str] = {}
198
+ for k, v in mapping.items():
199
+ if isinstance(v, str):
200
+ inv[v] = str(k)
201
+ return inv
202
+
203
+
204
+ def _discover_discriminated_unions(
205
+ components: Components,
206
+ ) -> Tuple[Dict[str, DiscriminatorBinding], Dict[str, List[Tuple[str, str]]]]:
207
+ """
208
+ Discover discriminated unions in BOTH:
209
+ - top-level component schemas (schema.discriminator + schema.oneOf)
210
+ - inline/property schemas (property_schema.discriminator + property_schema.oneOf)
211
+
212
+ Returns:
213
+ - bindings: { "<MemberModelName>": DiscriminatorBinding(...) }
214
+ - enum_members_by_name: { "<EnumName>": [(MEMBER_NAME, member_value), ...] }
215
+ """
216
+ bindings: Dict[str, DiscriminatorBinding] = {}
217
+ enum_members_by_name: Dict[str, List[Tuple[str, str]]] = {}
218
+
219
+ if not getattr(components, "schemas", None):
220
+ return bindings, enum_members_by_name
221
+
222
+ def register_union(alias_name: str, union_schema: Schema) -> None:
223
+ disc = getattr(union_schema, "discriminator", None)
224
+ one_of = getattr(union_schema, "oneOf", None)
225
+ if disc is None or not one_of:
226
+ return
227
+
228
+ # openapi_pydantic uses propertyName, but be defensive
229
+ discriminator_key = getattr(disc, "propertyName", None) or getattr(disc, "property_name", None)
230
+ if not discriminator_key:
231
+ return
232
+
233
+ enum_name = f"{_pascal_schema_name(alias_name)}{_pascal_discriminator(discriminator_key)}"
234
+ ref_to_value = _invert_discriminator_mapping(getattr(disc, "mapping", None))
235
+
236
+ members: List[Tuple[str, str]] = []
237
+ for sub in one_of:
238
+ if not isinstance(sub, (Reference30, Reference31)):
239
+ continue
240
+ ref = sub.ref
241
+ member_model = common.normalize_symbol(ref.split("/")[-1])
242
+ disc_value = ref_to_value.get(ref) or member_model
243
+
244
+ member_name = _enum_member_name(disc_value)
245
+ members.append((member_name, disc_value))
246
+
247
+ bindings[member_model] = DiscriminatorBinding(
248
+ enum_name=enum_name,
249
+ enum_member=member_name,
250
+ discriminator_key=discriminator_key,
251
+ )
252
+
253
+ if members:
254
+ # de-dupe by enum member name
255
+ seen = set()
256
+ deduped: List[Tuple[str, str]] = []
257
+ for mn, mv in members:
258
+ if mn in seen:
259
+ continue
260
+ seen.add(mn)
261
+ deduped.append((mn, mv))
262
+ enum_members_by_name[enum_name] = deduped
263
+
264
+ # 1) top-level discriminated unions
265
+ for schema_name, schema in components.schemas.items():
266
+ disc = getattr(schema, "discriminator", None)
267
+ one_of = getattr(schema, "oneOf", None)
268
+ if disc is not None and one_of:
269
+ register_union(schema_name, schema)
270
+
271
+ # 2) inline/property discriminated unions
272
+ for _parent_name, parent_schema in components.schemas.items():
273
+ props = getattr(parent_schema, "properties", None) or {}
274
+ for prop_name, prop_schema in props.items():
275
+ disc = getattr(prop_schema, "discriminator", None)
276
+ one_of = getattr(prop_schema, "oneOf", None)
277
+ if disc is not None and one_of:
278
+ # alias name should be based on the property name (e.g. oauth2_client -> OAuth2Client)
279
+ register_union(prop_name, prop_schema)
280
+
281
+ return bindings, enum_members_by_name
282
+
283
+
284
+ def _build_discriminator_bindings(components: Components) -> Dict[str, DiscriminatorBinding]:
285
+ """
286
+ Scan components.schemas for discriminator-based oneOf schemas and return:
287
+ { "<MemberModelName>": DiscriminatorBinding(...) }
288
+
289
+ We use:
290
+ - discriminator.propertyName as the key
291
+ - discriminator.mapping (preferred) to get per-member discriminator values
292
+ - fallback: schema name when mapping not present
293
+ """
294
+ bindings: Dict[str, DiscriminatorBinding] = {}
295
+
296
+ if not getattr(components, "schemas", None):
297
+ return bindings
298
+
299
+ for schema_name, schema in components.schemas.items():
300
+ disc = getattr(schema, "discriminator", None)
301
+ one_of = getattr(schema, "oneOf", None)
302
+
303
+ if disc is None or not one_of:
304
+ continue
305
+
306
+ discriminator_key = getattr(disc, "propertyName", None) or getattr(disc, "property_name", None)
307
+ if discriminator_key is None:
308
+ continue
309
+
310
+ enum_name = f"{_pascal_schema_name(schema_name)}{_pascal_discriminator(discriminator_key)}"
311
+
312
+ mapping = getattr(disc, "mapping", None) or {}
313
+ # invert mapping to get $ref -> value
314
+ ref_to_value: Dict[str, str] = {ref: value for value, ref in mapping.items()}
315
+
316
+ for sub in one_of:
317
+ if not (isinstance(sub, Reference30) or isinstance(sub, Reference31)):
318
+ continue
319
+
320
+ ref = sub.ref
321
+ member_model = common.normalize_symbol(ref.split("/")[-1])
322
+
323
+ disc_value = ref_to_value.get(ref)
324
+ if disc_value is None:
325
+ disc_value = member_model
326
+
327
+ bindings[member_model] = DiscriminatorBinding(
328
+ enum_name=enum_name,
329
+ enum_member=_enum_member_name(disc_value),
330
+ discriminator_key=discriminator_key,
331
+ )
332
+
333
+ return bindings
334
+
335
+
336
+ def _common_suffix(a: str, b: str) -> str:
337
+ # longest common suffix
338
+ i = 1
339
+ while i <= min(len(a), len(b)) and a[-i] == b[-i]:
340
+ i += 1
341
+ return a[-(i - 1) :] if i > 1 else ""
342
+
343
+
344
+ def _common_suffix_many(names: List[str]) -> str:
345
+ if not names:
346
+ return ""
347
+ suf = names[0]
348
+ for n in names[1:]:
349
+ suf = _common_suffix(suf, n)
350
+ if not suf:
351
+ break
352
+ return suf
353
+
354
+
355
+ def _alias_name_for_response_union(member_models: List[str], fallback: str) -> str:
356
+ suf = _common_suffix_many(member_models)
357
+ suf = common.normalize_symbol(suf)
358
+ if len(suf) >= 4:
359
+ return suf
360
+ return common.normalize_symbol(fallback)
361
+
362
+
363
+ def generate_response_union_alias_models(
364
+ paths: Dict[str, PathItem],
365
+ pydantic_version: PydanticVersion = PydanticVersion.V2,
366
+ ) -> List[Model]:
367
+ """
368
+ Finds response schemas like:
369
+ content.application/json.schema.oneOf + discriminator
370
+ and emits a named alias module via alias_union.jinja2.
371
+ """
372
+ jinja_env = create_jinja_env()
373
+ out: Dict[str, Model] = {}
374
+
375
+ for _path_name, path in paths.items():
376
+ for http_method in ["get", "post", "put", "delete", "patch", "head", "options", "trace"]:
377
+ op: Optional[Operation] = getattr(path, http_method, None)
378
+ if op is None or op.responses is None:
379
+ continue
380
+
381
+ for status_code, resp in op.responses.items():
382
+ if not str(status_code).startswith("2"):
383
+ continue
384
+ if not isinstance(resp, (Response30, Response31)):
385
+ continue
386
+
387
+ content = getattr(resp, "content", None)
388
+ if not isinstance(content, dict):
389
+ continue
390
+
391
+ mt = content.get("application/json")
392
+ if mt is None:
393
+ continue
394
+
395
+ schema = getattr(mt, "media_type_schema", None)
396
+ if not isinstance(schema, (Schema30, Schema31)):
397
+ continue
398
+
399
+ disc_key = _get_discriminator_key(schema)
400
+ used = schema.oneOf if schema.oneOf is not None else schema.anyOf
401
+ if not disc_key or not used:
402
+ continue
403
+
404
+ # Only support ref-only unions (your case)
405
+ member_models: List[str] = []
406
+ for sub in used:
407
+ if not isinstance(sub, (Reference30, Reference31)):
408
+ member_models = []
409
+ break
410
+ member_models.append(common.normalize_symbol(sub.ref.split("/")[-1]))
411
+ if len(member_models) < 2:
412
+ continue
413
+
414
+ alias_name = _alias_name_for_response_union(
415
+ member_models,
416
+ fallback=f"{common.normalize_symbol(op.operationId or 'Response')}Response",
417
+ )
418
+
419
+ # de-dupe
420
+ if alias_name in out:
421
+ continue
422
+
423
+ union_type = "Union[" + ", ".join(member_models) + "]"
424
+ member_imports = [f"from .{m} import {m}" for m in member_models]
425
+
426
+ alias_content = _render_union_alias_module(
427
+ jinja_env=jinja_env,
428
+ alias_name=alias_name,
429
+ union_type=union_type,
430
+ discriminator_key=disc_key,
431
+ member_imports=member_imports,
432
+ )
433
+
434
+ # placeholder schema for Model.openapi_object requirement
435
+ placeholder_schema = Schema31() if isinstance(schema, Schema31) else Schema30()
436
+
437
+ out[alias_name] = Model(
438
+ file_name=alias_name,
439
+ content=alias_content,
440
+ openapi_object=placeholder_schema,
441
+ properties=[],
442
+ )
443
+
444
+ return list(out.values())
445
+
446
+
447
+ def type_converter( # noqa: C901
448
+ schema: Union[Schema, Reference],
449
+ required: bool = False,
450
+ model_name: Optional[str] = None,
451
+ ) -> TypeConversion:
452
+ """
453
+ Converts an OpenAPI type to a Python type.
454
+ :param schema: Schema or Reference containing the type to be converted
455
+ :param model_name: Name of the original model on which the type is defined
456
+ :param required: Flag indicating if the type is required by the class
457
+ :return: The converted type
458
+ """
459
+ # Handle Reference objects by converting them to type references
460
+ if isinstance(schema, Reference30) or isinstance(schema, Reference31):
461
+ import_type = common.normalize_symbol(schema.ref.split("/")[-1])
462
+ # Nullable-wrapper collapse: ref to X may be overridden to Optional[Y]
463
+ override = _REFERENCE_TYPE_OVERRIDES.get(import_type)
464
+ if override is not None:
465
+ return TypeConversion(
466
+ original_type=schema.ref,
467
+ converted_type=override.converted_type,
468
+ import_types=override.import_types,
469
+ )
470
+
471
+ if required:
472
+ converted_type = import_type
473
+ else:
474
+ converted_type = f"Optional[{import_type}]"
475
+
476
+ return TypeConversion(
477
+ original_type=schema.ref,
478
+ converted_type=converted_type,
479
+ import_types=([f"from .{import_type} import {import_type}"] if import_type != model_name else None),
480
+ )
481
+
482
+ if required:
483
+ pre_type = ""
484
+ post_type = ""
485
+ else:
486
+ pre_type = "Optional["
487
+ post_type = "]"
488
+
489
+ original_type = (
490
+ schema.type.value
491
+ if hasattr(schema.type, "value") and schema.type is not None
492
+ else str(schema.type)
493
+ if schema.type is not None
494
+ else "object"
495
+ )
496
+ import_types: Optional[List[str]] = None
497
+
498
+ if schema.allOf is not None:
499
+ conversions = []
500
+ for sub_schema in schema.allOf:
501
+ if isinstance(sub_schema, Schema30) or isinstance(sub_schema, Schema31):
502
+ conversions.append(type_converter(sub_schema, True))
503
+ else:
504
+ import_type = common.normalize_symbol(sub_schema.ref.split("/")[-1])
505
+ if import_type == model_name and model_name is not None:
506
+ conversions.append(
507
+ TypeConversion(
508
+ original_type=sub_schema.ref,
509
+ converted_type='"' + model_name + '"',
510
+ import_types=None,
511
+ )
512
+ )
513
+ else:
514
+ import_types = [f"from .{import_type} import {import_type}"]
515
+ conversions.append(
516
+ TypeConversion(
517
+ original_type=sub_schema.ref,
518
+ converted_type=import_type,
519
+ import_types=import_types,
520
+ )
521
+ )
522
+
523
+ original_type = "tuple<" + ",".join([i.original_type for i in conversions]) + ">"
524
+ if len(conversions) == 1:
525
+ converted_type = conversions[0].converted_type
526
+ else:
527
+ converted_type = "Tuple[" + ",".join([i.converted_type for i in conversions]) + "]"
528
+
529
+ converted_type = pre_type + converted_type + post_type
530
+ # Collect *all* imports from sub-schemas (not just the first), then dedupe
531
+ import_types = (
532
+ _dedupe_imports(list(itertools.chain.from_iterable(i.import_types for i in conversions if i.import_types)))
533
+ or None
534
+ )
535
+
536
+ elif schema.oneOf is not None or schema.anyOf is not None:
537
+ used = schema.oneOf if schema.oneOf is not None else schema.anyOf
538
+ used = used if used is not None else []
539
+ # Special-case inline nullable wrapper: (ref | null) => Optional[Ref]
540
+ if len(used) == 2:
541
+ ref = next((v for v in used if isinstance(v, (Reference30, Reference31))), None)
542
+ nul = next((v for v in used if isinstance(v, (Schema30, Schema31)) and _is_null_schema(v)), None)
543
+ if ref is not None and nul is not None:
544
+ import_type = common.normalize_symbol(ref.ref.split("/")[-1])
545
+ override = _REFERENCE_TYPE_OVERRIDES.get(import_type)
546
+ if override is not None:
547
+ return TypeConversion(
548
+ original_type=f"union<{ref.ref},null>",
549
+ converted_type=override.converted_type,
550
+ import_types=override.import_types,
551
+ )
552
+ return TypeConversion(
553
+ original_type=f"union<{ref.ref},null>",
554
+ converted_type=f"Optional[{import_type}]",
555
+ import_types=([f"from .{import_type} import {import_type}"] if import_type != model_name else None),
556
+ )
557
+ conversions = []
558
+ for sub_schema in used:
559
+ if isinstance(sub_schema, Schema30) or isinstance(sub_schema, Schema31):
560
+ conversions.append(type_converter(sub_schema, True))
561
+ else:
562
+ import_type = common.normalize_symbol(sub_schema.ref.split("/")[-1])
563
+ import_types = [f"from .{import_type} import {import_type}"]
564
+ conversions.append(
565
+ TypeConversion(
566
+ original_type=sub_schema.ref,
567
+ converted_type=import_type,
568
+ import_types=import_types,
569
+ )
570
+ )
571
+ original_type = "union<" + ",".join([i.original_type for i in conversions]) + ">"
572
+
573
+ if len(conversions) == 1:
574
+ converted_type = conversions[0].converted_type
575
+ else:
576
+ converted_type = "Union[" + ",".join([i.converted_type for i in conversions]) + "]"
577
+
578
+ converted_type = pre_type + converted_type + post_type
579
+ import_types = list(itertools.chain(*[i.import_types for i in conversions if i.import_types is not None]))
580
+ # We only want to auto convert to datetime if orjson is used throghout the code, otherwise we can not
581
+ # serialize it to JSON.
582
+ elif (schema.type == "string" or str(schema.type) == "DataType.STRING") and (
583
+ schema.schema_format is None or not common.get_use_orjson()
584
+ ):
585
+ converted_type = pre_type + "str" + post_type
586
+ elif (
587
+ (schema.type == "string" or str(schema.type) == "DataType.STRING")
588
+ and schema.schema_format is not None
589
+ and schema.schema_format.startswith("uuid")
590
+ and common.get_use_orjson()
591
+ ):
592
+ if len(schema.schema_format) > 4 and schema.schema_format[4].isnumeric():
593
+ uuid_type = schema.schema_format.upper()
594
+ converted_type = pre_type + uuid_type + post_type
595
+ import_types = ["from pydantic import " + uuid_type]
596
+ else:
597
+ converted_type = pre_type + "UUID" + post_type
598
+ import_types = ["from uuid import UUID"]
599
+ elif (schema.type == "string" or str(schema.type) == "DataType.STRING") and schema.schema_format == "date-time":
600
+ converted_type = pre_type + "datetime" + post_type
601
+ import_types = ["from datetime import datetime"]
602
+ elif schema.type == "integer" or str(schema.type) == "DataType.INTEGER":
603
+ converted_type = pre_type + "int" + post_type
604
+ elif schema.type == "number" or str(schema.type) == "DataType.NUMBER":
605
+ converted_type = pre_type + "float" + post_type
606
+ elif schema.type == "boolean" or str(schema.type) == "DataType.BOOLEAN":
607
+ converted_type = pre_type + "bool" + post_type
608
+ elif schema.type == "array" or str(schema.type) == "DataType.ARRAY":
609
+ retVal = pre_type + "List["
610
+ item_imports: List[str] = []
611
+ if isinstance(schema.items, Reference30) or isinstance(schema.items, Reference31):
612
+ converted_reference = _generate_property_from_reference(
613
+ model_name or "", "", schema.items, schema, required
614
+ )
615
+ import_types = converted_reference.type.import_types
616
+ original_type = "array<" + converted_reference.type.original_type + ">"
617
+ retVal += converted_reference.type.converted_type
618
+ elif isinstance(schema.items, Schema30) or isinstance(schema.items, Schema31):
619
+ type_str = schema.items.type
620
+ if hasattr(type_str, "value"):
621
+ type_value = str(type_str.value) if type_str is not None else "unknown"
622
+ else:
623
+ type_value = str(type_str) if type_str is not None else "unknown"
624
+ original_type = "array<" + type_value + ">"
625
+ # IMPORTANT: propagate imports from the nested schema (e.g. Union[$ref...])
626
+ item_conv = type_converter(schema.items, True, model_name=model_name)
627
+ retVal += item_conv.converted_type
628
+ if item_conv.import_types:
629
+ item_imports.extend(item_conv.import_types)
630
+ else:
631
+ original_type = "array<unknown>"
632
+ retVal += "Any"
633
+
634
+ converted_type = retVal + "]" + post_type
635
+
636
+ # Merge imports from the items schema (when items is a Schema, not a Reference)
637
+ if item_imports:
638
+ import_types = _dedupe_imports((import_types or []) + item_imports)
639
+ elif schema.type == "object" or str(schema.type) == "DataType.OBJECT":
640
+ # Support "map" objects: type=object + additionalProperties schema/ref
641
+ addl = getattr(schema, "additionalProperties", None)
642
+ if isinstance(addl, (Reference30, Reference31)):
643
+ # Dict[str, <ref>]
644
+ v = type_converter(addl, required=True, model_name=model_name)
645
+ converted_type = f"{pre_type}Dict[str, {v.converted_type}]{post_type}"
646
+ import_types = _dedupe_imports((import_types or []) + (v.import_types or [])) or None
647
+ elif isinstance(addl, (Schema30, Schema31)):
648
+ # Dict[str, <schema>], including Union[...] etc.
649
+ v = type_converter(addl, required=True, model_name=model_name)
650
+ converted_type = f"{pre_type}Dict[str, {v.converted_type}]{post_type}"
651
+ import_types = _dedupe_imports((import_types or []) + (v.import_types or [])) or None
652
+ else:
653
+ converted_type = pre_type + "Dict[str, Any]" + post_type
654
+ elif schema.type == "null" or str(schema.type) == "DataType.NULL":
655
+ converted_type = pre_type + "None" + post_type
656
+ elif schema.type is None:
657
+ converted_type = pre_type + "Any" + post_type
658
+ else:
659
+ # Handle DataType enum types as strings
660
+ if hasattr(schema.type, "value"):
661
+ # Single DataType enum
662
+ if schema.type.value == "string":
663
+ # Check for UUID format first
664
+ if (
665
+ schema.schema_format is not None
666
+ and schema.schema_format.startswith("uuid")
667
+ and common.get_use_orjson()
668
+ ):
669
+ if len(schema.schema_format) > 4 and schema.schema_format[4].isnumeric():
670
+ uuid_type = schema.schema_format.upper()
671
+ converted_type = pre_type + uuid_type + post_type
672
+ import_types = ["from pydantic import " + uuid_type]
673
+ else:
674
+ converted_type = pre_type + "UUID" + post_type
675
+ import_types = ["from uuid import UUID"]
676
+ # Check for date-time format
677
+ elif schema.schema_format == "date-time":
678
+ converted_type = pre_type + "datetime" + post_type
679
+ import_types = ["from datetime import datetime"]
680
+ else:
681
+ converted_type = pre_type + "str" + post_type
682
+ elif schema.type.value == "integer":
683
+ converted_type = pre_type + "int" + post_type
684
+ elif schema.type.value == "number":
685
+ converted_type = pre_type + "float" + post_type
686
+ elif schema.type.value == "boolean":
687
+ converted_type = pre_type + "bool" + post_type
688
+ elif schema.type.value == "array":
689
+ converted_type = pre_type + "List[Any]" + post_type
690
+ elif schema.type.value == "object":
691
+ converted_type = pre_type + "Dict[str, Any]" + post_type
692
+ elif schema.type.value == "null":
693
+ converted_type = pre_type + "None" + post_type
694
+ else:
695
+ converted_type = pre_type + "str" + post_type # Default fallback
696
+ elif isinstance(schema.type, list) and len(schema.type) > 0:
697
+ # List of DataType enums - use first one
698
+ first_type = schema.type[0]
699
+ if hasattr(first_type, "value"):
700
+ if first_type.value == "string":
701
+ # Check for UUID format first
702
+ if (
703
+ schema.schema_format is not None
704
+ and schema.schema_format.startswith("uuid")
705
+ and common.get_use_orjson()
706
+ ):
707
+ if len(schema.schema_format) > 4 and schema.schema_format[4].isnumeric():
708
+ uuid_type = schema.schema_format.upper()
709
+ converted_type = pre_type + uuid_type + post_type
710
+ import_types = ["from pydantic import " + uuid_type]
711
+ else:
712
+ converted_type = pre_type + "UUID" + post_type
713
+ import_types = ["from uuid import UUID"]
714
+ # Check for date-time format
715
+ elif schema.schema_format == "date-time":
716
+ converted_type = pre_type + "datetime" + post_type
717
+ import_types = ["from datetime import datetime"]
718
+ else:
719
+ converted_type = pre_type + "str" + post_type
720
+ elif first_type.value == "integer":
721
+ converted_type = pre_type + "int" + post_type
722
+ elif first_type.value == "number":
723
+ converted_type = pre_type + "float" + post_type
724
+ elif first_type.value == "boolean":
725
+ converted_type = pre_type + "bool" + post_type
726
+ elif first_type.value == "array":
727
+ converted_type = pre_type + "List[Any]" + post_type
728
+ elif first_type.value == "object":
729
+ converted_type = pre_type + "Dict[str, Any]" + post_type
730
+ elif first_type.value == "null":
731
+ converted_type = pre_type + "None" + post_type
732
+ else:
733
+ converted_type = pre_type + "str" + post_type # Default fallback
734
+ else:
735
+ converted_type = pre_type + "str" + post_type # Default fallback
736
+ else:
737
+ converted_type = pre_type + "str" + post_type # Default fallback
738
+
739
+ return TypeConversion(
740
+ original_type=original_type,
741
+ converted_type=converted_type,
742
+ import_types=import_types,
743
+ )
744
+
745
+
746
+ def _generate_property_from_schema(
747
+ model_name: str, name: str, schema: Schema, parent_schema: Optional[Schema] = None
748
+ ) -> Property:
749
+ """
750
+ Generates a property from a schema. It takes the type of the schema and converts it to a python type, and then
751
+ creates the according property.
752
+ :param model_name: Name of the model this property belongs to
753
+ :param name: Name of the schema
754
+ :param schema: schema to be converted
755
+ :param parent_schema: Component this belongs to
756
+ :return: Property
757
+ """
758
+ required = parent_schema is not None and parent_schema.required is not None and name in parent_schema.required
759
+
760
+ # Pick up OpenAPI default (if present)
761
+ default_val = getattr(schema, "default", None)
762
+
763
+ if required:
764
+ rendered_default = None
765
+ else:
766
+ # If OpenAPI provides a default, use it. Otherwise fall back to None.
767
+ rendered_default = repr(default_val) if default_val is not None else "None"
768
+
769
+ return Property(
770
+ name=name,
771
+ type=type_converter(schema, required, model_name),
772
+ required=required,
773
+ default=rendered_default,
774
+ import_type=None if not required else ([] if name == model_name else [name]),
775
+ )
776
+
777
+
778
+ def _generate_property_from_reference(
779
+ model_name: str,
780
+ name: str,
781
+ reference: Reference,
782
+ parent_schema: Optional[Schema] = None,
783
+ force_required: bool = False,
784
+ ) -> Property:
785
+ """
786
+ Generates a property from a reference. It takes the name of the reference as the type, and then
787
+ returns a property type
788
+ :param name: Name of the schema
789
+ :param reference: reference to be converted
790
+ :param parent_schema: Component this belongs to
791
+ :param force_required: Force the property to be required
792
+ :return: Property and model to be imported by the file
793
+ """
794
+ required = (
795
+ parent_schema is not None and parent_schema.required is not None and name in parent_schema.required
796
+ ) or force_required
797
+ import_model = common.normalize_symbol(reference.ref.split("/")[-1])
798
+
799
+ if import_model == model_name:
800
+ type_conv = TypeConversion(
801
+ original_type=reference.ref,
802
+ converted_type=(import_model if required else 'Optional["' + import_model + '"]'),
803
+ import_types=None,
804
+ )
805
+ else:
806
+ type_conv = TypeConversion(
807
+ original_type=reference.ref,
808
+ converted_type=(import_model if required else "Optional[" + import_model + "]"),
809
+ import_types=[f"from .{import_model} import {import_model}"],
810
+ )
811
+ return Property(
812
+ name=name,
813
+ type=type_conv,
814
+ required=required,
815
+ default=None if required else "None",
816
+ import_type=[import_model],
817
+ )
818
+
819
+
820
+ def generate_models(components: Components, pydantic_version: PydanticVersion = PydanticVersion.V2) -> List[Model]:
821
+ """
822
+ Receives components from an OpenAPI 3.0+ specification and generates the models from it.
823
+ Additionally:
824
+ - Detects unions / discriminated unions in property schemas (oneOf/anyOf)
825
+ - Emits a named alias module (e.g. TokenIssuer.py)
826
+ - Rewrites the property type to use that alias (instead of Union[...])
827
+ """
828
+ models: List[Model] = []
829
+
830
+ if components.schemas is None:
831
+ return models
832
+
833
+ jinja_env = create_jinja_env()
834
+ # Build nullable-wrapper overrides so refs to simple wrappers (X = anyOf[$ref Y, null])
835
+ # are collapsed to Optional[Y] and we avoid generating X.py (which can collide on Windows).
836
+ global _REFERENCE_TYPE_OVERRIDES
837
+ _REFERENCE_TYPE_OVERRIDES = _build_nullable_wrapper_overrides(components)
838
+
839
+ discriminator_bindings, enum_members_by_name = _discover_discriminated_unions(components)
840
+
841
+ # Track alias modules so we only create each once
842
+ alias_models_by_name: Dict[str, Model] = {}
843
+
844
+ for schema_name, schema_or_reference in components.schemas.items():
845
+ name = common.normalize_symbol(schema_name)
846
+
847
+ # Don't generate standalone modules for nullable wrapper components
848
+ if name in _REFERENCE_TYPE_OVERRIDES:
849
+ continue
850
+
851
+ # --------------------------
852
+ # Enums
853
+ # --------------------------
854
+ if schema_or_reference.enum is not None:
855
+ value_dict = schema_or_reference.model_dump()
856
+ value_dict["enum"] = [(common.normalize_symbol(str(i)).upper(), i) for i in value_dict["enum"]]
857
+ m = Model(
858
+ file_name=name,
859
+ content=jinja_env.get_template(ENUM_TEMPLATE).render(name=name, **value_dict),
860
+ openapi_object=schema_or_reference,
861
+ properties=[],
862
+ )
863
+ try:
864
+ compile(m.content, "<string>", "exec")
865
+ models.append(m)
866
+ except SyntaxError as e: # pragma: no cover
867
+ click.echo(f"Error in model {name}: {e}")
868
+
869
+ continue # pragma: no cover
870
+
871
+ # --------------------------
872
+ # Normal models
873
+ # --------------------------
874
+ properties: List[Property] = []
875
+ property_iterator = schema_or_reference.properties.items() if schema_or_reference.properties is not None else {}
876
+
877
+ for prop_name, prop_schema in property_iterator:
878
+ # Reference property
879
+ if isinstance(prop_schema, Reference30) or isinstance(prop_schema, Reference31):
880
+ conv_property = _generate_property_from_reference(name, prop_name, prop_schema, schema_or_reference)
881
+ properties.append(conv_property)
882
+ continue
883
+
884
+ # Schema property
885
+ conv_property = _generate_property_from_schema(name, prop_name, prop_schema, schema_or_reference)
886
+
887
+ # --------------------------
888
+ # NEW: const / single-value enum -> Literal[...] with default
889
+ # --------------------------
890
+ if isinstance(prop_schema, (Schema30, Schema31)):
891
+ const_val = getattr(prop_schema, "const", None)
892
+ enum_vals = getattr(prop_schema, "enum", None)
893
+
894
+ literal_val = None
895
+ if const_val is not None:
896
+ literal_val = const_val
897
+ elif isinstance(enum_vals, list) and len(enum_vals) == 1:
898
+ literal_val = enum_vals[0]
899
+
900
+ if literal_val is not None:
901
+ # make sure discriminator is present for unions: required + default
902
+ conv_property.required = True
903
+ conv_property.default = repr(literal_val)
904
+
905
+ conv_property.type = TypeConversion(
906
+ original_type=conv_property.type.original_type,
907
+ converted_type=f"Literal[{repr(literal_val)}]",
908
+ import_types=_dedupe_imports(
909
+ (conv_property.type.import_types or []) + ["from typing import Literal"]
910
+ ),
911
+ )
912
+
913
+ # If this model is a discriminated union member, and this property
914
+ # is the discriminator key, make it a Literal[...] with a default
915
+ binding = discriminator_bindings.get(name)
916
+ if binding and common.normalize_symbol(conv_property.name) == common.normalize_symbol(
917
+ binding.discriminator_key
918
+ ):
919
+ conv_property.required = True
920
+ conv_property.default = f"{binding.enum_name}.{binding.enum_member}"
921
+
922
+ extra_imports = [
923
+ "from typing import Literal",
924
+ f"from .{binding.enum_name} import {binding.enum_name}",
925
+ ]
926
+
927
+ conv_property.type = TypeConversion(
928
+ original_type=conv_property.type.original_type,
929
+ converted_type=f"Literal[{binding.enum_name}.{binding.enum_member}]",
930
+ import_types=extra_imports,
931
+ )
932
+
933
+ # -----------------------------------------
934
+ # NEW: union / discriminated union factoring
935
+ # -----------------------------------------
936
+ if isinstance(prop_schema, (Schema30, Schema31)) and _schema_is_union(prop_schema):
937
+ alias_name = _alias_name_for_property(prop_name)
938
+ discriminator_key = _get_discriminator_key(prop_schema)
939
+
940
+ # Only generate standalone alias modules for DISCRIMINATED unions.
941
+ # Plain unions (including nullable wrappers) are left inline.
942
+ if discriminator_key is not None:
943
+ # Build the union type and gather imports from members.
944
+ # Important: we want a NON-optional union for the alias definition.
945
+ union_conv = type_converter(prop_schema, required=True, model_name=name)
946
+ union_type_str = union_conv.converted_type # e.g. Union[A,B,C]
947
+ member_imports = union_conv.import_types or []
948
+
949
+ # Create alias module once
950
+ if alias_name not in alias_models_by_name:
951
+ alias_content = _render_union_alias_module(
952
+ jinja_env=jinja_env,
953
+ alias_name=alias_name,
954
+ union_type=union_type_str,
955
+ discriminator_key=discriminator_key,
956
+ member_imports=member_imports,
957
+ )
958
+
959
+ # Validate alias module compiles
960
+ try:
961
+ compile(alias_content, "<string>", "exec")
962
+ except SyntaxError as e: # pragma: no cover
963
+ click.echo(f"Error in union alias {alias_name}: {e}") # pragma: no cover
964
+
965
+ alias_models_by_name[alias_name] = Model(
966
+ file_name=alias_name,
967
+ content=alias_content,
968
+ openapi_object=prop_schema,
969
+ properties=[],
970
+ )
971
+
972
+ # Rewrite property type to use alias
973
+ rewritten_type = alias_name if conv_property.required else f"Optional[{alias_name}]"
974
+ conv_property.type = TypeConversion(
975
+ original_type=conv_property.type.original_type,
976
+ converted_type=rewritten_type,
977
+ import_types=[f"from .{alias_name} import {alias_name}"],
978
+ )
979
+
980
+ properties.append(conv_property)
981
+
982
+ template_name = MODELS_TEMPLATE_PYDANTIC_V2 if pydantic_version == PydanticVersion.V2 else MODELS_TEMPLATE
983
+
984
+ generated_content = jinja_env.get_template(template_name).render(
985
+ schema_name=name, schema=schema_or_reference, properties=properties
986
+ )
987
+
988
+ try:
989
+ compile(generated_content, "<string>", "exec")
990
+ except SyntaxError as e: # pragma: no cover
991
+ click.echo(f"Error in model {name}: {e}") # pragma: no cover
992
+
993
+ models.append(
994
+ Model(
995
+ file_name=name,
996
+ content=generated_content,
997
+ openapi_object=schema_or_reference,
998
+ properties=properties,
999
+ )
1000
+ )
1001
+
1002
+ # Ensure enum modules for discriminators are included
1003
+ enum_models: List[Model] = []
1004
+ for enum_name, members in enum_members_by_name.items():
1005
+ enum_content = jinja_env.get_template(DISCRIMINATOR_ENUM_TEMPLATE).render(enum_name=enum_name, members=members)
1006
+ try:
1007
+ compile(enum_content, "<string>", "exec")
1008
+ except SyntaxError as e: # pragma: no cover
1009
+ click.echo(f"Error in enum {enum_name}: {e}") # pragma: no cover
1010
+
1011
+ # Model.openapi_object is required (non-Optional). Enum modules don't map to a real schema,
1012
+ # so attach a tiny placeholder schema to satisfy validation.
1013
+ placeholder_schema = Schema31() if isinstance(components, Components31) else Schema30()
1014
+
1015
+ enum_models.append(
1016
+ Model(file_name=enum_name, content=enum_content, openapi_object=placeholder_schema, properties=[])
1017
+ )
1018
+
1019
+ # Ensure alias modules are included in output
1020
+ models.extend(alias_models_by_name.values())
1021
+ # Append enum modules last
1022
+ models.extend(enum_models)
1023
+
1024
+ return models