hapic.py 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. # -*- coding: utf-8 -*-
  2. import json
  3. import typing
  4. import functools
  5. import bottle
  6. # TODO: Gérer les erreurs de schema
  7. # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb)
  8. # CHANGE
  9. flatten = lambda l: [item for sublist in l for item in sublist]
  10. _waiting = {}
  11. _endpoints = {}
  12. _default_global_context = None
  13. def set_fake_default_context(context):
  14. global _default_global_context
  15. _default_global_context = context
  16. def _register(func):
  17. assert func not in _endpoints
  18. global _waiting
  19. _endpoints[func] = _waiting
  20. _waiting = {}
  21. def with_api_doc():
  22. def decorator(func):
  23. @functools.wraps(func)
  24. def wrapper(*args, **kwargs):
  25. return func(*args, **kwargs)
  26. _register(wrapper)
  27. return wrapper
  28. return decorator
  29. def with_api_doc_bis():
  30. def decorator(func):
  31. @functools.wraps(func)
  32. def wrapper(*args, **kwargs):
  33. return func(*args, **kwargs)
  34. _register(func)
  35. return wrapper
  36. return decorator
  37. def generate_doc(app=None):
  38. # TODO @Damien bottle specific code !
  39. app = app or bottle.default_app()
  40. route_by_callbacks = []
  41. routes = flatten(app.router.dyna_routes.values())
  42. for path, path_regex, route, func_ in routes:
  43. route_by_callbacks.append(route.callback)
  44. for func, descriptions in _endpoints.items():
  45. routes = flatten(app.router.dyna_routes.values())
  46. for path, path_regex, route, func_ in routes:
  47. if route.callback == func:
  48. print(route.method, path, descriptions)
  49. continue
  50. class RequestParameters(object):
  51. def __init__(
  52. self,
  53. path_parameters,
  54. query_parameters,
  55. body_parameters,
  56. form_parameters,
  57. header_parameters,
  58. ):
  59. self.path_parameters = path_parameters
  60. self.query_parameters = query_parameters
  61. self.body_parameters = body_parameters
  62. self.form_parameters = form_parameters
  63. self.header_parameters = header_parameters
  64. class ContextInterface(object):
  65. def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
  66. raise NotImplementedError()
  67. def get_response(
  68. self,
  69. response: dict,
  70. http_code: int,
  71. ):
  72. raise NotImplementedError()
  73. class BottleContext(ContextInterface):
  74. def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
  75. return RequestParameters(
  76. path_parameters=bottle.request.url_args,
  77. query_parameters=bottle.request.params,
  78. body_parameters=bottle.request.json,
  79. form_parameters={}, # TODO
  80. header_parameters={}, # TODO
  81. )
  82. def get_response(
  83. self,
  84. response: dict,
  85. http_code: int,
  86. ) -> bottle.HTTPResponse:
  87. return bottle.HTTPResponse(
  88. body=json.dumps(response),
  89. headers=[
  90. ('Content-Type', 'application/json'),
  91. ],
  92. status=http_code,
  93. )
  94. class OutputProcessorInterface(object):
  95. def __init__(self):
  96. self.schema = None
  97. def process(self, value):
  98. raise NotImplementedError
  99. class InputProcessorInterface(object):
  100. def __init__(self):
  101. self.schema = None
  102. def process(self, request_context: RequestParameters):
  103. raise NotImplementedError
  104. class MarshmallowOutputProcessor(OutputProcessorInterface):
  105. def process(self, data: typing.Any):
  106. return self.schema.dump(data).data
  107. class MarshmallowInputProcessor(OutputProcessorInterface):
  108. def process(self, data: dict):
  109. return self.schema.load(data).data
  110. # class MarshmallowPathInputProcessor(OutputProcessorInterface):
  111. # def process(self, request_context: RequestParameters):
  112. # return self.schema.load(request_context.path_parameters).data
  113. #
  114. #
  115. # class MarshmallowQueryInputProcessor(OutputProcessorInterface):
  116. # def process(self, request_context: RequestParameters):
  117. # return self.schema.load(request_context.query_parameters).data
  118. #
  119. #
  120. # class MarshmallowJsonInputProcessor(OutputProcessorInterface):
  121. # def process(self, request_context: RequestParameters):
  122. # return self.schema.load(request_context.json_parameters).data
  123. # class MarshmallowFormInputProcessor(OutputProcessorInterface):
  124. # def process(self, request_context: RequestParameters):
  125. # return self.schema.load(xxx).data
  126. #
  127. #
  128. # class MarshmallowHeaderInputProcessor(OutputProcessorInterface):
  129. # def process(self, request_context: RequestParameters):
  130. # return self.schema.load(xxx).data
  131. class HapicData(object):
  132. def __init__(self):
  133. self.body = {}
  134. self.path = {}
  135. self.query = {}
  136. self.headers = {}
  137. # TODO: Il faut un output_body et un output_header
  138. def output(
  139. schema,
  140. processor: OutputProcessorInterface=None,
  141. context: ContextInterface=None,
  142. default_http_code=200,
  143. default_error_code=500,
  144. ):
  145. processor = processor or MarshmallowOutputProcessor()
  146. processor.schema = schema
  147. context = context or _default_global_context
  148. def decorator(func):
  149. # @functools.wraps(func)
  150. def wrapper(*args, **kwargs):
  151. raw_response = func(*args, **kwargs)
  152. processed_response = processor.process(raw_response)
  153. prepared_response = context.get_response(
  154. processed_response,
  155. default_http_code,
  156. )
  157. return prepared_response
  158. _waiting['output'] = schema
  159. return wrapper
  160. return decorator
  161. # TODO: raccourcis 'input' tout court ?
  162. def input_body(
  163. schema,
  164. processor: InputProcessorInterface=None,
  165. context: ContextInterface=None,
  166. error_http_code=400,
  167. ):
  168. processor = processor or MarshmallowInputProcessor()
  169. processor.schema = schema
  170. context = context or _default_global_context
  171. def decorator(func):
  172. # @functools.wraps(func)
  173. def wrapper(*args, **kwargs):
  174. updated_kwargs = {'hapic_data': HapicData()}
  175. updated_kwargs.update(kwargs)
  176. hapic_data = updated_kwargs['hapic_data']
  177. request_parameters = context.get_request_parameters(*args, **updated_kwargs)
  178. hapic_data.body = processor.process(request_parameters.body_parameters)
  179. return func(*args, **updated_kwargs)
  180. _waiting.setdefault('input', []).append(schema)
  181. return wrapper
  182. return decorator
  183. def input_path(
  184. schema,
  185. processor: InputProcessorInterface=None,
  186. context: ContextInterface=None,
  187. error_http_code=400,
  188. ):
  189. processor = processor or MarshmallowInputProcessor()
  190. processor.schema = schema
  191. context = context or _default_global_context
  192. def decorator(func):
  193. # @functools.wraps(func)
  194. def wrapper(*args, **kwargs):
  195. updated_kwargs = {'hapic_data': HapicData()}
  196. updated_kwargs.update(kwargs)
  197. hapic_data = updated_kwargs['hapic_data']
  198. request_parameters = context.get_request_parameters(*args, **updated_kwargs)
  199. hapic_data.path = processor.process(request_parameters.path_parameters)
  200. return func(*args, **updated_kwargs)
  201. _waiting.setdefault('input', []).append(schema)
  202. return wrapper
  203. return decorator
  204. def input_query(
  205. schema,
  206. processor: InputProcessorInterface=None,
  207. context: ContextInterface=None,
  208. error_http_code=400,
  209. ):
  210. processor = processor or MarshmallowInputProcessor()
  211. processor.schema = schema
  212. context = context or _default_global_context
  213. def decorator(func):
  214. # @functools.wraps(func)
  215. def wrapper(*args, **kwargs):
  216. updated_kwargs = {'hapic_data': HapicData()}
  217. updated_kwargs.update(kwargs)
  218. hapic_data = updated_kwargs['hapic_data']
  219. request_parameters = context.get_request_parameters(*args, **updated_kwargs)
  220. hapic_data.query = processor.process(request_parameters.query_parameters)
  221. return func(*args, **updated_kwargs)
  222. _waiting.setdefault('input', []).append(schema)
  223. return wrapper
  224. return decorator
  225. def input_headers(
  226. schema,
  227. processor: InputProcessorInterface,
  228. context: ContextInterface=None,
  229. error_http_code=400,
  230. ):
  231. processor = processor or MarshmallowInputProcessor()
  232. processor.schema = schema
  233. context = context or _default_global_context
  234. def decorator(func):
  235. # @functools.wraps(func)
  236. def wrapper(*args, **kwargs):
  237. updated_kwargs = {'hapic_data': HapicData()}
  238. updated_kwargs.update(kwargs)
  239. hapic_data = updated_kwargs['hapic_data']
  240. request_parameters = context.get_request_parameters(*args, **updated_kwargs)
  241. hapic_data.headers = processor.process(request_parameters.header_parameters)
  242. return func(*args, **updated_kwargs)
  243. _waiting.setdefault('input', []).append(schema)
  244. return wrapper
  245. return decorator