@@ -37,6 +37,38 @@ def load_openapi(
3737 return lambda : openapi_json
3838
3939
40+ def _validate_target_module (
41+ target_module : Union [list [str ], str , None ],
42+ target_obj : FastAPI | Callable ,
43+ ) -> list [str ]:
44+ if target_module is not None :
45+ return [target_module ] if isinstance (target_module , str ) else target_module
46+
47+ if isinstance (target_obj , FastAPI ):
48+ target_module = _find_module_name_where_app_defined_in (target_obj )
49+ else :
50+ target_module = _find_module_name_where_func_defined_in (target_obj )
51+
52+ if target_module is not None :
53+ target_module = target_module .split ("." )[0 ] # get the top-level module
54+
55+ if target_module is None or target_module == "__main__" :
56+ if isinstance (target_obj , FastAPI ):
57+ raise BaseRichAPIException (
58+ "Could not determine the module where the FastAPI instance was created.\n "
59+ "Please provide the module name as a string or list of strings.\n "
60+ "Example: enrich_openapi(app, target_module='src')\n "
61+ )
62+ else :
63+ raise BaseRichAPIException (
64+ "Could not determine the module where the function was created.\n "
65+ "Please provide the module name as a string or list of strings.\n "
66+ "Example: find_exceptions_in_api_functions([func], target_module='src')\n "
67+ )
68+
69+ return [target_module ]
70+
71+
4072def enrich_openapi (
4173 app : FastAPI ,
4274 target_module : Union [list [str ], str , None ] = None ,
@@ -47,16 +79,7 @@ def enrich_openapi(
4779 routes = app .routes ,
4880 ),
4981) -> Callable :
50- if target_module is None :
51- target_module = _find_module_name_where_app_defined_in (app )
52- if target_module is None or target_module == "__main__" :
53- raise BaseRichAPIException (
54- "Could not determine the module where the FastAPI instance was created.\n "
55- "Please provide the module name as a string or list of strings.\n "
56- "Example: enrich_openapi(app, target_module='src')\n "
57- )
58-
59- target_module = target_module .split ("." )[0 ] # get the top-level module
82+ target_module = _validate_target_module (target_module , app )
6083
6184 def _custom_openapi () -> dict :
6285 if app .openapi_schema : # pragma: no cover
@@ -72,7 +95,7 @@ def _custom_openapi() -> dict:
7295
7396def compile_openapi_from_fastapi (
7497 app : FastAPI ,
75- target_module : Union [ list [str ], str ] ,
98+ target_module : list [str ] | str ,
7699 open_api_getter : Callable [[FastAPI ], dict ] = lambda app : _get_openapi (
77100 title = app .title ,
78101 version = app .version ,
@@ -81,22 +104,58 @@ def compile_openapi_from_fastapi(
81104 ),
82105) -> dict :
83106 target_module = [target_module ] if isinstance (target_module , str ) else target_module
84- target_module . append ( "fastapi" )
107+
85108 openapi_schema = open_api_getter (app )
86109 for route in app .routes :
87110 if not isinstance (route , APIRoute ):
88111 continue
89112
90113 if route .include_in_schema :
91- exceptions = _extract_starlette_exceptions (route , target_module )
92-
114+ exceptions = _extract_starlette_exceptions (
115+ route , target_module + ["fastapi" ]
116+ )
93117 _fill_openapi_with_excpetions (openapi_schema , route , exceptions )
94118
95119 ExceptionFinder .clear_cache ()
96120
97121 return openapi_schema
98122
99123
124+ def _find_fastapi_route_from_callback (
125+ app : FastAPI , callback : Callable
126+ ) -> Union [APIRoute , None ]:
127+ for route in app .routes :
128+ if not isinstance (route , APIRoute ):
129+ continue
130+ if route .endpoint is callback :
131+ return route
132+
133+ return None
134+
135+
136+ def find_exceptions_in_api_functions (
137+ funcs : list [Callable ],
138+ fastapi_app : FastAPI ,
139+ target_module : str | list [str ] | None = None ,
140+ ) -> set [type [StarletteHTTPException ]]:
141+ result : set [type [StarletteHTTPException ]] = set ()
142+ for func in funcs :
143+ this_func_target_module = _validate_target_module (target_module , func )
144+ this_route = _find_fastapi_route_from_callback (fastapi_app , func )
145+ if this_route is None :
146+ raise BaseRichAPIException (
147+ f"Could not find the route for function { func } in FastAPI app { fastapi_app } "
148+ )
149+
150+ exceptions = _extract_starlette_exceptions (
151+ this_route , this_func_target_module + ["fastapi" ]
152+ )
153+ for exc , _ in exceptions :
154+ result .add (exc )
155+
156+ return result
157+
158+
100159def _find_module_name_where_app_defined_in (app : FastAPI ) -> Union [str , None ]:
101160 frame = inspect .currentframe ()
102161 target_module = None
@@ -112,6 +171,21 @@ def _find_module_name_where_app_defined_in(app: FastAPI) -> Union[str, None]:
112171 return target_module
113172
114173
174+ def _find_module_name_where_func_defined_in (func : Callable ) -> Union [str , None ]:
175+ frame = inspect .currentframe ()
176+ target_module = None
177+ while frame :
178+ for var_name , var_value in frame .f_globals .items ():
179+ if isinstance (var_value , Callable ) and var_value is func :
180+ target_module = frame .f_globals ["__name__" ]
181+ break
182+ if target_module :
183+ break
184+ frame = frame .f_back
185+
186+ return target_module
187+
188+
115189def _resolve_status_and_detail_from_exc_type (
116190 exc_type : type [Exception ],
117191 ast_raise : ast .Raise ,
@@ -142,7 +216,7 @@ def _resolve_status_and_detail_from_exc_type(
142216 status_code_value .value ,
143217 (int , str , NoneType ),
144218 ):
145- raise ValueError (
219+ raise BaseRichAPIException (
146220 f"Status code value must be an integer, string or None, got { type (status_code_value .value )} "
147221 )
148222 if status_code_value .value is not None :
@@ -166,7 +240,7 @@ def _resolve_status_and_detail_from_exc_type(
166240 detail_value = kwarg .value
167241 if isinstance (detail_value , ast .Constant ):
168242 if not isinstance (detail_value .value , (str , NoneType )):
169- raise ValueError (
243+ raise BaseRichAPIException (
170244 f"Detail value must be a string or None, got { type (detail_value .value )} "
171245 )
172246
0 commit comments