decorator.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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. 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 = self.before_wrapped_func(args, kwargs)
  67. if replacement_response:
  68. return replacement_response
  69. response = 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. class OutputControllerWrapper(InputOutputControllerWrapper):
  165. def __init__(
  166. self,
  167. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  168. processor: ProcessorInterface,
  169. error_http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  170. default_http_code: HTTPStatus=HTTPStatus.OK,
  171. ) -> None:
  172. super().__init__(
  173. context,
  174. processor,
  175. error_http_code,
  176. default_http_code,
  177. )
  178. def get_error_response(
  179. self,
  180. response: typing.Any,
  181. ) -> typing.Any:
  182. error = self.processor.get_validation_error(response)
  183. error_response = self.context.get_validation_error_response(
  184. error,
  185. http_code=self.error_http_code,
  186. )
  187. return error_response
  188. def after_wrapped_function(self, response: typing.Any) -> typing.Any:
  189. try:
  190. if self.context.by_pass_output_wrapping(response):
  191. return response
  192. processed_response = self.processor.process(response)
  193. prepared_response = self.context.get_response(
  194. json.dumps(processed_response),
  195. self.default_http_code,
  196. )
  197. return prepared_response
  198. except ProcessException:
  199. # TODO: ici ou ailleurs: il faut pas forcement donner le detail
  200. # de l'erreur (mode debug par exemple) see #8
  201. error_response = self.get_error_response(response)
  202. return error_response
  203. class DecoratedController(object):
  204. def __init__(
  205. self,
  206. reference: ControllerReference,
  207. description: ControllerDescription,
  208. name: str='',
  209. ) -> None:
  210. self._reference = reference
  211. self._description = description
  212. self._name = name
  213. @property
  214. def reference(self) -> ControllerReference:
  215. return self._reference
  216. @property
  217. def description(self) -> ControllerDescription:
  218. return self._description
  219. @property
  220. def name(self) -> str:
  221. return self._name
  222. class OutputBodyControllerWrapper(OutputControllerWrapper):
  223. pass
  224. class OutputHeadersControllerWrapper(OutputControllerWrapper):
  225. pass
  226. class OutputFileControllerWrapper(ControllerWrapper):
  227. def __init__(
  228. self,
  229. output_types: typing.List[str],
  230. default_http_code: HTTPStatus=HTTPStatus.OK,
  231. ) -> None:
  232. self.output_types = output_types
  233. self.default_http_code = default_http_code
  234. class InputPathControllerWrapper(InputControllerWrapper):
  235. def update_hapic_data(
  236. self, hapic_data: HapicData,
  237. processed_data: typing.Any,
  238. ) -> None:
  239. hapic_data.path = processed_data
  240. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  241. return request_parameters.path_parameters
  242. class InputQueryControllerWrapper(InputControllerWrapper):
  243. def __init__(
  244. self,
  245. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  246. processor: ProcessorInterface,
  247. error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  248. default_http_code: HTTPStatus=HTTPStatus.OK,
  249. as_list: typing.List[str]=None
  250. ) -> None:
  251. super().__init__(
  252. context,
  253. processor,
  254. error_http_code,
  255. default_http_code,
  256. )
  257. self.as_list = as_list or [] # FDV
  258. def update_hapic_data(
  259. self, hapic_data: HapicData,
  260. processed_data: typing.Any,
  261. ) -> None:
  262. hapic_data.query = processed_data
  263. def get_parameters_data(self, request_parameters: RequestParameters) -> MultiDict: # nopep8
  264. # Parameters are updated considering eventual as_list parameters
  265. if self.as_list:
  266. query_parameters = MultiDict()
  267. for parameter_name in request_parameters.query_parameters.keys():
  268. if parameter_name in query_parameters:
  269. continue
  270. if parameter_name in self.as_list:
  271. query_parameters[parameter_name] = \
  272. request_parameters.query_parameters.getall(
  273. parameter_name,
  274. )
  275. else:
  276. query_parameters[parameter_name] = \
  277. request_parameters.query_parameters.get(
  278. parameter_name,
  279. )
  280. return query_parameters
  281. return request_parameters.query_parameters
  282. class InputBodyControllerWrapper(InputControllerWrapper):
  283. def update_hapic_data(
  284. self, hapic_data: HapicData,
  285. processed_data: typing.Any,
  286. ) -> None:
  287. hapic_data.body = processed_data
  288. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  289. return request_parameters.body_parameters
  290. class InputHeadersControllerWrapper(InputControllerWrapper):
  291. def update_hapic_data(
  292. self, hapic_data: HapicData,
  293. processed_data: typing.Any,
  294. ) -> None:
  295. hapic_data.headers = processed_data
  296. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  297. return request_parameters.header_parameters
  298. class InputFormsControllerWrapper(InputControllerWrapper):
  299. def update_hapic_data(
  300. self, hapic_data: HapicData,
  301. processed_data: typing.Any,
  302. ) -> None:
  303. hapic_data.forms = processed_data
  304. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  305. return request_parameters.form_parameters
  306. class InputFilesControllerWrapper(InputControllerWrapper):
  307. def update_hapic_data(
  308. self, hapic_data: HapicData,
  309. processed_data: typing.Any,
  310. ) -> None:
  311. hapic_data.files = processed_data
  312. def get_parameters_data(self, request_parameters: RequestParameters) -> dict: # nopep8
  313. return request_parameters.files_parameters
  314. class ExceptionHandlerControllerWrapper(ControllerWrapper):
  315. """
  316. This wrapper is used to wrap a controller and catch given exception if
  317. raised. An error will be generated in collaboration with context and
  318. returned.
  319. """
  320. def __init__(
  321. self,
  322. handled_exception_class: typing.Type[Exception],
  323. context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]], # nopep8
  324. error_builder: typing.Union[ErrorBuilderInterface, typing.Callable[[], ErrorBuilderInterface]], # nopep8
  325. http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
  326. ) -> None:
  327. self.handled_exception_class = handled_exception_class
  328. self._context = context
  329. self.http_code = http_code
  330. self._error_builder = error_builder
  331. @property
  332. def context(self) -> ContextInterface:
  333. if callable(self._context):
  334. return self._context()
  335. return self._context
  336. @property
  337. def error_builder(self) -> ErrorBuilderInterface:
  338. if callable(self._error_builder):
  339. return self._error_builder()
  340. return self._error_builder
  341. def _execute_wrapped_function(
  342. self,
  343. func,
  344. func_args,
  345. func_kwargs,
  346. ) -> typing.Any:
  347. try:
  348. return super()._execute_wrapped_function(
  349. func,
  350. func_args,
  351. func_kwargs,
  352. )
  353. except self.handled_exception_class as exc:
  354. response_content = self.error_builder.build_from_exception(exc)
  355. # Check error format
  356. dumped = self.error_builder.dump(response_content).data
  357. unmarshall = self.error_builder.load(dumped)
  358. if unmarshall.errors:
  359. raise OutputValidationException(
  360. 'Validation error during dump of error response: {}'
  361. .format(
  362. str(unmarshall.errors)
  363. )
  364. )
  365. error_response = self.context.get_response(
  366. json.dumps(response_content),
  367. self.http_code,
  368. )
  369. return error_response