decorator.py 13KB

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