decorator.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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. def wrapper(*args, **kwargs) -> typing.Any:
  65. # Note: Design of before_wrapped_func can be to update kwargs
  66. # by reference here
  67. replacement_response = self.before_wrapped_func(args, kwargs)
  68. if replacement_response:
  69. return replacement_response
  70. response = self._execute_wrapped_function(func, args, kwargs)
  71. new_response = self.after_wrapped_function(response)
  72. return new_response
  73. return functools.update_wrapper(wrapper, func)
  74. def _execute_wrapped_function(
  75. self,
  76. func,
  77. func_args,
  78. func_kwargs,
  79. ) -> typing.Any:
  80. return func(*func_args, **func_kwargs)
  81. class InputOutputControllerWrapper(ControllerWrapper):
  82. def __init__(
  83. self,
  84. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  85. processor: ProcessorInterface,
  86. error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  87. default_http_code: HTTPStatus=HTTPStatus.OK,
  88. ) -> None:
  89. self._context = context
  90. self.processor = processor
  91. self.error_http_code = error_http_code
  92. self.default_http_code = default_http_code
  93. @property
  94. def context(self) -> ContextInterface:
  95. if callable(self._context):
  96. return self._context()
  97. return self._context
  98. class InputControllerWrapper(InputOutputControllerWrapper):
  99. def before_wrapped_func(
  100. self,
  101. func_args: typing.Tuple[typing.Any, ...],
  102. func_kwargs: typing.Dict[str, typing.Any],
  103. ) -> typing.Any:
  104. # Retrieve hapic_data instance or create new one
  105. # hapic_data is given though decorators
  106. # Important note here: func_kwargs is update by reference !
  107. hapic_data = self.ensure_hapic_data(func_kwargs)
  108. request_parameters = self.get_request_parameters(
  109. func_args,
  110. func_kwargs,
  111. )
  112. try:
  113. processed_data = self.get_processed_data(request_parameters)
  114. self.update_hapic_data(hapic_data, processed_data)
  115. except ProcessException:
  116. error_response = self.get_error_response(request_parameters)
  117. return error_response
  118. @classmethod
  119. def ensure_hapic_data(
  120. cls,
  121. func_kwargs: typing.Dict[str, typing.Any],
  122. ) -> HapicData:
  123. # TODO: Permit other name than "hapic_data" ? see #7
  124. try:
  125. return func_kwargs['hapic_data']
  126. except KeyError:
  127. hapic_data = HapicData()
  128. func_kwargs['hapic_data'] = hapic_data
  129. return hapic_data
  130. def get_request_parameters(
  131. self,
  132. func_args: typing.Tuple[typing.Any, ...],
  133. func_kwargs: typing.Dict[str, typing.Any],
  134. ) -> RequestParameters:
  135. return self.context.get_request_parameters(
  136. *func_args,
  137. **func_kwargs
  138. )
  139. def get_processed_data(
  140. self,
  141. request_parameters: RequestParameters,
  142. ) -> typing.Any:
  143. parameters_data = self.get_parameters_data(request_parameters)
  144. processed_data = self.processor.process(parameters_data)
  145. return processed_data
  146. def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
  147. raise NotImplementedError()
  148. def update_hapic_data(
  149. self,
  150. hapic_data: HapicData,
  151. processed_data: typing.Dict[str, typing.Any],
  152. ) -> None:
  153. raise NotImplementedError()
  154. def get_error_response(
  155. self,
  156. request_parameters: RequestParameters,
  157. ) -> typing.Any:
  158. parameters_data = self.get_parameters_data(request_parameters)
  159. error = self.processor.get_validation_error(parameters_data)
  160. error_response = self.context.get_validation_error_response(
  161. error,
  162. http_code=self.error_http_code,
  163. )
  164. return error_response
  165. # TODO BS 2018-07-23: This class is an async version of InputControllerWrapper
  166. # (and ControllerWrapper.get_wrapper rewrite) to permit async compatibility.
  167. # Please re-think about code refact. TAG: REFACT_ASYNC
  168. class AsyncInputControllerWrapper(InputControllerWrapper):
  169. def get_wrapper(
  170. self,
  171. func: 'typing.Callable[..., typing.Any]',
  172. ) -> 'typing.Callable[..., typing.Any]':
  173. async def wrapper(*args, **kwargs) -> typing.Any:
  174. # Note: Design of before_wrapped_func can be to update kwargs
  175. # by reference here
  176. replacement_response = await self.before_wrapped_func(args, kwargs)
  177. if replacement_response:
  178. return replacement_response
  179. response = await self._execute_wrapped_function(func, args, kwargs)
  180. new_response = self.after_wrapped_function(response)
  181. return new_response
  182. return functools.update_wrapper(wrapper, func)
  183. async def before_wrapped_func(
  184. self,
  185. func_args: typing.Tuple[typing.Any, ...],
  186. func_kwargs: typing.Dict[str, typing.Any],
  187. ) -> typing.Any:
  188. # Retrieve hapic_data instance or create new one
  189. # hapic_data is given though decorators
  190. # Important note here: func_kwargs is update by reference !
  191. hapic_data = self.ensure_hapic_data(func_kwargs)
  192. request_parameters = self.get_request_parameters(
  193. func_args,
  194. func_kwargs,
  195. )
  196. try:
  197. processed_data = await self.get_processed_data(request_parameters)
  198. self.update_hapic_data(hapic_data, processed_data)
  199. except ProcessException:
  200. error_response = self.get_error_response(request_parameters)
  201. return error_response
  202. async def get_processed_data(
  203. self,
  204. request_parameters: RequestParameters,
  205. ) -> typing.Any:
  206. parameters_data = await self.get_parameters_data(request_parameters)
  207. processed_data = self.processor.process(parameters_data)
  208. return processed_data
  209. class OutputControllerWrapper(InputOutputControllerWrapper):
  210. def __init__(
  211. self,
  212. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  213. processor: ProcessorInterface,
  214. error_http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  215. default_http_code: HTTPStatus=HTTPStatus.OK,
  216. ) -> None:
  217. super().__init__(
  218. context,
  219. processor,
  220. error_http_code,
  221. default_http_code,
  222. )
  223. def get_error_response(
  224. self,
  225. response: typing.Any,
  226. ) -> typing.Any:
  227. error = self.processor.get_validation_error(response)
  228. error_response = self.context.get_validation_error_response(
  229. error,
  230. http_code=self.error_http_code,
  231. )
  232. return error_response
  233. def after_wrapped_function(self, response: typing.Any) -> typing.Any:
  234. try:
  235. if self.context.by_pass_output_wrapping(response):
  236. return response
  237. processed_response = self.processor.process(response)
  238. prepared_response = self.context.get_response(
  239. json.dumps(processed_response),
  240. self.default_http_code,
  241. )
  242. return prepared_response
  243. except ProcessException:
  244. # TODO: ici ou ailleurs: il faut pas forcement donner le detail
  245. # de l'erreur (mode debug par exemple) see #8
  246. error_response = self.get_error_response(response)
  247. return error_response
  248. class DecoratedController(object):
  249. def __init__(
  250. self,
  251. reference: ControllerReference,
  252. description: ControllerDescription,
  253. name: str='',
  254. ) -> None:
  255. self._reference = reference
  256. self._description = description
  257. self._name = name
  258. @property
  259. def reference(self) -> ControllerReference:
  260. return self._reference
  261. @property
  262. def description(self) -> ControllerDescription:
  263. return self._description
  264. @property
  265. def name(self) -> str:
  266. return self._name
  267. class OutputBodyControllerWrapper(OutputControllerWrapper):
  268. pass
  269. class AsyncOutputBodyControllerWrapper(OutputControllerWrapper):
  270. def get_wrapper(
  271. self,
  272. func: 'typing.Callable[..., typing.Any]',
  273. ) -> 'typing.Callable[..., typing.Any]':
  274. # async def wrapper(*args, **kwargs) -> typing.Any:
  275. async def wrapper(*args, **kwargs) -> typing.Any:
  276. # Note: Design of before_wrapped_func can be to update kwargs
  277. # by reference here
  278. replacement_response = self.before_wrapped_func(args, kwargs)
  279. if replacement_response:
  280. return replacement_response
  281. response = await self._execute_wrapped_function(func, args, kwargs)
  282. new_response = self.after_wrapped_function(response)
  283. return new_response
  284. return functools.update_wrapper(wrapper, func)
  285. class OutputHeadersControllerWrapper(OutputControllerWrapper):
  286. pass
  287. class OutputFileControllerWrapper(ControllerWrapper):
  288. def __init__(
  289. self,
  290. output_types: typing.List[str],
  291. default_http_code: HTTPStatus=HTTPStatus.OK,
  292. ) -> None:
  293. self.output_types = output_types
  294. self.default_http_code = default_http_code
  295. class InputPathControllerWrapper(InputControllerWrapper):
  296. def update_hapic_data(
  297. self, hapic_data: HapicData,
  298. processed_data: typing.Any,
  299. ) -> None:
  300. hapic_data.path = processed_data
  301. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  302. return request_parameters.path_parameters
  303. class InputQueryControllerWrapper(InputControllerWrapper):
  304. def __init__(
  305. self,
  306. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  307. processor: ProcessorInterface,
  308. error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  309. default_http_code: HTTPStatus=HTTPStatus.OK,
  310. as_list: typing.List[str]=None
  311. ) -> None:
  312. super().__init__(
  313. context,
  314. processor,
  315. error_http_code,
  316. default_http_code,
  317. )
  318. self.as_list = as_list or [] # FDV
  319. def update_hapic_data(
  320. self, hapic_data: HapicData,
  321. processed_data: typing.Any,
  322. ) -> None:
  323. hapic_data.query = processed_data
  324. def get_parameters_data(self, request_parameters: RequestParameters) -> MultiDict: # nopep8
  325. # Parameters are updated considering eventual as_list parameters
  326. if self.as_list:
  327. query_parameters = MultiDict()
  328. for parameter_name in request_parameters.query_parameters.keys():
  329. if parameter_name in query_parameters:
  330. continue
  331. if parameter_name in self.as_list:
  332. query_parameters[parameter_name] = \
  333. request_parameters.query_parameters.getall(
  334. parameter_name,
  335. )
  336. else:
  337. query_parameters[parameter_name] = \
  338. request_parameters.query_parameters.get(
  339. parameter_name,
  340. )
  341. return query_parameters
  342. return request_parameters.query_parameters
  343. class InputBodyControllerWrapper(InputControllerWrapper):
  344. def update_hapic_data(
  345. self, hapic_data: HapicData,
  346. processed_data: typing.Any,
  347. ) -> None:
  348. hapic_data.body = processed_data
  349. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  350. return request_parameters.body_parameters
  351. # TODO BS 2018-07-23: This class is an async version of InputControllerWrapper
  352. # to permit async compatibility. Please re-think about code refact
  353. # TAG: REFACT_ASYNC
  354. class AsyncInputBodyControllerWrapper(AsyncInputControllerWrapper):
  355. def update_hapic_data(
  356. self, hapic_data: HapicData,
  357. processed_data: typing.Any,
  358. ) -> None:
  359. hapic_data.body = processed_data
  360. async def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  361. return await request_parameters.body_parameters
  362. class InputHeadersControllerWrapper(InputControllerWrapper):
  363. def update_hapic_data(
  364. self, hapic_data: HapicData,
  365. processed_data: typing.Any,
  366. ) -> None:
  367. hapic_data.headers = processed_data
  368. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  369. return request_parameters.header_parameters
  370. class InputFormsControllerWrapper(InputControllerWrapper):
  371. def update_hapic_data(
  372. self, hapic_data: HapicData,
  373. processed_data: typing.Any,
  374. ) -> None:
  375. hapic_data.forms = processed_data
  376. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  377. return request_parameters.form_parameters
  378. class InputFilesControllerWrapper(InputControllerWrapper):
  379. def update_hapic_data(
  380. self, hapic_data: HapicData,
  381. processed_data: typing.Any,
  382. ) -> None:
  383. hapic_data.files = processed_data
  384. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  385. return request_parameters.files_parameters
  386. class ExceptionHandlerControllerWrapper(ControllerWrapper):
  387. """
  388. This wrapper is used to wrap a controller and catch given exception if
  389. raised. An error will be generated in collaboration with context and
  390. returned.
  391. """
  392. def __init__(
  393. self,
  394. handled_exception_class: typing.Type[Exception],
  395. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  396. error_builder: typing.Union[ErrorBuilderInterface, typing.Callable[[], ErrorBuilderInterface]], # nopep8
  397. http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  398. ) -> None:
  399. self.handled_exception_class = handled_exception_class
  400. self._context = context
  401. self.http_code = http_code
  402. self._error_builder = error_builder
  403. @property
  404. def context(self) -> ContextInterface:
  405. if callable(self._context):
  406. return self._context()
  407. return self._context
  408. @property
  409. def error_builder(self) -> ErrorBuilderInterface:
  410. if callable(self._error_builder):
  411. return self._error_builder()
  412. return self._error_builder
  413. def _execute_wrapped_function(
  414. self,
  415. func,
  416. func_args,
  417. func_kwargs,
  418. ) -> typing.Any:
  419. try:
  420. return super()._execute_wrapped_function(
  421. func,
  422. func_args,
  423. func_kwargs,
  424. )
  425. except self.handled_exception_class as exc:
  426. response_content = self.error_builder.build_from_exception(
  427. exc,
  428. include_traceback=self.context.is_debug(),
  429. )
  430. # Check error format
  431. dumped = self.error_builder.dump(response_content).data
  432. unmarshall = self.error_builder.load(dumped)
  433. if unmarshall.errors:
  434. raise OutputValidationException(
  435. 'Validation error during dump of error response: {}'
  436. .format(
  437. str(unmarshall.errors)
  438. )
  439. )
  440. error_response = self.context.get_response(
  441. json.dumps(dumped),
  442. self.http_code,
  443. )
  444. return error_response