swagger2.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. """
  2. This module defines a Swagger2Operation class, a Connexion operation specific for Swagger 2 specs.
  3. """
  4. import logging
  5. from copy import deepcopy
  6. from connexion.operations.abstract import AbstractOperation
  7. from ..decorators.uri_parsing import Swagger2URIParser
  8. from ..exceptions import InvalidSpecification
  9. from ..utils import deep_get, is_null, is_nullable, make_type
  10. logger = logging.getLogger("connexion.operations.swagger2")
  11. class Swagger2Operation(AbstractOperation):
  12. """
  13. Exposes a Swagger 2.0 operation under the AbstractOperation interface.
  14. The primary purpose of this class is to provide the `function()` method
  15. to the API. A Swagger2Operation is plugged into the API with the provided
  16. (path, method) pair. It resolves the handler function for this operation
  17. with the provided resolver, and wraps the handler function with multiple
  18. decorators that provide security, validation, serialization,
  19. and deserialization.
  20. """
  21. def __init__(self, api, method, path, operation, resolver, app_produces, app_consumes,
  22. path_parameters=None, app_security=None, security_definitions=None,
  23. definitions=None, parameter_definitions=None,
  24. response_definitions=None, validate_responses=False, strict_validation=False,
  25. randomize_endpoint=None, validator_map=None, pythonic_params=False,
  26. uri_parser_class=None, pass_context_arg_name=None):
  27. """
  28. :param api: api that this operation is attached to
  29. :type api: apis.AbstractAPI
  30. :param method: HTTP method
  31. :type method: str
  32. :param path: relative path to this operation
  33. :type path: str
  34. :param operation: swagger operation object
  35. :type operation: dict
  36. :param resolver: Callable that maps operationID to a function
  37. :type resolver: resolver.Resolver
  38. :param app_produces: list of content types the application can return by default
  39. :type app_produces: list
  40. :param app_consumes: list of content types the application consumes by default
  41. :type app_consumes: list
  42. :param path_parameters: Parameters defined in the path level
  43. :type path_parameters: list
  44. :param app_security: list of security rules the application uses by default
  45. :type app_security: list
  46. :param security_definitions: `Security Definitions Object
  47. <https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object>`_
  48. :type security_definitions: dict
  49. :param definitions: `Definitions Object
  50. <https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#definitionsObject>`_
  51. :type definitions: dict
  52. :param parameter_definitions: Global parameter definitions
  53. :type parameter_definitions: dict
  54. :param response_definitions: Global response definitions
  55. :type response_definitions: dict
  56. :param validate_responses: True enables validation. Validation errors generate HTTP 500 responses.
  57. :type validate_responses: bool
  58. :param strict_validation: True enables validation on invalid request parameters
  59. :type strict_validation: bool
  60. :param randomize_endpoint: number of random characters to append to operation name
  61. :type randomize_endpoint: integer
  62. :param validator_map: Custom validators for the types "parameter", "body" and "response".
  63. :type validator_map: dict
  64. :param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended
  65. to any shadowed built-ins
  66. :type pythonic_params: bool
  67. :param uri_parser_class: class to use for uri parsing
  68. :type uri_parser_class: AbstractURIParser
  69. :param pass_context_arg_name: If not None will try to inject the request context to the function using this
  70. name.
  71. :type pass_context_arg_name: str|None
  72. """
  73. app_security = operation.get('security', app_security)
  74. uri_parser_class = uri_parser_class or Swagger2URIParser
  75. self._router_controller = operation.get('x-swagger-router-controller')
  76. super().__init__(
  77. api=api,
  78. method=method,
  79. path=path,
  80. operation=operation,
  81. resolver=resolver,
  82. app_security=app_security,
  83. security_schemes=security_definitions,
  84. validate_responses=validate_responses,
  85. strict_validation=strict_validation,
  86. randomize_endpoint=randomize_endpoint,
  87. validator_map=validator_map,
  88. pythonic_params=pythonic_params,
  89. uri_parser_class=uri_parser_class,
  90. pass_context_arg_name=pass_context_arg_name
  91. )
  92. self._produces = operation.get('produces', app_produces)
  93. self._consumes = operation.get('consumes', app_consumes)
  94. self.definitions = definitions or {}
  95. self.definitions_map = {
  96. 'definitions': self.definitions,
  97. 'parameters': parameter_definitions,
  98. 'responses': response_definitions
  99. }
  100. self._parameters = operation.get('parameters', [])
  101. if path_parameters:
  102. self._parameters += path_parameters
  103. self._responses = operation.get('responses', {})
  104. logger.debug(self._responses)
  105. logger.debug('consumes: %s', self.consumes)
  106. logger.debug('produces: %s', self.produces)
  107. @classmethod
  108. def from_spec(cls, spec, api, path, method, resolver, *args, **kwargs):
  109. return cls(
  110. api,
  111. method,
  112. path,
  113. spec.get_operation(path, method),
  114. resolver=resolver,
  115. path_parameters=spec.get_path_params(path),
  116. app_security=spec.security,
  117. app_produces=spec.produces,
  118. app_consumes=spec.consumes,
  119. security_definitions=spec.security_definitions,
  120. definitions=spec.definitions,
  121. parameter_definitions=spec.parameter_definitions,
  122. response_definitions=spec.response_definitions,
  123. *args,
  124. **kwargs
  125. )
  126. @property
  127. def parameters(self):
  128. return self._parameters
  129. @property
  130. def consumes(self):
  131. return self._consumes
  132. @property
  133. def produces(self):
  134. return self._produces
  135. def get_path_parameter_types(self):
  136. types = {}
  137. path_parameters = (p for p in self.parameters if p["in"] == "path")
  138. for path_defn in path_parameters:
  139. if path_defn.get('type') == 'string' and path_defn.get('format') == 'path':
  140. # path is special case for type 'string'
  141. path_type = 'path'
  142. else:
  143. path_type = path_defn.get('type')
  144. types[path_defn['name']] = path_type
  145. return types
  146. def with_definitions(self, schema):
  147. if "schema" in schema:
  148. schema['schema']['definitions'] = self.definitions
  149. return schema
  150. def response_schema(self, status_code=None, content_type=None):
  151. response_definition = self.response_definition(
  152. status_code, content_type
  153. )
  154. return self.with_definitions(response_definition.get("schema", {}))
  155. def example_response(self, status_code=None, *args, **kwargs):
  156. """
  157. Returns example response from spec
  158. """
  159. # simply use the first/lowest status code, this is probably 200 or 201
  160. status_code = status_code or sorted(self._responses.keys())[0]
  161. examples_path = [str(status_code), 'examples']
  162. schema_example_path = [str(status_code), 'schema', 'example']
  163. schema_path = [str(status_code), 'schema']
  164. try:
  165. status_code = int(status_code)
  166. except ValueError:
  167. status_code = 200
  168. try:
  169. return (
  170. list(deep_get(self._responses, examples_path).values())[0],
  171. status_code
  172. )
  173. except KeyError:
  174. pass
  175. try:
  176. return (deep_get(self._responses, schema_example_path),
  177. status_code)
  178. except KeyError:
  179. pass
  180. try:
  181. return (self._nested_example(deep_get(self._responses, schema_path)),
  182. status_code)
  183. except KeyError:
  184. return (None, status_code)
  185. def _nested_example(self, schema):
  186. try:
  187. return schema["example"]
  188. except KeyError:
  189. pass
  190. try:
  191. # Recurse if schema is an object
  192. return {key: self._nested_example(value)
  193. for (key, value) in schema["properties"].items()}
  194. except KeyError:
  195. pass
  196. try:
  197. # Recurse if schema is an array
  198. return [self._nested_example(schema["items"])]
  199. except KeyError:
  200. raise
  201. @property
  202. def body_schema(self):
  203. """
  204. The body schema definition for this operation.
  205. """
  206. return self.with_definitions(self.body_definition).get('schema', {})
  207. @property
  208. def body_definition(self):
  209. """
  210. The body complete definition for this operation.
  211. **There can be one "body" parameter at most.**
  212. :rtype: dict
  213. """
  214. body_parameters = [p for p in self.parameters if p['in'] == 'body']
  215. if len(body_parameters) > 1:
  216. raise InvalidSpecification(
  217. "{method} {path} There can be one 'body' parameter at most".format(
  218. method=self.method,
  219. path=self.path))
  220. return body_parameters[0] if body_parameters else {}
  221. def _get_query_arguments(self, query, arguments, has_kwargs, sanitize):
  222. query_defns = {p["name"]: p
  223. for p in self.parameters
  224. if p["in"] == "query"}
  225. default_query_params = {k: v['default']
  226. for k, v in query_defns.items()
  227. if 'default' in v}
  228. query_arguments = deepcopy(default_query_params)
  229. query_arguments.update(query)
  230. return self._query_args_helper(query_defns, query_arguments,
  231. arguments, has_kwargs, sanitize)
  232. def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
  233. kwargs = {}
  234. body_parameters = [p for p in self.parameters if p['in'] == 'body'] or [{}]
  235. if body is None:
  236. body = deepcopy(body_parameters[0].get('schema', {}).get('default'))
  237. body_name = sanitize(body_parameters[0].get('name'))
  238. form_defns = {p['name']: p
  239. for p in self.parameters
  240. if p['in'] == 'formData'}
  241. default_form_params = {k: v['default']
  242. for k, v in form_defns.items()
  243. if 'default' in v}
  244. # Add body parameters
  245. if body_name:
  246. if not has_kwargs and body_name not in arguments:
  247. logger.debug("Body parameter '%s' not in function arguments", body_name)
  248. else:
  249. logger.debug("Body parameter '%s' in function arguments", body_name)
  250. kwargs[body_name] = body
  251. # Add formData parameters
  252. form_arguments = deepcopy(default_form_params)
  253. if form_defns and body:
  254. form_arguments.update(body)
  255. for key, value in form_arguments.items():
  256. sanitized_key = sanitize(key)
  257. if not has_kwargs and sanitized_key not in arguments:
  258. logger.debug("FormData parameter '%s' (sanitized: '%s') not in function arguments",
  259. key, sanitized_key)
  260. else:
  261. logger.debug("FormData parameter '%s' (sanitized: '%s') in function arguments",
  262. key, sanitized_key)
  263. try:
  264. form_defn = form_defns[key]
  265. except KeyError: # pragma: no cover
  266. logger.error("Function argument '%s' (non-sanitized: %s) not defined in specification",
  267. key, sanitized_key)
  268. else:
  269. kwargs[sanitized_key] = self._get_val_from_param(value, form_defn)
  270. return kwargs
  271. def _get_val_from_param(self, value, query_defn):
  272. if is_nullable(query_defn) and is_null(value):
  273. return None
  274. query_schema = query_defn
  275. if query_schema["type"] == "array":
  276. return [make_type(part, query_defn["items"]["type"]) for part in value]
  277. else:
  278. return make_type(value, query_defn["type"])