Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/connexion/resolver.py: 24%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

132 statements  

1""" 

2This module contains resolvers, functions that resolves the user defined view functions 

3from the operations defined in the OpenAPI spec. 

4""" 

5 

6import inspect 

7import logging 

8import typing as t 

9 

10from inflection import camelize 

11 

12import connexion.utils as utils 

13from connexion.exceptions import ResolverError 

14 

15logger = logging.getLogger("connexion.resolver") 

16 

17 

18class Resolution: 

19 def __init__(self, function, operation_id): 

20 """ 

21 Represents the result of operation resolution 

22 

23 :param function: The endpoint function 

24 :type function: types.FunctionType 

25 """ 

26 self.function = function 

27 self.operation_id = operation_id 

28 

29 

30class Resolver: 

31 def __init__(self, function_resolver: t.Callable = utils.get_function_from_name): 

32 """ 

33 Standard resolver 

34 

35 :param function_resolver: Function that resolves functions using an operationId 

36 """ 

37 self.function_resolver = function_resolver 

38 

39 def resolve(self, operation): 

40 """ 

41 Default operation resolver 

42 

43 :type operation: connexion.operations.AbstractOperation 

44 """ 

45 operation_id = self.resolve_operation_id(operation) 

46 return Resolution( 

47 self.resolve_function_from_operation_id(operation_id), operation_id 

48 ) 

49 

50 def resolve_operation_id(self, operation): 

51 """ 

52 Default operationId resolver 

53 

54 :type operation: connexion.operations.AbstractOperation 

55 """ 

56 operation_id = operation.operation_id 

57 router_controller = operation.router_controller 

58 if router_controller is None: 

59 return operation_id 

60 return f"{router_controller}.{operation_id}" 

61 

62 def resolve_function_from_operation_id(self, operation_id): 

63 """ 

64 Invokes the function_resolver 

65 

66 :type operation_id: str 

67 """ 

68 try: 

69 return self.function_resolver(operation_id) 

70 except ImportError as e: 

71 msg = f'Cannot resolve operationId "{operation_id}"! Import error was "{str(e)}"' 

72 raise ResolverError(msg) 

73 except (AttributeError, ValueError) as e: 

74 raise ResolverError(str(e)) 

75 

76 

77class RelativeResolver(Resolver): 

78 """ 

79 Resolves endpoint functions relative to a given root path or module. 

80 """ 

81 

82 def __init__(self, root_path, function_resolver=utils.get_function_from_name): 

83 """ 

84 :param root_path: The root path relative to which an operationId is resolved. 

85 Can also be a module. Has the same effect as setting 

86 `x-swagger-router-controller` or `x-openapi-router-controller` equal to 

87 `root_path` for every operation individually. 

88 :type root_path: typing.Union[str, types.ModuleType] 

89 :param function_resolver: Function that resolves functions using an operationId 

90 :type function_resolver: types.FunctionType 

91 """ 

92 super().__init__(function_resolver=function_resolver) 

93 if inspect.ismodule(root_path): 

94 self.root_path = root_path.__name__ 

95 else: 

96 self.root_path = root_path 

97 

98 def resolve_operation_id(self, operation): 

99 """Resolves the operationId relative to the root path, unless 

100 x-swagger-router-controller or x-openapi-router-controller is specified. 

101 

102 :param operation: The operation to resolve 

103 :type operation: connexion.operations.AbstractOperation 

104 """ 

105 operation_id = operation.operation_id 

106 router_controller = operation.router_controller 

107 if router_controller is None: 

108 return f"{self.root_path}.{operation_id}" 

109 return f"{router_controller}.{operation_id}" 

110 

111 

112class RestyResolver(Resolver): 

113 """ 

114 Resolves endpoint functions using REST semantics (unless overridden by specifying operationId) 

115 """ 

116 

117 def __init__( 

118 self, default_module_name: str, *, collection_endpoint_name: str = "search" 

119 ): 

120 """ 

121 :param default_module_name: Default module name for operations 

122 :param collection_endpoint_name: Name of function to resolve collection endpoints to 

123 """ 

124 super().__init__() 

125 self.default_module_name = default_module_name 

126 self.collection_endpoint_name = collection_endpoint_name 

127 

128 def resolve_operation_id(self, operation): 

129 """ 

130 Resolves the operationId using REST semantics unless explicitly configured in the spec 

131 

132 :type operation: connexion.operations.AbstractOperation 

133 """ 

134 if operation.operation_id: 

135 return super().resolve_operation_id(operation) 

136 

137 return self.resolve_operation_id_using_rest_semantics(operation) 

138 

139 def resolve_operation_id_using_rest_semantics(self, operation): 

140 """ 

141 Resolves the operationId using REST semantics 

142 

143 :type operation: connexion.operations.AbstractOperation 

144 """ 

145 

146 # Split the path into components delimited by '/' 

147 path_components = [c for c in operation.path.split("/") if len(c)] 

148 

149 def is_var(component): 

150 """True if the path component is a var. eg, '{id}'""" 

151 return (component[0] == "{") and (component[-1] == "}") 

152 

153 resource_name = ".".join([c for c in path_components if not is_var(c)]).replace( 

154 "-", "_" 

155 ) 

156 

157 def get_controller_name(): 

158 x_router_controller = operation.router_controller 

159 

160 name = self.default_module_name 

161 

162 if x_router_controller: 

163 name = x_router_controller 

164 

165 elif resource_name: 

166 name += "." + resource_name 

167 

168 return name 

169 

170 def get_function_name(): 

171 method = operation.method 

172 

173 is_collection_endpoint = ( 

174 method.lower() == "get" 

175 and len(resource_name) 

176 and not is_var(path_components[-1]) 

177 ) 

178 

179 return ( 

180 self.collection_endpoint_name 

181 if is_collection_endpoint 

182 else method.lower() 

183 ) 

184 

185 return f"{get_controller_name()}.{get_function_name()}" 

186 

187 

188class MethodResolverBase(RestyResolver): 

189 """ 

190 Resolves endpoint functions based on Flask's MethodView semantics, e.g. 

191 

192 .. code-block:: yaml 

193 

194 paths: 

195 /foo_bar: 

196 get: 

197 # Implied function call: api.FooBarView().get 

198 

199 .. code-block:: python 

200 

201 class FooBarView(MethodView): 

202 def get(self): 

203 return ... 

204 def post(self): 

205 return ... 

206 

207 """ 

208 

209 _class_arguments_type = t.Dict[ 

210 str, t.Dict[str, t.Union[t.Iterable, t.Dict[str, t.Any]]] 

211 ] 

212 

213 def __init__(self, *args, class_arguments: _class_arguments_type = None, **kwargs): 

214 """ 

215 :param args: Arguments passed to :class:`~RestyResolver` 

216 :param class_arguments: Arguments to instantiate the View Class in the format below 

217 :param kwargs: Keywords arguments passed to :class:`~RestyResolver` 

218 

219 .. code-block:: python 

220 

221 { 

222 "ViewName": { 

223 "args": (positional arguments,) 

224 "kwargs": { 

225 "keyword": "argument" 

226 } 

227 } 

228 } 

229 """ 

230 self.class_arguments = class_arguments or {} 

231 super(MethodResolverBase, self).__init__(*args, **kwargs) 

232 self.initialized_views: list = [] 

233 

234 def resolve_operation_id(self, operation): 

235 """ 

236 Resolves the operationId using REST semantics unless explicitly configured in the spec 

237 Once resolved with REST semantics the view_name is capitalised and has 'View' added 

238 to it so it now matches the Class names of the MethodView 

239 

240 :type operation: connexion.operations.AbstractOperation 

241 """ 

242 if operation.operation_id: 

243 # If operation_id is defined then use the higher level API to resolve 

244 return RestyResolver.resolve_operation_id(self, operation) 

245 

246 # Use RestyResolver to get operation_id for us (follow their naming conventions/structure) 

247 operation_id = self.resolve_operation_id_using_rest_semantics(operation) 

248 module_name, view_base, meth_name = operation_id.rsplit(".", 2) 

249 view_name = camelize(view_base) + "View" 

250 

251 return f"{module_name}.{view_name}.{meth_name}" 

252 

253 def resolve_function_from_operation_id(self, operation_id): 

254 """ 

255 Invokes the function_resolver 

256 

257 :type operation_id: str 

258 """ 

259 

260 try: 

261 module_name, view_name, meth_name = operation_id.rsplit(".", 2) 

262 if operation_id and not view_name.endswith("View"): 

263 # If operation_id is not a view then assume it is a standard function 

264 return self.function_resolver(operation_id) 

265 

266 mod = __import__(module_name, fromlist=[view_name]) 

267 view_cls = getattr(mod, view_name) 

268 # find the view and return it 

269 return self.resolve_method_from_class(view_name, meth_name, view_cls) 

270 

271 except ImportError as e: 

272 msg = 'Cannot resolve operationId "{}"! Import error was "{}"'.format( 

273 operation_id, str(e) 

274 ) 

275 raise ResolverError(msg) 

276 except (AttributeError, ValueError) as e: 

277 raise ResolverError(str(e)) 

278 

279 def resolve_method_from_class(self, view_name, meth_name, view_cls): 

280 """ 

281 Returns the view function for the given view class. 

282 """ 

283 raise NotImplementedError() 

284 

285 

286class MethodResolver(MethodResolverBase): 

287 """ 

288 A generic method resolver that instantiates a class and extracts the method 

289 from it, based on the operation id. 

290 """ 

291 

292 def resolve_method_from_class(self, view_name, meth_name, view_cls): 

293 view = None 

294 for v in self.initialized_views: 

295 if v.__class__ == view_cls: 

296 view = v 

297 break 

298 if view is None: 

299 # get the args and kwargs for this view 

300 cls_arguments = self.class_arguments.get(view_name, {}) 

301 cls_args = cls_arguments.get("args", ()) 

302 cls_kwargs = cls_arguments.get("kwargs", {}) 

303 # instantiate the class with the args and kwargs 

304 view = view_cls(*cls_args, **cls_kwargs) 

305 self.initialized_views.append(view) 

306 # get the method if the class 

307 func = getattr(view, meth_name) 

308 # Return the method function of the class 

309 return func 

310 

311 

312class MethodViewResolver(MethodResolverBase): 

313 """ 

314 A specialized method resolver that works with flask's method views. 

315 It resolves the method by calling as_view on the class. 

316 """ 

317 

318 def __init__(self, *args, **kwargs): 

319 if "collection_endpoint_name" in kwargs: 

320 del kwargs["collection_endpoint_name"] 

321 # Dispatch of request is done by Flask 

322 logger.warning( 

323 "collection_endpoint_name is ignored by the MethodViewResolver. " 

324 "Requests to a collection endpoint will be routed to .get()" 

325 ) 

326 super().__init__(*args, **kwargs) 

327 

328 def resolve_method_from_class(self, view_name, meth_name, view_cls): 

329 view = None 

330 for v in self.initialized_views: 

331 # views returned by <class>.as_view 

332 # have the origin class attached as .view_class 

333 if v.view_class == view_cls: 

334 view = v 

335 break 

336 if view is None: 

337 # get the args and kwargs for this view 

338 cls_arguments = self.class_arguments.get(view_name, {}) 

339 cls_args = cls_arguments.get("args", ()) 

340 cls_kwargs = cls_arguments.get("kwargs", {}) 

341 # call as_view to get a view function 

342 # that is decorated with the classes 

343 # decorator list, if any 

344 view = view_cls.as_view(view_name, *cls_args, **cls_kwargs) 

345 # add the view to the list of initialized views 

346 # in order to call as_view only once 

347 self.initialized_views.append(view) 

348 # return the class as view function 

349 # for each operation so that requests 

350 # are dispatched with <class>.dispatch_request, 

351 # when calling the view function 

352 return view