context.py 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. # coding: utf-8
  2. import json
  3. import re
  4. import typing
  5. from http import HTTPStatus
  6. from aiohttp.web_request import Request
  7. from aiohttp.web_response import Response
  8. from multidict import MultiDict
  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.error import ErrorBuilderInterface
  14. from hapic.error import DefaultErrorBuilder
  15. from hapic.exception import WorkflowException
  16. from hapic.exception import OutputValidationException
  17. from hapic.exception import NoRoutesException
  18. from hapic.exception import RouteNotFound
  19. from hapic.processor import ProcessValidationError
  20. from hapic.processor import RequestParameters
  21. from aiohttp import web
  22. # Aiohttp regular expression to locate url parameters
  23. AIOHTTP_RE_PATH_URL = re.compile(r'{([^:<>]+)(?::[^<>]+)?}')
  24. class AiohttpRequestParameters(object):
  25. def __init__(
  26. self,
  27. request: Request,
  28. ) -> None:
  29. self._request = request
  30. self._parsed_body = None
  31. @property
  32. async def body_parameters(self) -> dict:
  33. if self._parsed_body is None:
  34. content_type = self.header_parameters.get('Content-Type')
  35. is_json = content_type == 'application/json'
  36. if is_json:
  37. self._parsed_body = await self._request.json()
  38. else:
  39. self._parsed_body = await self._request.post()
  40. return self._parsed_body
  41. @property
  42. def path_parameters(self):
  43. return dict(self._request.match_info)
  44. @property
  45. def query_parameters(self):
  46. return MultiDict(self._request.query.items())
  47. @property
  48. def form_parameters(self):
  49. # TODO BS 2018-07-24: There is misunderstanding around body/form/json
  50. return self.body_parameters
  51. @property
  52. def header_parameters(self):
  53. return dict(self._request.headers.items())
  54. @property
  55. def files_parameters(self):
  56. # TODO BS 2018-07-24: To do
  57. raise NotImplementedError('todo')
  58. class AiohttpContext(BaseContext):
  59. def __init__(
  60. self,
  61. app: web.Application,
  62. default_error_builder: ErrorBuilderInterface=None,
  63. debug: bool = False,
  64. ) -> None:
  65. self._app = app
  66. self._debug = debug
  67. self.default_error_builder = \
  68. default_error_builder or DefaultErrorBuilder() # FDV
  69. @property
  70. def app(self) -> web.Application:
  71. return self._app
  72. def get_request_parameters(
  73. self,
  74. *args,
  75. **kwargs
  76. ) -> RequestParameters:
  77. try:
  78. request = args[0]
  79. except IndexError:
  80. raise WorkflowException(
  81. 'Unable to get aiohttp request object',
  82. )
  83. request = typing.cast(Request, request)
  84. return AiohttpRequestParameters(request)
  85. def get_response(
  86. self,
  87. response: str,
  88. http_code: int,
  89. mimetype: str = 'application/json',
  90. ) -> typing.Any:
  91. return Response(
  92. body=response,
  93. status=http_code,
  94. content_type=mimetype,
  95. )
  96. def get_validation_error_response(
  97. self,
  98. error: ProcessValidationError,
  99. http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  100. ) -> typing.Any:
  101. error_builder = self.get_default_error_builder()
  102. error_content = error_builder.build_from_validation_error(
  103. error,
  104. )
  105. # Check error
  106. dumped = error_builder.dump(error_content).data
  107. unmarshall = error_builder.load(dumped)
  108. if unmarshall.errors:
  109. raise OutputValidationException(
  110. 'Validation error during dump of error response: {}'.format(
  111. str(unmarshall.errors)
  112. )
  113. )
  114. return web.Response(
  115. text=json.dumps(dumped),
  116. headers=[
  117. ('Content-Type', 'application/json'),
  118. ],
  119. status=int(http_code),
  120. )
  121. def find_route(
  122. self,
  123. decorated_controller: DecoratedController,
  124. ) -> RouteRepresentation:
  125. if not len(self.app.router.routes()):
  126. raise NoRoutesException('There is no routes in your aiohttp app')
  127. reference = decorated_controller.reference
  128. for route in self.app.router.routes():
  129. route_token = getattr(
  130. route.handler,
  131. DECORATION_ATTRIBUTE_NAME,
  132. None,
  133. )
  134. match_with_wrapper = route.handler == reference.wrapper
  135. match_with_wrapped = route.handler == reference.wrapped
  136. match_with_token = route_token == reference.token
  137. # TODO BS 2018-07-27: token is set in HEAD view to, must solve this
  138. # case
  139. if not match_with_wrapper \
  140. and not match_with_wrapped \
  141. and match_with_token \
  142. and route.method.lower() == 'head':
  143. continue
  144. if match_with_wrapper or match_with_wrapped or match_with_token:
  145. return RouteRepresentation(
  146. rule=self.get_swagger_path(route.resource.canonical),
  147. method=route.method.lower(),
  148. original_route_object=route,
  149. )
  150. # TODO BS 20171010: Raise exception or print error ? see #10
  151. raise RouteNotFound(
  152. 'Decorated route "{}" was not found in aiohttp routes'.format(
  153. decorated_controller.name,
  154. )
  155. )
  156. def get_swagger_path(
  157. self,
  158. contextualised_rule: str,
  159. ) -> str:
  160. return AIOHTTP_RE_PATH_URL.sub(r'{\1}', contextualised_rule)
  161. def by_pass_output_wrapping(
  162. self,
  163. response: typing.Any,
  164. ) -> bool:
  165. return isinstance(response, web.Response)
  166. def add_view(
  167. self,
  168. route: str,
  169. http_method: str,
  170. view_func: typing.Callable[..., typing.Any],
  171. ) -> None:
  172. # TODO BS 2018-07-15: to do
  173. raise NotImplementedError('todo')
  174. def serve_directory(
  175. self,
  176. route_prefix: str,
  177. directory_path: str,
  178. ) -> None:
  179. # TODO BS 2018-07-15: to do
  180. raise NotImplementedError('todo')
  181. def is_debug(
  182. self,
  183. ) -> bool:
  184. return self._debug
  185. def handle_exception(
  186. self,
  187. exception_class: typing.Type[Exception],
  188. http_code: int,
  189. ) -> None:
  190. # TODO BS 2018-07-15: to do
  191. raise NotImplementedError('todo')
  192. def handle_exceptions(
  193. self,
  194. exception_classes: typing.List[typing.Type[Exception]],
  195. http_code: int,
  196. ) -> None:
  197. # TODO BS 2018-07-15: to do
  198. raise NotImplementedError('todo')
  199. async def get_stream_response_object(
  200. self,
  201. func_args,
  202. func_kwargs,
  203. http_code: HTTPStatus = HTTPStatus.OK,
  204. headers: dict = None,
  205. ) -> web.StreamResponse:
  206. headers = headers or {
  207. 'Content-Type': 'text/plain; charset=utf-8',
  208. }
  209. response = web.StreamResponse(
  210. status=http_code,
  211. headers=headers,
  212. )
  213. try:
  214. request = func_args[0]
  215. except IndexError:
  216. raise WorkflowException(
  217. 'Unable to get aiohttp request object',
  218. )
  219. request = typing.cast(Request, request)
  220. await response.prepare(request)
  221. return response
  222. async def feed_stream_response(
  223. self,
  224. stream_response: web.StreamResponse,
  225. serialized_item: dict,
  226. ) -> None:
  227. await stream_response.write(
  228. # FIXME BS 2018-07-25: need \n :/
  229. json.dumps(serialized_item).encode('utf-8') + b'\n',
  230. )