ab-openapi-python-generator 2.2.3__tar.gz → 2.2.5__tar.gz

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 (94) hide show
  1. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.gitignore +1 -0
  2. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.vscode/launch.json +8 -0
  3. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/PKG-INFO +1 -1
  4. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/pyproject.toml +1 -1
  5. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/client_generator.py +38 -1
  6. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/jinja_config.py +2 -2
  7. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/model_generator.py +187 -6
  8. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +6 -6
  9. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +3 -3
  10. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.envrc.example +0 -0
  11. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.gitattributes +0 -0
  12. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.github/dependabot.yml +0 -0
  13. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.github/workflows/ci.yaml +0 -0
  14. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.github/workflows/publish.yaml +0 -0
  15. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.pre-commit-config.yaml +0 -0
  16. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/.vscode/tasks.json +0 -0
  17. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/LICENSE +0 -0
  18. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/Makefile +0 -0
  19. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/README.md +0 -0
  20. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/acknowledgements/index.md +0 -0
  21. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/css/custom.css +0 -0
  22. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/css/termynal.css +0 -0
  23. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/index.md +0 -0
  24. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/js/custom.js +0 -0
  25. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/js/termynal.js +0 -0
  26. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/openapi-definition.md +0 -0
  27. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/quick_start.md +0 -0
  28. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/references/index.md +0 -0
  29. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/references/module_usage.md +0 -0
  30. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/tutorial/advanced.md +0 -0
  31. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/tutorial/authentication.md +0 -0
  32. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/docs/tutorial/index.md +0 -0
  33. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/logo.png +0 -0
  34. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/__init__.py +0 -0
  35. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/__main__.py +0 -0
  36. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/common.py +0 -0
  37. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/generate_data.py +0 -0
  38. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/__init__.py +0 -0
  39. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/__init__.py +0 -0
  40. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/common.py +0 -0
  41. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/exception_generator.py +0 -0
  42. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/generator.py +0 -0
  43. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/templates/alias_union.jinja2 +0 -0
  44. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/templates/discriminator_enum.jinja2 +0 -0
  45. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/templates/enum.jinja2 +0 -0
  46. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/templates/http_exception.jinja2 +0 -0
  47. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/templates/models.jinja2 +0 -0
  48. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +0 -0
  49. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/models.py +0 -0
  50. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/parsers/__init__.py +0 -0
  51. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/parsers/openapi_30.py +0 -0
  52. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/parsers/openapi_31.py +0 -0
  53. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/py.typed +0 -0
  54. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/src/ab_openapi_python_generator/version_detector.py +0 -0
  55. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/__init__.py +0 -0
  56. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/build_test_api/api.py +0 -0
  57. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/conftest.py +0 -0
  58. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_common_normalize_symbol.py +0 -0
  59. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/failing_api.json +0 -0
  60. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/gitea_issue_11.json +0 -0
  61. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_117.json +0 -0
  62. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_120.json +0 -0
  63. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_17.json +0 -0
  64. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_30_87.json +0 -0
  65. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_51.json +0 -0
  66. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_55.json +0 -0
  67. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_71.json +0 -0
  68. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_71_31.json +0 -0
  69. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_illegal_character_in_operation_id.json +0 -0
  70. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/issue_keyword_parameter_name.json +0 -0
  71. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/openapi_gitea_converted.json +0 -0
  72. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/swagger_petstore_3_0_4.yaml +0 -0
  73. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/swagger_petstore_3_1.yaml +0 -0
  74. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/test_api.json +0 -0
  75. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_data/test_api_31.json +0 -0
  76. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_generate_data.py +0 -0
  77. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_generate_data_negative.py +0 -0
  78. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_generated_code.py +0 -0
  79. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_jinja_no_autoescape.py +0 -0
  80. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_main.py +0 -0
  81. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_model_docstring.py +0 -0
  82. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_model_generator.py +0 -0
  83. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_model_generator_edges.py +0 -0
  84. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_openapi_30.py +0 -0
  85. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_openapi_31.py +0 -0
  86. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_openapi_31_completeness.py +0 -0
  87. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_openapi_31_coverage.py +0 -0
  88. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_openapi_31_schema_features.py +0 -0
  89. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_service_generator.py +0 -0
  90. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_service_generator_edges.py +0 -0
  91. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_swagger_petstore_30.py +0 -0
  92. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_swagger_petstore_31.py +0 -0
  93. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tests/test_version_detector_edges.py +0 -0
  94. {ab_openapi_python_generator-2.2.3 → ab_openapi_python_generator-2.2.5}/tox.ini +0 -0
@@ -217,3 +217,4 @@ __marimo__/
217
217
 
218
218
  # Generated Files
219
219
  .DS_Store
220
+ openapi.json
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "version": "0.2.0",
3
3
  "configurations": [
4
+ {
5
+ "name": "Python Debugger: Current File with Arguments",
6
+ "type": "debugpy",
7
+ "request": "launch",
8
+ "program": "${file}",
9
+ "console": "integratedTerminal",
10
+ "args": "${command:pickArgs}"
11
+ },
4
12
  {
5
13
  "name": "pytest",
6
14
  "type": "python",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ab-openapi-python-generator
3
- Version: 2.2.3
3
+ Version: 2.2.5
4
4
  Summary: Openapi Python Generator
5
5
  Project-URL: Homepage, https://github.com/auth-broker/openapi-python-generator
6
6
  Project-URL: Repository, https://github.com/auth-broker/openapi-python-generator
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ab-openapi-python-generator"
3
- version = "2.2.3"
3
+ version = "2.2.5"
4
4
  description = "Openapi Python Generator"
5
5
  authors = [
6
6
  { name = "Marco Müllner", email = "muellnermarco@gmail.com" },
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import re
2
3
  from typing import Any, Dict, List, Literal, Optional, Tuple, Union
3
4
 
@@ -91,6 +92,24 @@ def is_schema_type(obj: Any) -> bool:
91
92
  return isinstance(obj, (Schema30, Schema31))
92
93
 
93
94
 
95
+ def _common_suffix(a: str, b: str) -> str:
96
+ i = 1
97
+ while i <= min(len(a), len(b)) and a[-i] == b[-i]:
98
+ i += 1
99
+ return a[-(i - 1) :] if i > 1 else ""
100
+
101
+
102
+ def _common_suffix_many(names: List[str]) -> str:
103
+ if not names:
104
+ return ""
105
+ suf = names[0]
106
+ for n in names[1:]:
107
+ suf = _common_suffix(suf, n)
108
+ if not suf:
109
+ break
110
+ return suf
111
+
112
+
94
113
  def operation_is_sse(op: Operation) -> bool:
95
114
  """Detect if an Operation advertises Server-Sent-Events (text/event-stream) in any 2xx response."""
96
115
  if not getattr(op, "responses", None):
@@ -100,7 +119,9 @@ def operation_is_sse(op: Operation) -> bool:
100
119
  try:
101
120
  if not str(status_code).startswith("2"):
102
121
  continue
103
- except Exception:
122
+ except Exception as e:
123
+ logger = logging.getLogger(__name__)
124
+ logger.debug("Skipping response status key; conversion failed", exc_info=e)
104
125
  continue
105
126
 
106
127
  # Concrete Response object
@@ -309,6 +330,22 @@ def generate_return_type(operation: Operation) -> OpReturnType:
309
330
  complex_type=True,
310
331
  )
311
332
  elif is_schema_type(inner_schema):
333
+ # NEW: if this is a discriminated response union of refs, prefer a named alias
334
+ disc = getattr(inner_schema, "discriminator", None)
335
+ used = getattr(inner_schema, "oneOf", None) or getattr(inner_schema, "anyOf", None)
336
+ disc_key = getattr(disc, "propertyName", None) if disc is not None else None
337
+
338
+ if disc_key and used and all(is_reference_type(s) for s in used):
339
+ member_models = [common.normalize_symbol(s.ref.split("/")[-1]) for s in used] # type: ignore
340
+ alias_name = common.normalize_symbol(_common_suffix_many(member_models)) or "Response"
341
+
342
+ type_conv = TypeConversion(
343
+ original_type="discriminated_union",
344
+ converted_type=alias_name,
345
+ import_types=None,
346
+ )
347
+ return OpReturnType(type=type_conv, status_code=good_responses[0][0], complex_type=True)
348
+
312
349
  converted_result = type_converter(inner_schema, True) # type: ignore
313
350
  if "array" in converted_result.original_type and isinstance(converted_result.import_types, list):
314
351
  matched = re.findall(r"List\[(.+)\]", converted_result.converted_type)
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from jinja2 import ChoiceLoader, Environment, FileSystemLoader
3
+ from jinja2 import ChoiceLoader, Environment, FileSystemLoader, select_autoescape
4
4
 
5
5
  from . import common
6
6
 
@@ -28,7 +28,7 @@ def create_jinja_env():
28
28
  if custom_template_path is not None
29
29
  else FileSystemLoader(TEMPLATE_PATH)
30
30
  ),
31
- autoescape=False,
31
+ autoescape=select_autoescape(default_for_string=False),
32
32
  trim_blocks=True,
33
33
  lstrip_blocks=True,
34
34
  )
@@ -6,12 +6,14 @@ from dataclasses import dataclass
6
6
  from typing import Dict, List, Optional, Set, Tuple, Union
7
7
 
8
8
  import click
9
+ from openapi_pydantic.v3 import Operation, PathItem
9
10
  from openapi_pydantic.v3.v3_0 import (
10
11
  Components as Components30,
11
12
  )
12
13
  from openapi_pydantic.v3.v3_0 import (
13
14
  Reference as Reference30,
14
15
  )
16
+ from openapi_pydantic.v3.v3_0 import Response as Response30
15
17
  from openapi_pydantic.v3.v3_0 import (
16
18
  Schema as Schema30,
17
19
  )
@@ -21,6 +23,7 @@ from openapi_pydantic.v3.v3_1 import (
21
23
  from openapi_pydantic.v3.v3_1 import (
22
24
  Reference as Reference31,
23
25
  )
26
+ from openapi_pydantic.v3.v3_1 import Response as Response31
24
27
  from openapi_pydantic.v3.v3_1 import (
25
28
  Schema as Schema31,
26
29
  )
@@ -330,6 +333,117 @@ def _build_discriminator_bindings(components: Components) -> Dict[str, Discrimin
330
333
  return bindings
331
334
 
332
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
+
333
447
  def type_converter( # noqa: C901
334
448
  schema: Union[Schema, Reference],
335
449
  required: bool = False,
@@ -413,14 +527,33 @@ def type_converter( # noqa: C901
413
527
  converted_type = "Tuple[" + ",".join([i.converted_type for i in conversions]) + "]"
414
528
 
415
529
  converted_type = pre_type + converted_type + post_type
416
- # Collect first import from referenced sub-schemas only (skip empty lists)
417
- import_types = [
418
- i.import_types[0] for i in conversions if i.import_types is not None and len(i.import_types) > 0
419
- ] or None
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
+ )
420
535
 
421
536
  elif schema.oneOf is not None or schema.anyOf is not None:
422
537
  used = schema.oneOf if schema.oneOf is not None else schema.anyOf
423
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
+ )
424
557
  conversions = []
425
558
  for sub_schema in used:
426
559
  if isinstance(sub_schema, Schema30) or isinstance(sub_schema, Schema31):
@@ -474,6 +607,7 @@ def type_converter( # noqa: C901
474
607
  converted_type = pre_type + "bool" + post_type
475
608
  elif schema.type == "array" or str(schema.type) == "DataType.ARRAY":
476
609
  retVal = pre_type + "List["
610
+ item_imports: List[str] = []
477
611
  if isinstance(schema.items, Reference30) or isinstance(schema.items, Reference31):
478
612
  converted_reference = _generate_property_from_reference(
479
613
  model_name or "", "", schema.items, schema, required
@@ -488,14 +622,35 @@ def type_converter( # noqa: C901
488
622
  else:
489
623
  type_value = str(type_str) if type_str is not None else "unknown"
490
624
  original_type = "array<" + type_value + ">"
491
- retVal += type_converter(schema.items, True).converted_type
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)
492
630
  else:
493
631
  original_type = "array<unknown>"
494
632
  retVal += "Any"
495
633
 
496
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)
497
639
  elif schema.type == "object" or str(schema.type) == "DataType.OBJECT":
498
- converted_type = pre_type + "Dict[str, Any]" + post_type
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
499
654
  elif schema.type == "null" or str(schema.type) == "DataType.NULL":
500
655
  converted_type = pre_type + "None" + post_type
501
656
  elif schema.type is None:
@@ -724,6 +879,32 @@ def generate_models(components: Components, pydantic_version: PydanticVersion =
724
879
  # Schema property
725
880
  conv_property = _generate_property_from_schema(name, prop_name, prop_schema, schema_or_reference)
726
881
 
882
+ # --------------------------
883
+ # NEW: const / single-value enum -> Literal[...] with default
884
+ # --------------------------
885
+ if isinstance(prop_schema, (Schema30, Schema31)):
886
+ const_val = getattr(prop_schema, "const", None)
887
+ enum_vals = getattr(prop_schema, "enum", None)
888
+
889
+ literal_val = None
890
+ if const_val is not None:
891
+ literal_val = const_val
892
+ elif isinstance(enum_vals, list) and len(enum_vals) == 1:
893
+ literal_val = enum_vals[0]
894
+
895
+ if literal_val is not None:
896
+ # make sure discriminator is present for unions: required + default
897
+ conv_property.required = True
898
+ conv_property.default = repr(literal_val)
899
+
900
+ conv_property.type = TypeConversion(
901
+ original_type=conv_property.type.original_type,
902
+ converted_type=f"Literal[{repr(literal_val)}]",
903
+ import_types=_dedupe_imports(
904
+ (conv_property.type.import_types or []) + ["from typing import Literal"]
905
+ ),
906
+ )
907
+
727
908
  # If this model is a discriminated union member, and this property
728
909
  # is the discriminator key, make it a Literal[...] with a default
729
910
  binding = discriminator_bindings.get(name)
@@ -7,7 +7,7 @@ from typing import Any, Dict, Optional, Union
7
7
  import json
8
8
 
9
9
  import httpx
10
- from pydantic import BaseModel
10
+ from pydantic import BaseModel, TypeAdapter
11
11
 
12
12
  from ..models import *
13
13
  from ..exceptions import HTTPException
@@ -24,7 +24,7 @@ class AsyncClient(BaseModel):
24
24
  access_token: Optional[str] = None
25
25
  {% endif %}
26
26
 
27
- def get_access_token(self) -> Optional[str]:
27
+ async def get_access_token(self) -> Optional[str]:
28
28
  {% if env_token_name is not none %}
29
29
  try:
30
30
  return os.environ["{{ env_token_name }}"]
@@ -34,7 +34,7 @@ class AsyncClient(BaseModel):
34
34
  return self.access_token
35
35
  {% endif %}
36
36
 
37
- def set_access_token(self, value: str) -> None:
37
+ async def set_access_token(self, value: str) -> None:
38
38
  {% if env_token_name is not none %}
39
39
  raise Exception(
40
40
  "This client was generated with an environment variable for the access token. "
@@ -63,7 +63,7 @@ AsyncGenerator[str | dict[str, Any], None]
63
63
  {% else %}
64
64
  "Accept": "application/json",
65
65
  {% endif %}
66
- "Authorization": f"Bearer { self.get_access_token() }",
66
+ "Authorization": f"Bearer { await self.get_access_token() }",
67
67
  }
68
68
 
69
69
  query_params: Dict[str, Any] = {
@@ -134,9 +134,9 @@ AsyncGenerator[str | dict[str, Any], None]
134
134
  return None
135
135
  {% elif op.return_type.complex_type %}
136
136
  {% if op.return_type.list_type is none %}
137
- return {{ op.return_type.type.converted_type }}.model_validate(body) if body is not None else {{ op.return_type.type.converted_type }}()
137
+ return TypeAdapter({{ op.return_type.type.converted_type }}).validate_python(body)
138
138
  {% else %}
139
- return [{{ op.return_type.list_type }}.model_validate(item) for item in (body or [])]
139
+ return TypeAdapter(list[{{ op.return_type.list_type }}]).validate_python(body)
140
140
  {% endif %}
141
141
  {% else %}
142
142
  return body
@@ -7,7 +7,7 @@ from typing import Any, Dict, Optional, Union
7
7
  import json
8
8
 
9
9
  import httpx
10
- from pydantic import BaseModel
10
+ from pydantic import BaseModel, TypeAdapter
11
11
 
12
12
  from ..models import *
13
13
  from ..exceptions import HTTPException
@@ -133,9 +133,9 @@ Generator[str | dict[str, Any], None, None]
133
133
  return None
134
134
  {% elif op.return_type.complex_type %}
135
135
  {% if op.return_type.list_type is none %}
136
- return {{ op.return_type.type.converted_type }}.model_validate(body) if body is not None else {{ op.return_type.type.converted_type }}()
136
+ return TypeAdapter({{ op.return_type.type.converted_type }}).validate_python(body)
137
137
  {% else %}
138
- return [{{ op.return_type.list_type }}.model_validate(item) for item in (body or [])]
138
+ return TypeAdapter(list[{{ op.return_type.list_type }}]).validate_python(body)
139
139
  {% endif %}
140
140
  {% else %}
141
141
  return body