hapic.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. # -*- coding: utf-8 -*-
  2. import os
  3. import typing
  4. import uuid
  5. import functools
  6. try: # Python 3.5+
  7. from http import HTTPStatus
  8. except ImportError:
  9. from http import client as HTTPStatus
  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 AsyncInputPathControllerWrapper
  20. from hapic.decorator import InputQueryControllerWrapper
  21. from hapic.decorator import AsyncInputQueryControllerWrapper
  22. from hapic.decorator import InputFilesControllerWrapper
  23. from hapic.decorator import OutputBodyControllerWrapper
  24. from hapic.decorator import OutputHeadersControllerWrapper
  25. from hapic.decorator import OutputFileControllerWrapper
  26. from hapic.description import InputBodyDescription
  27. from hapic.description import ErrorDescription
  28. from hapic.description import InputFormsDescription
  29. from hapic.description import InputHeadersDescription
  30. from hapic.description import InputPathDescription
  31. from hapic.description import InputQueryDescription
  32. from hapic.description import InputFilesDescription
  33. from hapic.description import OutputBodyDescription
  34. from hapic.description import OutputHeadersDescription
  35. from hapic.description import OutputFileDescription
  36. from hapic.doc import DocGenerator
  37. from hapic.processor import ProcessorInterface
  38. from hapic.processor import MarshmallowInputProcessor
  39. from hapic.processor import MarshmallowInputFilesProcessor
  40. from hapic.processor import MarshmallowOutputProcessor
  41. from hapic.error import ErrorBuilderInterface
  42. # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb), see #12
  43. # TODO: Confusion nommage body/json/forms, see #13
  44. class Hapic(object):
  45. def __init__(
  46. self,
  47. async: bool = False,
  48. ):
  49. self._buffer = DecorationBuffer()
  50. self._controllers = [] # type: typing.List[DecoratedController]
  51. self._context = None # type: ContextInterface
  52. self._error_builder = None # type: ErrorBuilderInterface
  53. self._async = async
  54. self.doc_generator = DocGenerator()
  55. # This local function will be pass to different components
  56. # who will need context but declared (like with decorator)
  57. # before context declaration
  58. def context_getter():
  59. return self._context
  60. # This local function will be pass to different components
  61. # who will need error_builder but declared (like with decorator)
  62. # before error_builder declaration
  63. def error_builder_getter():
  64. return self._context.get_default_error_builder()
  65. self._context_getter = context_getter
  66. self._error_builder_getter = error_builder_getter
  67. # TODO: Permettre la surcharge des classes utilisés ci-dessous, see #14
  68. @property
  69. def controllers(self) -> typing.List[DecoratedController]:
  70. return self._controllers
  71. @property
  72. def context(self) -> ContextInterface:
  73. return self._context
  74. def set_context(self, context: ContextInterface) -> None:
  75. assert not self._context
  76. self._context = context
  77. def reset_context(self) -> None:
  78. self._context = None
  79. def with_api_doc(self, tags: typing.List['str']=None):
  80. """
  81. Permit to generate doc about a controller. Use as a decorator:
  82. ```
  83. @hapic.with_api_doc()
  84. def my_controller(self):
  85. # ...
  86. ```
  87. What it do: Register this controller with all previous given
  88. information like `@hapic.input_path(...)` etc.
  89. :param tags: list of string tags (OpenApi)
  90. :return: The decorator
  91. """
  92. # FIXME BS 20171228: Documenter sur ce que ça fait vraiment (tester:
  93. # on peut l'enlever si on veut pas generer la doc ?)
  94. tags = tags or [] # FDV
  95. def decorator(func):
  96. @functools.wraps(func)
  97. def wrapper(*args, **kwargs):
  98. return func(*args, **kwargs)
  99. token = uuid.uuid4().hex
  100. setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
  101. setattr(func, DECORATION_ATTRIBUTE_NAME, token)
  102. description = self._buffer.get_description()
  103. description.tags = tags
  104. reference = ControllerReference(
  105. wrapper=wrapper,
  106. wrapped=func,
  107. token=token,
  108. )
  109. decorated_controller = DecoratedController(
  110. reference=reference,
  111. description=description,
  112. name=func.__name__,
  113. )
  114. self._buffer.clear()
  115. self._controllers.append(decorated_controller)
  116. return wrapper
  117. return decorator
  118. def output_body(
  119. self,
  120. schema: typing.Any,
  121. processor: ProcessorInterface = None,
  122. context: ContextInterface = None,
  123. error_http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
  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 = OutputBodyControllerWrapper(
  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_body = OutputBodyDescription(decoration)
  137. return decoration.get_wrapper(func)
  138. return decorator
  139. def output_headers(
  140. self,
  141. schema: typing.Any,
  142. processor: ProcessorInterface = None,
  143. context: ContextInterface = None,
  144. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  145. default_http_code: HTTPStatus = HTTPStatus.OK,
  146. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  147. processor = processor or MarshmallowOutputProcessor()
  148. processor.schema = schema
  149. context = context or self._context_getter
  150. decoration = OutputHeadersControllerWrapper(
  151. context=context,
  152. processor=processor,
  153. error_http_code=error_http_code,
  154. default_http_code=default_http_code,
  155. )
  156. def decorator(func):
  157. self._buffer.output_headers = OutputHeadersDescription(decoration)
  158. return decoration.get_wrapper(func)
  159. return decorator
  160. # TODO BS 20171102: Think about possibilities to validate output ?
  161. # (with mime type, or validator)
  162. def output_file(
  163. self,
  164. output_types: typing.List[str],
  165. default_http_code: HTTPStatus = HTTPStatus.OK,
  166. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  167. decoration = OutputFileControllerWrapper(
  168. output_types=output_types,
  169. default_http_code=default_http_code,
  170. )
  171. def decorator(func):
  172. self._buffer.output_file = OutputFileDescription(decoration)
  173. return decoration.get_wrapper(func)
  174. return decorator
  175. def input_headers(
  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 = InputHeadersControllerWrapper(
  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_headers = InputHeadersDescription(decoration)
  194. return decoration.get_wrapper(func)
  195. return decorator
  196. def input_path(
  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. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  204. processor = processor or MarshmallowInputProcessor()
  205. processor.schema = schema
  206. context = context or self._context_getter
  207. decoration = self._get_input_path_controller_wrapper(
  208. context=context,
  209. processor=processor,
  210. error_http_code=error_http_code,
  211. default_http_code=default_http_code,
  212. )
  213. def decorator(func):
  214. self._buffer.input_path = InputPathDescription(decoration)
  215. return decoration.get_wrapper(func)
  216. return decorator
  217. def input_query(
  218. self,
  219. schema: typing.Any,
  220. processor: ProcessorInterface = None,
  221. context: ContextInterface = None,
  222. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  223. default_http_code: HTTPStatus = HTTPStatus.OK,
  224. as_list: typing.List[str]=None,
  225. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  226. processor = processor or MarshmallowInputProcessor()
  227. processor.schema = schema
  228. context = context or self._context_getter
  229. decoration = self._get_input_query_controller_wrapper(
  230. context=context,
  231. processor=processor,
  232. error_http_code=error_http_code,
  233. default_http_code=default_http_code,
  234. as_list=as_list,
  235. )
  236. def decorator(func):
  237. self._buffer.input_query = InputQueryDescription(decoration)
  238. return decoration.get_wrapper(func)
  239. return decorator
  240. def input_body(
  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_body = InputBodyDescription(decoration)
  259. return decoration.get_wrapper(func)
  260. return decorator
  261. def input_forms(
  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 MarshmallowInputProcessor()
  270. processor.schema = schema
  271. context = context or self._context_getter
  272. decoration = InputBodyControllerWrapper(
  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_forms = InputFormsDescription(decoration)
  280. return decoration.get_wrapper(func)
  281. return decorator
  282. def input_files(
  283. self,
  284. schema: typing.Any,
  285. processor: ProcessorInterface=None,
  286. context: ContextInterface=None,
  287. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  288. default_http_code: HTTPStatus = HTTPStatus.OK,
  289. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  290. processor = processor or MarshmallowInputFilesProcessor()
  291. processor.schema = schema
  292. context = context or self._context_getter
  293. decoration = InputFilesControllerWrapper(
  294. context=context,
  295. processor=processor,
  296. error_http_code=error_http_code,
  297. default_http_code=default_http_code,
  298. )
  299. def decorator(func):
  300. self._buffer.input_files = InputFilesDescription(decoration)
  301. return decoration.get_wrapper(func)
  302. return decorator
  303. def handle_exception(
  304. self,
  305. handled_exception_class: typing.Type[Exception]=Exception,
  306. http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
  307. error_builder: ErrorBuilderInterface=None,
  308. context: ContextInterface = None,
  309. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  310. context = context or self._context_getter
  311. error_builder = error_builder or self._error_builder_getter
  312. decoration = ExceptionHandlerControllerWrapper(
  313. handled_exception_class,
  314. context,
  315. error_builder=error_builder,
  316. http_code=http_code,
  317. )
  318. def decorator(func):
  319. self._buffer.errors.append(ErrorDescription(decoration))
  320. return decoration.get_wrapper(func)
  321. return decorator
  322. def generate_doc(self, title: str='', description: str='') -> dict:
  323. """
  324. See hapic.doc.DocGenerator#get_doc docstring
  325. :param title: Title of generated doc
  326. :param description: Description of generated doc
  327. :return: dict containing apispec doc
  328. """
  329. return self.doc_generator.get_doc(
  330. self._controllers,
  331. self.context,
  332. title=title,
  333. description=description,
  334. )
  335. def save_doc_in_file(
  336. self,
  337. file_path: str,
  338. title: str='',
  339. description: str='',
  340. ) -> None:
  341. """
  342. See hapic.doc.DocGenerator#get_doc docstring
  343. :param file_path: The file path to write doc in YAML format
  344. :param title: Title of generated doc
  345. :param description: Description of generated doc
  346. """
  347. self.doc_generator.save_in_file(
  348. file_path,
  349. controllers=self._controllers,
  350. context=self.context,
  351. title=title,
  352. description=description,
  353. )
  354. def add_documentation_view(
  355. self,
  356. route: str,
  357. title: str='',
  358. description: str='',
  359. ) -> None:
  360. # Ensure "/" at end of route, else web browser will not consider it as
  361. # a path
  362. if not route.endswith('/'):
  363. route = '{}/'.format(route)
  364. swaggerui_path = os.path.join(
  365. os.path.dirname(os.path.abspath(__file__)),
  366. 'static',
  367. 'swaggerui',
  368. )
  369. # Documentation file view
  370. doc_yaml = self.doc_generator.get_doc_yaml(
  371. controllers=self._controllers,
  372. context=self.context,
  373. title=title,
  374. description=description,
  375. )
  376. def spec_yaml_view(*args, **kwargs):
  377. """
  378. Method to return swagger generated yaml spec file.
  379. This method will be call as a framework view, like those,
  380. it need to handle the default arguments of a framework view.
  381. As frameworks have different arguments patterns, we should
  382. allow any arguments patterns (args, kwargs).
  383. """
  384. return self.context.get_response(
  385. doc_yaml,
  386. mimetype='text/x-yaml',
  387. http_code=HTTPStatus.OK,
  388. )
  389. # Prepare views html content
  390. doc_index_path = os.path.join(swaggerui_path, 'index.html')
  391. with open(doc_index_path, 'r') as doc_page:
  392. doc_page_content = doc_page.read()
  393. doc_page_content = doc_page_content.replace(
  394. '{{ spec_uri }}',
  395. 'spec.yml',
  396. )
  397. # Declare the swaggerui view
  398. def api_doc_view(*args, **kwargs):
  399. """
  400. Method to return html index view of swagger ui.
  401. This method will be call as a framework view, like those,
  402. it need to handle the default arguments of a framework view.
  403. As frameworks have different arguments patterns, we should
  404. allow any arguments patterns (args, kwargs).
  405. """
  406. return self.context.get_response(
  407. doc_page_content,
  408. http_code=HTTPStatus.OK,
  409. mimetype='text/html',
  410. )
  411. # Add a view to generate the html index page of swagger-ui
  412. self.context.add_view(
  413. route=route,
  414. http_method='GET',
  415. view_func=api_doc_view,
  416. )
  417. # Add a doc yaml view
  418. self.context.add_view(
  419. route=os.path.join(route, 'spec.yml'),
  420. http_method='GET',
  421. view_func=spec_yaml_view,
  422. )
  423. # Add swagger directory as served static dir
  424. self.context.serve_directory(
  425. route,
  426. swaggerui_path,
  427. )
  428. def _get_input_path_controller_wrapper(
  429. self,
  430. processor: ProcessorInterface,
  431. context: ContextInterface,
  432. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  433. default_http_code: HTTPStatus = HTTPStatus.OK,
  434. ) -> typing.Union[
  435. InputPathControllerWrapper,
  436. AsyncInputPathControllerWrapper,
  437. ]:
  438. if not self._async:
  439. return InputPathControllerWrapper(
  440. context=context,
  441. processor=processor,
  442. error_http_code=error_http_code,
  443. default_http_code=default_http_code,
  444. )
  445. return AsyncInputPathControllerWrapper(
  446. context=context,
  447. processor=processor,
  448. error_http_code=error_http_code,
  449. default_http_code=default_http_code,
  450. )
  451. def _get_input_query_controller_wrapper(
  452. self,
  453. processor: ProcessorInterface,
  454. context: ContextInterface,
  455. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  456. default_http_code: HTTPStatus = HTTPStatus.OK,
  457. as_list: typing.List[str]=None,
  458. ) -> typing.Union[
  459. InputQueryControllerWrapper,
  460. AsyncInputQueryControllerWrapper,
  461. ]:
  462. if not self._async:
  463. return InputQueryControllerWrapper(
  464. context=context,
  465. processor=processor,
  466. error_http_code=error_http_code,
  467. default_http_code=default_http_code,
  468. as_list=as_list,
  469. )
  470. return AsyncInputQueryControllerWrapper(
  471. context=context,
  472. processor=processor,
  473. error_http_code=error_http_code,
  474. default_http_code=default_http_code,
  475. as_list=as_list,
  476. )