hapic.py 9.5KB

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