context.py 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. # -*- coding: utf-8 -*-
  2. import json
  3. import typing
  4. from hapic.error import ErrorBuilderInterface
  5. try: # Python 3.5+
  6. from http import HTTPStatus
  7. except ImportError:
  8. from http import client as HTTPStatus
  9. from hapic.processor import RequestParameters
  10. from hapic.processor import ProcessValidationError
  11. if typing.TYPE_CHECKING:
  12. from hapic.decorator import DecoratedController
  13. class RouteRepresentation(object):
  14. def __init__(
  15. self,
  16. rule: str,
  17. method: str,
  18. original_route_object: typing.Any=None,
  19. ) -> None:
  20. self.rule = rule
  21. self.method = method
  22. self.original_route_object = original_route_object
  23. class ContextInterface(object):
  24. def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
  25. raise NotImplementedError()
  26. def get_response(
  27. self,
  28. # TODO BS 20171228: rename into response_content
  29. response: str,
  30. http_code: int,
  31. mimetype: str='application/json',
  32. ) -> typing.Any:
  33. raise NotImplementedError()
  34. def get_validation_error_response(
  35. self,
  36. error: ProcessValidationError,
  37. http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  38. ) -> typing.Any:
  39. raise NotImplementedError()
  40. def find_route(
  41. self,
  42. decorated_controller: 'DecoratedController',
  43. ) -> RouteRepresentation:
  44. raise NotImplementedError()
  45. # TODO BS 20171228: rename into openapi !
  46. def get_swagger_path(self, contextualised_rule: str) -> str:
  47. """
  48. Return OpenAPI path with context path
  49. TODO BS 20171228: Give example
  50. :param contextualised_rule: path of original context
  51. :return: OpenAPI path
  52. """
  53. raise NotImplementedError()
  54. # TODO BS 20171228: rename into "bypass"
  55. def by_pass_output_wrapping(self, response: typing.Any) -> bool:
  56. """
  57. Return True if the controller response is the final response object:
  58. we do not have to apply any processing on it.
  59. :param response: the original response of controller
  60. :return:
  61. """
  62. raise NotImplementedError()
  63. def get_default_error_builder(self) -> ErrorBuilderInterface:
  64. """
  65. Return a ErrorBuilder who will be used to build default errors
  66. :return: ErrorBuilderInterface instance
  67. """
  68. raise NotImplementedError()
  69. def add_view(
  70. self,
  71. route: str,
  72. http_method: str,
  73. view_func: typing.Callable[..., typing.Any],
  74. ) -> None:
  75. """
  76. This method must permit to add a view in current context
  77. :param route: The route depending of framework format, ex "/foo"
  78. :param http_method: HTTP method like GET, POST, etc ...
  79. :param view_func: The view callable
  80. """
  81. raise NotImplementedError()
  82. def serve_directory(
  83. self,
  84. route_prefix: str,
  85. directory_path: str,
  86. ) -> None:
  87. """
  88. Configure a path to serve a directory content
  89. :param route_prefix: The base url for serve the directory, eg /static
  90. :param directory_path: The file system path
  91. """
  92. raise NotImplementedError()
  93. def handle_exception(
  94. self,
  95. exception_class: typing.Type[Exception],
  96. http_code: int,
  97. ) -> None:
  98. raise NotImplementedError()
  99. def handle_exceptions(
  100. self,
  101. exception_classes: typing.List[typing.Type[Exception]],
  102. http_code: int,
  103. ) -> None:
  104. raise NotImplementedError()
  105. def _add_exception_class_to_catch(
  106. self,
  107. exception_class: typing.List[typing.Type[Exception]],
  108. http_code: int,
  109. ) -> None:
  110. raise NotImplementedError()
  111. class HandledException(object):
  112. """
  113. Representation of an handled exception with it's http code
  114. """
  115. def __init__(
  116. self,
  117. exception_class: typing.Type[Exception],
  118. http_code: int = 500,
  119. ):
  120. self.exception_class = exception_class
  121. self.http_code = http_code
  122. class BaseContext(ContextInterface):
  123. def get_default_error_builder(self) -> ErrorBuilderInterface:
  124. """ see hapic.context.ContextInterface#get_default_error_builder"""
  125. return self.default_error_builder
  126. def handle_exception(
  127. self,
  128. exception_class: typing.Type[Exception],
  129. http_code: int,
  130. ) -> None:
  131. self._add_exception_class_to_catch(exception_class, http_code)
  132. def handle_exceptions(
  133. self,
  134. exception_classes: typing.List[typing.Type[Exception]],
  135. http_code: int,
  136. ) -> None:
  137. for exception_class in exception_classes:
  138. self._add_exception_class_to_catch(exception_class, http_code)
  139. def handle_exceptions_decorator_builder(
  140. self,
  141. func: typing.Callable[..., typing.Any],
  142. ) -> typing.Callable[..., typing.Any]:
  143. """
  144. Return a decorator who catch exceptions raised during given function
  145. execution and return a response built by the default error builder.
  146. :param func: decorated function
  147. :return: the decorator
  148. """
  149. def decorator(*args, **kwargs):
  150. try:
  151. return func(*args, **kwargs)
  152. except Exception as exc:
  153. # Reverse list to read first user given exception before
  154. # the hapic default Exception catch
  155. handled_exceptions = reversed(
  156. self._get_handled_exception_class_and_http_codes(),
  157. )
  158. for handled_exception in handled_exceptions:
  159. # TODO BS 2018-05-04: How to be attentive to hierarchy ?
  160. if isinstance(exc, handled_exception.exception_class):
  161. error_builder = self.get_default_error_builder()
  162. error_body = error_builder.build_from_exception(exc)
  163. return self.get_response(
  164. json.dumps(error_body),
  165. handled_exception.http_code,
  166. )
  167. raise exc
  168. return decorator
  169. def _get_handled_exception_class_and_http_codes(
  170. self,
  171. ) -> typing.List[HandledException]:
  172. """
  173. :return: A list of tuple where: thirst item of tuple is a exception
  174. class and second tuple item is a http code. This list will be used by
  175. `handle_exceptions_decorator_builder` decorator to catch exceptions.
  176. """
  177. raise NotImplementedError()
  178. def _add_exception_class_to_catch(
  179. self,
  180. exception_class: typing.Type[Exception],
  181. http_code: int,
  182. ) -> None:
  183. """
  184. Add an exception class to catch and matching http code. Will be used by
  185. `handle_exceptions_decorator_builder` decorator to catch exceptions.
  186. :param exception_class: exception class to catch
  187. :param http_code: http code to use if this exception catched
  188. :return:
  189. """
  190. raise NotImplementedError()