hapic.py 13KB

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