decorator.py 11KB

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