abstract.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. """
  2. This module defines an AbstractOperation class which implements an abstract Operation interface
  3. and functionality shared between Swagger 2 and OpenAPI 3 specifications.
  4. """
  5. import abc
  6. import logging
  7. from connexion.operations.secure import SecureOperation
  8. from ..decorators.metrics import UWSGIMetricsCollector
  9. from ..decorators.parameter import parameter_to_arg
  10. from ..decorators.produces import BaseSerializer, Produces
  11. from ..decorators.response import ResponseValidator
  12. from ..decorators.validation import ParameterValidator, RequestBodyValidator
  13. from ..utils import all_json, is_nullable
  14. logger = logging.getLogger('connexion.operations.abstract')
  15. DEFAULT_MIMETYPE = 'application/json'
  16. VALIDATOR_MAP = {
  17. 'parameter': ParameterValidator,
  18. 'body': RequestBodyValidator,
  19. 'response': ResponseValidator,
  20. }
  21. class AbstractOperation(SecureOperation, metaclass=abc.ABCMeta):
  22. """
  23. An API routes requests to an Operation by a (path, method) pair.
  24. The operation uses a resolver to resolve its handler function.
  25. We use the provided spec to do a bunch of heavy lifting before
  26. (and after) we call security_schemes handler.
  27. The registered handler function ends up looking something like::
  28. @secure_endpoint
  29. @validate_inputs
  30. @deserialize_function_inputs
  31. @serialize_function_outputs
  32. @validate_outputs
  33. def user_provided_handler_function(important, stuff):
  34. if important:
  35. serious_business(stuff)
  36. """
  37. def __init__(self, api, method, path, operation, resolver,
  38. app_security=None, security_schemes=None,
  39. validate_responses=False, strict_validation=False,
  40. randomize_endpoint=None, validator_map=None,
  41. pythonic_params=False, uri_parser_class=None,
  42. pass_context_arg_name=None):
  43. """
  44. :param api: api that this operation is attached to
  45. :type api: apis.AbstractAPI
  46. :param method: HTTP method
  47. :type method: str
  48. :param path:
  49. :type path: str
  50. :param operation: swagger operation object
  51. :type operation: dict
  52. :param resolver: Callable that maps operationID to a function
  53. :param app_produces: list of content types the application can return by default
  54. :param app_security: list of security rules the application uses by default
  55. :type app_security: list
  56. :param security_schemes: `Security Definitions Object
  57. <https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object>`_
  58. :type security_schemes: dict
  59. :param validate_responses: True enables validation. Validation errors generate HTTP 500 responses.
  60. :type validate_responses: bool
  61. :param strict_validation: True enables validation on invalid request parameters
  62. :type strict_validation: bool
  63. :param randomize_endpoint: number of random characters to append to operation name
  64. :type randomize_endpoint: integer
  65. :param validator_map: Custom validators for the types "parameter", "body" and "response".
  66. :type validator_map: dict
  67. :param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended
  68. to any shadowed built-ins
  69. :type pythonic_params: bool
  70. :param uri_parser_class: class to use for uri parsing
  71. :type uri_parser_class: AbstractURIParser
  72. :param pass_context_arg_name: If not None will try to inject the request context to the function using this
  73. name.
  74. :type pass_context_arg_name: str|None
  75. """
  76. self._api = api
  77. self._method = method
  78. self._path = path
  79. self._operation = operation
  80. self._resolver = resolver
  81. self._security = app_security
  82. self._security_schemes = security_schemes
  83. self._validate_responses = validate_responses
  84. self._strict_validation = strict_validation
  85. self._pythonic_params = pythonic_params
  86. self._uri_parser_class = uri_parser_class
  87. self._pass_context_arg_name = pass_context_arg_name
  88. self._randomize_endpoint = randomize_endpoint
  89. self._operation_id = self._operation.get("operationId")
  90. self._resolution = resolver.resolve(self)
  91. self._operation_id = self._resolution.operation_id
  92. self._responses = self._operation.get("responses", {})
  93. self._validator_map = dict(VALIDATOR_MAP)
  94. self._validator_map.update(validator_map or {})
  95. @property
  96. def method(self):
  97. """
  98. The HTTP method for this operation (ex. GET, POST)
  99. """
  100. return self._method
  101. @property
  102. def path(self):
  103. """
  104. The path of the operation, relative to the API base path
  105. """
  106. return self._path
  107. @property
  108. def responses(self):
  109. """
  110. Returns the responses for this operation
  111. """
  112. return self._responses
  113. @property
  114. def validator_map(self):
  115. """
  116. Validators to use for parameter, body, and response validation
  117. """
  118. return self._validator_map
  119. @property
  120. def operation_id(self):
  121. """
  122. The operation id used to identify the operation internally to the app
  123. """
  124. return self._operation_id
  125. @property
  126. def randomize_endpoint(self):
  127. """
  128. number of random digits to generate and append to the operation_id.
  129. """
  130. return self._randomize_endpoint
  131. @property
  132. def router_controller(self):
  133. """
  134. The router controller to use (python module where handler functions live)
  135. """
  136. return self._router_controller
  137. @property
  138. def strict_validation(self):
  139. """
  140. If True, validate all requests against the spec
  141. """
  142. return self._strict_validation
  143. @property
  144. def pythonic_params(self):
  145. """
  146. If True, convert CamelCase into pythonic_variable_names
  147. """
  148. return self._pythonic_params
  149. @property
  150. def validate_responses(self):
  151. """
  152. If True, check the response against the response schema, and return an
  153. error if the response does not validate.
  154. """
  155. return self._validate_responses
  156. @staticmethod
  157. def _get_file_arguments(files, arguments, has_kwargs=False):
  158. return {k: v for k, v in files.items() if k in arguments or has_kwargs}
  159. @abc.abstractmethod
  160. def _get_val_from_param(self, value, query_defn):
  161. """
  162. Convert input parameters into the correct type
  163. """
  164. def _query_args_helper(self, query_defns, query_arguments,
  165. function_arguments, has_kwargs, sanitize):
  166. res = {}
  167. for key, value in query_arguments.items():
  168. sanitized_key = sanitize(key)
  169. if not has_kwargs and sanitized_key not in function_arguments:
  170. logger.debug("Query Parameter '%s' (sanitized: '%s') not in function arguments",
  171. key, sanitized_key)
  172. else:
  173. logger.debug("Query Parameter '%s' (sanitized: '%s') in function arguments",
  174. key, sanitized_key)
  175. try:
  176. query_defn = query_defns[key]
  177. except KeyError: # pragma: no cover
  178. logger.error("Function argument '%s' (non-sanitized: %s) not defined in specification",
  179. sanitized_key, key)
  180. else:
  181. logger.debug('%s is a %s', key, query_defn)
  182. res.update({sanitized_key: self._get_val_from_param(value, query_defn)})
  183. return res
  184. @abc.abstractmethod
  185. def _get_query_arguments(self, query, arguments, has_kwargs, sanitize):
  186. """
  187. extract handler function arguments from the query parameters
  188. """
  189. @abc.abstractmethod
  190. def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
  191. """
  192. extract handler function arguments from the request body
  193. """
  194. def _get_path_arguments(self, path_params, sanitize):
  195. """
  196. extract handler function arguments from path parameters
  197. """
  198. kwargs = {}
  199. path_defns = {p["name"]: p for p in self.parameters if p["in"] == "path"}
  200. for key, value in path_params.items():
  201. sanitized_key = sanitize(key)
  202. if key in path_defns:
  203. kwargs[sanitized_key] = self._get_val_from_param(value, path_defns[key])
  204. else: # Assume path params mechanism used for injection
  205. kwargs[sanitized_key] = value
  206. return kwargs
  207. @property
  208. @abc.abstractmethod
  209. def parameters(self):
  210. """
  211. Returns the parameters for this operation
  212. """
  213. @property
  214. @abc.abstractmethod
  215. def produces(self):
  216. """
  217. Content-Types that the operation produces
  218. """
  219. @property
  220. @abc.abstractmethod
  221. def consumes(self):
  222. """
  223. Content-Types that the operation consumes
  224. """
  225. @property
  226. @abc.abstractmethod
  227. def body_schema(self):
  228. """
  229. The body schema definition for this operation.
  230. """
  231. @property
  232. @abc.abstractmethod
  233. def body_definition(self):
  234. """
  235. The body definition for this operation.
  236. :rtype: dict
  237. """
  238. def get_arguments(self, path_params, query_params, body, files, arguments,
  239. has_kwargs, sanitize):
  240. """
  241. get arguments for handler function
  242. """
  243. ret = {}
  244. ret.update(self._get_path_arguments(path_params, sanitize))
  245. ret.update(self._get_query_arguments(query_params, arguments,
  246. has_kwargs, sanitize))
  247. if self.method.upper() in ["PATCH", "POST", "PUT"]:
  248. ret.update(self._get_body_argument(body, arguments,
  249. has_kwargs, sanitize))
  250. ret.update(self._get_file_arguments(files, arguments, has_kwargs))
  251. return ret
  252. def response_definition(self, status_code=None,
  253. content_type=None):
  254. """
  255. response definition for this endpoint
  256. """
  257. content_type = content_type or self.get_mimetype()
  258. response_definition = self.responses.get(
  259. str(status_code),
  260. self.responses.get("default", {})
  261. )
  262. return response_definition
  263. @abc.abstractmethod
  264. def response_schema(self, status_code=None, content_type=None):
  265. """
  266. response schema for this endpoint
  267. """
  268. @abc.abstractmethod
  269. def example_response(self, status_code=None, content_type=None):
  270. """
  271. Returns an example from the spec
  272. """
  273. @abc.abstractmethod
  274. def get_path_parameter_types(self):
  275. """
  276. Returns the types for parameters in the path
  277. """
  278. @abc.abstractmethod
  279. def with_definitions(self, schema):
  280. """
  281. Returns the given schema, but with the definitions from the spec
  282. attached. This allows any remaining references to be resolved by a
  283. validator (for example).
  284. """
  285. def get_mimetype(self):
  286. """
  287. If the endpoint has no 'produces' then the default is
  288. 'application/json'.
  289. :rtype str
  290. """
  291. if all_json(self.produces):
  292. try:
  293. return self.produces[0]
  294. except IndexError:
  295. return DEFAULT_MIMETYPE
  296. elif len(self.produces) == 1:
  297. return self.produces[0]
  298. else:
  299. return DEFAULT_MIMETYPE
  300. @property
  301. def _uri_parsing_decorator(self):
  302. """
  303. Returns a decorator that parses request data and handles things like
  304. array types, and duplicate parameter definitions.
  305. """
  306. return self._uri_parser_class(self.parameters, self.body_definition)
  307. @property
  308. def function(self):
  309. """
  310. Operation function with decorators
  311. :rtype: types.FunctionType
  312. """
  313. function = parameter_to_arg(
  314. self, self._resolution.function, self.pythonic_params,
  315. self._pass_context_arg_name
  316. )
  317. if self.validate_responses:
  318. logger.debug('... Response validation enabled.')
  319. response_decorator = self.__response_validation_decorator
  320. logger.debug('... Adding response decorator (%r)', response_decorator)
  321. function = response_decorator(function)
  322. produces_decorator = self.__content_type_decorator
  323. logger.debug('... Adding produces decorator (%r)', produces_decorator)
  324. function = produces_decorator(function)
  325. for validation_decorator in self.__validation_decorators:
  326. function = validation_decorator(function)
  327. uri_parsing_decorator = self._uri_parsing_decorator
  328. function = uri_parsing_decorator(function)
  329. # NOTE: the security decorator should be applied last to check auth before anything else :-)
  330. security_decorator = self.security_decorator
  331. logger.debug('... Adding security decorator (%r)', security_decorator)
  332. function = security_decorator(function)
  333. function = self._request_response_decorator(function)
  334. if UWSGIMetricsCollector.is_available(): # pragma: no cover
  335. decorator = UWSGIMetricsCollector(self.path, self.method)
  336. function = decorator(function)
  337. return function
  338. @property
  339. def __content_type_decorator(self):
  340. """
  341. Get produces decorator.
  342. If the operation mimetype format is json then the function return value is jsonified
  343. From Swagger Specification:
  344. **Produces**
  345. A list of MIME types the operation can produce. This overrides the produces definition at the Swagger Object.
  346. An empty value MAY be used to clear the global definition.
  347. :rtype: types.FunctionType
  348. """
  349. logger.debug('... Produces: %s', self.produces, extra=vars(self))
  350. mimetype = self.get_mimetype()
  351. if all_json(self.produces): # endpoint will return json
  352. logger.debug('... Produces json', extra=vars(self))
  353. # TODO: Refactor this.
  354. return lambda f: f
  355. elif len(self.produces) == 1:
  356. logger.debug('... Produces %s', mimetype, extra=vars(self))
  357. decorator = Produces(mimetype)
  358. return decorator
  359. else:
  360. return BaseSerializer()
  361. @property
  362. def __validation_decorators(self):
  363. """
  364. :rtype: types.FunctionType
  365. """
  366. ParameterValidator = self.validator_map['parameter']
  367. RequestBodyValidator = self.validator_map['body']
  368. if self.parameters:
  369. yield ParameterValidator(self.parameters,
  370. self.api,
  371. strict_validation=self.strict_validation)
  372. if self.body_schema:
  373. yield RequestBodyValidator(self.body_schema, self.consumes, self.api,
  374. is_nullable(self.body_definition),
  375. strict_validation=self.strict_validation)
  376. @property
  377. def __response_validation_decorator(self):
  378. """
  379. Get a decorator for validating the generated Response.
  380. :rtype: types.FunctionType
  381. """
  382. ResponseValidator = self.validator_map['response']
  383. return ResponseValidator(self, self.get_mimetype())
  384. def json_loads(self, data):
  385. """
  386. A wrapper for calling the API specific JSON loader.
  387. :param data: The JSON data in textual form.
  388. :type data: bytes
  389. """
  390. return self.api.json_loads(data)