decorator.py 16KB

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