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