hapic.py 20KB

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