context.py 5.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  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. from flask import Flask
  19. from flask import send_from_directory
  20. if typing.TYPE_CHECKING:
  21. from flask import Response
  22. # flask regular expression to locate url parameters
  23. FLASK_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
  24. class FlaskContext(BaseContext):
  25. def __init__(
  26. self,
  27. app: Flask,
  28. default_error_builder: ErrorBuilderInterface=None,
  29. debug: bool = False,
  30. ):
  31. self._handled_exceptions = [] # type: typing.List[HandledException] # nopep8
  32. self.app = app
  33. self.default_error_builder = \
  34. default_error_builder or DefaultErrorBuilder() # FDV
  35. self.debug = debug
  36. def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
  37. from flask import request
  38. return RequestParameters(
  39. path_parameters=request.view_args,
  40. query_parameters=request.args, # TODO: Check
  41. body_parameters=request.get_json(), # TODO: Check
  42. form_parameters=request.form,
  43. header_parameters=request.headers,
  44. files_parameters=request.files,
  45. )
  46. def get_response(
  47. self,
  48. response: str,
  49. http_code: int,
  50. mimetype: str='application/json',
  51. ) -> 'Response':
  52. from flask import Response
  53. return Response(
  54. response=response,
  55. mimetype=mimetype,
  56. status=http_code,
  57. )
  58. def get_validation_error_response(
  59. self,
  60. error: ProcessValidationError,
  61. http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  62. ) -> typing.Any:
  63. error_content = self.default_error_builder.build_from_validation_error(
  64. error,
  65. )
  66. # Check error
  67. dumped = self.default_error_builder.dump(error).data
  68. unmarshall = self.default_error_builder.load(dumped)
  69. if unmarshall.errors:
  70. raise OutputValidationException(
  71. 'Validation error during dump of error response: {}'.format(
  72. str(unmarshall.errors)
  73. )
  74. )
  75. from flask import Response
  76. return Response(
  77. response=json.dumps(error_content),
  78. mimetype='application/json',
  79. status=int(http_code),
  80. )
  81. def find_route(
  82. self,
  83. decorated_controller: 'DecoratedController',
  84. ):
  85. reference = decorated_controller.reference
  86. for route in self.app.url_map.iter_rules():
  87. if route.endpoint not in self.app.view_functions:
  88. continue
  89. route_callback = self.app.view_functions[route.endpoint]
  90. route_token = getattr(
  91. route_callback,
  92. DECORATION_ATTRIBUTE_NAME,
  93. None,
  94. )
  95. match_with_wrapper = route_callback == reference.wrapper
  96. match_with_wrapped = route_callback == reference.wrapped
  97. match_with_token = route_token == reference.token
  98. # FIXME - G.M - 2017-12-04 - return list instead of one method
  99. # This fix, return only 1 allowed method, change this when
  100. # RouteRepresentation is adapted to return multiples methods.
  101. method = [x for x in route.methods
  102. if x not in ['OPTIONS', 'HEAD']][0]
  103. if match_with_wrapper or match_with_wrapped or match_with_token:
  104. return RouteRepresentation(
  105. rule=self.get_swagger_path(route.rule),
  106. method=method,
  107. original_route_object=route,
  108. )
  109. def get_swagger_path(self, contextualised_rule: str) -> str:
  110. # TODO - G.M - 2017-12-05 Check if all route path are handled correctly
  111. return FLASK_RE_PATH_URL.sub(r'{\1}', contextualised_rule)
  112. def by_pass_output_wrapping(self, response: typing.Any) -> bool:
  113. from flask import Response
  114. return isinstance(response, Response)
  115. def add_view(
  116. self,
  117. route: str,
  118. http_method: str,
  119. view_func: typing.Callable[..., typing.Any],
  120. ) -> None:
  121. self.app.add_url_rule(
  122. methods=[http_method],
  123. rule=route,
  124. view_func=view_func,
  125. )
  126. def serve_directory(
  127. self,
  128. route_prefix: str,
  129. directory_path: str,
  130. ) -> None:
  131. if not route_prefix.endswith('/'):
  132. route_prefix = '{}/'.format(route_prefix)
  133. @self.app.route(
  134. route_prefix,
  135. defaults={
  136. 'path': 'index.html',
  137. }
  138. )
  139. @self.app.route(
  140. '{}<path:path>'.format(route_prefix),
  141. )
  142. def api_doc(path):
  143. return send_from_directory(directory_path, path)
  144. def _add_exception_class_to_catch(
  145. self,
  146. exception_class: typing.Type[Exception],
  147. http_code: int,
  148. ) -> None:
  149. raise NotImplementedError('TODO')
  150. def is_debug(self) -> bool:
  151. return self.debug