abstract.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. """
  2. This module defines an AbstractApp, which defines a standardized user interface for a Connexion
  3. application.
  4. """
  5. import abc
  6. import logging
  7. import pathlib
  8. from ..options import ConnexionOptions
  9. from ..resolver import Resolver
  10. logger = logging.getLogger('connexion.app')
  11. class AbstractApp(metaclass=abc.ABCMeta):
  12. def __init__(self, import_name, api_cls, port=None, specification_dir='',
  13. host=None, server=None, server_args=None, arguments=None, auth_all_paths=False, debug=None,
  14. resolver=None, options=None, skip_error_handlers=False):
  15. """
  16. :param import_name: the name of the application package
  17. :type import_name: str
  18. :param host: the host interface to bind on.
  19. :type host: str
  20. :param port: port to listen to
  21. :type port: int
  22. :param specification_dir: directory where to look for specifications
  23. :type specification_dir: pathlib.Path | str
  24. :param server: which wsgi server to use
  25. :type server: str | None
  26. :param server_args: dictionary of arguments which are then passed to appropriate http server (Flask or aio_http)
  27. :type server_args: dict | None
  28. :param arguments: arguments to replace on the specification
  29. :type arguments: dict | None
  30. :param auth_all_paths: whether to authenticate not defined paths
  31. :type auth_all_paths: bool
  32. :param debug: include debugging information
  33. :type debug: bool
  34. :param resolver: Callable that maps operationID to a function
  35. """
  36. self.port = port
  37. self.host = host
  38. self.debug = debug
  39. self.resolver = resolver
  40. self.import_name = import_name
  41. self.arguments = arguments or {}
  42. self.api_cls = api_cls
  43. self.resolver_error = None
  44. # Options
  45. self.auth_all_paths = auth_all_paths
  46. self.options = ConnexionOptions(options)
  47. self.server = server
  48. self.server_args = dict() if server_args is None else server_args
  49. self.app = self.create_app()
  50. # we get our application root path to avoid duplicating logic
  51. self.root_path = self.get_root_path()
  52. logger.debug('Root Path: %s', self.root_path)
  53. specification_dir = pathlib.Path(specification_dir) # Ensure specification dir is a Path
  54. if specification_dir.is_absolute():
  55. self.specification_dir = specification_dir
  56. else:
  57. self.specification_dir = self.root_path / specification_dir
  58. logger.debug('Specification directory: %s', self.specification_dir)
  59. if not skip_error_handlers:
  60. logger.debug('Setting error handlers')
  61. self.set_errors_handlers()
  62. @abc.abstractmethod
  63. def create_app(self):
  64. """
  65. Creates the user framework application
  66. """
  67. @abc.abstractmethod
  68. def get_root_path(self):
  69. """
  70. Gets the root path of the user framework application
  71. """
  72. @abc.abstractmethod
  73. def set_errors_handlers(self):
  74. """
  75. Sets all errors handlers of the user framework application
  76. """
  77. def add_api(self, specification, base_path=None, arguments=None,
  78. auth_all_paths=None, validate_responses=False,
  79. strict_validation=False, resolver=None, resolver_error=None,
  80. pythonic_params=False, pass_context_arg_name=None, options=None,
  81. validator_map=None):
  82. """
  83. Adds an API to the application based on a swagger file or API dict
  84. :param specification: swagger file with the specification | specification dict
  85. :type specification: pathlib.Path or str or dict
  86. :param base_path: base path where to add this api
  87. :type base_path: str | None
  88. :param arguments: api version specific arguments to replace on the specification
  89. :type arguments: dict | None
  90. :param auth_all_paths: whether to authenticate not defined paths
  91. :type auth_all_paths: bool
  92. :param validate_responses: True enables validation. Validation errors generate HTTP 500 responses.
  93. :type validate_responses: bool
  94. :param strict_validation: True enables validation on invalid request parameters
  95. :type strict_validation: bool
  96. :param resolver: Operation resolver.
  97. :type resolver: Resolver | types.FunctionType
  98. :param resolver_error: If specified, turns ResolverError into error
  99. responses with the given status code.
  100. :type resolver_error: int | None
  101. :param pythonic_params: When True CamelCase parameters are converted to snake_case
  102. :type pythonic_params: bool
  103. :param options: New style options dictionary.
  104. :type options: dict | None
  105. :param pass_context_arg_name: Name of argument in handler functions to pass request context to.
  106. :type pass_context_arg_name: str | None
  107. :param validator_map: map of validators
  108. :type validator_map: dict
  109. :rtype: AbstractAPI
  110. """
  111. # Turn the resolver_error code into a handler object
  112. self.resolver_error = resolver_error
  113. resolver_error_handler = None
  114. if self.resolver_error is not None:
  115. resolver_error_handler = self._resolver_error_handler
  116. resolver = resolver or self.resolver
  117. resolver = Resolver(resolver) if hasattr(resolver, '__call__') else resolver
  118. auth_all_paths = auth_all_paths if auth_all_paths is not None else self.auth_all_paths
  119. # TODO test if base_path starts with an / (if not none)
  120. arguments = arguments or dict()
  121. arguments = dict(self.arguments, **arguments) # copy global arguments and update with api specific
  122. if isinstance(specification, dict):
  123. specification = specification
  124. else:
  125. specification = self.specification_dir / specification
  126. api_options = self.options.extend(options)
  127. api = self.api_cls(specification,
  128. base_path=base_path,
  129. arguments=arguments,
  130. resolver=resolver,
  131. resolver_error_handler=resolver_error_handler,
  132. validate_responses=validate_responses,
  133. strict_validation=strict_validation,
  134. auth_all_paths=auth_all_paths,
  135. debug=self.debug,
  136. validator_map=validator_map,
  137. pythonic_params=pythonic_params,
  138. pass_context_arg_name=pass_context_arg_name,
  139. options=api_options.as_dict())
  140. return api
  141. def _resolver_error_handler(self, *args, **kwargs):
  142. from connexion.handlers import ResolverErrorHandler
  143. return ResolverErrorHandler(self.api_cls, self.resolver_error, *args, **kwargs)
  144. def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
  145. """
  146. Connects a URL rule. Works exactly like the `route` decorator. If a view_func is provided it will be
  147. registered with the endpoint.
  148. Basically this example::
  149. @app.route('/')
  150. def index():
  151. pass
  152. Is equivalent to the following::
  153. def index():
  154. pass
  155. app.add_url_rule('/', 'index', index)
  156. If the view_func is not provided you will need to connect the endpoint to a view function like so::
  157. app.view_functions['index'] = index
  158. Internally`route` invokes `add_url_rule` so if you want to customize the behavior via subclassing you only need
  159. to change this method.
  160. :param rule: the URL rule as string
  161. :type rule: str
  162. :param endpoint: the endpoint for the registered URL rule. Flask itself assumes the name of the view function as
  163. endpoint
  164. :type endpoint: str
  165. :param view_func: the function to call when serving a request to the provided endpoint
  166. :type view_func: types.FunctionType
  167. :param options: the options to be forwarded to the underlying `werkzeug.routing.Rule` object. A change
  168. to Werkzeug is handling of method options. methods is a list of methods this rule should be
  169. limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly
  170. `HEAD`).
  171. """
  172. log_details = {'endpoint': endpoint, 'view_func': view_func.__name__}
  173. log_details.update(options)
  174. logger.debug('Adding %s', rule, extra=log_details)
  175. self.app.add_url_rule(rule, endpoint, view_func, **options)
  176. def route(self, rule, **options):
  177. """
  178. A decorator that is used to register a view function for a
  179. given URL rule. This does the same thing as `add_url_rule`
  180. but is intended for decorator usage::
  181. @app.route('/')
  182. def index():
  183. return 'Hello World'
  184. :param rule: the URL rule as string
  185. :type rule: str
  186. :param endpoint: the endpoint for the registered URL rule. Flask
  187. itself assumes the name of the view function as
  188. endpoint
  189. :param options: the options to be forwarded to the underlying `werkzeug.routing.Rule` object. A change
  190. to Werkzeug is handling of method options. methods is a list of methods this rule should be
  191. limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly
  192. `HEAD`).
  193. """
  194. logger.debug('Adding %s with decorator', rule, extra=options)
  195. return self.app.route(rule, **options)
  196. @abc.abstractmethod
  197. def run(self, port=None, server=None, debug=None, host=None, **options): # pragma: no cover
  198. """
  199. Runs the application on a local development server.
  200. :param host: the host interface to bind on.
  201. :type host: str
  202. :param port: port to listen to
  203. :type port: int
  204. :param server: which wsgi server to use
  205. :type server: str | None
  206. :param debug: include debugging information
  207. :type debug: bool
  208. :param options: options to be forwarded to the underlying server
  209. """
  210. def __call__(self, environ, start_response): # pragma: no cover
  211. """
  212. Makes the class callable to be WSGI-compliant. As Flask is used to handle requests,
  213. this is a passthrough-call to the Flask callable class.
  214. This is an abstraction to avoid directly referencing the app attribute from outside the
  215. class and protect it from unwanted modification.
  216. """
  217. return self.app(environ, start_response)