hapic.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. # -*- coding: utf-8 -*-
  2. import json
  3. import typing
  4. from http import HTTPStatus
  5. import functools
  6. import bottle
  7. # TODO: Gérer les erreurs de schema
  8. # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb)
  9. # CHANGE
  10. from hapic.exception import InputValidationException, \
  11. OutputValidationException, InputWorkflowException, ProcessException
  12. flatten = lambda l: [item for sublist in l for item in sublist]
  13. _waiting = {}
  14. _endpoints = {}
  15. _default_global_context = None
  16. _default_global_error_schema = None
  17. def error_schema(schema):
  18. global _default_global_error_schema
  19. _default_global_error_schema = schema
  20. def decorator(func):
  21. @functools.wraps(func)
  22. def wrapper(*args, **kwargs):
  23. return func(*args, **kwargs)
  24. return wrapper
  25. return decorator
  26. def set_fake_default_context(context):
  27. global _default_global_context
  28. _default_global_context = context
  29. def _register(func):
  30. assert func not in _endpoints
  31. global _waiting
  32. _endpoints[func] = _waiting
  33. _waiting = {}
  34. def with_api_doc():
  35. def decorator(func):
  36. @functools.wraps(func)
  37. def wrapper(*args, **kwargs):
  38. return func(*args, **kwargs)
  39. _register(wrapper)
  40. return wrapper
  41. return decorator
  42. def with_api_doc_bis():
  43. def decorator(func):
  44. @functools.wraps(func)
  45. def wrapper(*args, **kwargs):
  46. return func(*args, **kwargs)
  47. _register(func)
  48. return wrapper
  49. return decorator
  50. def generate_doc(app=None):
  51. # TODO @Damien bottle specific code !
  52. app = app or bottle.default_app()
  53. route_by_callbacks = []
  54. routes = flatten(app.router.dyna_routes.values())
  55. for path, path_regex, route, func_ in routes:
  56. route_by_callbacks.append(route.callback)
  57. for func, descriptions in _endpoints.items():
  58. routes = flatten(app.router.dyna_routes.values())
  59. for path, path_regex, route, func_ in routes:
  60. if route.callback == func:
  61. print(route.method, path, descriptions)
  62. continue
  63. class RequestParameters(object):
  64. def __init__(
  65. self,
  66. path_parameters,
  67. query_parameters,
  68. body_parameters,
  69. form_parameters,
  70. header_parameters,
  71. ):
  72. self.path_parameters = path_parameters
  73. self.query_parameters = query_parameters
  74. self.body_parameters = body_parameters
  75. self.form_parameters = form_parameters
  76. self.header_parameters = header_parameters
  77. class ProcessValidationError(object):
  78. def __init__(
  79. self,
  80. error_message: str,
  81. error_details: dict,
  82. ) -> None:
  83. self.error_message = error_message
  84. self.error_details = error_details
  85. class ContextInterface(object):
  86. def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
  87. raise NotImplementedError()
  88. def get_response(
  89. self,
  90. response: dict,
  91. http_code: int,
  92. ) -> typing.Any:
  93. raise NotImplementedError()
  94. def get_validation_error_response(
  95. self,
  96. error: ProcessValidationError,
  97. http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  98. ) -> typing.Any:
  99. raise NotImplementedError()
  100. class BottleContext(ContextInterface):
  101. def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
  102. return RequestParameters(
  103. path_parameters=bottle.request.url_args,
  104. query_parameters=bottle.request.params,
  105. body_parameters=bottle.request.json,
  106. form_parameters=bottle.request.forms,
  107. header_parameters=bottle.request.headers,
  108. )
  109. def get_response(
  110. self,
  111. response: dict,
  112. http_code: int,
  113. ) -> bottle.HTTPResponse:
  114. return bottle.HTTPResponse(
  115. body=json.dumps(response),
  116. headers=[
  117. ('Content-Type', 'application/json'),
  118. ],
  119. status=http_code,
  120. )
  121. def get_validation_error_response(
  122. self,
  123. error: ProcessValidationError,
  124. http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  125. ) -> typing.Any:
  126. unmarshall = _default_global_error_schema.dump(error)
  127. if unmarshall.errors:
  128. raise OutputValidationException(
  129. 'Validation error during dump of error response: {}'.format(
  130. str(unmarshall.errors)
  131. )
  132. )
  133. return bottle.HTTPResponse(
  134. body=json.dumps(unmarshall.data),
  135. headers=[
  136. ('Content-Type', 'application/json'),
  137. ],
  138. status=int(http_code),
  139. )
  140. class OutputProcessorInterface(object):
  141. def __init__(self):
  142. self.schema = None
  143. def process(self, value):
  144. raise NotImplementedError
  145. def get_validation_error(
  146. self,
  147. request_context: RequestParameters,
  148. ) -> ProcessValidationError:
  149. raise NotImplementedError
  150. class InputProcessorInterface(object):
  151. def __init__(self):
  152. self.schema = None
  153. def process(
  154. self,
  155. request_context: RequestParameters,
  156. ) -> typing.Any:
  157. raise NotImplementedError
  158. def get_validation_error(
  159. self,
  160. request_context: RequestParameters,
  161. ) -> ProcessValidationError:
  162. raise NotImplementedError
  163. class MarshmallowOutputProcessor(OutputProcessorInterface):
  164. def process(self, data: typing.Any):
  165. unmarshall = self.schema.dump(data)
  166. if unmarshall.errors:
  167. raise InputValidationException(
  168. 'Error when validate input: {}'.format(
  169. str(unmarshall.errors),
  170. )
  171. )
  172. return unmarshall.data
  173. def get_validation_error(self, data: dict) -> ProcessValidationError:
  174. marshmallow_errors = self.schema.dump(data).errors
  175. return ProcessValidationError(
  176. error_message='Validation error of output data',
  177. error_details=marshmallow_errors,
  178. )
  179. class MarshmallowInputProcessor(OutputProcessorInterface):
  180. def process(self, data: dict):
  181. unmarshall = self.schema.load(data)
  182. if unmarshall.errors:
  183. raise OutputValidationException(
  184. 'Error when validate ouput: {}'.format(
  185. str(unmarshall.errors),
  186. )
  187. )
  188. return unmarshall.data
  189. def get_validation_error(self, data: dict) -> ProcessValidationError:
  190. marshmallow_errors = self.schema.load(data).errors
  191. return ProcessValidationError(
  192. error_message='Validation error of input data',
  193. error_details=marshmallow_errors,
  194. )
  195. class HapicData(object):
  196. def __init__(self):
  197. self.body = {}
  198. self.path = {}
  199. self.query = {}
  200. self.headers = {}
  201. # TODO: Il faut un output_body et un output_header
  202. def output(
  203. schema,
  204. processor: OutputProcessorInterface=None,
  205. context: ContextInterface=None,
  206. default_http_code=200,
  207. default_error_code=500,
  208. ):
  209. processor = processor or MarshmallowOutputProcessor()
  210. processor.schema = schema
  211. context = context or _default_global_context
  212. def decorator(func):
  213. # @functools.wraps(func)
  214. def wrapper(*args, **kwargs):
  215. raw_response = func(*args, **kwargs)
  216. processed_response = processor.process(raw_response)
  217. prepared_response = context.get_response(
  218. processed_response,
  219. default_http_code,
  220. )
  221. return prepared_response
  222. _waiting['output'] = schema
  223. return wrapper
  224. return decorator
  225. # TODO: raccourcis 'input' tout court ?
  226. def input_body(
  227. schema,
  228. processor: InputProcessorInterface=None,
  229. context: ContextInterface=None,
  230. error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
  231. ):
  232. processor = processor or MarshmallowInputProcessor()
  233. processor.schema = schema
  234. context = context or _default_global_context
  235. def decorator(func):
  236. # @functools.wraps(func)
  237. def wrapper(*args, **kwargs):
  238. updated_kwargs = {'hapic_data': HapicData()}
  239. updated_kwargs.update(kwargs)
  240. hapic_data = updated_kwargs['hapic_data']
  241. request_parameters = context.get_request_parameters(
  242. *args,
  243. **updated_kwargs
  244. )
  245. try:
  246. hapic_data.body = processor.process(
  247. request_parameters.body_parameters,
  248. )
  249. except ProcessException:
  250. error = processor.get_validation_error(
  251. request_parameters.body_parameters,
  252. )
  253. error_response = context.get_validation_error_response(
  254. error,
  255. http_code=error_http_code,
  256. )
  257. return error_response
  258. return func(*args, **updated_kwargs)
  259. _waiting.setdefault('input', []).append(schema)
  260. return wrapper
  261. return decorator
  262. def input_path(
  263. schema,
  264. processor: InputProcessorInterface=None,
  265. context: ContextInterface=None,
  266. error_http_code=400,
  267. ):
  268. processor = processor or MarshmallowInputProcessor()
  269. processor.schema = schema
  270. context = context or _default_global_context
  271. def decorator(func):
  272. # @functools.wraps(func)
  273. def wrapper(*args, **kwargs):
  274. updated_kwargs = {'hapic_data': HapicData()}
  275. updated_kwargs.update(kwargs)
  276. hapic_data = updated_kwargs['hapic_data']
  277. request_parameters = context.get_request_parameters(*args, **updated_kwargs)
  278. hapic_data.path = processor.process(request_parameters.path_parameters)
  279. return func(*args, **updated_kwargs)
  280. _waiting.setdefault('input', []).append(schema)
  281. return wrapper
  282. return decorator
  283. def input_query(
  284. schema,
  285. processor: InputProcessorInterface=None,
  286. context: ContextInterface=None,
  287. error_http_code=400,
  288. ):
  289. processor = processor or MarshmallowInputProcessor()
  290. processor.schema = schema
  291. context = context or _default_global_context
  292. def decorator(func):
  293. # @functools.wraps(func)
  294. def wrapper(*args, **kwargs):
  295. updated_kwargs = {'hapic_data': HapicData()}
  296. updated_kwargs.update(kwargs)
  297. hapic_data = updated_kwargs['hapic_data']
  298. request_parameters = context.get_request_parameters(*args, **updated_kwargs)
  299. hapic_data.query = processor.process(request_parameters.query_parameters)
  300. return func(*args, **updated_kwargs)
  301. _waiting.setdefault('input', []).append(schema)
  302. return wrapper
  303. return decorator
  304. def input_headers(
  305. schema,
  306. processor: InputProcessorInterface,
  307. context: ContextInterface=None,
  308. error_http_code=400,
  309. ):
  310. processor = processor or MarshmallowInputProcessor()
  311. processor.schema = schema
  312. context = context or _default_global_context
  313. def decorator(func):
  314. # @functools.wraps(func)
  315. def wrapper(*args, **kwargs):
  316. updated_kwargs = {'hapic_data': HapicData()}
  317. updated_kwargs.update(kwargs)
  318. hapic_data = updated_kwargs['hapic_data']
  319. request_parameters = context.get_request_parameters(*args, **updated_kwargs)
  320. hapic_data.headers = processor.process(request_parameters.header_parameters)
  321. return func(*args, **updated_kwargs)
  322. _waiting.setdefault('input', []).append(schema)
  323. return wrapper
  324. return decorator