decorator.py 13KB

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