Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/connexion/middleware/abstract.py: 36%

128 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:12 +0000

1import abc 

2import logging 

3import pathlib 

4import typing as t 

5 

6from starlette.types import ASGIApp, Receive, Scope, Send 

7 

8from connexion.exceptions import MissingMiddleware, ResolverError 

9from connexion.http_facts import METHODS 

10from connexion.operations import AbstractOperation 

11from connexion.resolver import Resolver 

12from connexion.spec import Specification 

13 

14logger = logging.getLogger(__name__) 

15 

16ROUTING_CONTEXT = "connexion_routing" 

17 

18 

19class SpecMiddleware(abc.ABC): 

20 """Middlewares that need the specification(s) to be registered on them should inherit from this 

21 base class""" 

22 

23 @abc.abstractmethod 

24 def add_api( 

25 self, specification: t.Union[pathlib.Path, str, dict], **kwargs 

26 ) -> t.Any: 

27 """ 

28 Register an API represented by a single OpenAPI specification on this middleware. 

29 Multiple APIs can be registered on a single middleware. 

30 """ 

31 

32 @abc.abstractmethod 

33 async def __call__(self, scope: Scope, receive: Receive, send: Send): 

34 pass 

35 

36 

37class AbstractSpecAPI: 

38 """Base API class with only minimal behavior related to the specification.""" 

39 

40 def __init__( 

41 self, 

42 specification: t.Union[pathlib.Path, str, dict], 

43 base_path: t.Optional[str] = None, 

44 resolver: t.Optional[Resolver] = None, 

45 arguments: t.Optional[dict] = None, 

46 uri_parser_class=None, 

47 *args, 

48 **kwargs, 

49 ): 

50 self.specification = Specification.load(specification, arguments=arguments) 

51 self.uri_parser_class = uri_parser_class 

52 

53 self._set_base_path(base_path) 

54 

55 self.resolver = resolver or Resolver() 

56 

57 def _set_base_path(self, base_path: t.Optional[str] = None) -> None: 

58 if base_path is not None: 

59 # update spec to include user-provided base_path 

60 self.specification.base_path = base_path 

61 self.base_path = base_path 

62 else: 

63 self.base_path = self.specification.base_path 

64 

65 

66OP = t.TypeVar("OP") 

67"""Typevar representing an operation""" 

68 

69 

70class AbstractRoutingAPI(AbstractSpecAPI, t.Generic[OP]): 

71 """Base API class with shared functionality related to routing.""" 

72 

73 def __init__( 

74 self, 

75 *args, 

76 pythonic_params=False, 

77 resolver_error_handler: t.Optional[t.Callable] = None, 

78 **kwargs, 

79 ) -> None: 

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

81 self.pythonic_params = pythonic_params 

82 self.resolver_error_handler = resolver_error_handler 

83 

84 self.add_paths() 

85 

86 def add_paths(self, paths: t.Optional[dict] = None) -> None: 

87 """ 

88 Adds the paths defined in the specification as operations. 

89 """ 

90 paths = paths or self.specification.get("paths", dict()) 

91 for path, methods in paths.items(): 

92 logger.debug("Adding %s%s...", self.base_path, path) 

93 

94 for method in methods: 

95 if method not in METHODS: 

96 continue 

97 try: 

98 self.add_operation(path, method) 

99 except ResolverError as err: 

100 # If we have an error handler for resolver errors, add it as an operation. 

101 # Otherwise treat it as any other error. 

102 if self.resolver_error_handler is not None: 

103 self._add_resolver_error_handler(method, path, err) 

104 else: 

105 self._handle_add_operation_error(path, method, err) 

106 except Exception as e: 

107 # All other relevant exceptions should be handled as well. 

108 self._handle_add_operation_error(path, method, e) 

109 

110 def add_operation(self, path: str, method: str) -> None: 

111 """ 

112 Adds one operation to the api. 

113 

114 This method uses the OperationID identify the module and function that will handle the operation 

115 

116 From Swagger Specification: 

117 

118 **OperationID** 

119 

120 A friendly name for the operation. The id MUST be unique among all operations described in the API. 

121 Tools and libraries MAY use the operation id to uniquely identify an operation. 

122 """ 

123 spec_operation_cls = self.specification.operation_cls 

124 spec_operation = spec_operation_cls.from_spec( 

125 self.specification, 

126 path=path, 

127 method=method, 

128 resolver=self.resolver, 

129 uri_parser_class=self.uri_parser_class, 

130 ) 

131 operation = self.make_operation(spec_operation) 

132 path, name = self._framework_path_and_name(spec_operation, path) 

133 self._add_operation_internal(method, path, operation, name=name) 

134 

135 @abc.abstractmethod 

136 def make_operation(self, operation: AbstractOperation) -> OP: 

137 """Build an operation to register on the API.""" 

138 

139 @staticmethod 

140 def _framework_path_and_name( 

141 operation: AbstractOperation, path: str 

142 ) -> t.Tuple[str, str]: 

143 """Prepare the framework path & name to register the operation on the API.""" 

144 

145 @abc.abstractmethod 

146 def _add_operation_internal( 

147 self, method: str, path: str, operation: OP, name: str = None 

148 ) -> None: 

149 """ 

150 Adds the operation according to the user framework in use. 

151 It will be used to register the operation on the user framework router. 

152 """ 

153 

154 def _add_resolver_error_handler( 

155 self, method: str, path: str, err: ResolverError 

156 ) -> None: 

157 """ 

158 Adds a handler for ResolverError for the given method and path. 

159 """ 

160 self.resolver_error_handler = t.cast(t.Callable, self.resolver_error_handler) 

161 operation = self.resolver_error_handler( 

162 err, 

163 ) 

164 self._add_operation_internal(method, path, operation) 

165 

166 def _handle_add_operation_error( 

167 self, path: str, method: str, exc: Exception 

168 ) -> None: 

169 url = f"{self.base_path}{path}" 

170 error_msg = f"Failed to add operation for {method.upper()} {url}" 

171 logger.error(error_msg) 

172 raise exc from None 

173 

174 

175class RoutedAPI(AbstractSpecAPI, t.Generic[OP]): 

176 def __init__( 

177 self, 

178 specification: t.Union[pathlib.Path, str, dict], 

179 *args, 

180 next_app: ASGIApp, 

181 **kwargs, 

182 ) -> None: 

183 super().__init__(specification, *args, **kwargs) 

184 self.next_app = next_app 

185 self.operations: t.MutableMapping[str, OP] = {} 

186 

187 def add_paths(self) -> None: 

188 paths = self.specification.get("paths", {}) 

189 for path, methods in paths.items(): 

190 for method in methods: 

191 if method not in METHODS: 

192 continue 

193 try: 

194 self.add_operation(path, method) 

195 except ResolverError: 

196 # ResolverErrors are either raised or handled in routing middleware. 

197 pass 

198 

199 def add_operation(self, path: str, method: str) -> None: 

200 operation_spec_cls = self.specification.operation_cls 

201 operation = operation_spec_cls.from_spec( 

202 self.specification, 

203 path=path, 

204 method=method, 

205 resolver=self.resolver, 

206 uri_parser_class=self.uri_parser_class, 

207 ) 

208 routed_operation = self.make_operation(operation) 

209 self.operations[operation.operation_id] = routed_operation 

210 

211 @abc.abstractmethod 

212 def make_operation(self, operation: AbstractOperation) -> OP: 

213 """Create an operation of the `operation_cls` type.""" 

214 raise NotImplementedError 

215 

216 

217API = t.TypeVar("API", bound="RoutedAPI") 

218"""Typevar representing an API which subclasses RoutedAPI""" 

219 

220 

221class RoutedMiddleware(SpecMiddleware, t.Generic[API]): 

222 """Baseclass for middleware that wants to leverage the RoutingMiddleware to route requests to 

223 its operations. 

224 

225 The RoutingMiddleware adds the operation_id to the ASGI scope. This middleware registers its 

226 operations by operation_id at startup. At request time, the operation is fetched by an 

227 operation_id lookup. 

228 """ 

229 

230 api_cls: t.Type[API] 

231 """The subclass of RoutedAPI this middleware uses.""" 

232 

233 def __init__(self, app: ASGIApp, **kwargs) -> None: 

234 self.app = app 

235 self.apis: t.Dict[str, API] = {} 

236 

237 def add_api(self, specification: t.Union[pathlib.Path, str, dict], **kwargs) -> API: 

238 api = self.api_cls(specification, next_app=self.app, **kwargs) 

239 self.apis[api.base_path] = api 

240 return api 

241 

242 async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: 

243 """Fetches the operation related to the request and calls it.""" 

244 if scope["type"] != "http": 

245 await self.app(scope, receive, send) 

246 return 

247 

248 try: 

249 connexion_context = scope["extensions"][ROUTING_CONTEXT] 

250 except KeyError: 

251 raise MissingMiddleware( 

252 "Could not find routing information in scope. Please make sure " 

253 "you have a routing middleware registered upstream. " 

254 ) 

255 api_base_path = connexion_context.get("api_base_path") 

256 if api_base_path is not None and api_base_path in self.apis: 

257 api = self.apis[api_base_path] 

258 operation_id = connexion_context.get("operation_id") 

259 try: 

260 operation = api.operations[operation_id] 

261 except KeyError as e: 

262 if operation_id is None: 

263 logger.debug("Skipping operation without id.") 

264 await self.app(scope, receive, send) 

265 return 

266 else: 

267 raise MissingOperation("Encountered unknown operation_id.") from e 

268 else: 

269 return await operation(scope, receive, send) 

270 

271 await self.app(scope, receive, send) 

272 

273 

274class MissingOperation(Exception): 

275 """Missing operation"""