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
@@ -1,13 +1,17 @@
1
- """Alias package to preserve imports when distribution renamed.
1
+ """Python client from an OPENAPI 3.0+ specification in seconds."""
2
2
 
3
- This package re-exports the real package located at
4
- `openapi_python_generator` so existing imports like
5
- `import ab_openapi_python_generator` continue to work.
6
- """
7
- from openapi_python_generator import * # noqa: F401,F403
3
+ try:
4
+ from importlib.metadata import (
5
+ PackageNotFoundError, # type: ignore
6
+ version,
7
+ )
8
+ except ImportError: # pragma: no cover
9
+ from importlib_metadata import (
10
+ PackageNotFoundError, # type: ignore
11
+ version, # type: ignore
12
+ )
8
13
 
9
- # Preserve __all__ if present on the real package
10
14
  try:
11
- __all__ = getattr(__import__("openapi_python_generator"), "__all__")
12
- except Exception:
13
- __all__ = []
15
+ __version__ = version(__name__)
16
+ except PackageNotFoundError: # pragma: no cover
17
+ __version__ = "unknown"
@@ -0,0 +1,85 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+
5
+ from ab_openapi_python_generator import __version__
6
+ from ab_openapi_python_generator.common import Formatter, HTTPLibrary, PydanticVersion
7
+ from ab_openapi_python_generator.generate_data import generate_data
8
+
9
+
10
+ @click.command()
11
+ @click.argument("source")
12
+ @click.argument("output")
13
+ @click.option(
14
+ "--library",
15
+ default=HTTPLibrary.httpx,
16
+ type=HTTPLibrary,
17
+ show_default=True,
18
+ help="HTTP library to use in the generation of the client.",
19
+ )
20
+ @click.option(
21
+ "--env-token-name",
22
+ default=None,
23
+ show_default=True,
24
+ help="Name of the environment variable that contains the token. If you set this, the code expects this environment "
25
+ "variable to be set and will raise an error if it is not.",
26
+ )
27
+ @click.option(
28
+ "--use-orjson",
29
+ is_flag=True,
30
+ show_default=True,
31
+ default=False,
32
+ help="Use the orjson library to serialize the data. This is faster than the default json library and provides "
33
+ "serialization of datetimes and other types that are not supported by the default json library.",
34
+ )
35
+ @click.option(
36
+ "--custom-template-path",
37
+ type=str,
38
+ default=None,
39
+ help="Custom template path to use. Allows overriding of the built in templates",
40
+ )
41
+ @click.option(
42
+ "--pydantic-version",
43
+ type=click.Choice(["v1", "v2"]),
44
+ default="v2",
45
+ show_default=True,
46
+ help="Pydantic version to use for generated models.",
47
+ )
48
+ @click.option(
49
+ "--formatter",
50
+ type=click.Choice(["black", "none"]),
51
+ default="black",
52
+ show_default=True,
53
+ help="Option to choose which auto formatter is applied.",
54
+ )
55
+ @click.version_option(version=__version__)
56
+ def main(
57
+ source: str,
58
+ output: str,
59
+ library: Optional[HTTPLibrary] = HTTPLibrary.httpx,
60
+ env_token_name: Optional[str] = None,
61
+ use_orjson: bool = False,
62
+ custom_template_path: Optional[str] = None,
63
+ pydantic_version: PydanticVersion = PydanticVersion.V2,
64
+ formatter: Formatter = Formatter.BLACK,
65
+ ) -> None:
66
+ """
67
+ Generate Python code from an OpenAPI 3.0+ specification.
68
+
69
+ Provide a SOURCE (file or URL) containing the OpenAPI 3.0+ specification and
70
+ an OUTPUT path, where the resulting client is created.
71
+ """
72
+ generate_data(
73
+ source,
74
+ output,
75
+ library if library is not None else HTTPLibrary.httpx,
76
+ env_token_name,
77
+ use_orjson,
78
+ custom_template_path,
79
+ pydantic_version,
80
+ formatter,
81
+ )
82
+
83
+
84
+ if __name__ == "__main__": # pragma: no cover
85
+ main()
@@ -0,0 +1,58 @@
1
+ from enum import Enum
2
+ from typing import Dict, Optional
3
+
4
+ from ab_openapi_python_generator.models import LibraryConfig
5
+
6
+
7
+ class HTTPLibrary(str, Enum):
8
+ """
9
+ Enum for the available HTTP libraries.
10
+ """
11
+
12
+ httpx = "httpx"
13
+ requests = "requests"
14
+ aiohttp = "aiohttp"
15
+
16
+
17
+ class PydanticVersion(str, Enum):
18
+ V1 = "v1"
19
+ V2 = "v2"
20
+
21
+
22
+ class Formatter(str, Enum):
23
+ """
24
+ Enum for the available code formatters.
25
+ """
26
+
27
+ BLACK = "black"
28
+ NONE = "none"
29
+
30
+
31
+ class FormatOptions:
32
+ skip_validation: bool = False
33
+ line_length: int = 120
34
+
35
+
36
+ library_config_dict: Dict[Optional[HTTPLibrary], LibraryConfig] = {
37
+ HTTPLibrary.httpx: LibraryConfig(
38
+ name="httpx",
39
+ library_name="httpx",
40
+ template_name="httpx.jinja2",
41
+ include_async=True,
42
+ include_sync=True,
43
+ ),
44
+ HTTPLibrary.requests: LibraryConfig(
45
+ name="requests",
46
+ library_name="requests",
47
+ template_name="requests.jinja2",
48
+ include_async=False,
49
+ include_sync=True,
50
+ ),
51
+ HTTPLibrary.aiohttp: LibraryConfig(
52
+ name="aiohttp",
53
+ library_name="aiohttp",
54
+ template_name="aiohttp.jinja2",
55
+ include_async=True,
56
+ include_sync=False,
57
+ ),
58
+ }
@@ -0,0 +1,235 @@
1
+ from pathlib import Path
2
+ from typing import List, Optional, Union
3
+
4
+ import black
5
+ import click
6
+ import httpx
7
+ import isort
8
+ import orjson
9
+ import yaml # type: ignore
10
+ from black.report import NothingChanged # type: ignore
11
+ from httpx import ConnectError, ConnectTimeout
12
+ from pydantic import ValidationError
13
+
14
+ from .common import FormatOptions, Formatter, HTTPLibrary, PydanticVersion
15
+ from .models import ConversionResult
16
+ from .parsers import (
17
+ generate_code_3_0,
18
+ generate_code_3_1,
19
+ parse_openapi_3_0,
20
+ parse_openapi_3_1,
21
+ )
22
+ from .version_detector import detect_openapi_version
23
+
24
+
25
+ def write_code(path: Path, content: str, formatter: Formatter) -> None:
26
+ """
27
+ Write the content to the file at the given path.
28
+ :param path: The path to the file.
29
+ :param content: The content to write.
30
+ :param formatter: The formatter applied to the code written.
31
+ """
32
+ if formatter == Formatter.BLACK:
33
+ formatted_contend = format_using_black(content)
34
+ elif formatter == Formatter.NONE:
35
+ formatted_contend = content
36
+ else:
37
+ raise NotImplementedError(f"Missing implementation for formatter {formatter!r}.")
38
+ with open(path, "w") as f:
39
+ f.write(formatted_contend)
40
+
41
+
42
+ def format_using_black(content: str) -> str:
43
+ try:
44
+ formatted_contend = black.format_file_contents(
45
+ content,
46
+ fast=FormatOptions.skip_validation,
47
+ mode=black.FileMode(line_length=FormatOptions.line_length),
48
+ )
49
+ except NothingChanged:
50
+ return content
51
+ return isort.code(formatted_contend, line_length=FormatOptions.line_length)
52
+
53
+
54
+ def get_open_api(source: Union[str, Path]):
55
+ """
56
+ Tries to fetch the openapi specification file from the web or load from a local file.
57
+ Supports both JSON and YAML formats. Returns the according OpenAPI object.
58
+ Automatically supports OpenAPI 3.0 and 3.1 specifications with intelligent version detection.
59
+
60
+ Args:
61
+ source: URL or file path to the OpenAPI specification
62
+
63
+ Returns:
64
+ tuple: (OpenAPI object, version) where version is "3.0" or "3.1"
65
+
66
+ Raises:
67
+ FileNotFoundError: If the specified file cannot be found
68
+ ConnectError: If the URL cannot be accessed
69
+ ValidationError: If the specification is invalid
70
+ JSONDecodeError/YAMLError: If the file cannot be parsed
71
+ """
72
+ try:
73
+ # Handle remote files
74
+ if not isinstance(source, Path) and (source.startswith("http://") or source.startswith("https://")):
75
+ content = httpx.get(source).text
76
+ # Try JSON first, then YAML for remote files
77
+ try:
78
+ data = orjson.loads(content)
79
+ except orjson.JSONDecodeError:
80
+ data = yaml.safe_load(content)
81
+ else:
82
+ # Handle local files
83
+ with open(source, "r") as f:
84
+ file_content = f.read()
85
+
86
+ # Try JSON first
87
+ try:
88
+ data = orjson.loads(file_content)
89
+ except orjson.JSONDecodeError:
90
+ # If JSON fails, try YAML
91
+ try:
92
+ data = yaml.safe_load(file_content)
93
+ except yaml.YAMLError as e:
94
+ click.echo(f"File {source} is neither a valid JSON nor YAML file: {str(e)}")
95
+ raise
96
+
97
+ # Detect version and parse with appropriate parser
98
+ version = detect_openapi_version(data)
99
+
100
+ if version == "3.0":
101
+ openapi_obj = parse_openapi_3_0(data) # type: ignore[assignment]
102
+ elif version == "3.1":
103
+ openapi_obj = parse_openapi_3_1(data) # type: ignore[assignment]
104
+ else:
105
+ # Unsupported version detected (version detection already limited to 3.0 / 3.1)
106
+ raise ValueError(f"Unsupported OpenAPI version: {version}. Only 3.0.x and 3.1.x are supported.")
107
+
108
+ return openapi_obj, version
109
+
110
+ except FileNotFoundError:
111
+ click.echo(f"File {source} not found. Please make sure to pass the path to the OpenAPI specification.")
112
+ raise
113
+ except (ConnectError, ConnectTimeout):
114
+ click.echo(f"Could not connect to {source}.")
115
+ raise ConnectError(f"Could not connect to {source}.") from None
116
+ except ValidationError:
117
+ click.echo(f"File {source} is not a valid OpenAPI 3.0+ specification.")
118
+ raise
119
+
120
+
121
+ def write_data(data: ConversionResult, output: Union[str, Path], formatter: Formatter) -> None:
122
+ """
123
+ Write generated code to disk.
124
+
125
+ Creates:
126
+ - models/ (and models/__init__.py)
127
+ - clients/ (and clients/__init__.py)
128
+ - exceptions (package root/__init__.py)
129
+ - __init__.py (package root)
130
+ """
131
+ out = Path(output)
132
+ out.mkdir(parents=True, exist_ok=True)
133
+
134
+ # ----------------------------
135
+ # models/
136
+ # ----------------------------
137
+ models_path = out / "models"
138
+ models_path.mkdir(parents=True, exist_ok=True)
139
+
140
+ model_files: List[str] = []
141
+ for model in data.models:
142
+ model_files.append(model.file_name)
143
+ write_code(models_path / f"{model.file_name}.py", model.content, formatter)
144
+
145
+ write_code(
146
+ models_path / "__init__.py",
147
+ "\n".join([f"from .{f} import *" for f in model_files]) + ("\n" if model_files else ""),
148
+ formatter,
149
+ )
150
+
151
+ # ----------------------------
152
+ # clients/
153
+ # ----------------------------
154
+ clients_path = out / "clients"
155
+ clients_path.mkdir(parents=True, exist_ok=True)
156
+
157
+ client_files: List[str] = []
158
+ for client in data.clients:
159
+ client_files.append(client.file_name)
160
+ write_code(clients_path / f"{client.file_name}.py", client.content, formatter)
161
+
162
+ write_code(
163
+ clients_path / "__init__.py",
164
+ "\n".join([f"from .{f} import *" for f in client_files]) + ("\n" if client_files else ""),
165
+ formatter,
166
+ )
167
+
168
+ # ----------------------------
169
+ # exceptions/
170
+ # ----------------------------
171
+ exceptions_path = out / "exceptions"
172
+ exceptions_path.mkdir(parents=True, exist_ok=True)
173
+
174
+ exception_files: List[str] = []
175
+ for ex in data.exceptions:
176
+ exception_files.append(ex.file_name)
177
+ write_code(exceptions_path / f"{ex.file_name}.py", ex.content, formatter)
178
+
179
+ write_code(
180
+ exceptions_path / "__init__.py",
181
+ "\n".join([f"from .{f} import *" for f in exception_files]) + ("\n" if exception_files else ""),
182
+ formatter,
183
+ )
184
+
185
+ # ----------------------------
186
+ # package __init__.py (root)
187
+ # ----------------------------
188
+ init_lines: List[str] = [
189
+ "from .models import *",
190
+ "from .clients import *",
191
+ "from .exceptions import *",
192
+ ]
193
+
194
+ write_code(out / "__init__.py", "\n".join(init_lines) + "\n", formatter)
195
+
196
+
197
+ def generate_data(
198
+ source: Union[str, Path],
199
+ output: Union[str, Path],
200
+ library: HTTPLibrary = HTTPLibrary.httpx,
201
+ env_token_name: Optional[str] = None,
202
+ use_orjson: bool = False,
203
+ custom_template_path: Optional[str] = None,
204
+ pydantic_version: PydanticVersion = PydanticVersion.V2,
205
+ formatter: Formatter = Formatter.BLACK,
206
+ ) -> None:
207
+ """
208
+ Generate Python code from an OpenAPI 3.0+ specification.
209
+ """
210
+ openapi_obj, version = get_open_api(source)
211
+ click.echo(f"Generating data from {source} (OpenAPI {version})")
212
+
213
+ # Use version-specific generator
214
+ if version == "3.0":
215
+ result = generate_code_3_0(
216
+ openapi_obj, # type: ignore
217
+ library,
218
+ env_token_name,
219
+ use_orjson,
220
+ custom_template_path,
221
+ pydantic_version,
222
+ )
223
+ elif version == "3.1":
224
+ result = generate_code_3_1(
225
+ openapi_obj, # type: ignore
226
+ library,
227
+ env_token_name,
228
+ use_orjson,
229
+ custom_template_path,
230
+ pydantic_version,
231
+ )
232
+ else:
233
+ raise ValueError(f"Unsupported OpenAPI version: {version}")
234
+
235
+ write_data(result, output, formatter)