context.py 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. # -*- coding: utf-8 -*-
  2. import json
  3. import re
  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 hapic.context import BaseContext
  10. from hapic.context import RouteRepresentation
  11. from hapic.decorator import DecoratedController
  12. from hapic.decorator import DECORATION_ATTRIBUTE_NAME
  13. from hapic.exception import OutputValidationException
  14. from hapic.processor import RequestParameters
  15. from hapic.processor import ProcessValidationError
  16. from hapic.error import DefaultErrorBuilder
  17. from hapic.error import ErrorBuilderInterface
  18. if typing.TYPE_CHECKING:
  19. from pyramid.response import Response
  20. from pyramid.config import Configurator
  21. # Bottle regular expression to locate url parameters
  22. PYRAMID_RE_PATH_URL = re.compile(r'')
  23. class PyramidContext(BaseContext):
  24. def __init__(
  25. self,
  26. configurator: 'Configurator',
  27. default_error_builder: ErrorBuilderInterface = None,
  28. debug: bool = False,
  29. ):
  30. self._handled_exceptions = [] # type: typing.List[HandledException] # nopep8
  31. self.configurator = configurator
  32. self.default_error_builder = \
  33. default_error_builder or DefaultErrorBuilder() # FDV
  34. self.debug = debug
  35. def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
  36. req = args[-1] # TODO : Check
  37. # TODO : move this code to check_json
  38. # same idea as in : https://bottlepy.org/docs/dev/_modules/bottle.html#BaseRequest.json
  39. if req.body and req.content_type in ('application/json', 'application/json-rpc'):
  40. json_body = req.json_body
  41. # TODO : raise exception if not correct , return 400 if uncorrect instead ?
  42. else:
  43. json_body = {}
  44. return RequestParameters(
  45. path_parameters=req.matchdict,
  46. query_parameters=req.GET,
  47. body_parameters=json_body,
  48. form_parameters=req.POST,
  49. header_parameters=req.headers,
  50. files_parameters={}, # TODO - G.M - 2017-11-05 - Code it
  51. )
  52. def get_response(
  53. self,
  54. response: str,
  55. http_code: int,
  56. mimetype: str='application/json',
  57. ) -> 'Response':
  58. # INFO - G.M - 20-04-2018 - No message_body for some http code,
  59. # no Content-Type needed if no content
  60. # see: https://tools.ietf.org/html/rfc2616#section-4.3
  61. if http_code in [204, 304] or (100 <= http_code <= 199):
  62. headers = []
  63. else:
  64. headers = [
  65. ('Content-Type', mimetype),
  66. ]
  67. from pyramid.response import Response
  68. return Response(
  69. body=response,
  70. headers=headers,
  71. status=http_code,
  72. )
  73. def get_validation_error_response(
  74. self,
  75. error: ProcessValidationError,
  76. http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  77. ) -> typing.Any:
  78. from pyramid.response import Response
  79. error_content = self.default_error_builder.build_from_validation_error(
  80. error,
  81. )
  82. # Check error
  83. dumped = self.default_error_builder.dump(error).data
  84. unmarshall = self.default_error_builder.load(dumped)
  85. if unmarshall.errors:
  86. raise OutputValidationException(
  87. 'Validation error during dump of error response: {}'.format(
  88. str(unmarshall.errors)
  89. )
  90. )
  91. return Response(
  92. body=json.dumps(error_content),
  93. headers=[
  94. ('Content-Type', 'application/json'),
  95. ],
  96. status=int(http_code),
  97. )
  98. def find_route(
  99. self,
  100. decorated_controller: DecoratedController,
  101. ) -> RouteRepresentation:
  102. for category in self.configurator.introspector.get_category('views'):
  103. view_intr = category['introspectable']
  104. route_intr = category['related']
  105. reference = decorated_controller.reference
  106. route_token = getattr(
  107. view_intr.get('callable'),
  108. DECORATION_ATTRIBUTE_NAME,
  109. None,
  110. )
  111. match_with_wrapper = view_intr.get('callable') == reference.wrapper
  112. match_with_wrapped = view_intr.get('callable') == reference.wrapped
  113. match_with_token = route_token == reference.token
  114. if match_with_wrapper or match_with_wrapped or match_with_token:
  115. # TODO BS 20171107: C'est une liste de route sous pyramid !!!
  116. # Mais de toute maniere les framework womme pyramid, flask
  117. # peuvent avoir un controlleur pour plusieurs routes doc
  118. # .find_route doit retourner une liste au lieu d'une seule
  119. # route
  120. route_pattern = route_intr[0].get('pattern')
  121. route_method = route_intr[0].get('request_methods')[0]
  122. return RouteRepresentation(
  123. rule=self.get_swagger_path(route_pattern),
  124. method=route_method,
  125. original_route_object=route_intr[0],
  126. )
  127. def get_swagger_path(self, contextualised_rule: str) -> str:
  128. # TODO BS 20171110: Pyramid allow route like '/{foo:\d+}', so adapt
  129. # and USE regular expression (see https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html#custom-route-predicates) # nopep8
  130. # INFO - G.M - 27-04-2018 - route_pattern of pyramid without '/' case.
  131. # For example, when using config.include with route_prefix param,
  132. # there is no '/' at beginning of the path.
  133. if contextualised_rule[0] != '/':
  134. contextualised_rule = '/{}'.format(contextualised_rule)
  135. return contextualised_rule
  136. def by_pass_output_wrapping(self, response: typing.Any) -> bool:
  137. return False
  138. def add_view(
  139. self,
  140. route: str,
  141. http_method: str,
  142. view_func: typing.Callable[..., typing.Any],
  143. ) -> None:
  144. self.configurator.add_route(
  145. name=route,
  146. path=route,
  147. request_method=http_method
  148. )
  149. self.configurator.add_view(
  150. view_func,
  151. route_name=route,
  152. )
  153. def serve_directory(
  154. self,
  155. route_prefix: str,
  156. directory_path: str,
  157. ) -> None:
  158. self.configurator.add_static_view(
  159. name=route_prefix,
  160. path=directory_path,
  161. )
  162. def _add_exception_class_to_catch(
  163. self,
  164. exception_class: typing.Type[Exception],
  165. http_code: int,
  166. ) -> None:
  167. raise NotImplementedError('TODO')
  168. def is_debug(self) -> bool:
  169. return self.debug