ab-openapi-python-generator 2.2.2__tar.gz → 2.2.4__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 (96) hide show
  1. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.gitignore +1 -0
  2. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.vscode/launch.json +8 -0
  3. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/PKG-INFO +1 -1
  4. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/pyproject.toml +1 -1
  5. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/client_generator.py +38 -1
  6. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/jinja_config.py +2 -2
  7. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/model_generator.py +187 -6
  8. ab_openapi_python_generator-2.2.4/src/ab_openapi_python_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +146 -0
  9. ab_openapi_python_generator-2.2.4/src/ab_openapi_python_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +145 -0
  10. ab_openapi_python_generator-2.2.2/src/ab_openapi_python_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +0 -80
  11. ab_openapi_python_generator-2.2.2/src/ab_openapi_python_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +0 -80
  12. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.envrc.example +0 -0
  13. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.gitattributes +0 -0
  14. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.github/dependabot.yml +0 -0
  15. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.github/workflows/ci.yaml +0 -0
  16. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.github/workflows/publish.yaml +0 -0
  17. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.pre-commit-config.yaml +0 -0
  18. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/.vscode/tasks.json +0 -0
  19. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/LICENSE +0 -0
  20. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/Makefile +0 -0
  21. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/README.md +0 -0
  22. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/acknowledgements/index.md +0 -0
  23. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/css/custom.css +0 -0
  24. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/css/termynal.css +0 -0
  25. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/index.md +0 -0
  26. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/js/custom.js +0 -0
  27. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/js/termynal.js +0 -0
  28. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/openapi-definition.md +0 -0
  29. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/quick_start.md +0 -0
  30. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/references/index.md +0 -0
  31. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/references/module_usage.md +0 -0
  32. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/tutorial/advanced.md +0 -0
  33. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/tutorial/authentication.md +0 -0
  34. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/docs/tutorial/index.md +0 -0
  35. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/logo.png +0 -0
  36. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/__init__.py +0 -0
  37. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/__main__.py +0 -0
  38. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/common.py +0 -0
  39. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/generate_data.py +0 -0
  40. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/__init__.py +0 -0
  41. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/__init__.py +0 -0
  42. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/common.py +0 -0
  43. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/exception_generator.py +0 -0
  44. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/generator.py +0 -0
  45. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/templates/alias_union.jinja2 +0 -0
  46. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/templates/discriminator_enum.jinja2 +0 -0
  47. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/templates/enum.jinja2 +0 -0
  48. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/templates/http_exception.jinja2 +0 -0
  49. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/templates/models.jinja2 +0 -0
  50. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +0 -0
  51. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/models.py +0 -0
  52. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/parsers/__init__.py +0 -0
  53. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/parsers/openapi_30.py +0 -0
  54. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/parsers/openapi_31.py +0 -0
  55. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/py.typed +0 -0
  56. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/src/ab_openapi_python_generator/version_detector.py +0 -0
  57. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/__init__.py +0 -0
  58. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/build_test_api/api.py +0 -0
  59. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/conftest.py +0 -0
  60. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_common_normalize_symbol.py +0 -0
  61. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/failing_api.json +0 -0
  62. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/gitea_issue_11.json +0 -0
  63. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_117.json +0 -0
  64. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_120.json +0 -0
  65. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_17.json +0 -0
  66. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_30_87.json +0 -0
  67. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_51.json +0 -0
  68. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_55.json +0 -0
  69. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_71.json +0 -0
  70. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_71_31.json +0 -0
  71. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_illegal_character_in_operation_id.json +0 -0
  72. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/issue_keyword_parameter_name.json +0 -0
  73. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/openapi_gitea_converted.json +0 -0
  74. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/swagger_petstore_3_0_4.yaml +0 -0
  75. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/swagger_petstore_3_1.yaml +0 -0
  76. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/test_api.json +0 -0
  77. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_data/test_api_31.json +0 -0
  78. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_generate_data.py +0 -0
  79. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_generate_data_negative.py +0 -0
  80. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_generated_code.py +0 -0
  81. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_jinja_no_autoescape.py +0 -0
  82. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_main.py +0 -0
  83. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_model_docstring.py +0 -0
  84. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_model_generator.py +0 -0
  85. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_model_generator_edges.py +0 -0
  86. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_openapi_30.py +0 -0
  87. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_openapi_31.py +0 -0
  88. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_openapi_31_completeness.py +0 -0
  89. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_openapi_31_coverage.py +0 -0
  90. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_openapi_31_schema_features.py +0 -0
  91. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_service_generator.py +0 -0
  92. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_service_generator_edges.py +0 -0
  93. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_swagger_petstore_30.py +0 -0
  94. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_swagger_petstore_31.py +0 -0
  95. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/tests/test_version_detector_edges.py +0 -0
  96. {ab_openapi_python_generator-2.2.2 → ab_openapi_python_generator-2.2.4}/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.2
3
+ Version: 2.2.4
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.2"
3
+ version = "2.2.4"
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)
@@ -0,0 +1,146 @@
1
+ {% if env_token_name is not none %}import os{% endif %}
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncGenerator
6
+ from typing import Any, Dict, Optional, Union
7
+ import json
8
+
9
+ import httpx
10
+ from pydantic import BaseModel, TypeAdapter
11
+
12
+ from ..models import *
13
+ from ..exceptions import HTTPException
14
+
15
+ {% set _base_url = servers[0].url if servers|length > 0 else "/" %}
16
+
17
+
18
+ class AsyncClient(BaseModel):
19
+ model_config = {"validate_assignment": True}
20
+
21
+ base_url: str = "{{ _base_url }}"
22
+ verify: Union[bool, str] = True
23
+ {% if env_token_name is none %}
24
+ access_token: Optional[str] = None
25
+ {% endif %}
26
+
27
+ def get_access_token(self) -> Optional[str]:
28
+ {% if env_token_name is not none %}
29
+ try:
30
+ return os.environ["{{ env_token_name }}"]
31
+ except KeyError:
32
+ return None
33
+ {% else %}
34
+ return self.access_token
35
+ {% endif %}
36
+
37
+ def set_access_token(self, value: str) -> None:
38
+ {% if env_token_name is not none %}
39
+ raise Exception(
40
+ "This client was generated with an environment variable for the access token. "
41
+ "Please set '{{ env_token_name }}'."
42
+ )
43
+ {% else %}
44
+ self.access_token = value
45
+ {% endif %}
46
+
47
+ {% for op in operations %}
48
+ async def {{ op.operation_id }}(self{% if op.params %}, {{ op.params }}{% endif %}) -> {%- if op.is_sse -%}
49
+ AsyncGenerator[str | dict[str, Any], None]
50
+ {%- else -%}
51
+ {%- if op.return_type.type is none or op.return_type.type.converted_type is none -%}None
52
+ {%- elif op.return_type.list_type is not none -%}list[{{ op.return_type.list_type }}]
53
+ {%- else -%}{{ op.return_type.type.converted_type }}
54
+ {%- endif -%}
55
+ {%- endif -%}:
56
+ base_url = self.base_url
57
+ path = f"{{ op.path_name }}"
58
+
59
+ headers = {
60
+ "Content-Type": "application/json",
61
+ {% if op.is_sse %}
62
+ "Accept": "text/event-stream",
63
+ {% else %}
64
+ "Accept": "application/json",
65
+ {% endif %}
66
+ "Authorization": f"Bearer { self.get_access_token() }",
67
+ }
68
+
69
+ query_params: Dict[str, Any] = {
70
+ {% for qp in op.query_params %}
71
+ {{ qp }},
72
+ {% endfor %}
73
+ }
74
+ query_params = {k: v for (k, v) in query_params.items() if v is not None}
75
+
76
+ {% if op.is_sse %}
77
+ async with httpx.AsyncClient(base_url=base_url, verify=self.verify) as client:
78
+ async with client.stream(
79
+ "{{ op.method }}",
80
+ httpx.URL(path),
81
+ headers=headers,
82
+ params=query_params,
83
+ {% if op.body_param %}
84
+ json={{ op.body_param }},
85
+ {% endif %}
86
+ ) as response:
87
+ if response.status_code != {{ op.return_type.status_code }}:
88
+ raise HTTPException(
89
+ response.status_code,
90
+ f"{{ op.operation_id }} failed with status code: {response.status_code}",
91
+ )
92
+
93
+ async for line in response.aiter_lines():
94
+ if not line:
95
+ continue
96
+ if line.startswith("data:"):
97
+ payload = line[len("data:"):].strip()
98
+ if not payload:
99
+ continue
100
+ if payload == "[DONE]":
101
+ break
102
+ try:
103
+ obj = json.loads(payload)
104
+ if isinstance(obj, dict):
105
+ yield obj
106
+ else:
107
+ yield payload
108
+ except Exception:
109
+ yield payload
110
+ else:
111
+ # Non-data lines: yield as raw text for debugging/visibility
112
+ yield line
113
+ {% else %}
114
+ async with httpx.AsyncClient(base_url=base_url, verify=self.verify) as client:
115
+ response = await client.request(
116
+ "{{ op.method }}",
117
+ httpx.URL(path),
118
+ headers=headers,
119
+ params=query_params,
120
+ {% if op.body_param %}
121
+ json={{ op.body_param }},
122
+ {% endif %}
123
+ )
124
+
125
+ if response.status_code != {{ op.return_type.status_code }}:
126
+ raise HTTPException(
127
+ response.status_code,
128
+ f"{{ op.operation_id }} failed with status code: {response.status_code}",
129
+ )
130
+
131
+ body = None if {{ op.return_type.status_code }} == 204 else response.json()
132
+
133
+ {% if op.return_type.type is none or op.return_type.type.converted_type is none %}
134
+ return None
135
+ {% elif op.return_type.complex_type %}
136
+ {% if op.return_type.list_type is none %}
137
+ return TypeAdapter({{ op.return_type.type.converted_type }}).validate_python(body)
138
+ {% else %}
139
+ return TypeAdapter(list[{{ op.return_type.list_type }}]).validate_python(body)
140
+ {% endif %}
141
+ {% else %}
142
+ return body
143
+ {% endif %}
144
+ {% endif %}
145
+
146
+ {% endfor %}
@@ -0,0 +1,145 @@
1
+ {% if env_token_name is not none %}import os{% endif %}
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Generator
6
+ from typing import Any, Dict, Optional, Union
7
+ import json
8
+
9
+ import httpx
10
+ from pydantic import BaseModel, TypeAdapter
11
+
12
+ from ..models import *
13
+ from ..exceptions import HTTPException
14
+
15
+ {% set _base_url = servers[0].url if servers|length > 0 else "/" %}
16
+
17
+
18
+ class SyncClient(BaseModel):
19
+ model_config = {"validate_assignment": True}
20
+
21
+ base_url: str = "{{ _base_url }}"
22
+ verify: Union[bool, str] = True
23
+ {% if env_token_name is none %}
24
+ access_token: Optional[str] = None
25
+ {% endif %}
26
+
27
+ def get_access_token(self) -> Optional[str]:
28
+ {% if env_token_name is not none %}
29
+ try:
30
+ return os.environ["{{ env_token_name }}"]
31
+ except KeyError:
32
+ return None
33
+ {% else %}
34
+ return self.access_token
35
+ {% endif %}
36
+
37
+ def set_access_token(self, value: str) -> None:
38
+ {% if env_token_name is not none %}
39
+ raise Exception(
40
+ "This client was generated with an environment variable for the access token. "
41
+ "Please set '{{ env_token_name }}'."
42
+ )
43
+ {% else %}
44
+ self.access_token = value
45
+ {% endif %}
46
+
47
+ {% for op in operations %}
48
+ def {{ op.operation_id }}(self{% if op.params %}, {{ op.params }}{% endif %}) -> {%- if op.is_sse -%}
49
+ Generator[str | dict[str, Any], None, None]
50
+ {%- else -%}
51
+ {%- if op.return_type.type is none or op.return_type.type.converted_type is none -%}None
52
+ {%- elif op.return_type.list_type is not none -%}list[{{ op.return_type.list_type }}]
53
+ {%- else -%}{{ op.return_type.type.converted_type }}
54
+ {%- endif -%}
55
+ {%- endif -%}:
56
+ base_url = self.base_url
57
+ path = f"{{ op.path_name }}"
58
+
59
+ headers = {
60
+ "Content-Type": "application/json",
61
+ {% if op.is_sse %}
62
+ "Accept": "text/event-stream",
63
+ {% else %}
64
+ "Accept": "application/json",
65
+ {% endif %}
66
+ "Authorization": f"Bearer { self.get_access_token() }",
67
+ }
68
+
69
+ query_params: Dict[str, Any] = {
70
+ {% for qp in op.query_params %}
71
+ {{ qp }},
72
+ {% endfor %}
73
+ }
74
+ query_params = {k: v for (k, v) in query_params.items() if v is not None}
75
+
76
+ {% if op.is_sse %}
77
+ with httpx.Client(base_url=base_url, verify=self.verify) as client:
78
+ with client.stream(
79
+ "{{ op.method }}",
80
+ httpx.URL(path),
81
+ headers=headers,
82
+ params=query_params,
83
+ {% if op.body_param %}
84
+ json={{ op.body_param }},
85
+ {% endif %}
86
+ ) as response:
87
+ if response.status_code != {{ op.return_type.status_code }}:
88
+ raise HTTPException(
89
+ response.status_code,
90
+ f"{{ op.operation_id }} failed with status code: {response.status_code}",
91
+ )
92
+
93
+ for line in response.iter_lines():
94
+ if not line:
95
+ continue
96
+ if line.startswith("data:"):
97
+ payload = line[len("data:"):].strip()
98
+ if not payload:
99
+ continue
100
+ if payload == "[DONE]":
101
+ break
102
+ try:
103
+ obj = json.loads(payload)
104
+ if isinstance(obj, dict):
105
+ yield obj
106
+ else:
107
+ yield payload
108
+ except Exception:
109
+ yield payload
110
+ else:
111
+ yield line
112
+ {% else %}
113
+ with httpx.Client(base_url=base_url, verify=self.verify) as client:
114
+ response = client.request(
115
+ "{{ op.method }}",
116
+ httpx.URL(path),
117
+ headers=headers,
118
+ params=query_params,
119
+ {% if op.body_param %}
120
+ json={{ op.body_param }},
121
+ {% endif %}
122
+ )
123
+
124
+ if response.status_code != {{ op.return_type.status_code }}:
125
+ raise HTTPException(
126
+ response.status_code,
127
+ f"{{ op.operation_id }} failed with status code: {response.status_code}",
128
+ )
129
+
130
+ body = None if {{ op.return_type.status_code }} == 204 else response.json()
131
+
132
+ {% if op.return_type.type is none or op.return_type.type.converted_type is none %}
133
+ return None
134
+ {% elif op.return_type.complex_type %}
135
+ {% if op.return_type.list_type is none %}
136
+ return TypeAdapter({{ op.return_type.type.converted_type }}).validate_python(body)
137
+ {% else %}
138
+ return TypeAdapter(list[{{ op.return_type.list_type }}]).validate_python(body)
139
+ {% endif %}
140
+ {% else %}
141
+ return body
142
+ {% endif %}
143
+ {% endif %}
144
+
145
+ {% endfor %}
@@ -1,80 +0,0 @@
1
- {% if env_token_name is not none %}import os{% endif %}
2
-
3
- from typing import Any, Dict, Optional, Union
4
-
5
- import httpx
6
- from pydantic import BaseModel, HttpUrl
7
-
8
- from ..models import *
9
- {% set _base_url = servers[0].url if servers|length > 0 else "/" %}
10
- from ..exceptions import HTTPException
11
-
12
-
13
- class AsyncClient(BaseModel):
14
- model_config = {"validate_assignment": True}
15
-
16
- base_url: str = "{{ _base_url }}"
17
- verify: Union[bool, str] = True
18
- {% if env_token_name is none %}
19
- access_token: Optional[str] = None
20
- {% endif %}
21
-
22
- def get_access_token(self) -> Optional[str]:
23
- {% if env_token_name is not none %}
24
- try:
25
- return os.environ["{{ env_token_name }}"]
26
- except KeyError:
27
- return None
28
- {% else %}
29
- return self.access_token
30
- {% endif %}
31
-
32
- def set_access_token(self, value: str) -> None:
33
- {% if env_token_name is not none %}
34
- raise Exception(
35
- "This client was generated with an environment variable for the access token. "
36
- "Please set '{{ env_token_name }}'."
37
- )
38
- {% else %}
39
- self.access_token = value
40
- {% endif %}
41
-
42
- {% for op in operations %}
43
- async def {{ op.operation_id }}(self{% if op.params %}, {{ op.params }}{% endif %}) -> Any:
44
- base_url = self.base_url
45
- path = f"{{ op.path_name }}"
46
-
47
- headers = {
48
- "Content-Type": "application/json",
49
- "Accept": "application/json",
50
- "Authorization": f"Bearer { self.get_access_token() }",
51
- }
52
-
53
- query_params: Dict[str, Any] = {
54
- {% for qp in op.query_params %}
55
- {{ qp }},
56
- {% endfor %}
57
- }
58
- query_params = {k: v for (k, v) in query_params.items() if v is not None}
59
-
60
- async with httpx.AsyncClient(base_url=base_url, verify=self.verify) as client:
61
- response = await client.request(
62
- "{{ op.method }}",
63
- httpx.URL(path),
64
- headers=headers,
65
- params=query_params,
66
- {% if op.body_param %}
67
- json={{ op.body_param }},
68
- {% endif %}
69
- )
70
-
71
- if response.status_code != {{ op.return_type.status_code }}:
72
- raise HTTPException(
73
- response.status_code,
74
- f"{{ op.operation_id }} failed with status code: {response.status_code}",
75
- )
76
-
77
- body = None if {{ op.return_type.status_code }} == 204 else response.json()
78
- return body
79
-
80
- {% endfor %}
@@ -1,80 +0,0 @@
1
- {% if env_token_name is not none %}import os{% endif %}
2
-
3
- from typing import Any, Dict, Optional, Union
4
-
5
- import httpx
6
- from pydantic import BaseModel, HttpUrl
7
-
8
- from ..models import *
9
- {% set _base_url = servers[0].url if servers|length > 0 else "/" %}
10
- from ..exceptions import HTTPException
11
-
12
-
13
- class SyncClient(BaseModel):
14
- model_config = {"validate_assignment": True}
15
-
16
- base_url: str = "{{ _base_url }}"
17
- verify: Union[bool, str] = True
18
- {% if env_token_name is none %}
19
- access_token: Optional[str] = None
20
- {% endif %}
21
-
22
- def get_access_token(self) -> Optional[str]:
23
- {% if env_token_name is not none %}
24
- try:
25
- return os.environ["{{ env_token_name }}"]
26
- except KeyError:
27
- return None
28
- {% else %}
29
- return self.access_token
30
- {% endif %}
31
-
32
- def set_access_token(self, value: str) -> None:
33
- {% if env_token_name is not none %}
34
- raise Exception(
35
- "This client was generated with an environment variable for the access token. "
36
- "Please set '{{ env_token_name }}'."
37
- )
38
- {% else %}
39
- self.access_token = value
40
- {% endif %}
41
-
42
- {% for op in operations %}
43
- def {{ op.operation_id }}(self{% if op.params %}, {{ op.params }}{% endif %}) -> Any:
44
- base_url = self.base_url
45
- path = f"{{ op.path_name }}"
46
-
47
- headers = {
48
- "Content-Type": "application/json",
49
- "Accept": "application/json",
50
- "Authorization": f"Bearer { self.get_access_token() }",
51
- }
52
-
53
- query_params: Dict[str, Any] = {
54
- {% for qp in op.query_params %}
55
- {{ qp }},
56
- {% endfor %}
57
- }
58
- query_params = {k: v for (k, v) in query_params.items() if v is not None}
59
-
60
- with httpx.Client(base_url=base_url, verify=self.verify) as client:
61
- response = client.request(
62
- "{{ op.method }}",
63
- httpx.URL(path),
64
- headers=headers,
65
- params=query_params,
66
- {% if op.body_param %}
67
- json={{ op.body_param }},
68
- {% endif %}
69
- )
70
-
71
- if response.status_code != {{ op.return_type.status_code }}:
72
- raise HTTPException(
73
- response.status_code,
74
- f"{{ op.operation_id }} failed with status code: {response.status_code}",
75
- )
76
-
77
- body = None if {{ op.return_type.status_code }} == 204 else response.json()
78
- return body
79
-
80
- {% endfor %}