openapi-python-generator 1.1.2.dev1734089718__py3-none-any.whl → 1.2.1.dev1757310194__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.
- openapi_python_generator/__init__.py +2 -1
- openapi_python_generator/__main__.py +21 -6
- openapi_python_generator/common.py +15 -0
- openapi_python_generator/generate_data.py +112 -65
- openapi_python_generator/language_converters/python/api_config_generator.py +12 -6
- openapi_python_generator/language_converters/python/generator.py +8 -4
- openapi_python_generator/language_converters/python/model_generator.py +191 -54
- openapi_python_generator/language_converters/python/service_generator.py +159 -62
- openapi_python_generator/language_converters/python/templates/aiohttp.jinja2 +10 -9
- openapi_python_generator/language_converters/python/templates/httpx.jinja2 +8 -5
- openapi_python_generator/language_converters/python/templates/models.jinja2 +5 -3
- openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +5 -3
- openapi_python_generator/language_converters/python/templates/requests.jinja2 +8 -5
- openapi_python_generator/language_converters/python/templates/service.jinja2 +2 -1
- openapi_python_generator/models.py +16 -2
- openapi_python_generator/parsers/__init__.py +13 -0
- openapi_python_generator/parsers/openapi_30.py +65 -0
- openapi_python_generator/parsers/openapi_31.py +65 -0
- openapi_python_generator/py.typed +0 -0
- openapi_python_generator/version_detector.py +70 -0
- {openapi_python_generator-1.1.2.dev1734089718.dist-info → openapi_python_generator-1.2.1.dev1757310194.dist-info}/METADATA +3 -3
- openapi_python_generator-1.2.1.dev1757310194.dist-info/RECORD +32 -0
- openapi_python_generator-1.1.2.dev1734089718.dist-info/RECORD +0 -27
- {openapi_python_generator-1.1.2.dev1734089718.dist-info → openapi_python_generator-1.2.1.dev1757310194.dist-info}/LICENSE +0 -0
- {openapi_python_generator-1.1.2.dev1734089718.dist-info → openapi_python_generator-1.2.1.dev1757310194.dist-info}/WHEEL +0 -0
- {openapi_python_generator-1.1.2.dev1734089718.dist-info → openapi_python_generator-1.2.1.dev1757310194.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from typing import Any
|
|
2
3
|
from typing import Dict
|
|
3
4
|
from typing import List
|
|
4
5
|
from typing import Literal
|
|
@@ -7,7 +8,28 @@ from typing import Tuple
|
|
|
7
8
|
from typing import Union
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
|
-
from openapi_pydantic.v3
|
|
11
|
+
from openapi_pydantic.v3 import (
|
|
12
|
+
Reference,
|
|
13
|
+
Schema,
|
|
14
|
+
Operation,
|
|
15
|
+
Parameter,
|
|
16
|
+
Response,
|
|
17
|
+
PathItem,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Import version-specific types for isinstance checks
|
|
21
|
+
from openapi_pydantic.v3.v3_0 import (
|
|
22
|
+
Reference as Reference30,
|
|
23
|
+
Schema as Schema30,
|
|
24
|
+
Response as Response30,
|
|
25
|
+
MediaType as MediaType30,
|
|
26
|
+
)
|
|
27
|
+
from openapi_pydantic.v3.v3_1 import (
|
|
28
|
+
Reference as Reference31,
|
|
29
|
+
Schema as Schema31,
|
|
30
|
+
Response as Response31,
|
|
31
|
+
MediaType as MediaType31,
|
|
32
|
+
)
|
|
11
33
|
|
|
12
34
|
from openapi_python_generator.language_converters.python import common
|
|
13
35
|
from openapi_python_generator.language_converters.python.common import normalize_symbol
|
|
@@ -24,6 +46,39 @@ from openapi_python_generator.models import ServiceOperation
|
|
|
24
46
|
from openapi_python_generator.models import TypeConversion
|
|
25
47
|
|
|
26
48
|
|
|
49
|
+
# Helper functions for isinstance checks across OpenAPI versions
|
|
50
|
+
def is_response_type(obj) -> bool:
|
|
51
|
+
"""Check if object is a Response from any OpenAPI version"""
|
|
52
|
+
return isinstance(obj, (Response30, Response31))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_media_type_for_reference(reference_obj):
|
|
56
|
+
"""Create a MediaType wrapper for a reference object, using the correct version"""
|
|
57
|
+
# Check which version the reference object belongs to
|
|
58
|
+
if isinstance(reference_obj, Reference30):
|
|
59
|
+
return MediaType30(schema=reference_obj)
|
|
60
|
+
elif isinstance(reference_obj, Reference31):
|
|
61
|
+
return MediaType31(schema=reference_obj)
|
|
62
|
+
else:
|
|
63
|
+
# Fallback to v3.0 for generic Reference
|
|
64
|
+
return MediaType30(schema=reference_obj)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_media_type(obj) -> bool:
|
|
68
|
+
"""Check if object is a MediaType from any OpenAPI version"""
|
|
69
|
+
return isinstance(obj, (MediaType30, MediaType31))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_reference_type(obj: Any) -> bool:
|
|
73
|
+
"""Check if object is a Reference type across different versions."""
|
|
74
|
+
return isinstance(obj, (Reference, Reference30, Reference31))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_schema_type(obj: Any) -> bool:
|
|
78
|
+
"""Check if object is a Schema from any OpenAPI version"""
|
|
79
|
+
return isinstance(obj, (Schema30, Schema31))
|
|
80
|
+
|
|
81
|
+
|
|
27
82
|
HTTP_OPERATIONS = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
|
|
28
83
|
|
|
29
84
|
|
|
@@ -45,9 +100,14 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
|
|
|
45
100
|
if media_type is None:
|
|
46
101
|
return None # pragma: no cover
|
|
47
102
|
|
|
48
|
-
if isinstance(
|
|
103
|
+
if isinstance(
|
|
104
|
+
media_type.media_type_schema, (Reference, Reference30, Reference31)
|
|
105
|
+
):
|
|
106
|
+
return "data.dict()"
|
|
107
|
+
elif hasattr(media_type.media_type_schema, "ref"):
|
|
108
|
+
# Handle Reference objects from different OpenAPI versions
|
|
49
109
|
return "data.dict()"
|
|
50
|
-
elif isinstance(media_type.media_type_schema, Schema):
|
|
110
|
+
elif isinstance(media_type.media_type_schema, (Schema, Schema30, Schema31)):
|
|
51
111
|
schema = media_type.media_type_schema
|
|
52
112
|
if schema.type == "array":
|
|
53
113
|
return "[i.dict() for i in data]"
|
|
@@ -64,11 +124,14 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
|
|
|
64
124
|
|
|
65
125
|
|
|
66
126
|
def generate_params(operation: Operation) -> str:
|
|
67
|
-
def _generate_params_from_content(content:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
127
|
+
def _generate_params_from_content(content: Any):
|
|
128
|
+
# Accept reference from either 3.0 or 3.1
|
|
129
|
+
if isinstance(content, (Reference, Reference30, Reference31)):
|
|
130
|
+
return f"data : {content.ref.split('/')[-1]}" # type: ignore
|
|
131
|
+
elif isinstance(content, (Schema, Schema30, Schema31)):
|
|
132
|
+
return f"data : {type_converter(content, True).converted_type}" # type: ignore
|
|
133
|
+
else: # pragma: no cover
|
|
134
|
+
raise Exception(f"Unsupported request body schema type: {type(content)}")
|
|
72
135
|
|
|
73
136
|
if operation.parameters is None and operation.requestBody is None:
|
|
74
137
|
return ""
|
|
@@ -109,40 +172,30 @@ def generate_params(operation: Operation) -> str:
|
|
|
109
172
|
"application/json",
|
|
110
173
|
"text/plain",
|
|
111
174
|
"multipart/form-data",
|
|
175
|
+
"application/octet-stream",
|
|
112
176
|
]
|
|
113
177
|
|
|
114
|
-
if operation.requestBody is not None
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
for i in operation_request_body_types
|
|
122
|
-
]
|
|
123
|
-
)
|
|
178
|
+
if operation.requestBody is not None and not is_reference_type(
|
|
179
|
+
operation.requestBody
|
|
180
|
+
):
|
|
181
|
+
# Safe access only if it's a concrete RequestBody object
|
|
182
|
+
rb_content = getattr(operation.requestBody, "content", None)
|
|
183
|
+
if isinstance(rb_content, dict) and any(
|
|
184
|
+
rb_content.get(i) is not None for i in operation_request_body_types
|
|
124
185
|
):
|
|
125
|
-
get_keyword = [
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
else:
|
|
139
|
-
raise Exception(
|
|
140
|
-
f"Unsupported media type schema for {str(operation)}"
|
|
141
|
-
) # pragma: no cover
|
|
142
|
-
else:
|
|
143
|
-
raise Exception(
|
|
144
|
-
f"Unsupported request body type: {type(operation.requestBody)}"
|
|
145
|
-
)
|
|
186
|
+
get_keyword = [i for i in operation_request_body_types if rb_content.get(i)][
|
|
187
|
+
0
|
|
188
|
+
]
|
|
189
|
+
content = rb_content.get(get_keyword)
|
|
190
|
+
if content is not None and hasattr(content, "media_type_schema"):
|
|
191
|
+
mts = getattr(content, "media_type_schema", None)
|
|
192
|
+
if isinstance(mts, (Reference, Reference30, Reference31, Schema, Schema30, Schema31)):
|
|
193
|
+
params += f"{_generate_params_from_content(mts)}, "
|
|
194
|
+
else: # pragma: no cover
|
|
195
|
+
raise Exception(
|
|
196
|
+
f"Unsupported media type schema for {str(operation)}: {type(mts)}"
|
|
197
|
+
)
|
|
198
|
+
# else: silently ignore unsupported body shapes (could extend later)
|
|
146
199
|
# Replace - with _ in params
|
|
147
200
|
params = params.replace("-", "_")
|
|
148
201
|
default_params = default_params.replace("-", "_")
|
|
@@ -199,53 +252,59 @@ def generate_return_type(operation: Operation) -> OpReturnType:
|
|
|
199
252
|
return OpReturnType(type=None, status_code=200, complex_type=False)
|
|
200
253
|
|
|
201
254
|
chosen_response = good_responses[0][1]
|
|
255
|
+
media_type_schema = None
|
|
202
256
|
|
|
203
|
-
if
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
257
|
+
if is_response_type(chosen_response):
|
|
258
|
+
# It's a Response type, access content safely
|
|
259
|
+
if hasattr(chosen_response, "content") and getattr(chosen_response, "content") is not None: # type: ignore
|
|
260
|
+
media_type_schema = getattr(chosen_response, "content").get("application/json") # type: ignore
|
|
261
|
+
elif is_reference_type(chosen_response):
|
|
262
|
+
media_type_schema = create_media_type_for_reference(chosen_response)
|
|
263
|
+
|
|
264
|
+
if media_type_schema is None:
|
|
210
265
|
return OpReturnType(
|
|
211
266
|
type=None, status_code=good_responses[0][0], complex_type=False
|
|
212
267
|
)
|
|
213
268
|
|
|
214
|
-
if
|
|
215
|
-
|
|
269
|
+
if is_media_type(media_type_schema):
|
|
270
|
+
inner_schema = getattr(media_type_schema, "media_type_schema", None)
|
|
271
|
+
if is_reference_type(inner_schema):
|
|
216
272
|
type_conv = TypeConversion(
|
|
217
|
-
original_type=
|
|
218
|
-
converted_type=
|
|
219
|
-
import_types=[
|
|
273
|
+
original_type=inner_schema.ref, # type: ignore
|
|
274
|
+
converted_type=inner_schema.ref.split("/")[-1], # type: ignore
|
|
275
|
+
import_types=[inner_schema.ref.split("/")[-1]], # type: ignore
|
|
220
276
|
)
|
|
221
277
|
return OpReturnType(
|
|
222
278
|
type=type_conv,
|
|
223
279
|
status_code=good_responses[0][0],
|
|
224
280
|
complex_type=True,
|
|
225
281
|
)
|
|
226
|
-
elif
|
|
227
|
-
converted_result = type_converter(
|
|
228
|
-
if
|
|
229
|
-
converted_result.
|
|
282
|
+
elif is_schema_type(inner_schema):
|
|
283
|
+
converted_result = type_converter(inner_schema, True) # type: ignore
|
|
284
|
+
if (
|
|
285
|
+
"array" in converted_result.original_type
|
|
286
|
+
and isinstance(converted_result.import_types, list)
|
|
230
287
|
):
|
|
231
288
|
matched = re.findall(r"List\[(.+)\]", converted_result.converted_type)
|
|
232
289
|
if len(matched) > 0:
|
|
233
290
|
list_type = matched[0]
|
|
234
|
-
else:
|
|
291
|
+
else: # pragma: no cover
|
|
235
292
|
raise Exception(
|
|
236
293
|
f"Unable to parse list type from {converted_result.converted_type}"
|
|
237
|
-
)
|
|
294
|
+
)
|
|
238
295
|
else:
|
|
239
296
|
list_type = None
|
|
240
297
|
return OpReturnType(
|
|
241
298
|
type=converted_result,
|
|
242
299
|
status_code=good_responses[0][0],
|
|
243
|
-
complex_type=
|
|
244
|
-
|
|
300
|
+
complex_type=bool(
|
|
301
|
+
converted_result.import_types
|
|
302
|
+
and len(converted_result.import_types) > 0
|
|
303
|
+
),
|
|
245
304
|
list_type=list_type,
|
|
246
305
|
)
|
|
247
|
-
else:
|
|
248
|
-
raise Exception("Unknown media type schema type")
|
|
306
|
+
else: # pragma: no cover
|
|
307
|
+
raise Exception("Unknown media type schema type")
|
|
249
308
|
elif media_type_schema is None:
|
|
250
309
|
return OpReturnType(
|
|
251
310
|
type=None,
|
|
@@ -269,7 +328,40 @@ def generate_services(
|
|
|
269
328
|
def generate_service_operation(
|
|
270
329
|
op: Operation, path_name: str, async_type: bool
|
|
271
330
|
) -> ServiceOperation:
|
|
331
|
+
# Merge path-level parameters (always required by spec) into the
|
|
332
|
+
# operation-level parameters so they get turned into function args.
|
|
333
|
+
try:
|
|
334
|
+
path_level_params = []
|
|
335
|
+
if hasattr(path, "parameters") and getattr(path, "parameters") is not None: # type: ignore
|
|
336
|
+
path_level_params = [p for p in getattr(path, "parameters") if p is not None] # type: ignore
|
|
337
|
+
if path_level_params:
|
|
338
|
+
existing_names = set()
|
|
339
|
+
if op.parameters is not None:
|
|
340
|
+
for p in op.parameters: # type: ignore
|
|
341
|
+
if isinstance(p, Parameter):
|
|
342
|
+
existing_names.add(p.name)
|
|
343
|
+
for p in path_level_params:
|
|
344
|
+
if isinstance(p, Parameter) and p.name not in existing_names:
|
|
345
|
+
if op.parameters is None:
|
|
346
|
+
op.parameters = [] # type: ignore
|
|
347
|
+
op.parameters.append(p) # type: ignore
|
|
348
|
+
except Exception: # pragma: no cover
|
|
349
|
+
pass
|
|
350
|
+
|
|
272
351
|
params = generate_params(op)
|
|
352
|
+
# Fallback: ensure all {placeholders} in path are present as function params
|
|
353
|
+
try:
|
|
354
|
+
placeholder_names = [m.group(1) for m in re.finditer(r"\{([^}/]+)\}", path_name)]
|
|
355
|
+
existing_param_names = {
|
|
356
|
+
p.split(":")[0].strip()
|
|
357
|
+
for p in params.split(",") if ":" in p
|
|
358
|
+
}
|
|
359
|
+
for ph in placeholder_names:
|
|
360
|
+
norm_ph = common.normalize_symbol(ph)
|
|
361
|
+
if norm_ph not in existing_param_names and norm_ph:
|
|
362
|
+
params = f"{norm_ph}: Any, " + params
|
|
363
|
+
except Exception: # pragma: no cover
|
|
364
|
+
pass
|
|
273
365
|
operation_id = generate_operation_id(op, http_operation, path_name)
|
|
274
366
|
query_params = generate_query_params(op)
|
|
275
367
|
header_params = generate_header_params(op)
|
|
@@ -293,7 +385,7 @@ def generate_services(
|
|
|
293
385
|
)
|
|
294
386
|
|
|
295
387
|
so.content = jinja_env.get_template(library_config.template_name).render(
|
|
296
|
-
**so.
|
|
388
|
+
**so.model_dump()
|
|
297
389
|
)
|
|
298
390
|
|
|
299
391
|
if op.tags is not None and len(op.tags) > 0:
|
|
@@ -322,6 +414,11 @@ def generate_services(
|
|
|
322
414
|
async_so = generate_service_operation(op, path_name, True)
|
|
323
415
|
service_ops.append(async_so)
|
|
324
416
|
|
|
417
|
+
# Ensure every operation has a tag; fallback to "default" for untagged operations
|
|
418
|
+
for so in service_ops:
|
|
419
|
+
if not so.tag:
|
|
420
|
+
so.tag = "default"
|
|
421
|
+
|
|
325
422
|
tags = set([so.tag for so in service_ops])
|
|
326
423
|
|
|
327
424
|
for tag in tags:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
async def {{ operation_id }}(
|
|
1
|
+
async def {{ operation_id }}(api_config_override : Optional[APIConfig] = None{% if params.strip() %}, *, {{ params.rstrip(', ') }}{% endif %}) -> {% if return_type.type is none or return_type.type.converted_type is none %}None{% else %}{{ return_type.type.converted_type}}{% endif %}:
|
|
2
2
|
api_config = api_config_override if api_config_override else APIConfig()
|
|
3
3
|
|
|
4
4
|
base_path = api_config.base_path
|
|
@@ -30,19 +30,20 @@ async def {{ operation_id }}({{ params }} api_config_override : Optional[APIConf
|
|
|
30
30
|
json = {{ body_param }}
|
|
31
31
|
{% endif %}
|
|
32
32
|
{% endif %}
|
|
33
|
-
) as
|
|
34
|
-
if
|
|
35
|
-
raise HTTPException(
|
|
36
|
-
|
|
33
|
+
) as initial_response:
|
|
34
|
+
if initial_response.status != {{ return_type.status_code }}:
|
|
35
|
+
raise HTTPException(initial_response.status, f'{{ operation_id }} failed with status code: {initial_response.status}')
|
|
36
|
+
# Only parse JSON when a body is expected (avoid errors on 204 No Content)
|
|
37
|
+
body = None if {{ return_type.status_code }} == 204 else await initial_response.json()
|
|
37
38
|
|
|
38
39
|
{% if return_type.type is none or return_type.type.converted_type is none %}
|
|
39
|
-
|
|
40
|
+
return None
|
|
40
41
|
{% elif return_type.complex_type %}
|
|
41
42
|
{%- if return_type.list_type is none %}
|
|
42
|
-
|
|
43
|
+
return {{ return_type.type.converted_type }}(**body) if body is not None else {{ return_type.type.converted_type }}()
|
|
43
44
|
{%- else %}
|
|
44
|
-
|
|
45
|
+
return [{{ return_type.list_type }}(**item) for item in body]
|
|
45
46
|
{%- endif %}
|
|
46
47
|
{% else %}
|
|
47
|
-
|
|
48
|
+
return body
|
|
48
49
|
{% endif %}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{% if async_client %}async {% endif %}def {{ operation_id }}(
|
|
1
|
+
{% if async_client %}async {% endif %}def {{ operation_id }}(api_config_override : Optional[APIConfig] = None{% if params.strip() %}, *, {{ params.rstrip(', ') }}{% endif %}) -> {% if return_type.type is none or return_type.type.converted_type is none %}None{% else %}{{ return_type.type.converted_type}}{% endif %}:
|
|
2
2
|
api_config = api_config_override if api_config_override else APIConfig()
|
|
3
3
|
|
|
4
4
|
base_path = api_config.base_path
|
|
@@ -38,16 +38,19 @@ with httpx.Client(base_url=base_path, verify=api_config.verify) as client:
|
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
if response.status_code != {{ return_type.status_code }}:
|
|
41
|
-
raise HTTPException(response.status_code, f'{{
|
|
41
|
+
raise HTTPException(response.status_code, f'{{ operation_id }} failed with status code: {response.status_code}')
|
|
42
|
+
else:
|
|
43
|
+
{# Conditional body parsing: avoid calling .json() for 204 #}
|
|
44
|
+
body = None if {{ return_type.status_code }} == 204 else response.json()
|
|
42
45
|
|
|
43
46
|
{% if return_type.type is none or return_type.type.converted_type is none %}
|
|
44
47
|
return None
|
|
45
48
|
{% elif return_type.complex_type %}
|
|
46
49
|
{%- if return_type.list_type is none %}
|
|
47
|
-
return {{ return_type.type.converted_type }}(**
|
|
50
|
+
return {{ return_type.type.converted_type }}(**body) if body is not None else {{ return_type.type.converted_type }}()
|
|
48
51
|
{%- else %}
|
|
49
|
-
return [{{ return_type.list_type }}(**item) for item in
|
|
52
|
+
return [{{ return_type.list_type }}(**item) for item in body]
|
|
50
53
|
{%- endif %}
|
|
51
54
|
{% else %}
|
|
52
|
-
return
|
|
55
|
+
return body
|
|
53
56
|
{% endif %}
|
|
@@ -10,11 +10,13 @@ from pydantic import BaseModel, Field
|
|
|
10
10
|
|
|
11
11
|
class {{ schema_name }}(BaseModel):
|
|
12
12
|
"""
|
|
13
|
-
{{ schema.title }} model
|
|
14
|
-
{% if schema.description
|
|
13
|
+
{% if schema.title %}{{ schema.title }}{% else %}{{ schema_name }}{% endif %} model
|
|
14
|
+
{% if schema.description %}
|
|
15
15
|
{{ schema.description }}
|
|
16
16
|
{% endif %}
|
|
17
|
-
|
|
17
|
+
{% if parent_comment %}
|
|
18
|
+
{{ parent_comment }}
|
|
19
|
+
{% endif %}
|
|
18
20
|
"""
|
|
19
21
|
{% for property in properties %}
|
|
20
22
|
|
|
@@ -10,11 +10,13 @@ from pydantic import BaseModel, Field
|
|
|
10
10
|
|
|
11
11
|
class {{ schema_name }}(BaseModel):
|
|
12
12
|
"""
|
|
13
|
-
{{ schema.title }} model
|
|
14
|
-
{% if schema.description
|
|
13
|
+
{% if schema.title %}{{ schema.title }}{% else %}{{ schema_name }}{% endif %} model
|
|
14
|
+
{% if schema.description %}
|
|
15
15
|
{{ schema.description }}
|
|
16
16
|
{% endif %}
|
|
17
|
-
|
|
17
|
+
{% if parent_comment %}
|
|
18
|
+
{{ parent_comment }}
|
|
19
|
+
{% endif %}
|
|
18
20
|
"""
|
|
19
21
|
model_config = {
|
|
20
22
|
"populate_by_name": True,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
def {{ operation_id }}(
|
|
1
|
+
def {{ operation_id }}(api_config_override : Optional[APIConfig] = None{% if params.strip() %}, *, {{ params.rstrip(', ') }}{% endif %}) -> {% if return_type.type is none or return_type.type.converted_type is none %}None{% else %}{{ return_type.type.converted_type}}{% endif %}:
|
|
2
2
|
api_config = api_config_override if api_config_override else APIConfig()
|
|
3
3
|
|
|
4
4
|
base_path = api_config.base_path
|
|
@@ -32,16 +32,19 @@ def {{ operation_id }}({{ params }} api_config_override : Optional[APIConfig] =
|
|
|
32
32
|
{% endif %}
|
|
33
33
|
)
|
|
34
34
|
if response.status_code != {{ return_type.status_code }}:
|
|
35
|
-
raise HTTPException(response.status_code, f'{{
|
|
35
|
+
raise HTTPException(response.status_code, f'{{ operation_id }} failed with status code: {response.status_code}')
|
|
36
|
+
else:
|
|
37
|
+
{# Conditional body parsing: avoid calling .json() for 204 #}
|
|
38
|
+
body = None if {{ return_type.status_code }} == 204 else response.json()
|
|
36
39
|
|
|
37
40
|
{% if return_type.type is none or return_type.type.converted_type is none %}
|
|
38
41
|
return None
|
|
39
42
|
{% elif return_type.complex_type %}
|
|
40
43
|
{%- if return_type.list_type is none %}
|
|
41
|
-
return {{ return_type.type.converted_type }}(**
|
|
44
|
+
return {{ return_type.type.converted_type }}(**body) if body is not None else {{ return_type.type.converted_type }}()
|
|
42
45
|
{%- else %}
|
|
43
|
-
return [{{ return_type.list_type }}(**item) for item in
|
|
46
|
+
return [{{ return_type.list_type }}(**item) for item in body]
|
|
44
47
|
{%- endif %}
|
|
45
48
|
{% else %}
|
|
46
|
-
return
|
|
49
|
+
return body
|
|
47
50
|
{% endif %}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Union
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from openapi_pydantic.v3.v3_0 import
|
|
4
|
+
from openapi_pydantic.v3.v3_0 import (
|
|
5
|
+
Operation as Operation30,
|
|
6
|
+
PathItem as PathItem30,
|
|
7
|
+
Schema as Schema30,
|
|
8
|
+
)
|
|
9
|
+
from openapi_pydantic.v3.v3_1 import (
|
|
10
|
+
Operation as Operation31,
|
|
11
|
+
PathItem as PathItem31,
|
|
12
|
+
Schema as Schema31,
|
|
13
|
+
)
|
|
5
14
|
from pydantic import BaseModel
|
|
6
15
|
|
|
16
|
+
# Type unions for compatibility with both OpenAPI 3.0 and 3.1
|
|
17
|
+
Operation = Union[Operation30, Operation31]
|
|
18
|
+
PathItem = Union[PathItem30, PathItem31]
|
|
19
|
+
Schema = Union[Schema30, Schema31]
|
|
20
|
+
|
|
7
21
|
|
|
8
22
|
class LibraryConfig(BaseModel):
|
|
9
23
|
name: str
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI parsers for different specification versions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .openapi_30 import parse_openapi_3_0, generate_code_3_0
|
|
6
|
+
from .openapi_31 import parse_openapi_3_1, generate_code_3_1
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"parse_openapi_3_0",
|
|
10
|
+
"generate_code_3_0",
|
|
11
|
+
"parse_openapi_3_1",
|
|
12
|
+
"generate_code_3_1",
|
|
13
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI 3.0 specific parsing and generation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from openapi_pydantic.v3.v3_0 import OpenAPI
|
|
8
|
+
|
|
9
|
+
from openapi_python_generator.common import HTTPLibrary, PydanticVersion
|
|
10
|
+
from openapi_python_generator.language_converters.python.generator import (
|
|
11
|
+
generator as base_generator,
|
|
12
|
+
)
|
|
13
|
+
from openapi_python_generator.models import ConversionResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_openapi_3_0(spec_data: dict) -> OpenAPI:
|
|
17
|
+
"""
|
|
18
|
+
Parse OpenAPI 3.0 specification data.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
spec_data: Dictionary containing OpenAPI 3.0 specification
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
OpenAPI: Parsed OpenAPI 3.0 specification object
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValidationError: If the specification is invalid
|
|
28
|
+
"""
|
|
29
|
+
return OpenAPI(**spec_data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_code_3_0(
|
|
33
|
+
data: OpenAPI,
|
|
34
|
+
library: HTTPLibrary = HTTPLibrary.httpx,
|
|
35
|
+
env_token_name: Optional[str] = None,
|
|
36
|
+
use_orjson: bool = False,
|
|
37
|
+
custom_template_path: Optional[str] = None,
|
|
38
|
+
pydantic_version: PydanticVersion = PydanticVersion.V2,
|
|
39
|
+
) -> ConversionResult:
|
|
40
|
+
"""
|
|
41
|
+
Generate Python code from OpenAPI 3.0 specification.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
data: OpenAPI 3.0 specification object
|
|
45
|
+
library: HTTP library to use
|
|
46
|
+
env_token_name: Environment variable name for token
|
|
47
|
+
use_orjson: Whether to use orjson for serialization
|
|
48
|
+
custom_template_path: Custom template path
|
|
49
|
+
pydantic_version: Pydantic version to use
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
ConversionResult: Generated code and metadata
|
|
53
|
+
"""
|
|
54
|
+
from openapi_python_generator.common import library_config_dict
|
|
55
|
+
|
|
56
|
+
library_config = library_config_dict[library]
|
|
57
|
+
|
|
58
|
+
return base_generator(
|
|
59
|
+
data=data,
|
|
60
|
+
library_config=library_config,
|
|
61
|
+
env_token_name=env_token_name,
|
|
62
|
+
use_orjson=use_orjson,
|
|
63
|
+
custom_template_path=custom_template_path,
|
|
64
|
+
pydantic_version=pydantic_version,
|
|
65
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI 3.1 specific parsing and generation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from openapi_pydantic.v3.v3_1 import OpenAPI
|
|
8
|
+
|
|
9
|
+
from openapi_python_generator.common import HTTPLibrary, PydanticVersion
|
|
10
|
+
from openapi_python_generator.language_converters.python.generator import (
|
|
11
|
+
generator as base_generator,
|
|
12
|
+
)
|
|
13
|
+
from openapi_python_generator.models import ConversionResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_openapi_3_1(spec_data: dict) -> OpenAPI:
|
|
17
|
+
"""
|
|
18
|
+
Parse OpenAPI 3.1 specification data.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
spec_data: Dictionary containing OpenAPI 3.1 specification
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
OpenAPI: Parsed OpenAPI 3.1 specification object
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValidationError: If the specification is invalid
|
|
28
|
+
"""
|
|
29
|
+
return OpenAPI(**spec_data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_code_3_1(
|
|
33
|
+
data: OpenAPI,
|
|
34
|
+
library: HTTPLibrary = HTTPLibrary.httpx,
|
|
35
|
+
env_token_name: Optional[str] = None,
|
|
36
|
+
use_orjson: bool = False,
|
|
37
|
+
custom_template_path: Optional[str] = None,
|
|
38
|
+
pydantic_version: PydanticVersion = PydanticVersion.V2,
|
|
39
|
+
) -> ConversionResult:
|
|
40
|
+
"""
|
|
41
|
+
Generate Python code from OpenAPI 3.1 specification.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
data: OpenAPI 3.1 specification object
|
|
45
|
+
library: HTTP library to use
|
|
46
|
+
env_token_name: Environment variable name for token
|
|
47
|
+
use_orjson: Whether to use orjson for serialization
|
|
48
|
+
custom_template_path: Custom template path
|
|
49
|
+
pydantic_version: Pydantic version to use
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
ConversionResult: Generated code and metadata
|
|
53
|
+
"""
|
|
54
|
+
from openapi_python_generator.common import library_config_dict
|
|
55
|
+
|
|
56
|
+
library_config = library_config_dict[library]
|
|
57
|
+
|
|
58
|
+
return base_generator(
|
|
59
|
+
data=data,
|
|
60
|
+
library_config=library_config,
|
|
61
|
+
env_token_name=env_token_name,
|
|
62
|
+
use_orjson=use_orjson,
|
|
63
|
+
custom_template_path=custom_template_path,
|
|
64
|
+
pydantic_version=pydantic_version,
|
|
65
|
+
)
|
|
File without changes
|