From b4b175e0a8bb3844c1c6f846c1b4eb1970520864 Mon Sep 17 00:00:00 2001 From: Cédric Bonhomme Date: Thu, 14 Apr 2016 00:02:47 +0200 Subject: testing a new API --- src/web/views/api/v2/common.py | 218 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 src/web/views/api/v2/common.py (limited to 'src/web/views/api/v2/common.py') diff --git a/src/web/views/api/v2/common.py b/src/web/views/api/v2/common.py new file mode 100644 index 00000000..ace6ba3a --- /dev/null +++ b/src/web/views/api/v2/common.py @@ -0,0 +1,218 @@ +"""For a given resources, classes in the module intend to create the following +routes : + GET resource/ + -> to retrieve one + POST resource + -> to create one + PUT resource/ + -> to update one + DELETE resource/ + -> to delete one + + GET resources + -> to retrieve several + POST resources + -> to create several + PUT resources + -> to update several + DELETE resources + -> to delete several +""" +import logging +from functools import wraps +from werkzeug.exceptions import Unauthorized, BadRequest, Forbidden, NotFound +from flask import request +from flask.ext.restful import Resource, reqparse +from flask.ext.login import current_user + +from web.views.common import admin_permission, api_permission, \ + login_user_bundle, jsonify +from web.controllers import UserController + +logger = logging.getLogger(__name__) + + +def authenticate(func): + @wraps(func) + def wrapper(*args, **kwargs): + if request.authorization: + ucontr = UserController() + try: + user = ucontr.get(login=request.authorization.username) + except NotFound: + raise Forbidden("Couldn't authenticate your user") + if not ucontr.check_password(user, request.authorization.password): + raise Forbidden("Couldn't authenticate your user") + if not user.is_active: + raise Forbidden("User is desactivated") + login_user_bundle(user) + if current_user.is_authenticated: + return func(*args, **kwargs) + raise Unauthorized() + return wrapper + + +class PyAggAbstractResource(Resource): + method_decorators = [authenticate, jsonify] + controller_cls = None + attrs = None + + @property + def controller(self): + if admin_permission.can(): + return self.controller_cls() + return self.controller_cls(current_user.id) + + def reqparse_args(self, right, req=None, strict=False, default=True, + allow_empty=False): + """ + strict: bool + if True will throw 400 error if args are defined and not in request + default: bool + if True, won't return defaults + args: dict + the args to parse, if None, self.attrs will be used + """ + try: + in_values = req.json if req else (request.json or {}) + if not in_values and allow_empty: + return {} + except BadRequest: + if allow_empty: + return {} + raise + parser = reqparse.RequestParser() + if self.attrs is not None: + attrs = self.attrs + elif admin_permission.can(): + attrs = self.controller_cls._get_attrs_desc('admin') + elif api_permission.can(): + attrs = self.controller_cls._get_attrs_desc('api', right) + else: + attrs = self.controller_cls._get_attrs_desc('base', right) + assert attrs, "No defined attrs for %s" % self.__class__.__name__ + + for attr_name, attr in attrs.items(): + if not default and attr_name not in in_values: + continue + else: + parser.add_argument(attr_name, location='json', **attr) + return parser.parse_args(req=req, strict=strict) + + +class PyAggResourceNew(PyAggAbstractResource): + + @api_permission.require(http_exception=403) + def post(self): + """Create a single new object""" + return self.controller.create(**self.reqparse_args(right='write')), 201 + + +class PyAggResourceExisting(PyAggAbstractResource): + + def get(self, obj_id=None): + """Retreive a single object""" + return self.controller.get(id=obj_id) + + def put(self, obj_id=None): + """update an object, new attrs should be passed in the payload""" + args = self.reqparse_args(right='write', default=False) + if not args: + raise BadRequest() + return self.controller.update({'id': obj_id}, args), 200 + + def delete(self, obj_id=None): + """delete a object""" + self.controller.delete(obj_id) + return None, 204 + + +class PyAggResourceMulti(PyAggAbstractResource): + + def get(self): + """retrieve several objects. filters can be set in the payload on the + different fields of the object, and a limit can be set in there as well + """ + try: + limit = request.json.pop('limit', 10) + order_by = request.json.pop('order_by', None) + args = self.reqparse_args(right='read', default=False) + except BadRequest: + limit, order_by, args = 10, None, {} + query = self.controller.read(**args) + if order_by: + query = query.order_by(order_by) + if limit: + query = query.limit(limit) + return [res for res in query] + + @api_permission.require(http_exception=403) + def post(self): + """creating several objects. payload should be: + >>> payload + [{attr1: val1, attr2: val2}, {attr1: val1, attr2: val2}] + """ + assert 'application/json' in request.headers.get('Content-Type') + status, fail_count, results = 200, 0, [] + + class Proxy: + pass + for attrs in request.json: + try: + Proxy.json = attrs + args = self.reqparse_args('write', req=Proxy, default=False) + obj = self.controller.create(**args) + results.append(obj) + except Exception as error: + fail_count += 1 + results.append(str(error)) + if fail_count == len(results): # all failed => 500 + status = 500 + elif fail_count: # some failed => 206 + status = 206 + return results, status + + def put(self): + """updating several objects. payload should be: + >>> payload + [[obj_id1, {attr1: val1, attr2: val2}] + [obj_id2, {attr1: val1, attr2: val2}]] + """ + assert 'application/json' in request.headers.get('Content-Type') + status, results = 200, [] + + class Proxy: + pass + for obj_id, attrs in request.json: + try: + Proxy.json = attrs + args = self.reqparse_args('write', req=Proxy, default=False) + result = self.controller.update({'id': obj_id}, args) + if result: + results.append('ok') + else: + results.append('nok') + except Exception as error: + results.append(str(error)) + if results.count('ok') == 0: # all failed => 500 + status = 500 + elif results.count('ok') != len(results): # some failed => 206 + status = 206 + return results, status + + def delete(self): + """will delete several objects, + a list of their ids should be in the payload""" + assert 'application/json' in request.headers.get('Content-Type') + status, results = 204, [] + for obj_id in request.json: + try: + self.controller.delete(obj_id) + results.append('ok') + except Exception as error: + status = 206 + results.append(error) + # if no operation succeded, it's not partial anymore, returning err 500 + if status == 206 and results.count('ok') == 0: + status = 500 + return results, status -- cgit