44from typing import Union
55
66import black
7+ from black .report import NothingChanged # type: ignore
78import click
89import httpx
910import isort
1011import orjson
11- import yaml
12- from black import NothingChanged
12+ import yaml # type: ignore
1313from httpx import ConnectError
1414from httpx import ConnectTimeout
15- from openapi_pydantic .v3 .v3_0 import OpenAPI
1615from pydantic import ValidationError
1716
1817from .common import FormatOptions , Formatter , HTTPLibrary , PydanticVersion
19- from .common import library_config_dict
20- from .language_converters .python .generator import generator
2118from .language_converters .python .jinja_config import SERVICE_TEMPLATE
2219from .language_converters .python .jinja_config import create_jinja_env
2320from .models import ConversionResult
21+ from .version_detector import detect_openapi_version
22+ from .parsers import (
23+ parse_openapi_30 ,
24+ parse_openapi_31 ,
25+ generate_code_30 ,
26+ generate_code_31 ,
27+ )
2428
2529
2630def write_code (path : Path , content : str , formatter : Formatter ) -> None :
@@ -35,31 +39,36 @@ def write_code(path: Path, content: str, formatter: Formatter) -> None:
3539 elif formatter == Formatter .NONE :
3640 formatted_contend = content
3741 else :
38- raise NotImplementedError (f"Missing implementation for formatter { formatter !r} ." )
42+ raise NotImplementedError (
43+ f"Missing implementation for formatter { formatter !r} ."
44+ )
3945 with open (path , "w" ) as f :
4046 f .write (formatted_contend )
4147
4248
4349def format_using_black (content : str ) -> str :
4450 try :
4551 formatted_contend = black .format_file_contents (
46- content , fast = FormatOptions .skip_validation , mode = black .FileMode (line_length = FormatOptions .line_length )
52+ content ,
53+ fast = FormatOptions .skip_validation ,
54+ mode = black .FileMode (line_length = FormatOptions .line_length ),
4755 )
4856 except NothingChanged :
4957 return content
5058 return isort .code (formatted_contend , line_length = FormatOptions .line_length )
5159
5260
53- def get_open_api (source : Union [str , Path ]) -> OpenAPI :
61+ def get_open_api (source : Union [str , Path ]):
5462 """
5563 Tries to fetch the openapi specification file from the web or load from a local file.
5664 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.
5766
5867 Args:
5968 source: URL or file path to the OpenAPI specification
6069
6170 Returns:
62- OpenAPI: Parsed OpenAPI specification object
71+ tuple: ( OpenAPI object, version) where version is "3.0" or "3.1"
6372
6473 Raises:
6574 FileNotFoundError: If the specified file cannot be found
@@ -70,31 +79,46 @@ def get_open_api(source: Union[str, Path]) -> OpenAPI:
7079 try :
7180 # Handle remote files
7281 if not isinstance (source , Path ) and (
73- source .startswith ("http://" ) or source .startswith ("https://" )
82+ source .startswith ("http://" ) or source .startswith ("https://" )
7483 ):
7584 content = httpx .get (source ).text
7685 # Try JSON first, then YAML for remote files
7786 try :
78- return OpenAPI ( ** orjson .loads (content ) )
87+ data = orjson .loads (content )
7988 except orjson .JSONDecodeError :
80- return OpenAPI ( ** yaml .safe_load (content ) )
81-
82- # Handle local files
83- with open (source , "r" ) as f :
84- file_content = f .read ()
89+ data = yaml .safe_load (content )
90+ else :
91+ # Handle local files
92+ with open (source , "r" ) as f :
93+ file_content = f .read ()
8594
86- # Try JSON first
87- try :
88- return OpenAPI (** orjson .loads (file_content ))
89- except orjson .JSONDecodeError :
90- # If JSON fails, try YAML
95+ # Try JSON first
9196 try :
92- return OpenAPI (** yaml .safe_load (file_content ))
93- except yaml .YAMLError as e :
94- click .echo (
95- f"File { source } is neither a valid JSON nor YAML file: { str (e )} "
96- )
97- 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_30 (data ) # type: ignore[assignment]
113+ elif version == "3.1" :
114+ openapi_obj = parse_openapi_31 (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
98122
99123 except FileNotFoundError :
100124 click .echo (
@@ -105,13 +129,13 @@ def get_open_api(source: Union[str, Path]) -> OpenAPI:
105129 click .echo (f"Could not connect to { source } ." )
106130 raise ConnectError (f"Could not connect to { source } ." ) from None
107131 except ValidationError :
108- click .echo (
109- f"File { source } is not a valid OpenAPI 3.0 specification."
110- )
132+ click .echo (f"File { source } is not a valid OpenAPI 3.0+ specification." )
111133 raise
112134
113135
114- def write_data (data : ConversionResult , output : Union [str , Path ], formatter : Formatter ) -> None :
136+ def write_data (
137+ data : ConversionResult , output : Union [str , Path ], formatter : Formatter
138+ ) -> None :
115139 """
116140 This function will firstly create the folder structure of output, if it doesn't exist. Then it will create the
117141 models from data.models into the models sub module of the output folder. After this, the services will be created
@@ -156,7 +180,7 @@ def write_data(data: ConversionResult, output: Union[str, Path], formatter: Form
156180 files .append (service .file_name )
157181 write_code (
158182 services_path / f"{ service .file_name } .py" ,
159- jinja_env .get_template (SERVICE_TEMPLATE ).render (** service .dict ()),
183+ jinja_env .get_template (SERVICE_TEMPLATE ).render (** service .model_dump ()),
160184 formatter ,
161185 )
162186
@@ -177,26 +201,39 @@ def write_data(data: ConversionResult, output: Union[str, Path], formatter: Form
177201def generate_data (
178202 source : Union [str , Path ],
179203 output : Union [str , Path ],
180- library : Optional [ HTTPLibrary ] = HTTPLibrary .httpx ,
204+ library : HTTPLibrary = HTTPLibrary .httpx ,
181205 env_token_name : Optional [str ] = None ,
182206 use_orjson : bool = False ,
183207 custom_template_path : Optional [str ] = None ,
184208 pydantic_version : PydanticVersion = PydanticVersion .V2 ,
185209 formatter : Formatter = Formatter .BLACK ,
186210) -> None :
187211 """
188- Generate Python code from an OpenAPI 3.0 specification.
212+ Generate Python code from an OpenAPI 3.0+ specification.
189213 """
190- data = get_open_api (source )
191- click .echo (f"Generating data from { source } " )
192-
193- result = generator (
194- data ,
195- library_config_dict [library ],
196- env_token_name ,
197- use_orjson ,
198- custom_template_path ,
199- pydantic_version ,
200- )
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_30 (
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_31 (
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 } " )
201238
202239 write_data (result , output , formatter )
0 commit comments