ab-openapi-python-generator 2.1.4__tar.gz → 2.2.0__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 (112) hide show
  1. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/PKG-INFO +1 -1
  2. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/pyproject.toml +1 -1
  3. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/generate_data.py +60 -60
  4. ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/service_generator.py → ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/client_generator.py +83 -167
  5. ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/exception_generator.py +23 -0
  6. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/generator.py +9 -11
  7. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/jinja_config.py +3 -4
  8. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/model_generator.py +33 -91
  9. ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +80 -0
  10. ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/templates/http_exception.jinja2 +8 -0
  11. ab_openapi_python_generator-2.2.0/src/ab_openapi_python_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +80 -0
  12. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/models.py +5 -5
  13. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/version_detector.py +1 -4
  14. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_generate_data.py +30 -25
  15. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_service_generator.py +5 -0
  16. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_service_generator_edges.py +6 -0
  17. ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/api_config_generator.py +0 -35
  18. ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/aiohttp.jinja2 +0 -49
  19. ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/apiconfig.jinja2 +0 -38
  20. ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/apiconfig_pydantic_2.jinja2 +0 -42
  21. ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/httpx.jinja2 +0 -126
  22. ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/requests.jinja2 +0 -50
  23. ab_openapi_python_generator-2.1.4/src/ab_openapi_python_generator/language_converters/python/templates/service.jinja2 +0 -12
  24. ab_openapi_python_generator-2.1.4/tests/regression/__init__.py +0 -0
  25. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_11.py +0 -33
  26. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_117.py +0 -36
  27. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_120.py +0 -36
  28. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_17.py +0 -27
  29. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_30_87.py +0 -24
  30. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_51.py +0 -27
  31. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_55.py +0 -23
  32. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_7.py +0 -34
  33. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_71.py +0 -27
  34. ab_openapi_python_generator-2.1.4/tests/regression/test_issue_illegal_py_symbols.py +0 -51
  35. ab_openapi_python_generator-2.1.4/tests/test_api_config.py +0 -10
  36. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.envrc.example +0 -0
  37. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.gitattributes +0 -0
  38. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.github/dependabot.yml +0 -0
  39. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.github/workflows/ci.yaml +0 -0
  40. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.github/workflows/publish.yaml +0 -0
  41. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.gitignore +0 -0
  42. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.pre-commit-config.yaml +0 -0
  43. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.vscode/launch.json +0 -0
  44. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/.vscode/tasks.json +0 -0
  45. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/LICENSE +0 -0
  46. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/Makefile +0 -0
  47. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/README.md +0 -0
  48. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/acknowledgements/index.md +0 -0
  49. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/css/custom.css +0 -0
  50. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/css/termynal.css +0 -0
  51. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/index.md +0 -0
  52. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/js/custom.js +0 -0
  53. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/js/termynal.js +0 -0
  54. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/openapi-definition.md +0 -0
  55. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/quick_start.md +0 -0
  56. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/references/index.md +0 -0
  57. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/references/module_usage.md +0 -0
  58. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/tutorial/advanced.md +0 -0
  59. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/tutorial/authentication.md +0 -0
  60. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/docs/tutorial/index.md +0 -0
  61. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/logo.png +0 -0
  62. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/__init__.py +0 -0
  63. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/__main__.py +0 -0
  64. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/common.py +0 -0
  65. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/__init__.py +0 -0
  66. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/__init__.py +0 -0
  67. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/common.py +0 -0
  68. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/alias_union.jinja2 +0 -0
  69. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/discriminator_enum.jinja2 +0 -0
  70. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/enum.jinja2 +0 -0
  71. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/models.jinja2 +0 -0
  72. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +0 -0
  73. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/parsers/__init__.py +0 -0
  74. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/parsers/openapi_30.py +0 -0
  75. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/parsers/openapi_31.py +0 -0
  76. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/src/ab_openapi_python_generator/py.typed +0 -0
  77. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/__init__.py +0 -0
  78. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/build_test_api/api.py +0 -0
  79. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/conftest.py +0 -0
  80. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_common_normalize_symbol.py +0 -0
  81. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/failing_api.json +0 -0
  82. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/gitea_issue_11.json +0 -0
  83. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_117.json +0 -0
  84. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_120.json +0 -0
  85. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_17.json +0 -0
  86. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_30_87.json +0 -0
  87. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_51.json +0 -0
  88. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_55.json +0 -0
  89. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_71.json +0 -0
  90. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_71_31.json +0 -0
  91. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_illegal_character_in_operation_id.json +0 -0
  92. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/issue_keyword_parameter_name.json +0 -0
  93. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/openapi_gitea_converted.json +0 -0
  94. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/swagger_petstore_3_0_4.yaml +0 -0
  95. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/swagger_petstore_3_1.yaml +0 -0
  96. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/test_api.json +0 -0
  97. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_data/test_api_31.json +0 -0
  98. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_generate_data_negative.py +0 -0
  99. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_generated_code.py +0 -0
  100. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_main.py +0 -0
  101. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_model_docstring.py +0 -0
  102. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_model_generator.py +0 -0
  103. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_model_generator_edges.py +0 -0
  104. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_30.py +0 -0
  105. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_31.py +0 -0
  106. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_31_completeness.py +0 -0
  107. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_31_coverage.py +0 -0
  108. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_openapi_31_schema_features.py +0 -0
  109. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_swagger_petstore_30.py +0 -0
  110. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_swagger_petstore_31.py +0 -0
  111. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tests/test_version_detector_edges.py +0 -0
  112. {ab_openapi_python_generator-2.1.4 → ab_openapi_python_generator-2.2.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ab-openapi-python-generator
3
- Version: 2.1.4
3
+ Version: 2.2.0
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.1.4"
3
+ version = "2.2.0"
4
4
  description = "Openapi Python Generator"
5
5
  authors = [
6
6
  { name = "Marco Müllner", email = "muellnermarco@gmail.com" },
@@ -12,7 +12,6 @@ from httpx import ConnectError, ConnectTimeout
12
12
  from pydantic import ValidationError
13
13
 
14
14
  from .common import FormatOptions, Formatter, HTTPLibrary, PydanticVersion
15
- from .language_converters.python.jinja_config import SERVICE_TEMPLATE, create_jinja_env
16
15
  from .models import ConversionResult
17
16
  from .parsers import (
18
17
  generate_code_3_0,
@@ -35,9 +34,7 @@ def write_code(path: Path, content: str, formatter: Formatter) -> None:
35
34
  elif formatter == Formatter.NONE:
36
35
  formatted_contend = content
37
36
  else:
38
- raise NotImplementedError(
39
- f"Missing implementation for formatter {formatter!r}."
40
- )
37
+ raise NotImplementedError(f"Missing implementation for formatter {formatter!r}.")
41
38
  with open(path, "w") as f:
42
39
  f.write(formatted_contend)
43
40
 
@@ -74,9 +71,7 @@ def get_open_api(source: Union[str, Path]):
74
71
  """
75
72
  try:
76
73
  # Handle remote files
77
- if not isinstance(source, Path) and (
78
- source.startswith("http://") or source.startswith("https://")
79
- ):
74
+ if not isinstance(source, Path) and (source.startswith("http://") or source.startswith("https://")):
80
75
  content = httpx.get(source).text
81
76
  # Try JSON first, then YAML for remote files
82
77
  try:
@@ -96,9 +91,7 @@ def get_open_api(source: Union[str, Path]):
96
91
  try:
97
92
  data = yaml.safe_load(file_content)
98
93
  except yaml.YAMLError as e:
99
- click.echo(
100
- f"File {source} is neither a valid JSON nor YAML file: {str(e)}"
101
- )
94
+ click.echo(f"File {source} is neither a valid JSON nor YAML file: {str(e)}")
102
95
  raise
103
96
 
104
97
  # Detect version and parse with appropriate parser
@@ -110,16 +103,12 @@ def get_open_api(source: Union[str, Path]):
110
103
  openapi_obj = parse_openapi_3_1(data) # type: ignore[assignment]
111
104
  else:
112
105
  # Unsupported version detected (version detection already limited to 3.0 / 3.1)
113
- raise ValueError(
114
- f"Unsupported OpenAPI version: {version}. Only 3.0.x and 3.1.x are supported."
115
- )
106
+ raise ValueError(f"Unsupported OpenAPI version: {version}. Only 3.0.x and 3.1.x are supported.")
116
107
 
117
108
  return openapi_obj, version
118
109
 
119
110
  except FileNotFoundError:
120
- click.echo(
121
- f"File {source} not found. Please make sure to pass the path to the OpenAPI specification."
122
- )
111
+ click.echo(f"File {source} not found. Please make sure to pass the path to the OpenAPI specification.")
123
112
  raise
124
113
  except (ConnectError, ConnectTimeout):
125
114
  click.echo(f"Could not connect to {source}.")
@@ -129,70 +118,81 @@ def get_open_api(source: Union[str, Path]):
129
118
  raise
130
119
 
131
120
 
132
- def write_data(
133
- data: ConversionResult, output: Union[str, Path], formatter: Formatter
134
- ) -> None:
135
- """
136
- This function will firstly create the folder structure of output, if it doesn't exist. Then it will create the
137
- models from data.models into the models sub module of the output folder. After this, the services will be created
138
- into the services sub module of the output folder.
139
- :param data: The data to write.
140
- :param output: The path to the output folder.
141
- :param formatter: The formatter applied to the code written.
121
+ def write_data(data: ConversionResult, output: Union[str, Path], formatter: Formatter) -> None:
142
122
  """
123
+ Write generated code to disk.
143
124
 
144
- # Create the folder structure of the output folder.
145
- Path(output).mkdir(parents=True, exist_ok=True)
125
+ Creates:
126
+ - models/ (and models/__init__.py)
127
+ - clients/ (and clients/__init__.py)
128
+ - exceptions.py (package root, if present)
129
+ - __init__.py (package root)
130
+ """
131
+ out = Path(output)
132
+ out.mkdir(parents=True, exist_ok=True)
146
133
 
147
- # Create the models module.
148
- models_path = Path(output) / "models"
134
+ # ----------------------------
135
+ # models/
136
+ # ----------------------------
137
+ models_path = out / "models"
149
138
  models_path.mkdir(parents=True, exist_ok=True)
150
139
 
151
- # Create the services module.
152
- services_path = Path(output) / "services"
153
- services_path.mkdir(parents=True, exist_ok=True)
154
-
155
- files: List[str] = []
156
-
157
- # Write the models.
140
+ model_files: List[str] = []
158
141
  for model in data.models:
159
- files.append(model.file_name)
142
+ model_files.append(model.file_name)
160
143
  write_code(models_path / f"{model.file_name}.py", model.content, formatter)
161
144
 
162
- # Create models.__init__.py file containing imports to all models.
163
145
  write_code(
164
146
  models_path / "__init__.py",
165
- "\n".join([f"from .{file} import *" for file in files]),
147
+ "\n".join([f"from .{f} import *" for f in model_files]) + ("\n" if model_files else ""),
166
148
  formatter,
167
149
  )
168
150
 
169
- files = []
170
-
171
- # Write the services.
172
- jinja_env = create_jinja_env()
173
- for service in data.services:
174
- if len(service.operations) == 0:
175
- continue
176
- files.append(service.file_name)
177
- write_code(
178
- services_path / f"{service.file_name}.py",
179
- jinja_env.get_template(SERVICE_TEMPLATE).render(**service.model_dump()),
180
- formatter,
181
- )
151
+ # ----------------------------
152
+ # clients/
153
+ # ----------------------------
154
+ clients_path = out / "clients"
155
+ clients_path.mkdir(parents=True, exist_ok=True)
156
+
157
+ client_files: List[str] = []
158
+ for client in data.clients:
159
+ client_files.append(client.file_name)
160
+ write_code(clients_path / f"{client.file_name}.py", client.content, formatter)
161
+
162
+ write_code(
163
+ clients_path / "__init__.py",
164
+ "\n".join([f"from .{f} import *" for f in client_files]) + ("\n" if client_files else ""),
165
+ formatter,
166
+ )
182
167
 
183
- # Create services.__init__.py file containing imports to all services.
184
- write_code(services_path / "__init__.py", "", formatter)
168
+ # ----------------------------
169
+ # exceptions/
170
+ # ----------------------------
171
+ exceptions_path = out / "exceptions"
172
+ exceptions_path.mkdir(parents=True, exist_ok=True)
185
173
 
186
- # Write the api_config.py file.
187
- write_code(Path(output) / "api_config.py", data.api_config.content, formatter)
174
+ exception_files: List[str] = []
175
+ for ex in data.exceptions:
176
+ exception_files.append(ex.file_name)
177
+ write_code(exceptions_path / f"{ex.file_name}.py", ex.content, formatter)
188
178
 
189
- # Write the __init__.py file.
190
179
  write_code(
191
- Path(output) / "__init__.py",
192
- "from .models import *\nfrom .services import *\nfrom .api_config import *",
180
+ exceptions_path / "__init__.py",
181
+ "\n".join([f"from .{f} import *" for f in exception_files]) + ("\n" if exception_files else ""),
193
182
  formatter,
194
183
  )
195
184
 
185
+ # ----------------------------
186
+ # package __init__.py (root)
187
+ # ----------------------------
188
+ init_lines: List[str] = [
189
+ "from .models import *",
190
+ "from .clients import *",
191
+ "from .exceptions import *",
192
+ ]
193
+
194
+ write_code(out / "__init__.py", "\n".join(init_lines) + "\n", formatter)
195
+
196
196
 
197
197
  def generate_data(
198
198
  source: Union[str, Path],
@@ -1,7 +1,6 @@
1
1
  import re
2
2
  from typing import Any, Dict, List, Literal, Optional, Tuple, Union
3
3
 
4
- import click
5
4
  from openapi_pydantic.v3 import (
6
5
  Operation,
7
6
  PathItem,
@@ -38,9 +37,11 @@ from openapi_pydantic.v3.v3_1 import (
38
37
  )
39
38
  from openapi_pydantic.v3.v3_1.parameter import Parameter as Parameter31
40
39
 
40
+ from ab_openapi_python_generator.common import PydanticVersion
41
41
  from ab_openapi_python_generator.language_converters.python import common
42
- from ab_openapi_python_generator.language_converters.python.common import normalize_symbol
43
42
  from ab_openapi_python_generator.language_converters.python.jinja_config import (
43
+ ASYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2,
44
+ SYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2,
44
45
  create_jinja_env,
45
46
  )
46
47
  from ab_openapi_python_generator.language_converters.python.model_generator import (
@@ -48,8 +49,8 @@ from ab_openapi_python_generator.language_converters.python.model_generator impo
48
49
  )
49
50
  from ab_openapi_python_generator.models import (
50
51
  LibraryConfig,
52
+ Model,
51
53
  OpReturnType,
52
- Service,
53
54
  ServiceOperation,
54
55
  TypeConversion,
55
56
  )
@@ -123,9 +124,7 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
123
124
  if operation.requestBody is None:
124
125
  return None
125
126
  else:
126
- if isinstance(operation.requestBody, Reference30) or isinstance(
127
- operation.requestBody, Reference31
128
- ):
127
+ if isinstance(operation.requestBody, Reference30) or isinstance(operation.requestBody, Reference31):
129
128
  return "data.dict()"
130
129
 
131
130
  if operation.requestBody.content is None:
@@ -139,9 +138,7 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
139
138
  if media_type is None:
140
139
  return None # pragma: no cover
141
140
 
142
- if isinstance(
143
- media_type.media_type_schema, (Reference, Reference30, Reference31)
144
- ):
141
+ if isinstance(media_type.media_type_schema, (Reference, Reference30, Reference31)):
145
142
  return "data.dict()"
146
143
  elif hasattr(media_type.media_type_schema, "ref"):
147
144
  # Handle Reference objects from different OpenAPI versions
@@ -153,9 +150,7 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
153
150
  elif schema.type == "object":
154
151
  return "data"
155
152
  else:
156
- raise Exception(
157
- f"Unsupported schema type for request body: {schema.type}"
158
- ) # pragma: no cover
153
+ raise Exception(f"Unsupported schema type for request body: {schema.type}") # pragma: no cover
159
154
  else:
160
155
  raise Exception(
161
156
  f"Unsupported schema type for request body: {type(media_type.media_type_schema)}"
@@ -185,26 +180,17 @@ def generate_params(operation: Operation) -> str:
185
180
  required = False
186
181
  param_name_cleaned = common.normalize_symbol(param.name)
187
182
 
188
- if isinstance(param.param_schema, Schema30) or isinstance(
189
- param.param_schema, Schema31
190
- ):
183
+ if isinstance(param.param_schema, Schema30) or isinstance(param.param_schema, Schema31):
191
184
  converted_result = (
192
185
  f"{param_name_cleaned} : {type_converter(param.param_schema, param.required).converted_type}"
193
186
  + ("" if param.required else " = None")
194
187
  )
195
188
  required = param.required
196
- elif isinstance(param.param_schema, Reference30) or isinstance(
197
- param.param_schema, Reference31
198
- ):
199
- converted_result = (
200
- f"{param_name_cleaned} : {param.param_schema.ref.split('/')[-1] }"
201
- + (
202
- ""
203
- if isinstance(param, Reference30)
204
- or isinstance(param, Reference31)
205
- or param.required
206
- else " = None"
207
- )
189
+ elif isinstance(param.param_schema, Reference30) or isinstance(param.param_schema, Reference31):
190
+ converted_result = f"{param_name_cleaned} : {param.param_schema.ref.split('/')[-1]}" + (
191
+ ""
192
+ if isinstance(param, Reference30) or isinstance(param, Reference31) or param.required
193
+ else " = None"
208
194
  )
209
195
  required = isinstance(param, Reference) or param.required
210
196
 
@@ -220,17 +206,11 @@ def generate_params(operation: Operation) -> str:
220
206
  "application/octet-stream",
221
207
  ]
222
208
 
223
- if operation.requestBody is not None and not is_reference_type(
224
- operation.requestBody
225
- ):
209
+ if operation.requestBody is not None and not is_reference_type(operation.requestBody):
226
210
  # Safe access only if it's a concrete RequestBody object
227
211
  rb_content = getattr(operation.requestBody, "content", None)
228
- if isinstance(rb_content, dict) and any(
229
- rb_content.get(i) is not None for i in operation_request_body_types
230
- ):
231
- get_keyword = [
232
- i for i in operation_request_body_types if rb_content.get(i)
233
- ][0]
212
+ if isinstance(rb_content, dict) and any(rb_content.get(i) is not None for i in operation_request_body_types):
213
+ get_keyword = [i for i in operation_request_body_types if rb_content.get(i)][0]
234
214
  content = rb_content.get(get_keyword)
235
215
  if content is not None and hasattr(content, "media_type_schema"):
236
216
  mts = getattr(content, "media_type_schema", None)
@@ -240,9 +220,7 @@ def generate_params(operation: Operation) -> str:
240
220
  ):
241
221
  params += f"{_generate_params_from_content(mts)}, "
242
222
  else: # pragma: no cover
243
- raise Exception(
244
- f"Unsupported media type schema for {str(operation)}: {type(mts)}"
245
- )
223
+ raise Exception(f"Unsupported media type schema for {str(operation)}: {type(mts)}")
246
224
  # else: silently ignore unsupported body shapes (could extend later)
247
225
  # Replace - with _ in params
248
226
  params = params.replace("-", "_")
@@ -251,9 +229,7 @@ def generate_params(operation: Operation) -> str:
251
229
  return params + default_params
252
230
 
253
231
 
254
- def generate_operation_id(
255
- operation: Operation, http_op: str, path_name: Optional[str] = None
256
- ) -> str:
232
+ def generate_operation_id(operation: Operation, http_op: str, path_name: Optional[str] = None) -> str:
257
233
  if operation.operationId is not None:
258
234
  return common.normalize_symbol(operation.operationId)
259
235
  elif path_name is not None:
@@ -264,9 +240,7 @@ def generate_operation_id(
264
240
  ) # pragma: no cover
265
241
 
266
242
 
267
- def _generate_params(
268
- operation: Operation, param_in: Literal["query", "header"] = "query"
269
- ):
243
+ def _generate_params(operation: Operation, param_in: Literal["query", "header"] = "query"):
270
244
  if operation.parameters is None:
271
245
  return []
272
246
 
@@ -319,9 +293,7 @@ def generate_return_type(operation: Operation) -> OpReturnType:
319
293
  media_type_schema = create_media_type_for_reference(chosen_response)
320
294
 
321
295
  if media_type_schema is None:
322
- return OpReturnType(
323
- type=None, status_code=good_responses[0][0], complex_type=False
324
- )
296
+ return OpReturnType(type=None, status_code=good_responses[0][0], complex_type=False)
325
297
 
326
298
  if is_media_type(media_type_schema):
327
299
  inner_schema = getattr(media_type_schema, "media_type_schema", None)
@@ -338,25 +310,18 @@ def generate_return_type(operation: Operation) -> OpReturnType:
338
310
  )
339
311
  elif is_schema_type(inner_schema):
340
312
  converted_result = type_converter(inner_schema, True) # type: ignore
341
- if "array" in converted_result.original_type and isinstance(
342
- converted_result.import_types, list
343
- ):
313
+ if "array" in converted_result.original_type and isinstance(converted_result.import_types, list):
344
314
  matched = re.findall(r"List\[(.+)\]", converted_result.converted_type)
345
315
  if len(matched) > 0:
346
316
  list_type = matched[0]
347
317
  else: # pragma: no cover
348
- raise Exception(
349
- f"Unable to parse list type from {converted_result.converted_type}"
350
- )
318
+ raise Exception(f"Unable to parse list type from {converted_result.converted_type}")
351
319
  else:
352
320
  list_type = None
353
321
  return OpReturnType(
354
322
  type=converted_result,
355
323
  status_code=good_responses[0][0],
356
- complex_type=bool(
357
- converted_result.import_types
358
- and len(converted_result.import_types) > 0
359
- ),
324
+ complex_type=bool(converted_result.import_types and len(converted_result.import_types) > 0),
360
325
  list_type=list_type,
361
326
  )
362
327
  else: # pragma: no cover
@@ -371,63 +336,62 @@ def generate_return_type(operation: Operation) -> OpReturnType:
371
336
  raise Exception("Unknown media type schema type") # pragma: no cover
372
337
 
373
338
 
374
- def generate_services(
375
- paths: Dict[str, PathItem], library_config: LibraryConfig
376
- ) -> List[Service]:
339
+ def clean_up_path_name(path_name: str) -> str:
340
+ # Clean up path name: only replace dashes inside curly brackets for f-string compatibility, keep other dashes
341
+ def _replace_bracket_dashes(match):
342
+ return "{" + match.group(1).replace("-", "_") + "}"
343
+
344
+ return re.sub(r"\{([^}/]+)\}", _replace_bracket_dashes, path_name)
345
+
346
+
347
+ def generate_clients(
348
+ openapi: Any,
349
+ paths: Dict[str, PathItem],
350
+ library_config: LibraryConfig,
351
+ env_token_name: Optional[str],
352
+ pydantic_version: PydanticVersion,
353
+ ) -> List[Model]:
377
354
  """
378
- Generates services from a paths object.
379
- :param paths: paths object to be converted
380
- :return: List of services
355
+ Generate two client modules:
356
+ - sync_client.py (SyncClient)
357
+ - async_client.py (AsyncClient)
381
358
  """
382
359
  jinja_env = create_jinja_env()
383
360
 
384
- def generate_service_operation(
385
- op: Operation, path_name: str, async_type: bool
361
+ service_ops: List[ServiceOperation] = []
362
+
363
+ def _generate_service_operation(
364
+ op: Operation, path_obj: PathItem, path_name: str, http_operation: str, async_type: bool
386
365
  ) -> ServiceOperation:
387
- # Merge path-level parameters (always required by spec) into the
388
- # operation-level parameters so they get turned into function args.
389
366
  try:
390
367
  path_level_params = []
391
- if hasattr(path, "parameters") and path.parameters is not None: # type: ignore
392
- path_level_params = [p for p in path.parameters if p is not None] # type: ignore
368
+ if hasattr(path_obj, "parameters") and path_obj.parameters is not None:
369
+ path_level_params = [p for p in path_obj.parameters if p is not None]
393
370
  if path_level_params:
394
371
  existing_names = set()
395
372
  if op.parameters is not None:
396
- for p in op.parameters: # type: ignore
373
+ for p in op.parameters:
397
374
  if isinstance(p, (Parameter30, Parameter31)):
398
375
  existing_names.add(p.name)
399
376
  for p in path_level_params:
400
- if (
401
- isinstance(p, (Parameter30, Parameter31))
402
- and p.name not in existing_names
403
- ):
377
+ if isinstance(p, (Parameter30, Parameter31)) and p.name not in existing_names:
404
378
  if op.parameters is None:
405
379
  op.parameters = [] # type: ignore
406
380
  op.parameters.append(p) # type: ignore
407
- except Exception: # pragma: no cover
408
- print(
409
- f"Error merging path-level parameters for {path_name}"
410
- ) # pragma: no cover
381
+ except Exception:
411
382
  pass
412
383
 
413
384
  params = generate_params(op)
414
- # Fallback: ensure all {placeholders} in path are present as function params
415
385
  try:
416
- placeholder_names = [
417
- m.group(1) for m in re.finditer(r"\{([^}/]+)\}", path_name)
418
- ]
419
- existing_param_names = {
420
- p.split(":")[0].strip() for p in params.split(",") if ":" in p
421
- }
386
+ placeholder_names = [m.group(1) for m in re.finditer(r"\{([^}/]+)\}", path_name)]
387
+ existing_param_names = {p.split(":")[0].strip() for p in params.split(",") if ":" in p}
422
388
  for ph in placeholder_names:
423
389
  norm_ph = common.normalize_symbol(ph)
424
390
  if norm_ph not in existing_param_names and norm_ph:
425
391
  params = f"{norm_ph}: Any, " + params
426
- except Exception: # pragma: no cover
427
- print(
428
- f"Error ensuring path placeholders in params for {path_name}"
429
- ) # pragma: no cover
392
+ except Exception:
430
393
  pass
394
+
431
395
  operation_id = generate_operation_id(op, http_operation, path_name)
432
396
  query_params = generate_query_params(op)
433
397
  header_params = generate_header_params(op)
@@ -441,7 +405,7 @@ def generate_services(
441
405
  header_params=header_params,
442
406
  return_type=return_type,
443
407
  operation=op,
444
- pathItem=path,
408
+ pathItem=path_obj,
445
409
  content="",
446
410
  async_client=async_type,
447
411
  body_param=body_param,
@@ -451,90 +415,42 @@ def generate_services(
451
415
  use_orjson=common.get_use_orjson(),
452
416
  )
453
417
 
454
- so.content = jinja_env.get_template(library_config.template_name).render(
455
- **so.model_dump()
456
- )
457
-
458
- if op.tags is not None and len(op.tags) > 0:
459
- so.tag = normalize_symbol(op.tags[0])
460
-
461
- try:
462
- compile(so.content, "<string>", "exec")
463
- except SyntaxError as e: # pragma: no cover
464
- click.echo(f"Error in service {so.operation_id}: {e}") # pragma: no cover
465
-
466
418
  return so
467
419
 
468
- services = []
469
- service_ops = []
470
420
  for path_name, path in paths.items():
471
421
  clean_path_name = clean_up_path_name(path_name)
472
422
  for http_operation in HTTP_OPERATIONS:
473
- op = path.__getattribute__(http_operation)
423
+ op = getattr(path, http_operation)
474
424
  if op is None:
475
425
  continue
476
426
 
477
427
  if library_config.include_sync:
478
- sync_so = generate_service_operation(op, clean_path_name, False)
479
- service_ops.append(sync_so)
480
-
428
+ service_ops.append(_generate_service_operation(op, path, clean_path_name, http_operation, False))
481
429
  if library_config.include_async:
482
- async_so = generate_service_operation(op, clean_path_name, True)
483
- service_ops.append(async_so)
484
-
485
- # Ensure every operation has a tag; fallback to "default" for untagged operations
486
- for so in service_ops:
487
- if not so.tag:
488
- so.tag = "default"
489
-
490
- tags = list({so.tag for so in service_ops})
491
-
492
- for tag in tags:
493
- services.append(
494
- Service(
495
- file_name=f"{tag}_service",
496
- operations=[
497
- so for so in service_ops if so.tag == tag and not so.async_client
498
- ],
499
- content="\n".join(
500
- [
501
- so.content
502
- for so in service_ops
503
- if so.tag == tag and not so.async_client
504
- ]
505
- ),
506
- async_client=False,
507
- library_import=library_config.library_name,
508
- use_orjson=common.get_use_orjson(),
509
- )
510
- )
511
-
512
- for tag in tags:
513
- services.append(
514
- Service(
515
- file_name=f"async_{tag}_service",
516
- operations=[
517
- so for so in service_ops if so.tag == tag and so.async_client
518
- ],
519
- content="\n".join(
520
- [
521
- so.content
522
- for so in service_ops
523
- if so.tag == tag and so.async_client
524
- ]
525
- ),
526
- async_client=True,
527
- library_import=library_config.library_name,
528
- use_orjson=common.get_use_orjson(),
529
- )
530
- )
531
-
532
- return services
533
-
534
-
535
- def clean_up_path_name(path_name: str) -> str:
536
- # Clean up path name: only replace dashes inside curly brackets for f-string compatibility, keep other dashes
537
- def _replace_bracket_dashes(match):
538
- return "{" + match.group(1).replace("-", "_") + "}"
430
+ service_ops.append(_generate_service_operation(op, path, clean_path_name, http_operation, True))
431
+
432
+ sync_ops = [so for so in service_ops if not so.async_client]
433
+ async_ops = [so for so in service_ops if so.async_client]
434
+
435
+ openapi_dump = openapi.model_dump() if hasattr(openapi, "model_dump") else {}
436
+
437
+ sync_content = jinja_env.get_template(SYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2).render(
438
+ **openapi_dump,
439
+ env_token_name=env_token_name,
440
+ operations=[so.model_dump() for so in sync_ops],
441
+ )
442
+ async_content = jinja_env.get_template(ASYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2).render(
443
+ **openapi_dump,
444
+ env_token_name=env_token_name,
445
+ operations=[so.model_dump() for so in async_ops],
446
+ )
447
+
448
+ compile(sync_content, "<string>", "exec")
449
+ compile(async_content, "<string>", "exec")
450
+
451
+ clients: List[Model] = [
452
+ Model(file_name="sync_client", content=sync_content, openapi_object={}, properties=[]),
453
+ Model(file_name="async_client", content=async_content, openapi_object={}, properties=[]),
454
+ ]
539
455
 
540
- return re.sub(r"\{([^}/]+)\}", _replace_bracket_dashes, path_name)
456
+ return clients
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from ab_openapi_python_generator.language_converters.python.jinja_config import (
4
+ HTTP_EXCEPTION_TEMPLATE,
5
+ create_jinja_env,
6
+ )
7
+ from ab_openapi_python_generator.models import Model
8
+
9
+
10
+ def generate_exceptions() -> list[Model]:
11
+ """
12
+ Generate shared exception modules (package-local support code).
13
+ """
14
+ jinja_env = create_jinja_env()
15
+
16
+ http_exception = Model(
17
+ file_name="http_exception",
18
+ content=jinja_env.get_template(HTTP_EXCEPTION_TEMPLATE).render(),
19
+ openapi_object=None, # Model.openapi_object is optional now
20
+ properties=[],
21
+ )
22
+
23
+ return [http_exception]