hapic.py 10KB

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