decorator.py 14KB

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