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.
Files changed (33) hide show
  1. ab_openapi_python_generator/__init__.py +14 -10
  2. ab_openapi_python_generator/__main__.py +85 -0
  3. ab_openapi_python_generator/common.py +58 -0
  4. ab_openapi_python_generator/generate_data.py +235 -0
  5. ab_openapi_python_generator/language_converters/__init__.py +0 -0
  6. ab_openapi_python_generator/language_converters/python/__init__.py +0 -0
  7. ab_openapi_python_generator/language_converters/python/client_generator.py +450 -0
  8. ab_openapi_python_generator/language_converters/python/common.py +58 -0
  9. ab_openapi_python_generator/language_converters/python/exception_generator.py +23 -0
  10. ab_openapi_python_generator/language_converters/python/generator.py +52 -0
  11. ab_openapi_python_generator/language_converters/python/jinja_config.py +38 -0
  12. ab_openapi_python_generator/language_converters/python/model_generator.py +838 -0
  13. ab_openapi_python_generator/language_converters/python/templates/alias_union.jinja2 +17 -0
  14. ab_openapi_python_generator/language_converters/python/templates/async_client_httpx_pydantic_2.jinja2 +80 -0
  15. ab_openapi_python_generator/language_converters/python/templates/discriminator_enum.jinja2 +7 -0
  16. ab_openapi_python_generator/language_converters/python/templates/enum.jinja2 +11 -0
  17. ab_openapi_python_generator/language_converters/python/templates/http_exception.jinja2 +8 -0
  18. ab_openapi_python_generator/language_converters/python/templates/models.jinja2 +24 -0
  19. ab_openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +28 -0
  20. ab_openapi_python_generator/language_converters/python/templates/sync_client_httpx_pydantic_2.jinja2 +80 -0
  21. ab_openapi_python_generator/models.py +101 -0
  22. ab_openapi_python_generator/parsers/__init__.py +13 -0
  23. ab_openapi_python_generator/parsers/openapi_30.py +65 -0
  24. ab_openapi_python_generator/parsers/openapi_31.py +65 -0
  25. ab_openapi_python_generator/py.typed +0 -0
  26. ab_openapi_python_generator/version_detector.py +67 -0
  27. {ab_openapi_python_generator-2.1.4.dev1768280320.dist-info → ab_openapi_python_generator-2.2.1.dist-info}/METADATA +21 -27
  28. ab_openapi_python_generator-2.2.1.dist-info/RECORD +31 -0
  29. {ab_openapi_python_generator-2.1.4.dev1768280320.dist-info → ab_openapi_python_generator-2.2.1.dist-info}/WHEEL +1 -1
  30. ab_openapi_python_generator-2.2.1.dist-info/entry_points.txt +2 -0
  31. ab_openapi_python_generator-2.1.4.dev1768280320.dist-info/RECORD +0 -6
  32. ab_openapi_python_generator-2.1.4.dev1768280320.dist-info/entry_points.txt +0 -3
  33. {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