openapi-python-generator 1.1.2.dev1734089718__py3-none-any.whl → 1.2.1.dev1757310194__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. openapi_python_generator/__init__.py +2 -1
  2. openapi_python_generator/__main__.py +21 -6
  3. openapi_python_generator/common.py +15 -0
  4. openapi_python_generator/generate_data.py +112 -65
  5. openapi_python_generator/language_converters/python/api_config_generator.py +12 -6
  6. openapi_python_generator/language_converters/python/generator.py +8 -4
  7. openapi_python_generator/language_converters/python/model_generator.py +191 -54
  8. openapi_python_generator/language_converters/python/service_generator.py +159 -62
  9. openapi_python_generator/language_converters/python/templates/aiohttp.jinja2 +10 -9
  10. openapi_python_generator/language_converters/python/templates/httpx.jinja2 +8 -5
  11. openapi_python_generator/language_converters/python/templates/models.jinja2 +5 -3
  12. openapi_python_generator/language_converters/python/templates/models_pydantic_2.jinja2 +5 -3
  13. openapi_python_generator/language_converters/python/templates/requests.jinja2 +8 -5
  14. openapi_python_generator/language_converters/python/templates/service.jinja2 +2 -1
  15. openapi_python_generator/models.py +16 -2
  16. openapi_python_generator/parsers/__init__.py +13 -0
  17. openapi_python_generator/parsers/openapi_30.py +65 -0
  18. openapi_python_generator/parsers/openapi_31.py +65 -0
  19. openapi_python_generator/py.typed +0 -0
  20. openapi_python_generator/version_detector.py +70 -0
  21. {openapi_python_generator-1.1.2.dev1734089718.dist-info → openapi_python_generator-1.2.1.dev1757310194.dist-info}/METADATA +3 -3
  22. openapi_python_generator-1.2.1.dev1757310194.dist-info/RECORD +32 -0
  23. openapi_python_generator-1.1.2.dev1734089718.dist-info/RECORD +0 -27
  24. {openapi_python_generator-1.1.2.dev1734089718.dist-info → openapi_python_generator-1.2.1.dev1757310194.dist-info}/LICENSE +0 -0
  25. {openapi_python_generator-1.1.2.dev1734089718.dist-info → openapi_python_generator-1.2.1.dev1757310194.dist-info}/WHEEL +0 -0
  26. {openapi_python_generator-1.1.2.dev1734089718.dist-info → openapi_python_generator-1.2.1.dev1757310194.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,5 @@
1
- """Python client from an OPENAPI 3.0 specification in seconds."""
1
+ """Python client from an OPENAPI 3.0+ specification in seconds."""
2
+
2
3
  try:
3
4
  from importlib.metadata import PackageNotFoundError # type: ignore
4
5
  from importlib.metadata import version
@@ -1,12 +1,12 @@
1
1
  from typing import Optional
2
- from enum import Enum
3
2
 
4
3
  import click
5
4
 
6
5
  from openapi_python_generator import __version__
7
- from openapi_python_generator.common import HTTPLibrary, PydanticVersion
6
+ from openapi_python_generator.common import Formatter, HTTPLibrary, PydanticVersion
8
7
  from openapi_python_generator.generate_data import generate_data
9
8
 
9
+
10
10
  @click.command()
11
11
  @click.argument("source")
12
12
  @click.argument("output")
@@ -45,6 +45,13 @@ from openapi_python_generator.generate_data import generate_data
45
45
  show_default=True,
46
46
  help="Pydantic version to use for generated models.",
47
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
+ )
48
55
  @click.version_option(version=__version__)
49
56
  def main(
50
57
  source: str,
@@ -54,17 +61,25 @@ def main(
54
61
  use_orjson: bool = False,
55
62
  custom_template_path: Optional[str] = None,
56
63
  pydantic_version: PydanticVersion = PydanticVersion.V2,
64
+ formatter: Formatter = Formatter.BLACK,
57
65
  ) -> None:
58
66
  """
59
- Generate Python code from an OpenAPI 3.0 specification.
67
+ Generate Python code from an OpenAPI 3.0+ specification.
60
68
 
61
- Provide a SOURCE (file or URL) containing the OpenAPI 3 specification and
69
+ Provide a SOURCE (file or URL) containing the OpenAPI 3.0+ specification and
62
70
  an OUTPUT path, where the resulting client is created.
63
71
  """
64
72
  generate_data(
65
- source, output, library, env_token_name, use_orjson, custom_template_path,pydantic_version
73
+ source,
74
+ output,
75
+ library,
76
+ env_token_name,
77
+ use_orjson,
78
+ custom_template_path,
79
+ pydantic_version,
80
+ formatter,
66
81
  )
67
82
 
68
83
 
69
84
  if __name__ == "__main__": # pragma: no cover
70
- main()
85
+ main()
@@ -14,11 +14,26 @@ class HTTPLibrary(str, Enum):
14
14
  requests = "requests"
15
15
  aiohttp = "aiohttp"
16
16
 
17
+
17
18
  class PydanticVersion(str, Enum):
18
19
  V1 = "v1"
19
20
  V2 = "v2"
20
21
 
21
22
 
23
+ class Formatter(str, Enum):
24
+ """
25
+ Enum for the available code formatters.
26
+ """
27
+
28
+ BLACK = "black"
29
+ NONE = "none"
30
+
31
+
32
+ class FormatOptions:
33
+ skip_validation: bool = False
34
+ line_length: int = 120
35
+
36
+
22
37
  library_config_dict: Dict[Optional[HTTPLibrary], LibraryConfig] = {
23
38
  HTTPLibrary.httpx: LibraryConfig(
24
39
  name="httpx",
@@ -1,59 +1,74 @@
1
1
  from pathlib import Path
2
+ from typing import List
2
3
  from typing import Optional
3
4
  from typing import Union
4
5
 
5
6
  import black
7
+ from black.report import NothingChanged # type: ignore
6
8
  import click
7
9
  import httpx
8
10
  import isort
9
11
  import orjson
10
- import yaml
11
- from black import NothingChanged
12
+ import yaml # type: ignore
12
13
  from httpx import ConnectError
13
14
  from httpx import ConnectTimeout
14
- from openapi_pydantic.v3.v3_0 import OpenAPI
15
15
  from pydantic import ValidationError
16
16
 
17
- from .common import HTTPLibrary, PydanticVersion
18
- from .common import library_config_dict
19
- from .language_converters.python.generator import generator
17
+ from .common import FormatOptions, Formatter, HTTPLibrary, PydanticVersion
20
18
  from .language_converters.python.jinja_config import SERVICE_TEMPLATE
21
19
  from .language_converters.python.jinja_config import create_jinja_env
22
20
  from .models import ConversionResult
21
+ from .version_detector import detect_openapi_version
22
+ from .parsers import (
23
+ parse_openapi_3_0,
24
+ parse_openapi_3_1,
25
+ generate_code_3_0,
26
+ generate_code_3_1,
27
+ )
23
28
 
24
29
 
25
- def write_code(path: Path, content) -> None:
30
+ def write_code(path: Path, content: str, formatter: Formatter) -> None:
26
31
  """
27
32
  Write the content to the file at the given path.
28
- :param autoformat: The autoformat applied to the code written.
29
33
  :param path: The path to the file.
30
34
  :param content: The content to write.
35
+ :param formatter: The formatter applied to the code written.
31
36
  """
32
- try:
33
- with open(path, "w") as f:
34
- try:
35
- formatted_contend = black.format_file_contents(
36
- content, fast=False, mode=black.FileMode(line_length=120)
37
- )
37
+ if formatter == Formatter.BLACK:
38
+ formatted_contend = format_using_black(content)
39
+ elif formatter == Formatter.NONE:
40
+ formatted_contend = content
41
+ else:
42
+ raise NotImplementedError(
43
+ f"Missing implementation for formatter {formatter!r}."
44
+ )
45
+ with open(path, "w") as f:
46
+ f.write(formatted_contend)
47
+
38
48
 
39
- except NothingChanged:
40
- formatted_contend = content
41
- formatted_contend = isort.code(formatted_contend, line_length=120)
42
- f.write(formatted_contend)
43
- except Exception as e:
44
- raise e
49
+ def format_using_black(content: str) -> str:
50
+ try:
51
+ formatted_contend = black.format_file_contents(
52
+ content,
53
+ fast=FormatOptions.skip_validation,
54
+ mode=black.FileMode(line_length=FormatOptions.line_length),
55
+ )
56
+ except NothingChanged:
57
+ return content
58
+ return isort.code(formatted_contend, line_length=FormatOptions.line_length)
45
59
 
46
60
 
47
- def get_open_api(source: Union[str, Path]) -> OpenAPI:
61
+ def get_open_api(source: Union[str, Path]):
48
62
  """
49
63
  Tries to fetch the openapi specification file from the web or load from a local file.
50
64
  Supports both JSON and YAML formats. Returns the according OpenAPI object.
65
+ Automatically supports OpenAPI 3.0 and 3.1 specifications with intelligent version detection.
51
66
 
52
67
  Args:
53
68
  source: URL or file path to the OpenAPI specification
54
69
 
55
70
  Returns:
56
- OpenAPI: Parsed OpenAPI specification object
71
+ tuple: (OpenAPI object, version) where version is "3.0" or "3.1"
57
72
 
58
73
  Raises:
59
74
  FileNotFoundError: If the specified file cannot be found
@@ -64,31 +79,46 @@ def get_open_api(source: Union[str, Path]) -> OpenAPI:
64
79
  try:
65
80
  # Handle remote files
66
81
  if not isinstance(source, Path) and (
67
- source.startswith("http://") or source.startswith("https://")
82
+ source.startswith("http://") or source.startswith("https://")
68
83
  ):
69
84
  content = httpx.get(source).text
70
85
  # Try JSON first, then YAML for remote files
71
86
  try:
72
- return OpenAPI(**orjson.loads(content))
87
+ data = orjson.loads(content)
73
88
  except orjson.JSONDecodeError:
74
- return OpenAPI(**yaml.safe_load(content))
89
+ data = yaml.safe_load(content)
90
+ else:
91
+ # Handle local files
92
+ with open(source, "r") as f:
93
+ file_content = f.read()
75
94
 
76
- # Handle local files
77
- with open(source, "r") as f:
78
- file_content = f.read()
79
-
80
- # Try JSON first
81
- try:
82
- return OpenAPI(**orjson.loads(file_content))
83
- except orjson.JSONDecodeError:
84
- # If JSON fails, try YAML
95
+ # Try JSON first
85
96
  try:
86
- return OpenAPI(**yaml.safe_load(file_content))
87
- except yaml.YAMLError as e:
88
- click.echo(
89
- f"File {source} is neither a valid JSON nor YAML file: {str(e)}"
90
- )
91
- raise
97
+ data = orjson.loads(file_content)
98
+ except orjson.JSONDecodeError:
99
+ # If JSON fails, try YAML
100
+ try:
101
+ data = yaml.safe_load(file_content)
102
+ except yaml.YAMLError as e:
103
+ click.echo(
104
+ f"File {source} is neither a valid JSON nor YAML file: {str(e)}"
105
+ )
106
+ raise
107
+
108
+ # Detect version and parse with appropriate parser
109
+ version = detect_openapi_version(data)
110
+
111
+ if version == "3.0":
112
+ openapi_obj = parse_openapi_3_0(data) # type: ignore[assignment]
113
+ elif version == "3.1":
114
+ openapi_obj = parse_openapi_3_1(data) # type: ignore[assignment]
115
+ else:
116
+ # Unsupported version detected (version detection already limited to 3.0 / 3.1)
117
+ raise ValueError(
118
+ f"Unsupported OpenAPI version: {version}. Only 3.0.x and 3.1.x are supported."
119
+ )
120
+
121
+ return openapi_obj, version
92
122
 
93
123
  except FileNotFoundError:
94
124
  click.echo(
@@ -99,20 +129,20 @@ def get_open_api(source: Union[str, Path]) -> OpenAPI:
99
129
  click.echo(f"Could not connect to {source}.")
100
130
  raise ConnectError(f"Could not connect to {source}.") from None
101
131
  except ValidationError:
102
- click.echo(
103
- f"File {source} is not a valid OpenAPI 3.0 specification."
104
- )
132
+ click.echo(f"File {source} is not a valid OpenAPI 3.0+ specification.")
105
133
  raise
106
134
 
107
135
 
108
- def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
136
+ def write_data(
137
+ data: ConversionResult, output: Union[str, Path], formatter: Formatter
138
+ ) -> None:
109
139
  """
110
- This function will firstly create the folderstrucutre of output, if it doesn't exist. Then it will create the
140
+ This function will firstly create the folder structure of output, if it doesn't exist. Then it will create the
111
141
  models from data.models into the models sub module of the output folder. After this, the services will be created
112
142
  into the services sub module of the output folder.
113
- :param autoformat: The autoformat applied to the code written.
114
143
  :param data: The data to write.
115
144
  :param output: The path to the output folder.
145
+ :param formatter: The formatter applied to the code written.
116
146
  """
117
147
 
118
148
  # Create the folder structure of the output folder.
@@ -126,17 +156,18 @@ def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
126
156
  services_path = Path(output) / "services"
127
157
  services_path.mkdir(parents=True, exist_ok=True)
128
158
 
129
- files = []
159
+ files: List[str] = []
130
160
 
131
161
  # Write the models.
132
162
  for model in data.models:
133
163
  files.append(model.file_name)
134
- write_code(models_path / f"{model.file_name}.py", model.content)
164
+ write_code(models_path / f"{model.file_name}.py", model.content, formatter)
135
165
 
136
166
  # Create models.__init__.py file containing imports to all models.
137
167
  write_code(
138
168
  models_path / "__init__.py",
139
169
  "\n".join([f"from .{file} import *" for file in files]),
170
+ formatter,
140
171
  )
141
172
 
142
173
  files = []
@@ -149,44 +180,60 @@ def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
149
180
  files.append(service.file_name)
150
181
  write_code(
151
182
  services_path / f"{service.file_name}.py",
152
- jinja_env.get_template(SERVICE_TEMPLATE).render(**service.dict()),
183
+ jinja_env.get_template(SERVICE_TEMPLATE).render(**service.model_dump()),
184
+ formatter,
153
185
  )
154
186
 
155
187
  # Create services.__init__.py file containing imports to all services.
156
- write_code(services_path / "__init__.py", "")
188
+ write_code(services_path / "__init__.py", "", formatter)
157
189
 
158
190
  # Write the api_config.py file.
159
- write_code(Path(output) / "api_config.py", data.api_config.content)
191
+ write_code(Path(output) / "api_config.py", data.api_config.content, formatter)
160
192
 
161
193
  # Write the __init__.py file.
162
194
  write_code(
163
195
  Path(output) / "__init__.py",
164
196
  "from .models import *\nfrom .services import *\nfrom .api_config import *",
197
+ formatter,
165
198
  )
166
199
 
167
200
 
168
201
  def generate_data(
169
202
  source: Union[str, Path],
170
203
  output: Union[str, Path],
171
- library: Optional[HTTPLibrary] = HTTPLibrary.httpx,
204
+ library: HTTPLibrary = HTTPLibrary.httpx,
172
205
  env_token_name: Optional[str] = None,
173
206
  use_orjson: bool = False,
174
207
  custom_template_path: Optional[str] = None,
175
208
  pydantic_version: PydanticVersion = PydanticVersion.V2,
209
+ formatter: Formatter = Formatter.BLACK,
176
210
  ) -> None:
177
211
  """
178
- Generate Python code from an OpenAPI 3.0 specification.
212
+ Generate Python code from an OpenAPI 3.0+ specification.
179
213
  """
180
- data = get_open_api(source)
181
- click.echo(f"Generating data from {source}")
182
-
183
- result = generator(
184
- data,
185
- library_config_dict[library],
186
- env_token_name,
187
- use_orjson,
188
- custom_template_path,
189
- pydantic_version,
190
- )
214
+ openapi_obj, version = get_open_api(source)
215
+ click.echo(f"Generating data from {source} (OpenAPI {version})")
216
+
217
+ # Use version-specific generator
218
+ if version == "3.0":
219
+ result = generate_code_3_0(
220
+ openapi_obj, # type: ignore
221
+ library,
222
+ env_token_name,
223
+ use_orjson,
224
+ custom_template_path,
225
+ pydantic_version,
226
+ )
227
+ elif version == "3.1":
228
+ result = generate_code_3_1(
229
+ openapi_obj, # type: ignore
230
+ library,
231
+ env_token_name,
232
+ use_orjson,
233
+ custom_template_path,
234
+ pydantic_version,
235
+ )
236
+ else:
237
+ raise ValueError(f"Unsupported OpenAPI version: {version}")
191
238
 
192
- write_data(result, output)
239
+ write_data(result, output, formatter)
@@ -1,10 +1,11 @@
1
1
  from typing import Optional
2
2
 
3
- from openapi_pydantic.v3.v3_0 import OpenAPI
3
+ from openapi_pydantic.v3 import OpenAPI
4
4
 
5
5
  from openapi_python_generator.common import PydanticVersion
6
6
  from openapi_python_generator.language_converters.python.jinja_config import (
7
- API_CONFIG_TEMPLATE, API_CONFIG_TEMPLATE_PYDANTIC_V2,
7
+ API_CONFIG_TEMPLATE,
8
+ API_CONFIG_TEMPLATE_PYDANTIC_V2,
8
9
  )
9
10
  from openapi_python_generator.language_converters.python.jinja_config import (
10
11
  create_jinja_env,
@@ -13,19 +14,24 @@ from openapi_python_generator.models import APIConfig
13
14
 
14
15
 
15
16
  def generate_api_config(
16
- data: OpenAPI, env_token_name: Optional[str] = None,
17
- pydantic_version: PydanticVersion = PydanticVersion.V2,
17
+ data: OpenAPI,
18
+ env_token_name: Optional[str] = None,
19
+ pydantic_version: PydanticVersion = PydanticVersion.V2,
18
20
  ) -> APIConfig:
19
21
  """
20
22
  Generate the API model.
21
23
  """
22
24
 
23
- template_name = API_CONFIG_TEMPLATE_PYDANTIC_V2 if pydantic_version == PydanticVersion.V2 else API_CONFIG_TEMPLATE
25
+ template_name = (
26
+ API_CONFIG_TEMPLATE_PYDANTIC_V2
27
+ if pydantic_version == PydanticVersion.V2
28
+ else API_CONFIG_TEMPLATE
29
+ )
24
30
  jinja_env = create_jinja_env()
25
31
  return APIConfig(
26
32
  file_name="api_config",
27
33
  content=jinja_env.get_template(template_name).render(
28
- env_token_name=env_token_name, **data.dict()
34
+ env_token_name=env_token_name, **data.model_dump()
29
35
  ),
30
36
  base_url=data.servers[0].url if len(data.servers) > 0 else "NO SERVER",
31
37
  )
@@ -1,6 +1,7 @@
1
- from typing import Optional
1
+ from typing import Optional, Union
2
2
 
3
- from openapi_pydantic.v3.v3_0 import OpenAPI
3
+ from openapi_pydantic.v3.v3_0 import OpenAPI as OpenAPI30
4
+ from openapi_pydantic.v3.v3_1 import OpenAPI as OpenAPI31
4
5
 
5
6
  from openapi_python_generator.common import PydanticVersion
6
7
  from openapi_python_generator.language_converters.python import common
@@ -16,9 +17,12 @@ from openapi_python_generator.language_converters.python.service_generator impor
16
17
  from openapi_python_generator.models import ConversionResult
17
18
  from openapi_python_generator.models import LibraryConfig
18
19
 
20
+ # Type alias for both OpenAPI versions
21
+ OpenAPISpec = Union[OpenAPI30, OpenAPI31]
22
+
19
23
 
20
24
  def generator(
21
- data: OpenAPI,
25
+ data: OpenAPISpec,
22
26
  library_config: LibraryConfig,
23
27
  env_token_name: Optional[str] = None,
24
28
  use_orjson: bool = False,
@@ -26,7 +30,7 @@ def generator(
26
30
  pydantic_version: PydanticVersion = PydanticVersion.V2,
27
31
  ) -> ConversionResult:
28
32
  """
29
- Generate Python code from an OpenAPI 3.0 specification.
33
+ Generate Python code from an OpenAPI 3.0+ specification.
30
34
  """
31
35
 
32
36
  common.set_use_orjson(use_orjson)