decorator.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. # -*- coding: utf-8 -*-
  2. import functools
  3. import typing
  4. from http import HTTPStatus
  5. from hapic.data import HapicData
  6. from hapic.description import ControllerDescription
  7. from hapic.exception import ProcessException
  8. from hapic.context import ContextInterface
  9. from hapic.processor import ProcessorInterface
  10. from hapic.processor import RequestParameters
  11. # TODO: Ensure usage of DECORATION_ATTRIBUTE_NAME is documented and
  12. # var names correctly choose.
  13. DECORATION_ATTRIBUTE_NAME = '_hapic_decoration_token'
  14. class ControllerWrapper(object):
  15. def before_wrapped_func(
  16. self,
  17. func_args: typing.Tuple[typing.Any, ...],
  18. func_kwargs: typing.Dict[str, typing.Any],
  19. ) -> typing.Union[None, typing.Any]:
  20. pass
  21. def after_wrapped_function(self, response: typing.Any) -> typing.Any:
  22. return response
  23. def get_wrapper(
  24. self,
  25. func: 'typing.Callable[..., typing.Any]',
  26. ) -> 'typing.Callable[..., typing.Any]':
  27. def wrapper(*args, **kwargs) -> typing.Any:
  28. # Note: Design of before_wrapped_func can be to update kwargs
  29. # by reference here
  30. replacement_response = self.before_wrapped_func(args, kwargs)
  31. if replacement_response:
  32. return replacement_response
  33. response = self._execute_wrapped_function(func, args, kwargs)
  34. new_response = self.after_wrapped_function(response)
  35. return new_response
  36. return functools.update_wrapper(wrapper, func)
  37. def _execute_wrapped_function(
  38. self,
  39. func,
  40. func_args,
  41. func_kwargs,
  42. ) -> typing.Any:
  43. return func(*func_args, **func_kwargs)
  44. class InputOutputControllerWrapper(ControllerWrapper):
  45. def __init__(
  46. self,
  47. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  48. processor: ProcessorInterface,
  49. error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  50. default_http_code: HTTPStatus=HTTPStatus.OK,
  51. ) -> None:
  52. self._context = context
  53. self.processor = processor
  54. self.error_http_code = error_http_code
  55. self.default_http_code = default_http_code
  56. @property
  57. def context(self) -> ContextInterface:
  58. if callable(self._context):
  59. return self._context()
  60. return self._context
  61. class InputControllerWrapper(InputOutputControllerWrapper):
  62. def before_wrapped_func(
  63. self,
  64. func_args: typing.Tuple[typing.Any, ...],
  65. func_kwargs: typing.Dict[str, typing.Any],
  66. ) -> typing.Any:
  67. # Retrieve hapic_data instance or create new one
  68. # hapic_data is given though decorators
  69. # Important note here: func_kwargs is update by reference !
  70. hapic_data = self.ensure_hapic_data(func_kwargs)
  71. request_parameters = self.get_request_parameters(
  72. func_args,
  73. func_kwargs,
  74. )
  75. try:
  76. processed_data = self.get_processed_data(request_parameters)
  77. self.update_hapic_data(hapic_data, processed_data)
  78. except ProcessException:
  79. error_response = self.get_error_response(request_parameters)
  80. return error_response
  81. @classmethod
  82. def ensure_hapic_data(
  83. cls,
  84. func_kwargs: typing.Dict[str, typing.Any],
  85. ) -> HapicData:
  86. # TODO: Permit other name than "hapic_data" ?
  87. try:
  88. return func_kwargs['hapic_data']
  89. except KeyError:
  90. hapic_data = HapicData()
  91. func_kwargs['hapic_data'] = hapic_data
  92. return hapic_data
  93. def get_request_parameters(
  94. self,
  95. func_args: typing.Tuple[typing.Any, ...],
  96. func_kwargs: typing.Dict[str, typing.Any],
  97. ) -> RequestParameters:
  98. return self.context.get_request_parameters(
  99. *func_args,
  100. **func_kwargs
  101. )
  102. def get_processed_data(
  103. self,
  104. request_parameters: RequestParameters,
  105. ) -> typing.Any:
  106. raise NotImplementedError()
  107. def update_hapic_data(
  108. self,
  109. hapic_data: HapicData,
  110. processed_data: typing.Dict[str, typing.Any],
  111. ) -> None:
  112. raise NotImplementedError()
  113. def get_error_response(
  114. self,
  115. request_parameters: RequestParameters,
  116. ) -> typing.Any:
  117. error = self.processor.get_validation_error(
  118. request_parameters.body_parameters,
  119. )
  120. error_response = self.context.get_validation_error_response(
  121. error,
  122. http_code=self.error_http_code,
  123. )
  124. return error_response
  125. class OutputControllerWrapper(InputOutputControllerWrapper):
  126. def __init__(
  127. self,
  128. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  129. processor: ProcessorInterface,
  130. error_http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  131. default_http_code: HTTPStatus=HTTPStatus.OK,
  132. ) -> None:
  133. super().__init__(
  134. context,
  135. processor,
  136. error_http_code,
  137. default_http_code,
  138. )
  139. def get_error_response(
  140. self,
  141. response: typing.Any,
  142. ) -> typing.Any:
  143. error = self.processor.get_validation_error(response)
  144. error_response = self.context.get_validation_error_response(
  145. error,
  146. http_code=self.error_http_code,
  147. )
  148. return error_response
  149. def after_wrapped_function(self, response: typing.Any) -> typing.Any:
  150. try:
  151. processed_response = self.processor.process(response)
  152. prepared_response = self.context.get_response(
  153. processed_response,
  154. self.default_http_code,
  155. )
  156. return prepared_response
  157. except ProcessException:
  158. # TODO: ici ou ailleurs: il faut pas forcement donner le detail
  159. # de l'erreur (mode debug par exemple)
  160. error_response = self.get_error_response(response)
  161. return error_response
  162. class DecoratedController(object):
  163. def __init__(
  164. self,
  165. token: str,
  166. description: ControllerDescription,
  167. name: str='',
  168. ) -> None:
  169. self._token = token
  170. self._description = description
  171. self._name = name
  172. @property
  173. def token(self) -> str:
  174. return self._token
  175. @property
  176. def description(self) -> ControllerDescription:
  177. return self._description
  178. @property
  179. def name(self) -> str:
  180. return self._name
  181. class OutputBodyControllerWrapper(OutputControllerWrapper):
  182. pass
  183. class OutputHeadersControllerWrapper(OutputControllerWrapper):
  184. # TODO: write me
  185. pass
  186. class InputPathControllerWrapper(InputControllerWrapper):
  187. def update_hapic_data(
  188. self, hapic_data: HapicData,
  189. processed_data: typing.Any,
  190. ) -> None:
  191. hapic_data.path = processed_data
  192. def get_processed_data(
  193. self,
  194. request_parameters: RequestParameters,
  195. ) -> typing.Any:
  196. processed_data = self.processor.process(
  197. request_parameters.path_parameters,
  198. )
  199. return processed_data
  200. class InputQueryControllerWrapper(InputControllerWrapper):
  201. def update_hapic_data(
  202. self, hapic_data: HapicData,
  203. processed_data: typing.Any,
  204. ) -> None:
  205. hapic_data.query = processed_data
  206. def get_processed_data(
  207. self,
  208. request_parameters: RequestParameters,
  209. ) -> typing.Any:
  210. processed_data = self.processor.process(
  211. request_parameters.query_parameters,
  212. )
  213. return processed_data
  214. class InputBodyControllerWrapper(InputControllerWrapper):
  215. def update_hapic_data(
  216. self, hapic_data: HapicData,
  217. processed_data: typing.Any,
  218. ) -> None:
  219. hapic_data.body = processed_data
  220. def get_processed_data(
  221. self,
  222. request_parameters: RequestParameters,
  223. ) -> typing.Any:
  224. processed_data = self.processor.process(
  225. request_parameters.body_parameters,
  226. )
  227. return processed_data
  228. class InputHeadersControllerWrapper(InputControllerWrapper):
  229. def update_hapic_data(
  230. self, hapic_data: HapicData,
  231. processed_data: typing.Any,
  232. ) -> None:
  233. hapic_data.headers = processed_data
  234. def get_processed_data(
  235. self,
  236. request_parameters: RequestParameters,
  237. ) -> typing.Any:
  238. processed_data = self.processor.process(
  239. request_parameters.header_parameters,
  240. )
  241. return processed_data
  242. class InputFormsControllerWrapper(InputControllerWrapper):
  243. def update_hapic_data(
  244. self, hapic_data: HapicData,
  245. processed_data: typing.Any,
  246. ) -> None:
  247. hapic_data.forms = processed_data
  248. def get_processed_data(
  249. self,
  250. request_parameters: RequestParameters,
  251. ) -> typing.Any:
  252. processed_data = self.processor.process(
  253. request_parameters.form_parameters,
  254. )
  255. return processed_data
  256. class ExceptionHandlerControllerWrapper(ControllerWrapper):
  257. def __init__(
  258. self,
  259. handled_exception_class: typing.Type[Exception],
  260. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  261. http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  262. ) -> None:
  263. self.handled_exception_class = handled_exception_class
  264. self.context = context
  265. self.http_code = http_code
  266. def _execute_wrapped_function(
  267. self,
  268. func,
  269. func_args,
  270. func_kwargs,
  271. ) -> typing.Any:
  272. try:
  273. return super()._execute_wrapped_function(
  274. func,
  275. func_args,
  276. func_kwargs,
  277. )
  278. except self.handled_exception_class as exc:
  279. # TODO: error_dict configurable name
  280. # TODO: Who assume error structure ? We have to rethink it
  281. error_dict = {
  282. 'error_message': str(exc),
  283. }
  284. if hasattr(exc, 'error_dict'):
  285. error_dict.update(exc.error_dict)
  286. error_response = self.context.get_response(
  287. error_dict,
  288. self.http_code,
  289. )
  290. return error_response