context.py 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. # coding: utf-8
  2. import asyncio
  3. import json
  4. import typing
  5. from http import HTTPStatus
  6. from json import JSONDecodeError
  7. from aiohttp.web_request import Request
  8. from aiohttp.web_response import Response
  9. from multidict import MultiDict
  10. from hapic.context import BaseContext
  11. from hapic.context import RouteRepresentation
  12. from hapic.decorator import DecoratedController
  13. from hapic.error import ErrorBuilderInterface, DefaultErrorBuilder
  14. from hapic.exception import WorkflowException, OutputValidationException
  15. from hapic.processor import ProcessValidationError
  16. from hapic.processor import RequestParameters
  17. from aiohttp import web
  18. class AiohttpRequestParameters(object):
  19. def __init__(
  20. self,
  21. request: Request,
  22. ) -> None:
  23. self._request = request
  24. self._parsed_body = None
  25. @property
  26. async def body_parameters(self) -> dict:
  27. if self._parsed_body is None:
  28. content_type = self.header_parameters.get('Content-Type')
  29. is_json = content_type == 'application/json'
  30. if is_json:
  31. self._parsed_body = await self._request.json()
  32. else:
  33. self._parsed_body = await self._request.post()
  34. return self._parsed_body
  35. @property
  36. def path_parameters(self):
  37. return dict(self._request.match_info)
  38. @property
  39. def query_parameters(self):
  40. return MultiDict(self._request.query.items())
  41. @property
  42. def form_parameters(self):
  43. # TODO BS 2018-07-24: There is misunderstanding around body/form/json
  44. return self.body_parameters
  45. @property
  46. def header_parameters(self):
  47. return dict(self._request.headers.items())
  48. @property
  49. def files_parameters(self):
  50. # TODO BS 2018-07-24: To do
  51. raise NotImplementedError('todo')
  52. class AiohttpContext(BaseContext):
  53. def __init__(
  54. self,
  55. app: web.Application,
  56. default_error_builder: ErrorBuilderInterface=None,
  57. debug: bool = False,
  58. ) -> None:
  59. self._app = app
  60. self._debug = debug
  61. self.default_error_builder = \
  62. default_error_builder or DefaultErrorBuilder() # FDV
  63. @property
  64. def app(self) -> web.Application:
  65. return self._app
  66. def get_request_parameters(
  67. self,
  68. *args,
  69. **kwargs
  70. ) -> RequestParameters:
  71. try:
  72. request = args[0]
  73. except IndexError:
  74. raise WorkflowException(
  75. 'Unable to get aiohttp request object',
  76. )
  77. request = typing.cast(Request, request)
  78. return AiohttpRequestParameters(request)
  79. def get_response(
  80. self,
  81. response: str,
  82. http_code: int,
  83. mimetype: str = 'application/json',
  84. ) -> typing.Any:
  85. return Response(
  86. body=response,
  87. status=http_code,
  88. content_type=mimetype,
  89. )
  90. def get_validation_error_response(
  91. self,
  92. error: ProcessValidationError,
  93. http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  94. ) -> typing.Any:
  95. error_builder = self.get_default_error_builder()
  96. error_content = error_builder.build_from_validation_error(
  97. error,
  98. )
  99. # Check error
  100. dumped = error_builder.dump(error_content).data
  101. unmarshall = error_builder.load(dumped)
  102. if unmarshall.errors:
  103. raise OutputValidationException(
  104. 'Validation error during dump of error response: {}'.format(
  105. str(unmarshall.errors)
  106. )
  107. )
  108. return web.Response(
  109. text=json.dumps(dumped),
  110. headers=[
  111. ('Content-Type', 'application/json'),
  112. ],
  113. status=int(http_code),
  114. )
  115. def find_route(
  116. self,
  117. decorated_controller: DecoratedController,
  118. ) -> RouteRepresentation:
  119. # TODO BS 2018-07-15: to do
  120. raise NotImplementedError('todo')
  121. def get_swagger_path(
  122. self,
  123. contextualised_rule: str,
  124. ) -> str:
  125. # TODO BS 2018-07-15: to do
  126. raise NotImplementedError('todo')
  127. def by_pass_output_wrapping(
  128. self,
  129. response: typing.Any,
  130. ) -> bool:
  131. return isinstance(response, web.Response)
  132. def add_view(
  133. self,
  134. route: str,
  135. http_method: str,
  136. view_func: typing.Callable[..., typing.Any],
  137. ) -> None:
  138. # TODO BS 2018-07-15: to do
  139. raise NotImplementedError('todo')
  140. def serve_directory(
  141. self,
  142. route_prefix: str,
  143. directory_path: str,
  144. ) -> None:
  145. # TODO BS 2018-07-15: to do
  146. raise NotImplementedError('todo')
  147. def is_debug(
  148. self,
  149. ) -> bool:
  150. return self._debug
  151. def handle_exception(
  152. self,
  153. exception_class: typing.Type[Exception],
  154. http_code: int,
  155. ) -> None:
  156. # TODO BS 2018-07-15: to do
  157. raise NotImplementedError('todo')
  158. def handle_exceptions(
  159. self,
  160. exception_classes: typing.List[typing.Type[Exception]],
  161. http_code: int,
  162. ) -> None:
  163. # TODO BS 2018-07-15: to do
  164. raise NotImplementedError('todo')
  165. async def get_stream_response_object(
  166. self,
  167. func_args,
  168. func_kwargs,
  169. http_code: HTTPStatus = HTTPStatus.OK,
  170. headers: dict = None,
  171. ) -> web.StreamResponse:
  172. headers = headers or {
  173. 'Content-Type': 'text/plain; charset=utf-8',
  174. }
  175. response = web.StreamResponse(
  176. status=http_code,
  177. headers=headers,
  178. )
  179. try:
  180. request = func_args[0]
  181. except IndexError:
  182. raise WorkflowException(
  183. 'Unable to get aiohttp request object',
  184. )
  185. request = typing.cast(Request, request)
  186. await response.prepare(request)
  187. return response
  188. async def feed_stream_response(
  189. self,
  190. stream_response: web.StreamResponse,
  191. serialized_item: dict,
  192. ) -> None:
  193. await stream_response.write(
  194. # FIXME BS 2018-07-25: need \n :/
  195. json.dumps(serialized_item).encode('utf-8') + b'\n',
  196. )