hapic.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. # -*- coding: utf-8 -*-
  2. import typing
  3. import uuid
  4. import functools
  5. try: # Python 3.5+
  6. from http import HTTPStatus
  7. except ImportError:
  8. from http import client as HTTPStatus
  9. from hapic.buffer import DecorationBuffer
  10. from hapic.context import ContextInterface
  11. from hapic.decorator import DecoratedController
  12. from hapic.decorator import DECORATION_ATTRIBUTE_NAME
  13. from hapic.decorator import ControllerReference
  14. from hapic.decorator import ExceptionHandlerControllerWrapper
  15. from hapic.decorator import InputBodyControllerWrapper
  16. from hapic.decorator import InputHeadersControllerWrapper
  17. from hapic.decorator import InputPathControllerWrapper
  18. from hapic.decorator import InputQueryControllerWrapper
  19. from hapic.decorator import InputFilesControllerWrapper
  20. from hapic.decorator import OutputBodyControllerWrapper
  21. from hapic.decorator import OutputHeadersControllerWrapper
  22. from hapic.decorator import OutputFileControllerWrapper
  23. from hapic.description import InputBodyDescription
  24. from hapic.description import ErrorDescription
  25. from hapic.description import InputFormsDescription
  26. from hapic.description import InputHeadersDescription
  27. from hapic.description import InputPathDescription
  28. from hapic.description import InputQueryDescription
  29. from hapic.description import InputFilesDescription
  30. from hapic.description import OutputBodyDescription
  31. from hapic.description import OutputHeadersDescription
  32. from hapic.description import OutputFileDescription
  33. from hapic.doc import DocGenerator
  34. from hapic.processor import ProcessorInterface
  35. from hapic.processor import MarshmallowInputProcessor
  36. from hapic.processor import MarshmallowInputFilesProcessor
  37. from hapic.processor import MarshmallowOutputProcessor
  38. from hapic.error import ErrorBuilderInterface
  39. # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb), see #12
  40. # TODO: Confusion nommage body/json/forms, see #13
  41. class Hapic(object):
  42. def __init__(self):
  43. self._buffer = DecorationBuffer()
  44. self._controllers = [] # type: typing.List[DecoratedController]
  45. self._context = None # type: ContextInterface
  46. self._error_builder = None # type: ErrorBuilderInterface
  47. self.doc_generator = DocGenerator()
  48. # This local function will be pass to different components
  49. # who will need context but declared (like with decorator)
  50. # before context declaration
  51. def context_getter():
  52. return self._context
  53. # This local function will be pass to different components
  54. # who will need error_builder but declared (like with decorator)
  55. # before error_builder declaration
  56. def error_builder_getter():
  57. return self._context.get_default_error_builder()
  58. self._context_getter = context_getter
  59. self._error_builder_getter = error_builder_getter
  60. # TODO: Permettre la surcharge des classes utilisés ci-dessous, see #14
  61. @property
  62. def controllers(self) -> typing.List[DecoratedController]:
  63. return self._controllers
  64. @property
  65. def context(self) -> ContextInterface:
  66. return self._context
  67. def set_context(self, context: ContextInterface) -> None:
  68. assert not self._context
  69. self._context = context
  70. def reset_context(self) -> None:
  71. self._context = None
  72. def with_api_doc(self, tags: typing.List['str']=None):
  73. """
  74. Permit to generate doc about a controller. Use as a decorator:
  75. ```
  76. @hapic.with_api_doc()
  77. def my_controller(self):
  78. # ...
  79. ```
  80. What it do: Register this controller with all previous given
  81. information like `@hapic.input_path(...)` etc.
  82. :param tags: list of string tags (OpenApi)
  83. :return: The decorator
  84. """
  85. # FIXME BS 20171228: Documenter sur ce que ça fait vraiment (tester:
  86. # on peut l'enlever si on veut pas generer la doc ?)
  87. tags = tags or [] # FDV
  88. def decorator(func):
  89. @functools.wraps(func)
  90. def wrapper(*args, **kwargs):
  91. return func(*args, **kwargs)
  92. token = uuid.uuid4().hex
  93. setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
  94. setattr(func, DECORATION_ATTRIBUTE_NAME, token)
  95. description = self._buffer.get_description()
  96. description.tags = tags
  97. reference = ControllerReference(
  98. wrapper=wrapper,
  99. wrapped=func,
  100. token=token,
  101. )
  102. decorated_controller = DecoratedController(
  103. reference=reference,
  104. description=description,
  105. name=func.__name__,
  106. )
  107. self._buffer.clear()
  108. self._controllers.append(decorated_controller)
  109. return wrapper
  110. return decorator
  111. def output_body(
  112. self,
  113. schema: typing.Any,
  114. processor: ProcessorInterface = None,
  115. context: ContextInterface = None,
  116. error_http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
  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 = OutputBodyControllerWrapper(
  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_body = OutputBodyDescription(decoration)
  130. return decoration.get_wrapper(func)
  131. return decorator
  132. def output_headers(
  133. self,
  134. schema: typing.Any,
  135. processor: ProcessorInterface = None,
  136. context: ContextInterface = None,
  137. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  138. default_http_code: HTTPStatus = HTTPStatus.OK,
  139. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  140. processor = processor or MarshmallowOutputProcessor()
  141. processor.schema = schema
  142. context = context or self._context_getter
  143. decoration = OutputHeadersControllerWrapper(
  144. context=context,
  145. processor=processor,
  146. error_http_code=error_http_code,
  147. default_http_code=default_http_code,
  148. )
  149. def decorator(func):
  150. self._buffer.output_headers = OutputHeadersDescription(decoration)
  151. return decoration.get_wrapper(func)
  152. return decorator
  153. # TODO BS 20171102: Think about possibilities to validate output ?
  154. # (with mime type, or validator)
  155. def output_file(
  156. self,
  157. output_types: typing.List[str],
  158. default_http_code: HTTPStatus = HTTPStatus.OK,
  159. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  160. decoration = OutputFileControllerWrapper(
  161. output_types=output_types,
  162. default_http_code=default_http_code,
  163. )
  164. def decorator(func):
  165. self._buffer.output_file = OutputFileDescription(decoration)
  166. return decoration.get_wrapper(func)
  167. return decorator
  168. def input_headers(
  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 = InputHeadersControllerWrapper(
  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_headers = InputHeadersDescription(decoration)
  187. return decoration.get_wrapper(func)
  188. return decorator
  189. def input_path(
  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. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  197. processor = processor or MarshmallowInputProcessor()
  198. processor.schema = schema
  199. context = context or self._context_getter
  200. decoration = InputPathControllerWrapper(
  201. context=context,
  202. processor=processor,
  203. error_http_code=error_http_code,
  204. default_http_code=default_http_code,
  205. )
  206. def decorator(func):
  207. self._buffer.input_path = InputPathDescription(decoration)
  208. return decoration.get_wrapper(func)
  209. return decorator
  210. def input_query(
  211. self,
  212. schema: typing.Any,
  213. processor: ProcessorInterface = None,
  214. context: ContextInterface = None,
  215. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  216. default_http_code: HTTPStatus = HTTPStatus.OK,
  217. as_list: typing.List[str]=None,
  218. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  219. processor = processor or MarshmallowInputProcessor()
  220. processor.schema = schema
  221. context = context or self._context_getter
  222. decoration = InputQueryControllerWrapper(
  223. context=context,
  224. processor=processor,
  225. error_http_code=error_http_code,
  226. default_http_code=default_http_code,
  227. as_list=as_list,
  228. )
  229. def decorator(func):
  230. self._buffer.input_query = InputQueryDescription(decoration)
  231. return decoration.get_wrapper(func)
  232. return decorator
  233. def input_body(
  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_body = InputBodyDescription(decoration)
  252. return decoration.get_wrapper(func)
  253. return decorator
  254. def input_forms(
  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 MarshmallowInputProcessor()
  263. processor.schema = schema
  264. context = context or self._context_getter
  265. decoration = InputBodyControllerWrapper(
  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_forms = InputFormsDescription(decoration)
  273. return decoration.get_wrapper(func)
  274. return decorator
  275. def input_files(
  276. self,
  277. schema: typing.Any,
  278. processor: ProcessorInterface=None,
  279. context: ContextInterface=None,
  280. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  281. default_http_code: HTTPStatus = HTTPStatus.OK,
  282. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  283. processor = processor or MarshmallowInputFilesProcessor()
  284. processor.schema = schema
  285. context = context or self._context_getter
  286. decoration = InputFilesControllerWrapper(
  287. context=context,
  288. processor=processor,
  289. error_http_code=error_http_code,
  290. default_http_code=default_http_code,
  291. )
  292. def decorator(func):
  293. self._buffer.input_files = InputFilesDescription(decoration)
  294. return decoration.get_wrapper(func)
  295. return decorator
  296. def handle_exception(
  297. self,
  298. handled_exception_class: typing.Type[Exception]=Exception,
  299. http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
  300. error_builder: ErrorBuilderInterface=None,
  301. context: ContextInterface = None,
  302. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  303. context = context or self._context_getter
  304. error_builder = error_builder or self._error_builder_getter
  305. decoration = ExceptionHandlerControllerWrapper(
  306. handled_exception_class,
  307. context,
  308. error_builder=error_builder,
  309. http_code=http_code,
  310. )
  311. def decorator(func):
  312. self._buffer.errors.append(ErrorDescription(decoration))
  313. return decoration.get_wrapper(func)
  314. return decorator
  315. def generate_doc(self, title: str='', description: str='') -> dict:
  316. """
  317. See hapic.doc.DocGenerator#get_doc docstring
  318. :param title: Title of generated doc
  319. :param description: Description of generated doc
  320. :return: dict containing apispec doc
  321. """
  322. return self.doc_generator.get_doc(
  323. self._controllers,
  324. self.context,
  325. title=title,
  326. description=description,
  327. )