context.py 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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. """
  99. Enable management of this exception during execution of views. If this
  100. exception caught, an http response will be returned with this http
  101. code.
  102. :param exception_class: Exception class to catch
  103. :param http_code: HTTP code to use in response if exception caught
  104. """
  105. raise NotImplementedError()
  106. def handle_exceptions(
  107. self,
  108. exception_classes: typing.List[typing.Type[Exception]],
  109. http_code: int,
  110. ) -> None:
  111. """
  112. Enable management of these exceptions during execution of views. If
  113. this exception caught, an http response will be returned with this http
  114. code.
  115. :param exception_classes: Exception classes to catch
  116. :param http_code: HTTP code to use in response if exception caught
  117. """
  118. raise NotImplementedError()
  119. def is_debug(self) -> bool:
  120. """
  121. Method called to know if Hapic has been called in debug mode.
  122. Debug mode provide some informations like debug trace and error
  123. message in body when internal error happen.
  124. :return: True if in debug mode
  125. """
  126. raise NotImplementedError()
  127. class HandledException(object):
  128. """
  129. Representation of an handled exception with it's http code
  130. """
  131. def __init__(
  132. self,
  133. exception_class: typing.Type[Exception],
  134. http_code: int = 500,
  135. ):
  136. self.exception_class = exception_class
  137. self.http_code = http_code
  138. class BaseContext(ContextInterface):
  139. def get_default_error_builder(self) -> ErrorBuilderInterface:
  140. """ see hapic.context.ContextInterface#get_default_error_builder"""
  141. return self.default_error_builder
  142. def handle_exception(
  143. self,
  144. exception_class: typing.Type[Exception],
  145. http_code: int,
  146. ) -> None:
  147. self._add_exception_class_to_catch(exception_class, http_code)
  148. def handle_exceptions(
  149. self,
  150. exception_classes: typing.List[typing.Type[Exception]],
  151. http_code: int,
  152. ) -> None:
  153. for exception_class in exception_classes:
  154. self._add_exception_class_to_catch(exception_class, http_code)
  155. def handle_exceptions_decorator_builder(
  156. self,
  157. func: typing.Callable[..., typing.Any],
  158. ) -> typing.Callable[..., typing.Any]:
  159. """
  160. Return a decorator who catch exceptions raised during given function
  161. execution and return a response built by the default error builder.
  162. :param func: decorated function
  163. :return: the decorator
  164. """
  165. def decorator(*args, **kwargs):
  166. try:
  167. return func(*args, **kwargs)
  168. except Exception as exc:
  169. # Reverse list to read first user given exception before
  170. # the hapic default Exception catch
  171. handled_exceptions = reversed(
  172. self._get_handled_exception_class_and_http_codes(),
  173. )
  174. for handled_exception in handled_exceptions:
  175. # TODO BS 2018-05-04: How to be attentive to hierarchy ?
  176. if isinstance(exc, handled_exception.exception_class):
  177. error_builder = self.get_default_error_builder()
  178. error_body = error_builder.build_from_exception(
  179. exc,
  180. include_traceback=self.is_debug(),
  181. )
  182. dumped = error_builder.dump(error_body).data
  183. return self.get_response(
  184. json.dumps(dumped),
  185. handled_exception.http_code,
  186. )
  187. raise exc
  188. return decorator
  189. def _get_handled_exception_class_and_http_codes(
  190. self,
  191. ) -> typing.List[HandledException]:
  192. """
  193. :return: A list of tuple where: thirst item of tuple is a exception
  194. class and second tuple item is a http code. This list will be used by
  195. `handle_exceptions_decorator_builder` decorator to catch exceptions.
  196. """
  197. raise NotImplementedError()
  198. def _add_exception_class_to_catch(
  199. self,
  200. exception_class: typing.Type[Exception],
  201. http_code: int,
  202. ) -> None:
  203. """
  204. Add an exception class to catch and matching http code. Will be used by
  205. `handle_exceptions_decorator_builder` decorator to catch exceptions.
  206. :param exception_class: exception class to catch
  207. :param http_code: http code to use if this exception catched
  208. :return:
  209. """
  210. raise NotImplementedError()