ab-openapi-python-generator 2.1.4.dev1768280320__py3-none-any.whl → 2.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ab_openapi_python_generator/__init__.py +14 -10
- ab_openapi_python_generator/__main__.py +85 -0
- ab_openapi_python_generator/common.py +58 -0
- ab_openapi_python_generator/generate_data.py +235 -0
- ab_openapi_python_generator/language_converters/__init__.py +0 -0
- ab_openapi_python_generator/language_converters/python/__init__.py +0 -0
- ab_openapi_python_generator/language_converters/python/client_generator.py +450 -0
- ab_openapi_python_generator/language_converters/python/common.py +58 -0
- ab_openapi_python_generator/language_converters/python/exception_generator.py +23 -0
- ab_openapi_python_generator/language_converters/python/generator.py +52 -0
- ab_openapi_python_generator/language_converters/python/jinja_config.py +38 -0
- ab_openapi_python_generator/language_converters/python/model_generator.py +838 -0
- ab_openapi_python_generator/language_converters/python/templates/alias_union.jinja2 +17 -0
- ab_openapi_python_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +80 -0
- ab_openapi_python_generator/language_converters/python/templates/discriminator_enum.jinja2 +7 -0
- ab_openapi_python_generator/language_converters/python/templates/enum.jinja2 +11 -0
- ab_openapi_python_generator/language_converters/python/templates/http_exception.jinja2 +8 -0
- ab_openapi_python_generator/language_converters/python/templates/models.jinja2 +24 -0
- ab_openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +28 -0
- ab_openapi_python_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +80 -0
- ab_openapi_python_generator/models.py +101 -0
- ab_openapi_python_generator/parsers/__init__.py +13 -0
- ab_openapi_python_generator/parsers/openapi_30.py +65 -0
- ab_openapi_python_generator/parsers/openapi_31.py +65 -0
- ab_openapi_python_generator/py.typed +0 -0
- ab_openapi_python_generator/version_detector.py +67 -0
- {ab_openapi_python_generator-2.1.4.dev1768280320.dist-info → ab_openapi_python_generator-2.2.1.dist-info}/METADATA +21 -27
- ab_openapi_python_generator-2.2.1.dist-info/RECORD +31 -0
- {ab_openapi_python_generator-2.1.4.dev1768280320.dist-info → ab_openapi_python_generator-2.2.1.dist-info}/WHEEL +1 -1
- ab_openapi_python_generator-2.2.1.dist-info/entry_points.txt +2 -0
- ab_openapi_python_generator-2.1.4.dev1768280320.dist-info/RECORD +0 -6
- ab_openapi_python_generator-2.1.4.dev1768280320.dist-info/entry_points.txt +0 -3
- {ab_openapi_python_generator-2.1.4.dev1768280320.dist-info → ab_openapi_python_generator-2.2.1.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
from openapi_pydantic.v3 import (
|
|
5
|
+
Operation,
|
|
6
|
+
PathItem,
|
|
7
|
+
Reference,
|
|
8
|
+
Response,
|
|
9
|
+
Schema,
|
|
10
|
+
)
|
|
11
|
+
from openapi_pydantic.v3.v3_0 import (
|
|
12
|
+
MediaType as MediaType30,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Import version-specific types for isinstance checks
|
|
16
|
+
from openapi_pydantic.v3.v3_0 import (
|
|
17
|
+
Reference as Reference30,
|
|
18
|
+
)
|
|
19
|
+
from openapi_pydantic.v3.v3_0 import (
|
|
20
|
+
Response as Response30,
|
|
21
|
+
)
|
|
22
|
+
from openapi_pydantic.v3.v3_0 import (
|
|
23
|
+
Schema as Schema30,
|
|
24
|
+
)
|
|
25
|
+
from openapi_pydantic.v3.v3_0.parameter import Parameter as Parameter30
|
|
26
|
+
from openapi_pydantic.v3.v3_1 import (
|
|
27
|
+
MediaType as MediaType31,
|
|
28
|
+
)
|
|
29
|
+
from openapi_pydantic.v3.v3_1 import (
|
|
30
|
+
Reference as Reference31,
|
|
31
|
+
)
|
|
32
|
+
from openapi_pydantic.v3.v3_1 import (
|
|
33
|
+
Response as Response31,
|
|
34
|
+
)
|
|
35
|
+
from openapi_pydantic.v3.v3_1 import (
|
|
36
|
+
Schema as Schema31,
|
|
37
|
+
)
|
|
38
|
+
from openapi_pydantic.v3.v3_1.parameter import Parameter as Parameter31
|
|
39
|
+
|
|
40
|
+
from ab_openapi_python_generator.common import PydanticVersion
|
|
41
|
+
from ab_openapi_python_generator.language_converters.python import common
|
|
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,
|
|
45
|
+
create_jinja_env,
|
|
46
|
+
)
|
|
47
|
+
from ab_openapi_python_generator.language_converters.python.model_generator import (
|
|
48
|
+
type_converter,
|
|
49
|
+
)
|
|
50
|
+
from ab_openapi_python_generator.models import (
|
|
51
|
+
LibraryConfig,
|
|
52
|
+
Model,
|
|
53
|
+
OpReturnType,
|
|
54
|
+
ServiceOperation,
|
|
55
|
+
TypeConversion,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Helper functions for isinstance checks across OpenAPI versions
|
|
60
|
+
def is_response_type(obj) -> bool:
|
|
61
|
+
"""Check if object is a Response from any OpenAPI version"""
|
|
62
|
+
return isinstance(obj, (Response30, Response31))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_media_type_for_reference(
|
|
66
|
+
reference_obj: Union[Response30, Reference30, Response31, Reference31],
|
|
67
|
+
):
|
|
68
|
+
"""Create a MediaType wrapper for a reference object, using the correct version"""
|
|
69
|
+
# Check which version the reference object belongs to
|
|
70
|
+
if isinstance(reference_obj, Reference30):
|
|
71
|
+
return MediaType30(schema=reference_obj) # type: ignore - pydantic issue with generics
|
|
72
|
+
elif isinstance(reference_obj, Reference31):
|
|
73
|
+
return MediaType31(schema=reference_obj) # type: ignore - pydantic issue with generics
|
|
74
|
+
else:
|
|
75
|
+
# Fallback to v3.0 for generic Reference
|
|
76
|
+
return MediaType30(schema=reference_obj) # type: ignore - pydantic issue with generics
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_media_type(obj) -> bool:
|
|
80
|
+
"""Check if object is a MediaType from any OpenAPI version"""
|
|
81
|
+
return isinstance(obj, (MediaType30, MediaType31))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_reference_type(obj: Any) -> bool:
|
|
85
|
+
"""Check if object is a Reference type across different versions."""
|
|
86
|
+
return isinstance(obj, (Reference, Reference30, Reference31))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_schema_type(obj: Any) -> bool:
|
|
90
|
+
"""Check if object is a Schema from any OpenAPI version"""
|
|
91
|
+
return isinstance(obj, (Schema30, Schema31))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def operation_is_sse(op: Operation) -> bool:
|
|
95
|
+
"""Detect if an Operation advertises Server-Sent-Events (text/event-stream) in any 2xx response."""
|
|
96
|
+
if not getattr(op, "responses", None):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
for status_code, resp in op.responses.items():
|
|
100
|
+
try:
|
|
101
|
+
if not str(status_code).startswith("2"):
|
|
102
|
+
continue
|
|
103
|
+
except Exception:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# Concrete Response object
|
|
107
|
+
if is_response_type(resp):
|
|
108
|
+
content = getattr(resp, "content", None)
|
|
109
|
+
if isinstance(content, dict) and "text/event-stream" in content:
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
# Reference responses could be resolved externally; skip for now
|
|
113
|
+
if is_reference_type(resp):
|
|
114
|
+
# If you need supporting $ref'ed SSE responses, resolve via components
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
HTTP_OPERATIONS = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def generate_body_param(operation: Operation) -> Union[str, None]:
|
|
124
|
+
if operation.requestBody is None:
|
|
125
|
+
return None
|
|
126
|
+
else:
|
|
127
|
+
if isinstance(operation.requestBody, Reference30) or isinstance(operation.requestBody, Reference31):
|
|
128
|
+
return "data.dict()"
|
|
129
|
+
|
|
130
|
+
if operation.requestBody.content is None:
|
|
131
|
+
return None # pragma: no cover
|
|
132
|
+
|
|
133
|
+
if operation.requestBody.content.get("application/json") is None:
|
|
134
|
+
return None # pragma: no cover
|
|
135
|
+
|
|
136
|
+
media_type = operation.requestBody.content.get("application/json")
|
|
137
|
+
|
|
138
|
+
if media_type is None:
|
|
139
|
+
return None # pragma: no cover
|
|
140
|
+
|
|
141
|
+
if isinstance(media_type.media_type_schema, (Reference, Reference30, Reference31)):
|
|
142
|
+
return "data.dict()"
|
|
143
|
+
elif hasattr(media_type.media_type_schema, "ref"):
|
|
144
|
+
# Handle Reference objects from different OpenAPI versions
|
|
145
|
+
return "data.dict()"
|
|
146
|
+
elif isinstance(media_type.media_type_schema, (Schema, Schema30, Schema31)):
|
|
147
|
+
schema = media_type.media_type_schema
|
|
148
|
+
if schema.type == "array":
|
|
149
|
+
return "[i.dict() for i in data]"
|
|
150
|
+
elif schema.type == "object":
|
|
151
|
+
return "data"
|
|
152
|
+
else:
|
|
153
|
+
raise Exception(f"Unsupported schema type for request body: {schema.type}") # pragma: no cover
|
|
154
|
+
else:
|
|
155
|
+
raise Exception(
|
|
156
|
+
f"Unsupported schema type for request body: {type(media_type.media_type_schema)}"
|
|
157
|
+
) # pragma: no cover
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def generate_params(operation: Operation) -> str:
|
|
161
|
+
def _generate_params_from_content(content: Any):
|
|
162
|
+
# Accept reference from either 3.0 or 3.1
|
|
163
|
+
if isinstance(content, (Reference, Reference30, Reference31)):
|
|
164
|
+
return f"data : {content.ref.split('/')[-1]}" # type: ignore
|
|
165
|
+
elif isinstance(content, (Schema, Schema30, Schema31)):
|
|
166
|
+
return f"data : {type_converter(content, True).converted_type}" # type: ignore
|
|
167
|
+
else: # pragma: no cover
|
|
168
|
+
raise Exception(f"Unsupported request body schema type: {type(content)}")
|
|
169
|
+
|
|
170
|
+
if operation.parameters is None and operation.requestBody is None:
|
|
171
|
+
return ""
|
|
172
|
+
|
|
173
|
+
params = ""
|
|
174
|
+
default_params = ""
|
|
175
|
+
if operation.parameters is not None:
|
|
176
|
+
for param in operation.parameters:
|
|
177
|
+
if not isinstance(param, (Parameter30, Parameter31)):
|
|
178
|
+
continue # pragma: no cover
|
|
179
|
+
converted_result = ""
|
|
180
|
+
required = False
|
|
181
|
+
param_name_cleaned = common.normalize_symbol(param.name)
|
|
182
|
+
|
|
183
|
+
if isinstance(param.param_schema, Schema30) or isinstance(param.param_schema, Schema31):
|
|
184
|
+
converted_result = (
|
|
185
|
+
f"{param_name_cleaned} : {type_converter(param.param_schema, param.required).converted_type}"
|
|
186
|
+
+ ("" if param.required else " = None")
|
|
187
|
+
)
|
|
188
|
+
required = param.required
|
|
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"
|
|
194
|
+
)
|
|
195
|
+
required = isinstance(param, Reference) or param.required
|
|
196
|
+
|
|
197
|
+
if required:
|
|
198
|
+
params += f"{converted_result}, "
|
|
199
|
+
else:
|
|
200
|
+
default_params += f"{converted_result}, "
|
|
201
|
+
|
|
202
|
+
operation_request_body_types = [
|
|
203
|
+
"application/json",
|
|
204
|
+
"text/plain",
|
|
205
|
+
"multipart/form-data",
|
|
206
|
+
"application/octet-stream",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
if operation.requestBody is not None and not is_reference_type(operation.requestBody):
|
|
210
|
+
# Safe access only if it's a concrete RequestBody object
|
|
211
|
+
rb_content = getattr(operation.requestBody, "content", None)
|
|
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]
|
|
214
|
+
content = rb_content.get(get_keyword)
|
|
215
|
+
if content is not None and hasattr(content, "media_type_schema"):
|
|
216
|
+
mts = getattr(content, "media_type_schema", None)
|
|
217
|
+
if isinstance(
|
|
218
|
+
mts,
|
|
219
|
+
(Reference, Reference30, Reference31, Schema, Schema30, Schema31),
|
|
220
|
+
):
|
|
221
|
+
params += f"{_generate_params_from_content(mts)}, "
|
|
222
|
+
else: # pragma: no cover
|
|
223
|
+
raise Exception(f"Unsupported media type schema for {str(operation)}: {type(mts)}")
|
|
224
|
+
# else: silently ignore unsupported body shapes (could extend later)
|
|
225
|
+
# Replace - with _ in params
|
|
226
|
+
params = params.replace("-", "_")
|
|
227
|
+
default_params = default_params.replace("-", "_")
|
|
228
|
+
|
|
229
|
+
return params + default_params
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def generate_operation_id(operation: Operation, http_op: str, path_name: Optional[str] = None) -> str:
|
|
233
|
+
if operation.operationId is not None:
|
|
234
|
+
return common.normalize_symbol(operation.operationId)
|
|
235
|
+
elif path_name is not None:
|
|
236
|
+
return common.normalize_symbol(f"{http_op}_{path_name}")
|
|
237
|
+
else:
|
|
238
|
+
raise Exception(
|
|
239
|
+
f"OperationId is not defined for {http_op} of path_name {path_name} --> {operation.summary}"
|
|
240
|
+
) # pragma: no cover
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _generate_params(operation: Operation, param_in: Literal["query", "header"] = "query"):
|
|
244
|
+
if operation.parameters is None:
|
|
245
|
+
return []
|
|
246
|
+
|
|
247
|
+
params = []
|
|
248
|
+
for param in operation.parameters:
|
|
249
|
+
if isinstance(param, (Parameter30, Parameter31)) and param.param_in == param_in:
|
|
250
|
+
param_name_cleaned = common.normalize_symbol(param.name)
|
|
251
|
+
params.append(f"{param.name!r} : {param_name_cleaned}")
|
|
252
|
+
|
|
253
|
+
return params
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def generate_query_params(operation: Operation) -> List[str]:
|
|
257
|
+
return _generate_params(operation, "query")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def generate_header_params(operation: Operation) -> List[str]:
|
|
261
|
+
return _generate_params(operation, "header")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def generate_return_type(operation: Operation) -> OpReturnType:
|
|
265
|
+
if operation.responses is None:
|
|
266
|
+
return OpReturnType(type=None, status_code=200, complex_type=False)
|
|
267
|
+
|
|
268
|
+
good_responses: List[Tuple[int, Union[Response, Reference]]] = [
|
|
269
|
+
(int(status_code), response)
|
|
270
|
+
for status_code, response in operation.responses.items()
|
|
271
|
+
if status_code.startswith("2")
|
|
272
|
+
]
|
|
273
|
+
if len(good_responses) == 0:
|
|
274
|
+
return OpReturnType(type=None, status_code=200, complex_type=False)
|
|
275
|
+
|
|
276
|
+
chosen_response = good_responses[0][1]
|
|
277
|
+
media_type_schema = None
|
|
278
|
+
|
|
279
|
+
if is_response_type(chosen_response):
|
|
280
|
+
# It's a Response type, access content safely
|
|
281
|
+
if hasattr(chosen_response, "content") and chosen_response.content is not None: # type: ignore
|
|
282
|
+
content = chosen_response.content # type: ignore
|
|
283
|
+
# Prefer application/json, then text/event-stream, then first available
|
|
284
|
+
if isinstance(content, dict):
|
|
285
|
+
media_type_schema = (
|
|
286
|
+
content.get("application/json")
|
|
287
|
+
or content.get("text/event-stream")
|
|
288
|
+
or next(iter(content.values()), None)
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
media_type_schema = None
|
|
292
|
+
elif is_reference_type(chosen_response):
|
|
293
|
+
media_type_schema = create_media_type_for_reference(chosen_response)
|
|
294
|
+
|
|
295
|
+
if media_type_schema is None:
|
|
296
|
+
return OpReturnType(type=None, status_code=good_responses[0][0], complex_type=False)
|
|
297
|
+
|
|
298
|
+
if is_media_type(media_type_schema):
|
|
299
|
+
inner_schema = getattr(media_type_schema, "media_type_schema", None)
|
|
300
|
+
if is_reference_type(inner_schema):
|
|
301
|
+
type_conv = TypeConversion(
|
|
302
|
+
original_type=inner_schema.ref, # type: ignore
|
|
303
|
+
converted_type=inner_schema.ref.split("/")[-1], # type: ignore
|
|
304
|
+
import_types=[inner_schema.ref.split("/")[-1]], # type: ignore
|
|
305
|
+
)
|
|
306
|
+
return OpReturnType(
|
|
307
|
+
type=type_conv,
|
|
308
|
+
status_code=good_responses[0][0],
|
|
309
|
+
complex_type=True,
|
|
310
|
+
)
|
|
311
|
+
elif is_schema_type(inner_schema):
|
|
312
|
+
converted_result = type_converter(inner_schema, True) # type: ignore
|
|
313
|
+
if "array" in converted_result.original_type and isinstance(converted_result.import_types, list):
|
|
314
|
+
matched = re.findall(r"List\[(.+)\]", converted_result.converted_type)
|
|
315
|
+
if len(matched) > 0:
|
|
316
|
+
list_type = matched[0]
|
|
317
|
+
else: # pragma: no cover
|
|
318
|
+
raise Exception(f"Unable to parse list type from {converted_result.converted_type}")
|
|
319
|
+
else:
|
|
320
|
+
list_type = None
|
|
321
|
+
return OpReturnType(
|
|
322
|
+
type=converted_result,
|
|
323
|
+
status_code=good_responses[0][0],
|
|
324
|
+
complex_type=bool(converted_result.import_types and len(converted_result.import_types) > 0),
|
|
325
|
+
list_type=list_type,
|
|
326
|
+
)
|
|
327
|
+
else: # pragma: no cover
|
|
328
|
+
raise Exception("Unknown media type schema type")
|
|
329
|
+
elif media_type_schema is None:
|
|
330
|
+
return OpReturnType(
|
|
331
|
+
type=None,
|
|
332
|
+
status_code=good_responses[0][0],
|
|
333
|
+
complex_type=False,
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
raise Exception("Unknown media type schema type") # pragma: no cover
|
|
337
|
+
|
|
338
|
+
|
|
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]:
|
|
354
|
+
"""
|
|
355
|
+
Generate two client modules:
|
|
356
|
+
- sync_client.py (SyncClient)
|
|
357
|
+
- async_client.py (AsyncClient)
|
|
358
|
+
"""
|
|
359
|
+
jinja_env = create_jinja_env()
|
|
360
|
+
|
|
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
|
|
365
|
+
) -> ServiceOperation:
|
|
366
|
+
path_level_params = []
|
|
367
|
+
if hasattr(path_obj, "parameters") and path_obj.parameters is not None:
|
|
368
|
+
path_level_params = [p for p in path_obj.parameters if p is not None]
|
|
369
|
+
if path_level_params:
|
|
370
|
+
existing_names = set()
|
|
371
|
+
if op.parameters is not None:
|
|
372
|
+
for p in op.parameters:
|
|
373
|
+
if isinstance(p, (Parameter30, Parameter31)):
|
|
374
|
+
existing_names.add(p.name)
|
|
375
|
+
for p in path_level_params:
|
|
376
|
+
if isinstance(p, (Parameter30, Parameter31)) and p.name not in existing_names:
|
|
377
|
+
if op.parameters is None:
|
|
378
|
+
op.parameters = [] # type: ignore
|
|
379
|
+
op.parameters.append(p) # type: ignore
|
|
380
|
+
|
|
381
|
+
params = generate_params(op)
|
|
382
|
+
placeholder_names = [m.group(1) for m in re.finditer(r"\{([^}/]+)\}", path_name)]
|
|
383
|
+
existing_param_names = {p.split(":")[0].strip() for p in params.split(",") if ":" in p}
|
|
384
|
+
for ph in placeholder_names:
|
|
385
|
+
norm_ph = common.normalize_symbol(ph)
|
|
386
|
+
if norm_ph not in existing_param_names and norm_ph:
|
|
387
|
+
params = f"{norm_ph}: Any, " + params
|
|
388
|
+
|
|
389
|
+
operation_id = generate_operation_id(op, http_operation, path_name)
|
|
390
|
+
query_params = generate_query_params(op)
|
|
391
|
+
header_params = generate_header_params(op)
|
|
392
|
+
return_type = generate_return_type(op)
|
|
393
|
+
body_param = generate_body_param(op)
|
|
394
|
+
|
|
395
|
+
so = ServiceOperation(
|
|
396
|
+
params=params,
|
|
397
|
+
operation_id=operation_id,
|
|
398
|
+
query_params=query_params,
|
|
399
|
+
header_params=header_params,
|
|
400
|
+
return_type=return_type,
|
|
401
|
+
operation=op,
|
|
402
|
+
pathItem=path_obj,
|
|
403
|
+
content="",
|
|
404
|
+
async_client=async_type,
|
|
405
|
+
body_param=body_param,
|
|
406
|
+
path_name=path_name,
|
|
407
|
+
method=http_operation,
|
|
408
|
+
is_sse=operation_is_sse(op),
|
|
409
|
+
use_orjson=common.get_use_orjson(),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return so
|
|
413
|
+
|
|
414
|
+
for path_name, path in paths.items():
|
|
415
|
+
clean_path_name = clean_up_path_name(path_name)
|
|
416
|
+
for http_operation in HTTP_OPERATIONS:
|
|
417
|
+
op = getattr(path, http_operation)
|
|
418
|
+
if op is None:
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
if library_config.include_sync:
|
|
422
|
+
service_ops.append(_generate_service_operation(op, path, clean_path_name, http_operation, False))
|
|
423
|
+
if library_config.include_async:
|
|
424
|
+
service_ops.append(_generate_service_operation(op, path, clean_path_name, http_operation, True))
|
|
425
|
+
|
|
426
|
+
sync_ops = [so for so in service_ops if not so.async_client]
|
|
427
|
+
async_ops = [so for so in service_ops if so.async_client]
|
|
428
|
+
|
|
429
|
+
openapi_dump = openapi.model_dump() if hasattr(openapi, "model_dump") else {}
|
|
430
|
+
|
|
431
|
+
sync_content = jinja_env.get_template(SYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2).render(
|
|
432
|
+
**openapi_dump,
|
|
433
|
+
env_token_name=env_token_name,
|
|
434
|
+
operations=[so.model_dump() for so in sync_ops],
|
|
435
|
+
)
|
|
436
|
+
async_content = jinja_env.get_template(ASYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2).render(
|
|
437
|
+
**openapi_dump,
|
|
438
|
+
env_token_name=env_token_name,
|
|
439
|
+
operations=[so.model_dump() for so in async_ops],
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
compile(sync_content, "<string>", "exec")
|
|
443
|
+
compile(async_content, "<string>", "exec")
|
|
444
|
+
|
|
445
|
+
clients: List[Model] = [
|
|
446
|
+
Model(file_name="sync_client", content=sync_content, openapi_object={}, properties=[]),
|
|
447
|
+
Model(file_name="async_client", content=async_content, openapi_object={}, properties=[]),
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
return clients
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import keyword
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
_use_orjson: bool = False
|
|
6
|
+
_custom_template_path: Optional[str] = None
|
|
7
|
+
_symbol_ascii_strip_re = re.compile(r"[^A-Za-z0-9_]")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def set_use_orjson(value: bool) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Set the value of the global variable _use_orjson.
|
|
13
|
+
:param value: value of the variable
|
|
14
|
+
"""
|
|
15
|
+
global _use_orjson
|
|
16
|
+
_use_orjson = value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_use_orjson() -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Get the value of the global variable _use_orjson.
|
|
22
|
+
:return: value of the variable
|
|
23
|
+
"""
|
|
24
|
+
global _use_orjson
|
|
25
|
+
return _use_orjson
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_custom_template_path(value: Optional[str]) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Set the value of the global variable _custom_template_path.
|
|
31
|
+
:param value: value of the variable
|
|
32
|
+
"""
|
|
33
|
+
global _custom_template_path
|
|
34
|
+
_custom_template_path = value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_custom_template_path() -> Optional[str]:
|
|
38
|
+
"""
|
|
39
|
+
Get the value of the global variable _custom_template_path.
|
|
40
|
+
:return: value of the variable
|
|
41
|
+
"""
|
|
42
|
+
global _custom_template_path
|
|
43
|
+
return _custom_template_path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_symbol(symbol: str) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Remove invalid characters & keywords in Python symbol names
|
|
49
|
+
:param symbol: name of the identifier
|
|
50
|
+
:return: normalized identifier name
|
|
51
|
+
"""
|
|
52
|
+
symbol = symbol.replace("-", "_").replace(" ", "_")
|
|
53
|
+
normalized_symbol = _symbol_ascii_strip_re.sub("", symbol)
|
|
54
|
+
if normalized_symbol in keyword.kwlist:
|
|
55
|
+
normalized_symbol = normalized_symbol + "_"
|
|
56
|
+
if len(normalized_symbol) > 0 and normalized_symbol[0].isnumeric():
|
|
57
|
+
normalized_symbol = "_" + normalized_symbol
|
|
58
|
+
return normalized_symbol
|
|
@@ -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]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
from openapi_pydantic.v3.v3_0 import OpenAPI as OpenAPI30
|
|
4
|
+
from openapi_pydantic.v3.v3_1 import OpenAPI as OpenAPI31
|
|
5
|
+
|
|
6
|
+
from ab_openapi_python_generator.common import PydanticVersion
|
|
7
|
+
from ab_openapi_python_generator.language_converters.python import common
|
|
8
|
+
from ab_openapi_python_generator.language_converters.python.client_generator import (
|
|
9
|
+
generate_clients,
|
|
10
|
+
)
|
|
11
|
+
from ab_openapi_python_generator.language_converters.python.exception_generator import (
|
|
12
|
+
generate_exceptions,
|
|
13
|
+
)
|
|
14
|
+
from ab_openapi_python_generator.language_converters.python.model_generator import (
|
|
15
|
+
generate_models,
|
|
16
|
+
)
|
|
17
|
+
from ab_openapi_python_generator.models import ConversionResult, LibraryConfig
|
|
18
|
+
|
|
19
|
+
# Type alias for both OpenAPI versions
|
|
20
|
+
OpenAPISpec = Union[OpenAPI30, OpenAPI31]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generator(
|
|
24
|
+
data: OpenAPISpec,
|
|
25
|
+
library_config: LibraryConfig,
|
|
26
|
+
env_token_name: Optional[str] = None,
|
|
27
|
+
use_orjson: bool = False,
|
|
28
|
+
custom_template_path: Optional[str] = None,
|
|
29
|
+
pydantic_version: PydanticVersion = PydanticVersion.V2,
|
|
30
|
+
) -> ConversionResult:
|
|
31
|
+
"""
|
|
32
|
+
Generate Python code from an OpenAPI 3.0+ specification.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
common.set_use_orjson(use_orjson)
|
|
36
|
+
common.set_custom_template_path(custom_template_path)
|
|
37
|
+
|
|
38
|
+
if data.components is not None:
|
|
39
|
+
models = generate_models(data.components, pydantic_version)
|
|
40
|
+
else:
|
|
41
|
+
models = []
|
|
42
|
+
|
|
43
|
+
if data.paths is not None:
|
|
44
|
+
clients = generate_clients(data, data.paths, library_config, env_token_name, pydantic_version)
|
|
45
|
+
else:
|
|
46
|
+
clients = []
|
|
47
|
+
|
|
48
|
+
return ConversionResult(
|
|
49
|
+
models=models,
|
|
50
|
+
clients=clients,
|
|
51
|
+
exceptions=generate_exceptions(),
|
|
52
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from jinja2 import ChoiceLoader, Environment, FileSystemLoader
|
|
4
|
+
|
|
5
|
+
from . import common
|
|
6
|
+
|
|
7
|
+
ENUM_TEMPLATE = "enum.jinja2"
|
|
8
|
+
MODELS_TEMPLATE = "models.jinja2"
|
|
9
|
+
MODELS_TEMPLATE_PYDANTIC_V2 = "models_pydantic_2.jinja2"
|
|
10
|
+
ALIAS_UNION_TEMPLATE = "alias_union.jinja2"
|
|
11
|
+
DISCRIMINATOR_ENUM_TEMPLATE = "discriminator_enum.jinja2"
|
|
12
|
+
HTTP_EXCEPTION_TEMPLATE = "http_exception.jinja2"
|
|
13
|
+
SYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2 = "sync_client_httpx_pydantic_2.jinja2"
|
|
14
|
+
ASYNC_CLIENT_HTTPX_TEMPLATE_PYDANTIC_V2 = "async_client_httpx_pydantic_2.jinja2"
|
|
15
|
+
TEMPLATE_PATH = Path(__file__).parent / "templates"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_jinja_env():
|
|
19
|
+
custom_template_path = common.get_custom_template_path()
|
|
20
|
+
environment = Environment(
|
|
21
|
+
loader=(
|
|
22
|
+
ChoiceLoader(
|
|
23
|
+
[
|
|
24
|
+
FileSystemLoader(custom_template_path),
|
|
25
|
+
FileSystemLoader(TEMPLATE_PATH),
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
if custom_template_path is not None
|
|
29
|
+
else FileSystemLoader(TEMPLATE_PATH)
|
|
30
|
+
),
|
|
31
|
+
autoescape=False,
|
|
32
|
+
trim_blocks=True,
|
|
33
|
+
lstrip_blocks=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
environment.filters["normalize_symbol"] = common.normalize_symbol
|
|
37
|
+
|
|
38
|
+
return environment
|