11from pathlib import Path
2- from typing import List , Optional , Union
2+ from typing import Any , List , Optional , Sequence , Union
33
44import black
55import click
@@ -53,14 +53,54 @@ def format_using_black(content: str) -> str:
5353 return isort .code (formatted_contend , line_length = FormatOptions .line_length )
5454
5555
56- def get_open_api (source : Union [str , Path ]):
56+ def load_openapi_data (source : Union [str , Path ]) -> dict [str , Any ]:
57+ """
58+ Load an OpenAPI JSON/YAML document from a URL or local file path.
59+ """
60+ if not isinstance (source , Path ) and (source .startswith ("http://" ) or source .startswith ("https://" )):
61+ content = httpx .get (source ).text
62+ else :
63+ with open (source , "r" ) as f :
64+ content = f .read ()
65+
66+ try :
67+ data = orjson .loads (content )
68+ except orjson .JSONDecodeError :
69+ try :
70+ data = yaml .safe_load (content )
71+ except yaml .YAMLError as e :
72+ click .echo (f"File { source } is neither a valid JSON nor YAML file: { str (e )} " )
73+ raise
74+
75+ if not isinstance (data , dict ):
76+ raise ValueError (f"OpenAPI data loaded from { source } must be a JSON/YAML object." )
77+
78+ return data
79+
80+
81+ def deep_merge (base : dict [str , Any ], overlay : dict [str , Any ]) -> dict [str , Any ]:
82+ """
83+ Recursively merge two dictionaries. Overlay lists and scalar values replace base values.
84+ """
85+ result = base .copy ()
86+ for key , overlay_value in overlay .items ():
87+ base_value = result .get (key )
88+ if isinstance (base_value , dict ) and isinstance (overlay_value , dict ):
89+ result [key ] = deep_merge (base_value , overlay_value )
90+ else :
91+ result [key ] = overlay_value
92+ return result
93+
94+
95+ def get_open_api (source : Union [str , Path ], overlay_paths : Optional [Sequence [Union [str , Path ]]] = None ):
5796 """
5897 Tries to fetch the openapi specification file from the web or load from a local file.
5998 Supports both JSON and YAML formats. Returns the according OpenAPI object.
6099 Automatically supports OpenAPI 3.0 and 3.1 specifications with intelligent version detection.
61100
62101 Args:
63102 source: URL or file path to the OpenAPI specification
103+ overlay_paths: Optional JSON/YAML overlay files to deep-merge into source in order
64104
65105 Returns:
66106 tuple: (OpenAPI object, version) where version is "3.0" or "3.1"
@@ -72,29 +112,9 @@ def get_open_api(source: Union[str, Path]):
72112 JSONDecodeError/YAMLError: If the file cannot be parsed
73113 """
74114 try :
75- # Handle remote files
76- if not isinstance (source , Path ) and (source .startswith ("http://" ) or source .startswith ("https://" )):
77- content = httpx .get (source ).text
78- # Try JSON first, then YAML for remote files
79- try :
80- data = orjson .loads (content )
81- except orjson .JSONDecodeError :
82- data = yaml .safe_load (content )
83- else :
84- # Handle local files
85- with open (source , "r" ) as f :
86- file_content = f .read ()
87-
88- # Try JSON first
89- try :
90- data = orjson .loads (file_content )
91- except orjson .JSONDecodeError :
92- # If JSON fails, try YAML
93- try :
94- data = yaml .safe_load (file_content )
95- except yaml .YAMLError as e :
96- click .echo (f"File { source } is neither a valid JSON nor YAML file: { str (e )} " )
97- raise
115+ data = load_openapi_data (source )
116+ for overlay_path in overlay_paths or ():
117+ data = deep_merge (data , load_openapi_data (overlay_path ))
98118
99119 # Detect version and parse with appropriate parser
100120 version = detect_openapi_version (data )
@@ -206,12 +226,13 @@ def generate_data(
206226 pydantic_version : PydanticVersion = PydanticVersion .V2 ,
207227 formatter : Formatter = Formatter .BLACK ,
208228 config_path : Optional [Union [str , Path ]] = None ,
229+ overlay_paths : Optional [Sequence [Union [str , Path ]]] = None ,
209230 config : Optional [PydanticOpenAPIGeneratorConfig ] = None ,
210231) -> None :
211232 """
212233 Generate Python code from an OpenAPI 3.0+ specification.
213234 """
214- openapi_obj , version = get_open_api (source )
235+ openapi_obj , version = get_open_api (source , overlay_paths )
215236 loaded_config = config if config is not None else load_config (config_path )
216237 click .echo (f"Generating data from { source } (OpenAPI { version } )" )
217238
0 commit comments