hapic.py 12KB

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