decorator.py 11KB

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