pydantic-openapi-generator 2.2.8__tar.gz → 2.3.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.
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/PKG-INFO +1 -1
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/pyproject.toml +1 -1
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/client_generator.py +251 -106
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +52 -17
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +52 -17
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/models.py +21 -1
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/.gitignore +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/LICENSE +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/README.md +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/__init__.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/__main__.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/common.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/generate_data.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/__init__.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/__init__.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/common.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/exception_generator.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/generator.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/jinja_config.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/model_generator.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/templates/alias_union.jinja2 +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/templates/discriminator_enum.jinja2 +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/templates/enum.jinja2 +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/templates/http_exception.jinja2 +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/templates/models.jinja2 +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/language_converters/python/templates/models_pydantic_2.jinja2 +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/parsers/__init__.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/parsers/openapi_30.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/parsers/openapi_31.py +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/py.typed +0 -0
- {pydantic_openapi_generator-2.2.8 → pydantic_openapi_generator-2.3.0}/src/pydantic_openapi_generator/version_detector.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pydantic-openapi-generator
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Openapi Python Generator
|
|
5
5
|
Project-URL: Homepage, https://github.com/mattcoulter7/pydantic-openapi-generator
|
|
6
6
|
Project-URL: Repository, https://github.com/mattcoulter7/pydantic-openapi-generator
|
|
@@ -52,6 +52,8 @@ from pydantic_openapi_generator.models import (
|
|
|
52
52
|
LibraryConfig,
|
|
53
53
|
Model,
|
|
54
54
|
OpReturnType,
|
|
55
|
+
RequestBodyDefinition,
|
|
56
|
+
ResponseVariant,
|
|
55
57
|
ServiceOperation,
|
|
56
58
|
TypeConversion,
|
|
57
59
|
)
|
|
@@ -141,52 +143,98 @@ def operation_is_sse(op: Operation) -> bool:
|
|
|
141
143
|
HTTP_OPERATIONS = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
|
|
142
144
|
|
|
143
145
|
|
|
144
|
-
def
|
|
146
|
+
def _json_body_expression(media_type_schema: Any) -> str:
|
|
147
|
+
if isinstance(media_type_schema, (Reference, Reference30, Reference31)) or hasattr(media_type_schema, "ref"):
|
|
148
|
+
return "data.model_dump(by_alias=True, exclude_none=True)"
|
|
149
|
+
|
|
150
|
+
if isinstance(media_type_schema, (Schema, Schema30, Schema31)):
|
|
151
|
+
schema = media_type_schema
|
|
152
|
+
|
|
153
|
+
if schema.type == "array":
|
|
154
|
+
return "[i.model_dump(by_alias=True, exclude_none=True) if hasattr(i, 'model_dump') else i for i in data]"
|
|
155
|
+
|
|
156
|
+
if schema.type == "object":
|
|
157
|
+
return "data.model_dump(by_alias=True, exclude_none=True) if hasattr(data, 'model_dump') else data"
|
|
158
|
+
|
|
159
|
+
return "data"
|
|
160
|
+
|
|
161
|
+
raise Exception(f"Unsupported schema type for request body: {type(media_type_schema)}") # pragma: no cover
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def generate_request_body(operation: Operation) -> Union[RequestBodyDefinition, None]:
|
|
145
165
|
if operation.requestBody is None:
|
|
146
166
|
return None
|
|
147
167
|
|
|
148
168
|
# If requestBody is a $ref, it will be a Pydantic model instance in the client.
|
|
149
169
|
if isinstance(operation.requestBody, (Reference30, Reference31)):
|
|
150
|
-
return
|
|
170
|
+
return RequestBodyDefinition(
|
|
171
|
+
content_type="application/json",
|
|
172
|
+
encoding="json",
|
|
173
|
+
expression="data.model_dump(by_alias=True, exclude_none=True)",
|
|
174
|
+
)
|
|
151
175
|
|
|
152
176
|
rb_content = getattr(operation.requestBody, "content", None)
|
|
153
177
|
if rb_content is None:
|
|
154
178
|
return None # pragma: no cover
|
|
155
179
|
|
|
156
|
-
|
|
157
|
-
|
|
180
|
+
ordered_content_types = [
|
|
181
|
+
"application/json",
|
|
182
|
+
"multipart/form-data",
|
|
183
|
+
"application/x-www-form-urlencoded",
|
|
184
|
+
"application/octet-stream",
|
|
185
|
+
"text/plain",
|
|
186
|
+
]
|
|
187
|
+
content_type = next((ct for ct in ordered_content_types if rb_content.get(ct) is not None), None)
|
|
188
|
+
if content_type is None:
|
|
189
|
+
return None
|
|
158
190
|
|
|
159
|
-
media_type = rb_content.get(
|
|
191
|
+
media_type = rb_content.get(content_type)
|
|
160
192
|
if media_type is None:
|
|
161
193
|
return None # pragma: no cover
|
|
162
194
|
|
|
163
195
|
mts = getattr(media_type, "media_type_schema", None)
|
|
164
|
-
if
|
|
165
|
-
|
|
196
|
+
if content_type == "application/json":
|
|
197
|
+
if mts is None:
|
|
198
|
+
return None # pragma: no cover
|
|
199
|
+
return RequestBodyDefinition(
|
|
200
|
+
content_type=content_type,
|
|
201
|
+
encoding="json",
|
|
202
|
+
expression=_json_body_expression(mts),
|
|
203
|
+
)
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return "data.model_dump(by_alias=True, exclude_none=True)"
|
|
205
|
+
if content_type == "multipart/form-data":
|
|
206
|
+
return RequestBodyDefinition(content_type=None, encoding="multipart", expression="data")
|
|
170
207
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
schema = mts
|
|
208
|
+
if content_type == "application/x-www-form-urlencoded":
|
|
209
|
+
return RequestBodyDefinition(content_type=content_type, encoding="form", expression="data")
|
|
174
210
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return "[i.model_dump(by_alias=True, exclude_none=True) if hasattr(i, 'model_dump') else i for i in data]"
|
|
211
|
+
if content_type == "application/octet-stream":
|
|
212
|
+
return RequestBodyDefinition(content_type=content_type, encoding="binary", expression="data")
|
|
178
213
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
if content_type == "text/plain":
|
|
215
|
+
return RequestBodyDefinition(content_type=content_type, encoding="text", expression="data")
|
|
216
|
+
|
|
217
|
+
return None # pragma: no cover
|
|
182
218
|
|
|
183
|
-
# Primitive (string/int/etc.)
|
|
184
|
-
return "data"
|
|
185
219
|
|
|
186
|
-
|
|
220
|
+
def generate_body_param(operation: Operation) -> Union[str, None]:
|
|
221
|
+
request_body = generate_request_body(operation)
|
|
222
|
+
return None if request_body is None else request_body.expression
|
|
187
223
|
|
|
188
224
|
|
|
189
225
|
def generate_params(operation: Operation) -> str:
|
|
226
|
+
def _schema_default(schema: Any) -> Any:
|
|
227
|
+
default = getattr(schema, "default", None)
|
|
228
|
+
if default is None:
|
|
229
|
+
return None
|
|
230
|
+
return default
|
|
231
|
+
|
|
232
|
+
def _default_suffix(schema: Any, required: bool) -> str:
|
|
233
|
+
if required:
|
|
234
|
+
return ""
|
|
235
|
+
default = _schema_default(schema)
|
|
236
|
+
return f" = {default!r}" if default is not None else " = None"
|
|
237
|
+
|
|
190
238
|
def _generate_params_from_content(content: Any):
|
|
191
239
|
# Accept reference from either 3.0 or 3.1
|
|
192
240
|
if isinstance(content, (Reference, Reference30, Reference31)):
|
|
@@ -212,7 +260,7 @@ def generate_params(operation: Operation) -> str:
|
|
|
212
260
|
if isinstance(param.param_schema, Schema30) or isinstance(param.param_schema, Schema31):
|
|
213
261
|
converted_result = (
|
|
214
262
|
f"{param_name_cleaned} : {type_converter(param.param_schema, param.required).converted_type}"
|
|
215
|
-
+ (
|
|
263
|
+
+ _default_suffix(param.param_schema, param.required)
|
|
216
264
|
)
|
|
217
265
|
required = param.required
|
|
218
266
|
elif isinstance(param.param_schema, Reference30) or isinstance(param.param_schema, Reference31):
|
|
@@ -232,6 +280,7 @@ def generate_params(operation: Operation) -> str:
|
|
|
232
280
|
"application/json",
|
|
233
281
|
"text/plain",
|
|
234
282
|
"multipart/form-data",
|
|
283
|
+
"application/x-www-form-urlencoded",
|
|
235
284
|
"application/octet-stream",
|
|
236
285
|
]
|
|
237
286
|
|
|
@@ -290,95 +339,175 @@ def generate_header_params(operation: Operation) -> List[str]:
|
|
|
290
339
|
return _generate_params(operation, "header")
|
|
291
340
|
|
|
292
341
|
|
|
293
|
-
def
|
|
294
|
-
|
|
295
|
-
|
|
342
|
+
def _is_binary_schema(schema: Any) -> bool:
|
|
343
|
+
schema_format = getattr(schema, "schema_format", None)
|
|
344
|
+
schema_type = getattr(schema, "type", None)
|
|
345
|
+
return (schema_type == "string" or str(schema_type) == "DataType.STRING") and schema_format == "binary"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _body_kind_for_content(
|
|
349
|
+
content_type: Optional[str], schema: Any = None
|
|
350
|
+
) -> Literal["empty", "json", "text", "binary"]:
|
|
351
|
+
if content_type is None:
|
|
352
|
+
return "empty"
|
|
353
|
+
lowered = content_type.lower()
|
|
354
|
+
if lowered == "application/json" or lowered.endswith("+json"):
|
|
355
|
+
return "json"
|
|
356
|
+
if lowered == "application/pdf" or lowered == "application/octet-stream" or _is_binary_schema(schema):
|
|
357
|
+
return "binary"
|
|
358
|
+
if lowered.startswith("text/"):
|
|
359
|
+
return "text"
|
|
360
|
+
return "binary"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _response_variant_from_schema(
|
|
364
|
+
status_code: int,
|
|
365
|
+
content_type: Optional[str],
|
|
366
|
+
inner_schema: Any,
|
|
367
|
+
) -> ResponseVariant:
|
|
368
|
+
body_kind = _body_kind_for_content(content_type, inner_schema)
|
|
369
|
+
if body_kind == "empty":
|
|
370
|
+
return ResponseVariant(status_code=status_code, content_type=content_type, body_kind="empty")
|
|
371
|
+
|
|
372
|
+
if body_kind == "binary":
|
|
373
|
+
return ResponseVariant(
|
|
374
|
+
status_code=status_code,
|
|
375
|
+
content_type=content_type,
|
|
376
|
+
type=TypeConversion(original_type=content_type or "binary", converted_type="bytes"),
|
|
377
|
+
body_kind="binary",
|
|
378
|
+
)
|
|
296
379
|
|
|
297
|
-
|
|
298
|
-
(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
380
|
+
if body_kind == "text":
|
|
381
|
+
return ResponseVariant(
|
|
382
|
+
status_code=status_code,
|
|
383
|
+
content_type=content_type,
|
|
384
|
+
type=TypeConversion(original_type=content_type or "text", converted_type="str"),
|
|
385
|
+
body_kind="text",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if is_reference_type(inner_schema):
|
|
389
|
+
type_conv = TypeConversion(
|
|
390
|
+
original_type=inner_schema.ref, # type: ignore
|
|
391
|
+
converted_type=inner_schema.ref.split("/")[-1], # type: ignore
|
|
392
|
+
import_types=[inner_schema.ref.split("/")[-1]], # type: ignore
|
|
393
|
+
)
|
|
394
|
+
return ResponseVariant(
|
|
395
|
+
status_code=status_code,
|
|
396
|
+
content_type=content_type,
|
|
397
|
+
type=type_conv,
|
|
398
|
+
complex_type=True,
|
|
399
|
+
body_kind="json",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
if is_schema_type(inner_schema):
|
|
403
|
+
disc = getattr(inner_schema, "discriminator", None)
|
|
404
|
+
used = getattr(inner_schema, "oneOf", None) or getattr(inner_schema, "anyOf", None)
|
|
405
|
+
disc_key = getattr(disc, "propertyName", None) if disc is not None else None
|
|
323
406
|
|
|
324
|
-
|
|
325
|
-
|
|
407
|
+
if disc_key and used and all(is_reference_type(s) for s in used):
|
|
408
|
+
member_models = [common.normalize_symbol(s.ref.split("/")[-1]) for s in used] # type: ignore
|
|
409
|
+
alias_name = common.normalize_symbol(_common_suffix_many(member_models)) or "Response"
|
|
326
410
|
|
|
327
|
-
if is_media_type(media_type_schema):
|
|
328
|
-
inner_schema = getattr(media_type_schema, "media_type_schema", None)
|
|
329
|
-
if is_reference_type(inner_schema):
|
|
330
411
|
type_conv = TypeConversion(
|
|
331
|
-
original_type=
|
|
332
|
-
converted_type=
|
|
333
|
-
import_types=
|
|
412
|
+
original_type="discriminated_union",
|
|
413
|
+
converted_type=alias_name,
|
|
414
|
+
import_types=None,
|
|
334
415
|
)
|
|
335
|
-
return
|
|
416
|
+
return ResponseVariant(
|
|
417
|
+
status_code=status_code,
|
|
418
|
+
content_type=content_type,
|
|
336
419
|
type=type_conv,
|
|
337
|
-
status_code=good_responses[0][0],
|
|
338
420
|
complex_type=True,
|
|
421
|
+
body_kind="json",
|
|
339
422
|
)
|
|
340
|
-
elif is_schema_type(inner_schema):
|
|
341
|
-
# NEW: if this is a discriminated response union of refs, prefer a named alias
|
|
342
|
-
disc = getattr(inner_schema, "discriminator", None)
|
|
343
|
-
used = getattr(inner_schema, "oneOf", None) or getattr(inner_schema, "anyOf", None)
|
|
344
|
-
disc_key = getattr(disc, "propertyName", None) if disc is not None else None
|
|
345
|
-
|
|
346
|
-
if disc_key and used and all(is_reference_type(s) for s in used):
|
|
347
|
-
member_models = [common.normalize_symbol(s.ref.split("/")[-1]) for s in used] # type: ignore
|
|
348
|
-
alias_name = common.normalize_symbol(_common_suffix_many(member_models)) or "Response"
|
|
349
|
-
|
|
350
|
-
type_conv = TypeConversion(
|
|
351
|
-
original_type="discriminated_union",
|
|
352
|
-
converted_type=alias_name,
|
|
353
|
-
import_types=None,
|
|
354
|
-
)
|
|
355
|
-
return OpReturnType(type=type_conv, status_code=good_responses[0][0], complex_type=True)
|
|
356
423
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
raise Exception("Unknown media type schema type")
|
|
374
|
-
elif media_type_schema is None:
|
|
375
|
-
return OpReturnType(
|
|
376
|
-
type=None,
|
|
377
|
-
status_code=good_responses[0][0],
|
|
378
|
-
complex_type=False,
|
|
424
|
+
converted_result = type_converter(inner_schema, True) # type: ignore
|
|
425
|
+
list_type = None
|
|
426
|
+
if "array" in converted_result.original_type and isinstance(converted_result.import_types, list):
|
|
427
|
+
matched = re.findall(r"List\[(.+)\]", converted_result.converted_type)
|
|
428
|
+
if len(matched) > 0:
|
|
429
|
+
list_type = matched[0]
|
|
430
|
+
else: # pragma: no cover
|
|
431
|
+
raise Exception(f"Unable to parse list type from {converted_result.converted_type}")
|
|
432
|
+
|
|
433
|
+
return ResponseVariant(
|
|
434
|
+
status_code=status_code,
|
|
435
|
+
content_type=content_type,
|
|
436
|
+
type=converted_result,
|
|
437
|
+
complex_type=bool(converted_result.import_types and len(converted_result.import_types) > 0),
|
|
438
|
+
list_type=list_type,
|
|
439
|
+
body_kind="json",
|
|
379
440
|
)
|
|
380
|
-
|
|
381
|
-
|
|
441
|
+
|
|
442
|
+
return ResponseVariant(status_code=status_code, content_type=content_type, body_kind="empty")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _response_variants_for_response(status_code: int, response: Union[Response, Reference]) -> List[ResponseVariant]:
|
|
446
|
+
if is_reference_type(response):
|
|
447
|
+
media_type = create_media_type_for_reference(response)
|
|
448
|
+
inner_schema = getattr(media_type, "media_type_schema", None)
|
|
449
|
+
return [_response_variant_from_schema(status_code, "application/json", inner_schema)]
|
|
450
|
+
|
|
451
|
+
if not is_response_type(response):
|
|
452
|
+
return []
|
|
453
|
+
|
|
454
|
+
content = getattr(response, "content", None)
|
|
455
|
+
if not isinstance(content, dict) or not content:
|
|
456
|
+
return [ResponseVariant(status_code=status_code, content_type=None, body_kind="empty")]
|
|
457
|
+
|
|
458
|
+
variants: List[ResponseVariant] = []
|
|
459
|
+
for content_type, media_type in content.items():
|
|
460
|
+
if not is_media_type(media_type):
|
|
461
|
+
continue
|
|
462
|
+
inner_schema = getattr(media_type, "media_type_schema", None)
|
|
463
|
+
variants.append(_response_variant_from_schema(status_code, content_type, inner_schema))
|
|
464
|
+
|
|
465
|
+
return variants or [ResponseVariant(status_code=status_code, content_type=None, body_kind="empty")]
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _return_type_hint(variants: List[ResponseVariant]) -> str:
|
|
469
|
+
hints: List[str] = []
|
|
470
|
+
for variant in variants:
|
|
471
|
+
if variant.body_kind == "empty" or variant.type is None:
|
|
472
|
+
hint = "None"
|
|
473
|
+
elif variant.list_type is not None:
|
|
474
|
+
hint = f"list[{variant.list_type}]"
|
|
475
|
+
else:
|
|
476
|
+
hint = variant.type.converted_type
|
|
477
|
+
if hint not in hints:
|
|
478
|
+
hints.append(hint)
|
|
479
|
+
if not hints:
|
|
480
|
+
return "None"
|
|
481
|
+
return hints[0] if len(hints) == 1 else " | ".join(hints)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def generate_return_type(operation: Operation) -> OpReturnType:
|
|
485
|
+
if operation.responses is None:
|
|
486
|
+
return OpReturnType(type=None, status_code=200, complex_type=False, accepted_status_codes=[200])
|
|
487
|
+
|
|
488
|
+
good_responses: List[Tuple[int, Union[Response, Reference]]] = [
|
|
489
|
+
(int(status_code), response)
|
|
490
|
+
for status_code, response in operation.responses.items()
|
|
491
|
+
if status_code.startswith("2")
|
|
492
|
+
]
|
|
493
|
+
if len(good_responses) == 0:
|
|
494
|
+
return OpReturnType(type=None, status_code=200, complex_type=False, accepted_status_codes=[200])
|
|
495
|
+
|
|
496
|
+
variants: List[ResponseVariant] = []
|
|
497
|
+
for status_code, response in good_responses:
|
|
498
|
+
variants.extend(_response_variants_for_response(status_code, response))
|
|
499
|
+
|
|
500
|
+
first_variant = variants[0] if variants else ResponseVariant(status_code=good_responses[0][0])
|
|
501
|
+
return OpReturnType(
|
|
502
|
+
type=first_variant.type,
|
|
503
|
+
status_code=first_variant.status_code,
|
|
504
|
+
complex_type=first_variant.complex_type,
|
|
505
|
+
list_type=first_variant.list_type,
|
|
506
|
+
variants=variants,
|
|
507
|
+
accepted_status_codes=sorted({status_code for status_code, _ in good_responses}),
|
|
508
|
+
accept_content_types=list(dict.fromkeys(v.content_type for v in variants if v.content_type is not None)),
|
|
509
|
+
return_type_hint=_return_type_hint(variants),
|
|
510
|
+
)
|
|
382
511
|
|
|
383
512
|
|
|
384
513
|
def clean_up_path_name(path_name: str) -> str:
|
|
@@ -406,7 +535,11 @@ def generate_clients(
|
|
|
406
535
|
service_ops: List[ServiceOperation] = []
|
|
407
536
|
|
|
408
537
|
def _generate_service_operation(
|
|
409
|
-
op: Operation,
|
|
538
|
+
op: Operation,
|
|
539
|
+
path_obj: PathItem,
|
|
540
|
+
path_name: str,
|
|
541
|
+
http_operation: str,
|
|
542
|
+
async_type: bool,
|
|
410
543
|
) -> ServiceOperation:
|
|
411
544
|
path_level_params = []
|
|
412
545
|
if hasattr(path_obj, "parameters") and path_obj.parameters is not None:
|
|
@@ -435,7 +568,8 @@ def generate_clients(
|
|
|
435
568
|
query_params = generate_query_params(op)
|
|
436
569
|
header_params = generate_header_params(op)
|
|
437
570
|
return_type = generate_return_type(op)
|
|
438
|
-
|
|
571
|
+
request_body = generate_request_body(op)
|
|
572
|
+
body_param = None if request_body is None else request_body.expression
|
|
439
573
|
|
|
440
574
|
so = ServiceOperation(
|
|
441
575
|
params=params,
|
|
@@ -448,6 +582,7 @@ def generate_clients(
|
|
|
448
582
|
content="",
|
|
449
583
|
async_client=async_type,
|
|
450
584
|
body_param=body_param,
|
|
585
|
+
request_body=request_body,
|
|
451
586
|
path_name=path_name,
|
|
452
587
|
method=http_operation,
|
|
453
588
|
is_sse=operation_is_sse(op),
|
|
@@ -488,8 +623,18 @@ def generate_clients(
|
|
|
488
623
|
compile(async_content, "<string>", "exec")
|
|
489
624
|
|
|
490
625
|
clients: List[Model] = [
|
|
491
|
-
Model(
|
|
492
|
-
|
|
626
|
+
Model(
|
|
627
|
+
file_name="sync_client",
|
|
628
|
+
content=sync_content,
|
|
629
|
+
openapi_object={},
|
|
630
|
+
properties=[],
|
|
631
|
+
),
|
|
632
|
+
Model(
|
|
633
|
+
file_name="async_client",
|
|
634
|
+
content=async_content,
|
|
635
|
+
openapi_object={},
|
|
636
|
+
properties=[],
|
|
637
|
+
),
|
|
493
638
|
]
|
|
494
639
|
|
|
495
640
|
return clients
|
|
@@ -48,24 +48,27 @@ class AsyncClient(BaseModel):
|
|
|
48
48
|
async def {{ op.operation_id }}(self{% if op.params %}, {{ op.params }}{% endif %}) -> {%- if op.is_sse -%}
|
|
49
49
|
AsyncGenerator[str | dict[str, Any], None]
|
|
50
50
|
{%- else -%}
|
|
51
|
-
{
|
|
52
|
-
{%- elif op.return_type.list_type is not none -%}list[{{ op.return_type.list_type }}]
|
|
53
|
-
{%- else -%}{{ op.return_type.type.converted_type }}
|
|
54
|
-
{%- endif -%}
|
|
51
|
+
{{ op.return_type.return_type_hint or "None" }}
|
|
55
52
|
{%- endif -%}:
|
|
56
53
|
base_url = self.base_url
|
|
57
54
|
path = f"{{ op.path_name }}"
|
|
58
55
|
|
|
59
56
|
headers = {
|
|
60
|
-
{% if op.
|
|
57
|
+
{% if op.request_body and op.request_body.content_type %}
|
|
58
|
+
"Content-Type": "{{ op.request_body.content_type }}",
|
|
59
|
+
{% elif op.body_param and not op.request_body %}
|
|
61
60
|
"Content-Type": "application/json",
|
|
62
61
|
{% endif %}
|
|
63
62
|
{% if op.is_sse %}
|
|
64
63
|
"Accept": "text/event-stream",
|
|
65
64
|
{% else %}
|
|
66
|
-
"Accept": "application/json",
|
|
65
|
+
"Accept": "{{ op.return_type.accept_content_types | join(', ') or 'application/json' }}",
|
|
67
66
|
{% endif %}
|
|
67
|
+
{% for hp in op.header_params %}
|
|
68
|
+
{{ hp }},
|
|
69
|
+
{% endfor %}
|
|
68
70
|
}
|
|
71
|
+
headers = {k: v for (k, v) in headers.items() if v is not None}
|
|
69
72
|
|
|
70
73
|
_token = await self.get_access_token()
|
|
71
74
|
if _token:
|
|
@@ -86,10 +89,20 @@ AsyncGenerator[str | dict[str, Any], None]
|
|
|
86
89
|
headers=headers,
|
|
87
90
|
params=query_params,
|
|
88
91
|
{% if op.body_param %}
|
|
92
|
+
{% if op.request_body and op.request_body.encoding == "json" %}
|
|
93
|
+
json={{ op.request_body.expression }},
|
|
94
|
+
{% elif op.request_body and op.request_body.encoding == "multipart" %}
|
|
95
|
+
files={{ op.request_body.expression }},
|
|
96
|
+
{% elif op.request_body and op.request_body.encoding == "form" %}
|
|
97
|
+
data={{ op.request_body.expression }},
|
|
98
|
+
{% elif op.request_body and op.request_body.encoding in ["binary", "text"] %}
|
|
99
|
+
content={{ op.request_body.expression }},
|
|
100
|
+
{% else %}
|
|
89
101
|
json={{ op.body_param }},
|
|
102
|
+
{% endif %}
|
|
90
103
|
{% endif %}
|
|
91
104
|
) as response:
|
|
92
|
-
if response.status_code
|
|
105
|
+
if response.status_code not in {{ op.return_type.accepted_status_codes or [op.return_type.status_code] }}:
|
|
93
106
|
raise HTTPException(
|
|
94
107
|
response.status_code,
|
|
95
108
|
f"{{ op.operation_id }} failed with status code: {response.status_code}",
|
|
@@ -123,29 +136,51 @@ AsyncGenerator[str | dict[str, Any], None]
|
|
|
123
136
|
headers=headers,
|
|
124
137
|
params=query_params,
|
|
125
138
|
{% if op.body_param %}
|
|
139
|
+
{% if op.request_body and op.request_body.encoding == "json" %}
|
|
140
|
+
json={{ op.request_body.expression }},
|
|
141
|
+
{% elif op.request_body and op.request_body.encoding == "multipart" %}
|
|
142
|
+
files={{ op.request_body.expression }},
|
|
143
|
+
{% elif op.request_body and op.request_body.encoding == "form" %}
|
|
144
|
+
data={{ op.request_body.expression }},
|
|
145
|
+
{% elif op.request_body and op.request_body.encoding in ["binary", "text"] %}
|
|
146
|
+
content={{ op.request_body.expression }},
|
|
147
|
+
{% else %}
|
|
126
148
|
json={{ op.body_param }},
|
|
149
|
+
{% endif %}
|
|
127
150
|
{% endif %}
|
|
128
151
|
)
|
|
129
152
|
|
|
130
|
-
if response.status_code
|
|
153
|
+
if response.status_code not in {{ op.return_type.accepted_status_codes or [op.return_type.status_code] }}:
|
|
131
154
|
raise HTTPException(
|
|
132
155
|
response.status_code,
|
|
133
156
|
f"{{ op.operation_id }} failed with status code: {response.status_code}",
|
|
134
157
|
)
|
|
135
158
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
{%
|
|
139
|
-
|
|
140
|
-
{%
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
_response_content_type = response.headers.get("content-type", "").split(";", 1)[0].lower()
|
|
160
|
+
{% for variant in op.return_type.variants %}
|
|
161
|
+
{% set same_status_variants = op.return_type.variants | selectattr("status_code", "equalto", variant.status_code) | list %}
|
|
162
|
+
if response.status_code == {{ variant.status_code }}{% if same_status_variants|length > 1 and variant.content_type %} and _response_content_type == "{{ variant.content_type }}"{% endif %}:
|
|
163
|
+
{% if variant.body_kind == "empty" or variant.type is none or variant.type.converted_type is none %}
|
|
164
|
+
return None
|
|
165
|
+
{% elif variant.body_kind == "binary" %}
|
|
166
|
+
return response.content
|
|
167
|
+
{% elif variant.body_kind == "text" %}
|
|
168
|
+
return response.text
|
|
169
|
+
{% elif variant.complex_type %}
|
|
170
|
+
{% if variant.list_type is none %}
|
|
171
|
+
return TypeAdapter({{ variant.type.converted_type }}).validate_python(response.json())
|
|
143
172
|
{% else %}
|
|
144
|
-
|
|
173
|
+
return TypeAdapter(list[{{ variant.list_type }}]).validate_python(response.json())
|
|
145
174
|
{% endif %}
|
|
146
175
|
{% else %}
|
|
147
|
-
|
|
176
|
+
return response.json()
|
|
148
177
|
{% endif %}
|
|
178
|
+
|
|
179
|
+
{% endfor %}
|
|
180
|
+
raise HTTPException(
|
|
181
|
+
response.status_code,
|
|
182
|
+
f"{{ op.operation_id }} failed with unsupported content type: {_response_content_type}",
|
|
183
|
+
)
|
|
149
184
|
{% endif %}
|
|
150
185
|
|
|
151
186
|
{% endfor %}
|
|
@@ -48,24 +48,27 @@ class SyncClient(BaseModel):
|
|
|
48
48
|
def {{ op.operation_id }}(self{% if op.params %}, {{ op.params }}{% endif %}) -> {%- if op.is_sse -%}
|
|
49
49
|
Generator[str | dict[str, Any], None, None]
|
|
50
50
|
{%- else -%}
|
|
51
|
-
{
|
|
52
|
-
{%- elif op.return_type.list_type is not none -%}list[{{ op.return_type.list_type }}]
|
|
53
|
-
{%- else -%}{{ op.return_type.type.converted_type }}
|
|
54
|
-
{%- endif -%}
|
|
51
|
+
{{ op.return_type.return_type_hint or "None" }}
|
|
55
52
|
{%- endif -%}:
|
|
56
53
|
base_url = self.base_url
|
|
57
54
|
path = f"{{ op.path_name }}"
|
|
58
55
|
|
|
59
56
|
headers = {
|
|
60
|
-
{% if op.
|
|
57
|
+
{% if op.request_body and op.request_body.content_type %}
|
|
58
|
+
"Content-Type": "{{ op.request_body.content_type }}",
|
|
59
|
+
{% elif op.body_param and not op.request_body %}
|
|
61
60
|
"Content-Type": "application/json",
|
|
62
61
|
{% endif %}
|
|
63
62
|
{% if op.is_sse %}
|
|
64
63
|
"Accept": "text/event-stream",
|
|
65
64
|
{% else %}
|
|
66
|
-
"Accept": "application/json",
|
|
65
|
+
"Accept": "{{ op.return_type.accept_content_types | join(', ') or 'application/json' }}",
|
|
67
66
|
{% endif %}
|
|
67
|
+
{% for hp in op.header_params %}
|
|
68
|
+
{{ hp }},
|
|
69
|
+
{% endfor %}
|
|
68
70
|
}
|
|
71
|
+
headers = {k: v for (k, v) in headers.items() if v is not None}
|
|
69
72
|
|
|
70
73
|
_token = self.get_access_token()
|
|
71
74
|
if _token:
|
|
@@ -86,10 +89,20 @@ Generator[str | dict[str, Any], None, None]
|
|
|
86
89
|
headers=headers,
|
|
87
90
|
params=query_params,
|
|
88
91
|
{% if op.body_param %}
|
|
92
|
+
{% if op.request_body and op.request_body.encoding == "json" %}
|
|
93
|
+
json={{ op.request_body.expression }},
|
|
94
|
+
{% elif op.request_body and op.request_body.encoding == "multipart" %}
|
|
95
|
+
files={{ op.request_body.expression }},
|
|
96
|
+
{% elif op.request_body and op.request_body.encoding == "form" %}
|
|
97
|
+
data={{ op.request_body.expression }},
|
|
98
|
+
{% elif op.request_body and op.request_body.encoding in ["binary", "text"] %}
|
|
99
|
+
content={{ op.request_body.expression }},
|
|
100
|
+
{% else %}
|
|
89
101
|
json={{ op.body_param }},
|
|
102
|
+
{% endif %}
|
|
90
103
|
{% endif %}
|
|
91
104
|
) as response:
|
|
92
|
-
if response.status_code
|
|
105
|
+
if response.status_code not in {{ op.return_type.accepted_status_codes or [op.return_type.status_code] }}:
|
|
93
106
|
raise HTTPException(
|
|
94
107
|
response.status_code,
|
|
95
108
|
f"{{ op.operation_id }} failed with status code: {response.status_code}",
|
|
@@ -122,29 +135,51 @@ Generator[str | dict[str, Any], None, None]
|
|
|
122
135
|
headers=headers,
|
|
123
136
|
params=query_params,
|
|
124
137
|
{% if op.body_param %}
|
|
138
|
+
{% if op.request_body and op.request_body.encoding == "json" %}
|
|
139
|
+
json={{ op.request_body.expression }},
|
|
140
|
+
{% elif op.request_body and op.request_body.encoding == "multipart" %}
|
|
141
|
+
files={{ op.request_body.expression }},
|
|
142
|
+
{% elif op.request_body and op.request_body.encoding == "form" %}
|
|
143
|
+
data={{ op.request_body.expression }},
|
|
144
|
+
{% elif op.request_body and op.request_body.encoding in ["binary", "text"] %}
|
|
145
|
+
content={{ op.request_body.expression }},
|
|
146
|
+
{% else %}
|
|
125
147
|
json={{ op.body_param }},
|
|
148
|
+
{% endif %}
|
|
126
149
|
{% endif %}
|
|
127
150
|
)
|
|
128
151
|
|
|
129
|
-
if response.status_code
|
|
152
|
+
if response.status_code not in {{ op.return_type.accepted_status_codes or [op.return_type.status_code] }}:
|
|
130
153
|
raise HTTPException(
|
|
131
154
|
response.status_code,
|
|
132
155
|
f"{{ op.operation_id }} failed with status code: {response.status_code}",
|
|
133
156
|
)
|
|
134
157
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
{%
|
|
138
|
-
|
|
139
|
-
{%
|
|
140
|
-
|
|
141
|
-
|
|
158
|
+
_response_content_type = response.headers.get("content-type", "").split(";", 1)[0].lower()
|
|
159
|
+
{% for variant in op.return_type.variants %}
|
|
160
|
+
{% set same_status_variants = op.return_type.variants | selectattr("status_code", "equalto", variant.status_code) | list %}
|
|
161
|
+
if response.status_code == {{ variant.status_code }}{% if same_status_variants|length > 1 and variant.content_type %} and _response_content_type == "{{ variant.content_type }}"{% endif %}:
|
|
162
|
+
{% if variant.body_kind == "empty" or variant.type is none or variant.type.converted_type is none %}
|
|
163
|
+
return None
|
|
164
|
+
{% elif variant.body_kind == "binary" %}
|
|
165
|
+
return response.content
|
|
166
|
+
{% elif variant.body_kind == "text" %}
|
|
167
|
+
return response.text
|
|
168
|
+
{% elif variant.complex_type %}
|
|
169
|
+
{% if variant.list_type is none %}
|
|
170
|
+
return TypeAdapter({{ variant.type.converted_type }}).validate_python(response.json())
|
|
142
171
|
{% else %}
|
|
143
|
-
|
|
172
|
+
return TypeAdapter(list[{{ variant.list_type }}]).validate_python(response.json())
|
|
144
173
|
{% endif %}
|
|
145
174
|
{% else %}
|
|
146
|
-
|
|
175
|
+
return response.json()
|
|
147
176
|
{% endif %}
|
|
177
|
+
|
|
178
|
+
{% endfor %}
|
|
179
|
+
raise HTTPException(
|
|
180
|
+
response.status_code,
|
|
181
|
+
f"{{ op.operation_id }} failed with unsupported content type: {_response_content_type}",
|
|
182
|
+
)
|
|
148
183
|
{% endif %}
|
|
149
184
|
|
|
150
185
|
{% endfor %}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Optional, Union
|
|
1
|
+
from typing import List, Literal, Optional, Union
|
|
2
2
|
|
|
3
3
|
from openapi_pydantic.v3.v3_0 import (
|
|
4
4
|
Operation as Operation30,
|
|
@@ -45,6 +45,25 @@ class OpReturnType(BaseModel):
|
|
|
45
45
|
status_code: int
|
|
46
46
|
complex_type: bool = False
|
|
47
47
|
list_type: Optional[str] = None
|
|
48
|
+
variants: List["ResponseVariant"] = Field(default_factory=list)
|
|
49
|
+
accepted_status_codes: List[int] = Field(default_factory=list)
|
|
50
|
+
accept_content_types: List[str] = Field(default_factory=list)
|
|
51
|
+
return_type_hint: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResponseVariant(BaseModel):
|
|
55
|
+
status_code: int
|
|
56
|
+
content_type: Optional[str] = None
|
|
57
|
+
type: Optional[TypeConversion] = None
|
|
58
|
+
complex_type: bool = False
|
|
59
|
+
list_type: Optional[str] = None
|
|
60
|
+
body_kind: Literal["empty", "json", "text", "binary"] = "empty"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RequestBodyDefinition(BaseModel):
|
|
64
|
+
content_type: Optional[str] = None
|
|
65
|
+
encoding: Literal["json", "form", "multipart", "binary", "text"]
|
|
66
|
+
expression: str
|
|
48
67
|
|
|
49
68
|
|
|
50
69
|
class ServiceOperation(BaseModel):
|
|
@@ -60,6 +79,7 @@ class ServiceOperation(BaseModel):
|
|
|
60
79
|
tag: Optional[str] = None
|
|
61
80
|
path_name: str
|
|
62
81
|
body_param: Optional[str] = None
|
|
82
|
+
request_body: Optional[RequestBodyDefinition] = None
|
|
63
83
|
method: str
|
|
64
84
|
is_sse: bool = False
|
|
65
85
|
use_orjson: bool = False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|