context.py 5.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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. import bottle
  10. from multidict import MultiDict
  11. from hapic.context import BaseContext
  12. from hapic.context import HandledException
  13. from hapic.context import RouteRepresentation
  14. from hapic.decorator import DecoratedController
  15. from hapic.decorator import DECORATION_ATTRIBUTE_NAME
  16. from hapic.exception import OutputValidationException
  17. from hapic.exception import NoRoutesException
  18. from hapic.exception import RouteNotFound
  19. from hapic.processor import RequestParameters
  20. from hapic.processor import ProcessValidationError
  21. from hapic.error import DefaultErrorBuilder
  22. from hapic.error import ErrorBuilderInterface
  23. # Bottle regular expression to locate url parameters
  24. BOTTLE_RE_PATH_URL = re.compile(r'<([^:<>]+)(?::[^<>]+)?>')
  25. class BottleContext(BaseContext):
  26. def __init__(
  27. self,
  28. app: bottle.Bottle,
  29. default_error_builder: ErrorBuilderInterface=None,
  30. debug: bool = False,
  31. ):
  32. self._handled_exceptions = [] # type: typing.List[HandledException] # nopep8
  33. self._exceptions_handler_installed = False
  34. self.app = app
  35. self.default_error_builder = \
  36. default_error_builder or DefaultErrorBuilder() # FDV
  37. self.debug = debug
  38. def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
  39. path_parameters = dict(bottle.request.url_args)
  40. query_parameters = MultiDict(bottle.request.query.allitems())
  41. body_parameters = dict(bottle.request.json or {})
  42. form_parameters = MultiDict(bottle.request.forms.allitems())
  43. header_parameters = dict(bottle.request.headers)
  44. files_parameters = dict(bottle.request.files)
  45. return RequestParameters(
  46. path_parameters=path_parameters,
  47. query_parameters=query_parameters,
  48. body_parameters=body_parameters,
  49. form_parameters=form_parameters,
  50. header_parameters=header_parameters,
  51. files_parameters=files_parameters,
  52. )
  53. def get_response(
  54. self,
  55. response: str,
  56. http_code: int,
  57. mimetype: str='application/json',
  58. ) -> bottle.HTTPResponse:
  59. return bottle.HTTPResponse(
  60. body=response,
  61. headers=[
  62. ('Content-Type', mimetype),
  63. ],
  64. status=http_code,
  65. )
  66. def get_validation_error_response(
  67. self,
  68. error: ProcessValidationError,
  69. http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  70. ) -> typing.Any:
  71. error_content = self.default_error_builder.build_from_validation_error(
  72. error,
  73. )
  74. # Check error
  75. dumped = self.default_error_builder.dump(error).data
  76. unmarshall = self.default_error_builder.load(dumped)
  77. if unmarshall.errors:
  78. raise OutputValidationException(
  79. 'Validation error during dump of error response: {}'.format(
  80. str(unmarshall.errors)
  81. )
  82. )
  83. return bottle.HTTPResponse(
  84. body=json.dumps(error_content),
  85. headers=[
  86. ('Content-Type', 'application/json'),
  87. ],
  88. status=int(http_code),
  89. )
  90. def find_route(
  91. self,
  92. decorated_controller: DecoratedController,
  93. ) -> RouteRepresentation:
  94. if not self.app.routes:
  95. raise NoRoutesException('There is no routes in your bottle app')
  96. reference = decorated_controller.reference
  97. for route in self.app.routes:
  98. route_token = getattr(
  99. route.callback,
  100. DECORATION_ATTRIBUTE_NAME,
  101. None,
  102. )
  103. match_with_wrapper = route.callback == reference.wrapper
  104. match_with_wrapped = route.callback == reference.wrapped
  105. match_with_token = route_token == reference.token
  106. if match_with_wrapper or match_with_wrapped or match_with_token:
  107. return RouteRepresentation(
  108. rule=self.get_swagger_path(route.rule),
  109. method=route.method.lower(),
  110. original_route_object=route,
  111. )
  112. # TODO BS 20171010: Raise exception or print error ? see #10
  113. raise RouteNotFound(
  114. 'Decorated route "{}" was not found in bottle routes'.format(
  115. decorated_controller.name,
  116. )
  117. )
  118. def get_swagger_path(self, contextualised_rule: str) -> str:
  119. return BOTTLE_RE_PATH_URL.sub(r'{\1}', contextualised_rule)
  120. def by_pass_output_wrapping(self, response: typing.Any) -> bool:
  121. if isinstance(response, bottle.HTTPResponse):
  122. return True
  123. return False
  124. def _add_exception_class_to_catch(
  125. self,
  126. exception_class: typing.Type[Exception],
  127. http_code: int,
  128. ) -> None:
  129. if not self._exceptions_handler_installed:
  130. self._install_exceptions_handler()
  131. self._handled_exceptions.append(
  132. HandledException(exception_class, http_code),
  133. )
  134. def _install_exceptions_handler(self) -> None:
  135. """
  136. Setup the bottle app to enable exception catching with internal
  137. hapic exception catcher.
  138. """
  139. self.app.install(self.handle_exceptions_decorator_builder)
  140. def _get_handled_exception_class_and_http_codes(
  141. self,
  142. ) -> typing.List[HandledException]:
  143. """
  144. See hapic.context.BaseContext#_get_handled_exception_class_and_http_codes # nopep8
  145. """
  146. return self._handled_exceptions
  147. def is_debug(self) -> bool:
  148. return self.debug