decorator.py 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  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 is not None:
  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 is not None:
  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 = await 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. # TODO BS 2018-07-23: This class is an async version of
  270. # OutputBodyControllerWrapper (ControllerWrapper.get_wrapper rewrite)
  271. # to permit async compatibility.
  272. # Please re-think about code refact. TAG: REFACT_ASYNC
  273. class AsyncOutputBodyControllerWrapper(OutputControllerWrapper):
  274. def get_wrapper(
  275. self,
  276. func: 'typing.Callable[..., typing.Any]',
  277. ) -> 'typing.Callable[..., typing.Any]':
  278. # async def wrapper(*args, **kwargs) -> typing.Any:
  279. async def wrapper(*args, **kwargs) -> typing.Any:
  280. # Note: Design of before_wrapped_func can be to update kwargs
  281. # by reference here
  282. replacement_response = self.before_wrapped_func(args, kwargs)
  283. if replacement_response is not None:
  284. return replacement_response
  285. response = await self._execute_wrapped_function(func, args, kwargs)
  286. new_response = self.after_wrapped_function(response)
  287. return new_response
  288. return functools.update_wrapper(wrapper, func)
  289. class AsyncOutputStreamControllerWrapper(OutputControllerWrapper):
  290. """
  291. This controller wrapper produce a wrapper who caught the http view items
  292. to check and serialize them into a stream response.
  293. """
  294. def __init__(
  295. self,
  296. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  297. processor: ProcessorInterface,
  298. error_http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  299. default_http_code: HTTPStatus=HTTPStatus.OK,
  300. ignore_on_error: bool = True,
  301. ) -> None:
  302. super().__init__(
  303. context,
  304. processor,
  305. error_http_code,
  306. default_http_code,
  307. )
  308. self.ignore_on_error = ignore_on_error
  309. def get_wrapper(
  310. self,
  311. func: 'typing.Callable[..., typing.Any]',
  312. ) -> 'typing.Callable[..., typing.Any]':
  313. # async def wrapper(*args, **kwargs) -> typing.Any:
  314. async def wrapper(*args, **kwargs) -> typing.Any:
  315. # Note: Design of before_wrapped_func can be to update kwargs
  316. # by reference here
  317. replacement_response = self.before_wrapped_func(args, kwargs)
  318. if replacement_response is not None:
  319. return replacement_response
  320. stream_response = await self.context.get_stream_response_object(
  321. args,
  322. kwargs,
  323. )
  324. async for stream_item in await self._execute_wrapped_function(
  325. func,
  326. args,
  327. kwargs,
  328. ):
  329. try:
  330. serialized_item = self._get_serialized_item(stream_item)
  331. await self.context.feed_stream_response(
  332. stream_response,
  333. serialized_item,
  334. )
  335. except OutputValidationException as exc:
  336. if not self.ignore_on_error:
  337. # TODO BS 2018-07-31: Something should inform about
  338. # error, a log ?
  339. return stream_response
  340. return stream_response
  341. return functools.update_wrapper(wrapper, func)
  342. def _get_serialized_item(
  343. self,
  344. item_object: typing.Any,
  345. ) -> dict:
  346. return self.processor.process(item_object)
  347. class OutputHeadersControllerWrapper(OutputControllerWrapper):
  348. pass
  349. class OutputFileControllerWrapper(ControllerWrapper):
  350. def __init__(
  351. self,
  352. output_types: typing.List[str],
  353. default_http_code: HTTPStatus=HTTPStatus.OK,
  354. ) -> None:
  355. self.output_types = output_types
  356. self.default_http_code = default_http_code
  357. class InputPathControllerWrapper(InputControllerWrapper):
  358. def update_hapic_data(
  359. self, hapic_data: HapicData,
  360. processed_data: typing.Any,
  361. ) -> None:
  362. hapic_data.path = processed_data
  363. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  364. return request_parameters.path_parameters
  365. class InputQueryControllerWrapper(InputControllerWrapper):
  366. def __init__(
  367. self,
  368. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  369. processor: ProcessorInterface,
  370. error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  371. default_http_code: HTTPStatus=HTTPStatus.OK,
  372. as_list: typing.List[str]=None
  373. ) -> None:
  374. super().__init__(
  375. context,
  376. processor,
  377. error_http_code,
  378. default_http_code,
  379. )
  380. self.as_list = as_list or [] # FDV
  381. def update_hapic_data(
  382. self, hapic_data: HapicData,
  383. processed_data: typing.Any,
  384. ) -> None:
  385. hapic_data.query = processed_data
  386. def get_parameters_data(self, request_parameters: RequestParameters) -> MultiDict: # nopep8
  387. # Parameters are updated considering eventual as_list parameters
  388. if self.as_list:
  389. query_parameters = MultiDict()
  390. for parameter_name in request_parameters.query_parameters.keys():
  391. if parameter_name in query_parameters:
  392. continue
  393. if parameter_name in self.as_list:
  394. query_parameters[parameter_name] = \
  395. request_parameters.query_parameters.getall(
  396. parameter_name,
  397. )
  398. else:
  399. query_parameters[parameter_name] = \
  400. request_parameters.query_parameters.get(
  401. parameter_name,
  402. )
  403. return query_parameters
  404. return request_parameters.query_parameters
  405. class InputBodyControllerWrapper(InputControllerWrapper):
  406. def update_hapic_data(
  407. self, hapic_data: HapicData,
  408. processed_data: typing.Any,
  409. ) -> None:
  410. hapic_data.body = processed_data
  411. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  412. return request_parameters.body_parameters
  413. # TODO BS 2018-07-23: This class is an async version of InputControllerWrapper
  414. # to permit async compatibility. Please re-think about code refact
  415. # TAG: REFACT_ASYNC
  416. class AsyncInputBodyControllerWrapper(AsyncInputControllerWrapper):
  417. def update_hapic_data(
  418. self, hapic_data: HapicData,
  419. processed_data: typing.Any,
  420. ) -> None:
  421. hapic_data.body = processed_data
  422. async def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  423. return await request_parameters.body_parameters
  424. async def get_error_response(
  425. self,
  426. request_parameters: RequestParameters,
  427. ) -> typing.Any:
  428. parameters_data = await self.get_parameters_data(request_parameters)
  429. error = self.processor.get_validation_error(parameters_data)
  430. error_response = self.context.get_validation_error_response(
  431. error,
  432. http_code=self.error_http_code,
  433. )
  434. return error_response
  435. class InputHeadersControllerWrapper(InputControllerWrapper):
  436. def update_hapic_data(
  437. self, hapic_data: HapicData,
  438. processed_data: typing.Any,
  439. ) -> None:
  440. hapic_data.headers = processed_data
  441. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  442. return request_parameters.header_parameters
  443. class InputFormsControllerWrapper(InputControllerWrapper):
  444. def update_hapic_data(
  445. self, hapic_data: HapicData,
  446. processed_data: typing.Any,
  447. ) -> None:
  448. hapic_data.forms = processed_data
  449. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  450. return request_parameters.form_parameters
  451. class InputFilesControllerWrapper(InputControllerWrapper):
  452. def update_hapic_data(
  453. self, hapic_data: HapicData,
  454. processed_data: typing.Any,
  455. ) -> None:
  456. hapic_data.files = processed_data
  457. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  458. return request_parameters.files_parameters
  459. class ExceptionHandlerControllerWrapper(ControllerWrapper):
  460. """
  461. This wrapper is used to wrap a controller and catch given exception if
  462. raised. An error will be generated in collaboration with context and
  463. returned.
  464. """
  465. def __init__(
  466. self,
  467. handled_exception_class: typing.Type[Exception],
  468. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  469. error_builder: typing.Union[ErrorBuilderInterface, typing.Callable[[], ErrorBuilderInterface]], # nopep8
  470. http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  471. ) -> None:
  472. self.handled_exception_class = handled_exception_class
  473. self._context = context
  474. self.http_code = http_code
  475. self._error_builder = error_builder
  476. @property
  477. def context(self) -> ContextInterface:
  478. if callable(self._context):
  479. return self._context()
  480. return self._context
  481. @property
  482. def error_builder(self) -> ErrorBuilderInterface:
  483. if callable(self._error_builder):
  484. return self._error_builder()
  485. return self._error_builder
  486. def _execute_wrapped_function(
  487. self,
  488. func,
  489. func_args,
  490. func_kwargs,
  491. ) -> typing.Any:
  492. try:
  493. return super()._execute_wrapped_function(
  494. func,
  495. func_args,
  496. func_kwargs,
  497. )
  498. except self.handled_exception_class as exc:
  499. return self._build_error_response(exc)
  500. def _build_error_response(self, exc: Exception) -> typing.Any:
  501. response_content = self.error_builder.build_from_exception(
  502. exc,
  503. include_traceback=self.context.is_debug(),
  504. )
  505. # Check error format
  506. dumped = self.error_builder.dump(response_content).data
  507. unmarshall = self.error_builder.load(dumped)
  508. if unmarshall.errors:
  509. raise OutputValidationException(
  510. 'Validation error during dump of error response: {}'
  511. .format(
  512. str(unmarshall.errors)
  513. )
  514. )
  515. error_response = self.context.get_response(
  516. json.dumps(dumped),
  517. self.http_code,
  518. )
  519. return error_response
  520. # TODO BS 2018-07-23: This class is an async version of
  521. # ExceptionHandlerControllerWrapper
  522. # to permit async compatibility. Please re-think about code refact
  523. # TAG: REFACT_ASYNC
  524. class AsyncExceptionHandlerControllerWrapper(ExceptionHandlerControllerWrapper):
  525. def get_wrapper(
  526. self,
  527. func: 'typing.Callable[..., typing.Any]',
  528. ) -> 'typing.Callable[..., typing.Any]':
  529. # async def wrapper(*args, **kwargs) -> typing.Any:
  530. async def wrapper(*args, **kwargs) -> typing.Any:
  531. # Note: Design of before_wrapped_func can be to update kwargs
  532. # by reference here
  533. replacement_response = self.before_wrapped_func(args, kwargs)
  534. if replacement_response is not None:
  535. return replacement_response
  536. response = await self._execute_wrapped_function(func, args, kwargs)
  537. new_response = self.after_wrapped_function(response)
  538. return new_response
  539. return functools.update_wrapper(wrapper, func)
  540. async def _execute_wrapped_function(
  541. self,
  542. func,
  543. func_args,
  544. func_kwargs,
  545. ) -> typing.Any:
  546. try:
  547. return await super()._execute_wrapped_function(
  548. func,
  549. func_args,
  550. func_kwargs,
  551. )
  552. except self.handled_exception_class as exc:
  553. return self._build_error_response(exc)