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