hapic.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. # -*- coding: utf-8 -*-
  2. import typing
  3. import uuid
  4. from http import HTTPStatus
  5. import functools
  6. import marshmallow
  7. from hapic.buffer import DecorationBuffer
  8. from hapic.context import ContextInterface
  9. from hapic.decorator import DecoratedController
  10. from hapic.decorator import DECORATION_ATTRIBUTE_NAME
  11. from hapic.decorator import ControllerReference
  12. from hapic.decorator import ExceptionHandlerControllerWrapper
  13. from hapic.decorator import InputBodyControllerWrapper
  14. from hapic.decorator import InputHeadersControllerWrapper
  15. from hapic.decorator import InputPathControllerWrapper
  16. from hapic.decorator import InputQueryControllerWrapper
  17. from hapic.decorator import OutputBodyControllerWrapper
  18. from hapic.decorator import OutputHeadersControllerWrapper
  19. from hapic.description import InputBodyDescription
  20. from hapic.description import ErrorDescription
  21. from hapic.description import InputFormsDescription
  22. from hapic.description import InputHeadersDescription
  23. from hapic.description import InputPathDescription
  24. from hapic.description import InputQueryDescription
  25. from hapic.description import OutputBodyDescription
  26. from hapic.description import OutputHeadersDescription
  27. from hapic.doc import DocGenerator
  28. from hapic.processor import ProcessorInterface
  29. from hapic.processor import MarshmallowInputProcessor
  30. from hapic.processor import MarshmallowOutputProcessor
  31. class ErrorResponseSchema(marshmallow.Schema):
  32. message = marshmallow.fields.String(required=True)
  33. details = marshmallow.fields.Dict(required=False, missing={})
  34. code = marshmallow.fields.Raw(missing=None)
  35. _default_global_error_schema = ErrorResponseSchema()
  36. # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb), see #12
  37. # TODO: Confusion nommage body/json/forms, see #13
  38. class Hapic(object):
  39. def __init__(self):
  40. self._buffer = DecorationBuffer()
  41. self._controllers = [] # type: typing.List[DecoratedController]
  42. self._context = None # type: ContextInterface
  43. # This local function will be pass to different components
  44. # who will need context but declared (like with decorator)
  45. # before context declaration
  46. def context_getter():
  47. return self._context
  48. self._context_getter = context_getter
  49. # TODO: Permettre la surcharge des classes utilisés ci-dessous, see #14
  50. @property
  51. def controllers(self) -> typing.List[DecoratedController]:
  52. return self._controllers
  53. @property
  54. def context(self) -> ContextInterface:
  55. return self._context
  56. def with_api_doc(self):
  57. def decorator(func):
  58. @functools.wraps(func)
  59. def wrapper(*args, **kwargs):
  60. return func(*args, **kwargs)
  61. token = uuid.uuid4().hex
  62. setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
  63. setattr(func, DECORATION_ATTRIBUTE_NAME, token)
  64. description = self._buffer.get_description()
  65. reference = ControllerReference(
  66. wrapper=wrapper,
  67. wrapped=func,
  68. token=token,
  69. )
  70. decorated_controller = DecoratedController(
  71. reference=reference,
  72. description=description,
  73. name=func.__name__,
  74. )
  75. self._buffer.clear()
  76. self._controllers.append(decorated_controller)
  77. return wrapper
  78. return decorator
  79. def set_context(self, context: ContextInterface) -> None:
  80. assert not self._context
  81. self._context = context
  82. def output_body(
  83. self,
  84. schema: typing.Any,
  85. processor: ProcessorInterface = None,
  86. context: ContextInterface = None,
  87. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  88. default_http_code: HTTPStatus = HTTPStatus.OK,
  89. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  90. processor = processor or MarshmallowOutputProcessor()
  91. processor.schema = schema
  92. context = context or self._context_getter
  93. decoration = OutputBodyControllerWrapper(
  94. context=context,
  95. processor=processor,
  96. error_http_code=error_http_code,
  97. default_http_code=default_http_code,
  98. )
  99. def decorator(func):
  100. self._buffer.output_body = OutputBodyDescription(decoration)
  101. return decoration.get_wrapper(func)
  102. return decorator
  103. def output_headers(
  104. self,
  105. schema: typing.Any,
  106. processor: ProcessorInterface = None,
  107. context: ContextInterface = None,
  108. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  109. default_http_code: HTTPStatus = HTTPStatus.OK,
  110. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  111. processor = processor or MarshmallowOutputProcessor()
  112. processor.schema = schema
  113. context = context or self._context_getter
  114. decoration = OutputHeadersControllerWrapper(
  115. context=context,
  116. processor=processor,
  117. error_http_code=error_http_code,
  118. default_http_code=default_http_code,
  119. )
  120. def decorator(func):
  121. self._buffer.output_headers = OutputHeadersDescription(decoration)
  122. return decoration.get_wrapper(func)
  123. return decorator
  124. def input_headers(
  125. self,
  126. schema: typing.Any,
  127. processor: ProcessorInterface = None,
  128. context: ContextInterface = None,
  129. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  130. default_http_code: HTTPStatus = HTTPStatus.OK,
  131. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  132. processor = processor or MarshmallowInputProcessor()
  133. processor.schema = schema
  134. context = context or self._context_getter
  135. decoration = InputHeadersControllerWrapper(
  136. context=context,
  137. processor=processor,
  138. error_http_code=error_http_code,
  139. default_http_code=default_http_code,
  140. )
  141. def decorator(func):
  142. self._buffer.input_headers = InputHeadersDescription(decoration)
  143. return decoration.get_wrapper(func)
  144. return decorator
  145. def input_path(
  146. self,
  147. schema: typing.Any,
  148. processor: ProcessorInterface = None,
  149. context: ContextInterface = None,
  150. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  151. default_http_code: HTTPStatus = HTTPStatus.OK,
  152. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  153. processor = processor or MarshmallowInputProcessor()
  154. processor.schema = schema
  155. context = context or self._context_getter
  156. decoration = InputPathControllerWrapper(
  157. context=context,
  158. processor=processor,
  159. error_http_code=error_http_code,
  160. default_http_code=default_http_code,
  161. )
  162. def decorator(func):
  163. self._buffer.input_path = InputPathDescription(decoration)
  164. return decoration.get_wrapper(func)
  165. return decorator
  166. def input_query(
  167. self,
  168. schema: typing.Any,
  169. processor: ProcessorInterface = None,
  170. context: ContextInterface = None,
  171. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  172. default_http_code: HTTPStatus = HTTPStatus.OK,
  173. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  174. processor = processor or MarshmallowInputProcessor()
  175. processor.schema = schema
  176. context = context or self._context_getter
  177. decoration = InputQueryControllerWrapper(
  178. context=context,
  179. processor=processor,
  180. error_http_code=error_http_code,
  181. default_http_code=default_http_code,
  182. )
  183. def decorator(func):
  184. self._buffer.input_query = InputQueryDescription(decoration)
  185. return decoration.get_wrapper(func)
  186. return decorator
  187. def input_body(
  188. self,
  189. schema: typing.Any,
  190. processor: ProcessorInterface = None,
  191. context: ContextInterface = None,
  192. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  193. default_http_code: HTTPStatus = HTTPStatus.OK,
  194. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  195. processor = processor or MarshmallowInputProcessor()
  196. processor.schema = schema
  197. context = context or self._context_getter
  198. decoration = InputBodyControllerWrapper(
  199. context=context,
  200. processor=processor,
  201. error_http_code=error_http_code,
  202. default_http_code=default_http_code,
  203. )
  204. def decorator(func):
  205. self._buffer.input_body = InputBodyDescription(decoration)
  206. return decoration.get_wrapper(func)
  207. return decorator
  208. def input_forms(
  209. self,
  210. schema: typing.Any,
  211. processor: ProcessorInterface=None,
  212. context: ContextInterface=None,
  213. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  214. default_http_code: HTTPStatus = HTTPStatus.OK,
  215. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  216. processor = processor or MarshmallowInputProcessor()
  217. processor.schema = schema
  218. context = context or self._context_getter
  219. decoration = InputBodyControllerWrapper(
  220. context=context,
  221. processor=processor,
  222. error_http_code=error_http_code,
  223. default_http_code=default_http_code,
  224. )
  225. def decorator(func):
  226. self._buffer.input_forms = InputFormsDescription(decoration)
  227. return decoration.get_wrapper(func)
  228. return decorator
  229. def handle_exception(
  230. self,
  231. handled_exception_class: typing.Type[Exception],
  232. http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
  233. context: ContextInterface = None,
  234. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  235. context = context or self._context_getter
  236. decoration = ExceptionHandlerControllerWrapper(
  237. handled_exception_class,
  238. context,
  239. # TODO BS 20171013: Permit schema overriding, see #15
  240. schema=_default_global_error_schema,
  241. http_code=http_code,
  242. )
  243. def decorator(func):
  244. self._buffer.errors.append(ErrorDescription(decoration))
  245. return decoration.get_wrapper(func)
  246. return decorator
  247. def generate_doc(self, app):
  248. # FIXME: j'ai du tricher avec app, see #11
  249. # FIXME @Damien bottle specific code ! see #11
  250. # rendre ca generique
  251. app = app or self._context.get_app()
  252. doc_generator = DocGenerator()
  253. return doc_generator.get_doc(self._controllers, app)