decorator.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  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. def get_wrapper(
  291. self,
  292. func: 'typing.Callable[..., typing.Any]',
  293. ) -> 'typing.Callable[..., typing.Any]':
  294. # async def wrapper(*args, **kwargs) -> typing.Any:
  295. async def wrapper(*args, **kwargs) -> typing.Any:
  296. # Note: Design of before_wrapped_func can be to update kwargs
  297. # by reference here
  298. replacement_response = self.before_wrapped_func(args, kwargs)
  299. if replacement_response is not None:
  300. return replacement_response
  301. stream_response = await self.context.get_stream_response_object(
  302. args,
  303. kwargs,
  304. )
  305. async for stream_item in await self._execute_wrapped_function(
  306. func,
  307. args,
  308. kwargs,
  309. ):
  310. serialized_item = self._get_serialized_item(stream_item)
  311. await self.context.feed_stream_response(
  312. stream_response,
  313. serialized_item,
  314. )
  315. return stream_response
  316. return functools.update_wrapper(wrapper, func)
  317. def _get_serialized_item(
  318. self,
  319. item_object: typing.Any,
  320. ) -> dict:
  321. try:
  322. return self.processor.process(item_object)
  323. except ProcessException:
  324. # TODO BS 2018-07-25: Must interrupt stream response: but how
  325. # inform about error ?
  326. raise NotImplementedError('todo')
  327. class OutputHeadersControllerWrapper(OutputControllerWrapper):
  328. pass
  329. class OutputFileControllerWrapper(ControllerWrapper):
  330. def __init__(
  331. self,
  332. output_types: typing.List[str],
  333. default_http_code: HTTPStatus=HTTPStatus.OK,
  334. ) -> None:
  335. self.output_types = output_types
  336. self.default_http_code = default_http_code
  337. class InputPathControllerWrapper(InputControllerWrapper):
  338. def update_hapic_data(
  339. self, hapic_data: HapicData,
  340. processed_data: typing.Any,
  341. ) -> None:
  342. hapic_data.path = processed_data
  343. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  344. return request_parameters.path_parameters
  345. class InputQueryControllerWrapper(InputControllerWrapper):
  346. def __init__(
  347. self,
  348. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  349. processor: ProcessorInterface,
  350. error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  351. default_http_code: HTTPStatus=HTTPStatus.OK,
  352. as_list: typing.List[str]=None
  353. ) -> None:
  354. super().__init__(
  355. context,
  356. processor,
  357. error_http_code,
  358. default_http_code,
  359. )
  360. self.as_list = as_list or [] # FDV
  361. def update_hapic_data(
  362. self, hapic_data: HapicData,
  363. processed_data: typing.Any,
  364. ) -> None:
  365. hapic_data.query = processed_data
  366. def get_parameters_data(self, request_parameters: RequestParameters) -> MultiDict: # nopep8
  367. # Parameters are updated considering eventual as_list parameters
  368. if self.as_list:
  369. query_parameters = MultiDict()
  370. for parameter_name in request_parameters.query_parameters.keys():
  371. if parameter_name in query_parameters:
  372. continue
  373. if parameter_name in self.as_list:
  374. query_parameters[parameter_name] = \
  375. request_parameters.query_parameters.getall(
  376. parameter_name,
  377. )
  378. else:
  379. query_parameters[parameter_name] = \
  380. request_parameters.query_parameters.get(
  381. parameter_name,
  382. )
  383. return query_parameters
  384. return request_parameters.query_parameters
  385. class InputBodyControllerWrapper(InputControllerWrapper):
  386. def update_hapic_data(
  387. self, hapic_data: HapicData,
  388. processed_data: typing.Any,
  389. ) -> None:
  390. hapic_data.body = processed_data
  391. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  392. return request_parameters.body_parameters
  393. # TODO BS 2018-07-23: This class is an async version of InputControllerWrapper
  394. # to permit async compatibility. Please re-think about code refact
  395. # TAG: REFACT_ASYNC
  396. class AsyncInputBodyControllerWrapper(AsyncInputControllerWrapper):
  397. def update_hapic_data(
  398. self, hapic_data: HapicData,
  399. processed_data: typing.Any,
  400. ) -> None:
  401. hapic_data.body = processed_data
  402. async def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  403. return await request_parameters.body_parameters
  404. async def get_error_response(
  405. self,
  406. request_parameters: RequestParameters,
  407. ) -> typing.Any:
  408. parameters_data = await self.get_parameters_data(request_parameters)
  409. error = self.processor.get_validation_error(parameters_data)
  410. error_response = self.context.get_validation_error_response(
  411. error,
  412. http_code=self.error_http_code,
  413. )
  414. return error_response
  415. class InputHeadersControllerWrapper(InputControllerWrapper):
  416. def update_hapic_data(
  417. self, hapic_data: HapicData,
  418. processed_data: typing.Any,
  419. ) -> None:
  420. hapic_data.headers = processed_data
  421. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  422. return request_parameters.header_parameters
  423. class InputFormsControllerWrapper(InputControllerWrapper):
  424. def update_hapic_data(
  425. self, hapic_data: HapicData,
  426. processed_data: typing.Any,
  427. ) -> None:
  428. hapic_data.forms = processed_data
  429. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  430. return request_parameters.form_parameters
  431. class InputFilesControllerWrapper(InputControllerWrapper):
  432. def update_hapic_data(
  433. self, hapic_data: HapicData,
  434. processed_data: typing.Any,
  435. ) -> None:
  436. hapic_data.files = processed_data
  437. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  438. return request_parameters.files_parameters
  439. class ExceptionHandlerControllerWrapper(ControllerWrapper):
  440. """
  441. This wrapper is used to wrap a controller and catch given exception if
  442. raised. An error will be generated in collaboration with context and
  443. returned.
  444. """
  445. def __init__(
  446. self,
  447. handled_exception_class: typing.Type[Exception],
  448. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  449. error_builder: typing.Union[ErrorBuilderInterface, typing.Callable[[], ErrorBuilderInterface]], # nopep8
  450. http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  451. ) -> None:
  452. self.handled_exception_class = handled_exception_class
  453. self._context = context
  454. self.http_code = http_code
  455. self._error_builder = error_builder
  456. @property
  457. def context(self) -> ContextInterface:
  458. if callable(self._context):
  459. return self._context()
  460. return self._context
  461. @property
  462. def error_builder(self) -> ErrorBuilderInterface:
  463. if callable(self._error_builder):
  464. return self._error_builder()
  465. return self._error_builder
  466. def _execute_wrapped_function(
  467. self,
  468. func,
  469. func_args,
  470. func_kwargs,
  471. ) -> typing.Any:
  472. try:
  473. return super()._execute_wrapped_function(
  474. func,
  475. func_args,
  476. func_kwargs,
  477. )
  478. except self.handled_exception_class as exc:
  479. response_content = self.error_builder.build_from_exception(
  480. exc,
  481. include_traceback=self.context.is_debug(),
  482. )
  483. # Check error format
  484. dumped = self.error_builder.dump(response_content).data
  485. unmarshall = self.error_builder.load(dumped)
  486. if unmarshall.errors:
  487. raise OutputValidationException(
  488. 'Validation error during dump of error response: {}'
  489. .format(
  490. str(unmarshall.errors)
  491. )
  492. )
  493. error_response = self.context.get_response(
  494. json.dumps(dumped),
  495. self.http_code,
  496. )
  497. return error_response