hapic.py 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. # -*- coding: utf-8 -*-
  2. import typing
  3. from http import HTTPStatus
  4. import bottle
  5. import functools
  6. # CHANGE
  7. import marshmallow
  8. from hapic.buffer import DecorationBuffer
  9. from hapic.context import ContextInterface, BottleContext
  10. from hapic.decorator import DecoratedController
  11. from hapic.decorator import InputBodyControllerWrapper
  12. from hapic.decorator import InputHeadersControllerWrapper
  13. from hapic.decorator import InputPathControllerWrapper
  14. from hapic.decorator import InputQueryControllerWrapper
  15. from hapic.decorator import OutputBodyControllerWrapper
  16. from hapic.decorator import OutputHeadersControllerWrapper
  17. from hapic.description import InputBodyDescription
  18. from hapic.description import InputFormsDescription
  19. from hapic.description import InputHeadersDescription
  20. from hapic.description import InputPathDescription
  21. from hapic.description import InputQueryDescription
  22. from hapic.description import OutputBodyDescription
  23. from hapic.description import OutputHeadersDescription
  24. from hapic.processor import ProcessorInterface, MarshmallowInputProcessor
  25. flatten = lambda l: [item for sublist in l for item in sublist]
  26. # TODO: Gérer les erreurs de schema
  27. # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb)
  28. # TODO: Confusion nommage body/json/forms
  29. # _waiting = {}
  30. # _endpoints = {}
  31. # TODO NOW: C'est un gros gros fake !
  32. class ErrorResponseSchema(marshmallow.Schema):
  33. error_message = marshmallow.fields.String(required=True)
  34. error_details = marshmallow.fields.Dict(required=True)
  35. _default_global_context = BottleContext()
  36. _default_global_error_schema = ErrorResponseSchema()
  37. class Hapic(object):
  38. def __init__(self):
  39. self._buffer = DecorationBuffer()
  40. self._controllers = []
  41. def with_api_doc(self):
  42. def decorator(func):
  43. @functools.wraps(func)
  44. def wrapper(*args, **kwargs):
  45. return func(*args, **kwargs)
  46. description = self._buffer.get_description()
  47. decorated_controller = DecoratedController(
  48. reference=wrapper,
  49. description=description,
  50. )
  51. self._buffer.clear()
  52. self._controllers.append(decorated_controller)
  53. return wrapper
  54. return decorator
  55. def output_body(
  56. self,
  57. schema: typing.Any,
  58. processor: ProcessorInterface = None,
  59. context: ContextInterface = None,
  60. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  61. default_http_code: HTTPStatus = HTTPStatus.OK,
  62. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  63. processor = processor or MarshmallowInputProcessor()
  64. processor.schema = schema
  65. context = context or _default_global_context
  66. decoration = OutputBodyControllerWrapper(
  67. context=context,
  68. processor=processor,
  69. error_http_code=error_http_code,
  70. default_http_code=default_http_code,
  71. )
  72. def decorator(func):
  73. self._buffer.output_body = OutputBodyDescription(decoration)
  74. return decoration.get_wrapper(func)
  75. return decorator
  76. def output_headers(
  77. self,
  78. schema: typing.Any,
  79. processor: ProcessorInterface = None,
  80. context: ContextInterface = None,
  81. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  82. default_http_code: HTTPStatus = HTTPStatus.OK,
  83. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  84. processor = processor or MarshmallowInputProcessor()
  85. processor.schema = schema
  86. context = context or _default_global_context
  87. decoration = OutputHeadersControllerWrapper(
  88. context=context,
  89. processor=processor,
  90. error_http_code=error_http_code,
  91. default_http_code=default_http_code,
  92. )
  93. def decorator(func):
  94. self._buffer.output_headers = OutputHeadersDescription(decoration)
  95. return decoration.get_wrapper(func)
  96. return decorator
  97. def input_headers(
  98. self,
  99. schema: typing.Any,
  100. processor: ProcessorInterface = None,
  101. context: ContextInterface = None,
  102. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  103. default_http_code: HTTPStatus = HTTPStatus.OK,
  104. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  105. processor = processor or MarshmallowInputProcessor()
  106. processor.schema = schema
  107. context = context or _default_global_context
  108. decoration = InputHeadersControllerWrapper(
  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.input_headers = InputHeadersDescription(decoration)
  116. return decoration.get_wrapper(func)
  117. return decorator
  118. def input_path(
  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 MarshmallowInputProcessor()
  127. processor.schema = schema
  128. context = context or _default_global_context
  129. decoration = InputPathControllerWrapper(
  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.input_path = InputPathDescription(decoration)
  137. return decoration.get_wrapper(func)
  138. return decorator
  139. def input_query(
  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 MarshmallowInputProcessor()
  148. processor.schema = schema
  149. context = context or _default_global_context
  150. decoration = InputQueryControllerWrapper(
  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.input_query = InputQueryDescription(decoration)
  158. return decoration.get_wrapper(func)
  159. return decorator
  160. def input_body(
  161. self,
  162. schema: typing.Any,
  163. processor: ProcessorInterface = None,
  164. context: ContextInterface = None,
  165. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  166. default_http_code: HTTPStatus = HTTPStatus.OK,
  167. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  168. processor = processor or MarshmallowInputProcessor()
  169. processor.schema = schema
  170. context = context or _default_global_context
  171. decoration = InputBodyControllerWrapper(
  172. context=context,
  173. processor=processor,
  174. error_http_code=error_http_code,
  175. default_http_code=default_http_code,
  176. )
  177. def decorator(func):
  178. self._buffer.input_body = InputBodyDescription(decoration)
  179. return decoration.get_wrapper(func)
  180. return decorator
  181. def input_forms(
  182. self,
  183. schema: typing.Any,
  184. processor: ProcessorInterface=None,
  185. context: ContextInterface=None,
  186. error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
  187. default_http_code: HTTPStatus = HTTPStatus.OK,
  188. ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
  189. processor = processor or MarshmallowInputProcessor()
  190. processor.schema = schema
  191. context = context or _default_global_context
  192. decoration = InputBodyControllerWrapper(
  193. context=context,
  194. processor=processor,
  195. error_http_code=error_http_code,
  196. default_http_code=default_http_code,
  197. )
  198. def decorator(func):
  199. self._buffer.input_forms = InputFormsDescription(decoration)
  200. return decoration.get_wrapper(func)
  201. return decorator
  202. def generate_doc(self, app=None):
  203. # TODO @Damien bottle specific code !
  204. app = app or bottle.default_app()
  205. route_by_callbacks = []
  206. routes = flatten(app.router.dyna_routes.values())
  207. for path, path_regex, route, func_ in routes:
  208. route_by_callbacks.append(route.callback)
  209. for description in self._controllers:
  210. for path, path_regex, route, func_ in routes:
  211. if route.callback == description.reference:
  212. # TODO: use description to feed apispec
  213. print(route.method, path, description)
  214. continue