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/__init__.py | 5 +- src/web/views/api/__init__.py | 3 - src/web/views/api/article.py | 53 ---------- src/web/views/api/category.py | 27 ----- src/web/views/api/common.py | 218 --------------------------------------- src/web/views/api/feed.py | 49 --------- src/web/views/api/v2/__init__.py | 3 + src/web/views/api/v2/article.py | 53 ++++++++++ src/web/views/api/v2/category.py | 27 +++++ src/web/views/api/v2/common.py | 218 +++++++++++++++++++++++++++++++++++++++ src/web/views/api/v2/feed.py | 49 +++++++++ src/web/views/api/v3/__init__.py | 0 12 files changed, 353 insertions(+), 352 deletions(-) delete mode 100644 src/web/views/api/article.py delete mode 100644 src/web/views/api/category.py delete mode 100644 src/web/views/api/common.py delete mode 100644 src/web/views/api/feed.py create mode 100644 src/web/views/api/v2/__init__.py create mode 100644 src/web/views/api/v2/article.py create mode 100644 src/web/views/api/v2/category.py create mode 100644 src/web/views/api/v2/common.py create mode 100644 src/web/views/api/v2/feed.py create mode 100644 src/web/views/api/v3/__init__.py diff --git a/src/web/views/__init__.py b/src/web/views/__init__.py index 6de83818..4af8975b 100644 --- a/src/web/views/__init__.py +++ b/src/web/views/__init__.py @@ -1,4 +1,5 @@ -from web.views import views, home, session_mgmt, api +from web.views.api import v2 +from web.views import views, home, session_mgmt from web.views.article import article_bp, articles_bp from web.views.feed import feed_bp, feeds_bp from web.views.category import category_bp, categories_bp @@ -6,7 +7,7 @@ from web.views.icon import icon_bp from web.views.admin import admin_bp from web.views.user import user_bp, users_bp -__all__ = ['views', 'home', 'session_mgmt', 'api', +__all__ = ['views', 'home', 'session_mgmt', 'v2', 'article_bp', 'articles_bp', 'feed_bp', 'feeds_bp', 'category_bp', 'categories_bp', 'icon_bp', 'admin_bp', 'user_bp', 'users_bp'] diff --git a/src/web/views/api/__init__.py b/src/web/views/api/__init__.py index 458e031b..e69de29b 100644 --- a/src/web/views/api/__init__.py +++ b/src/web/views/api/__init__.py @@ -1,3 +0,0 @@ -from web.views.api import article, feed, category - -__all__ = ['article', 'feed', 'category'] diff --git a/src/web/views/api/article.py b/src/web/views/api/article.py deleted file mode 100644 index 5971f47d..00000000 --- a/src/web/views/api/article.py +++ /dev/null @@ -1,53 +0,0 @@ -from conf import API_ROOT -import dateutil.parser -from datetime import datetime -from flask import current_app -from flask.ext.restful import Api - -from web.views.common import api_permission -from web.controllers import ArticleController -from web.views.api.common import (PyAggAbstractResource, - PyAggResourceNew, PyAggResourceExisting, PyAggResourceMulti) - - -class ArticleNewAPI(PyAggResourceNew): - controller_cls = ArticleController - - -class ArticleAPI(PyAggResourceExisting): - controller_cls = ArticleController - - -class ArticlesAPI(PyAggResourceMulti): - controller_cls = ArticleController - - -class ArticlesChallenge(PyAggAbstractResource): - controller_cls = ArticleController - attrs = {'ids': {'type': list, 'default': []}} - - @api_permission.require(http_exception=403) - def get(self): - parsed_args = self.reqparse_args(right='read') - # collecting all attrs for casting purpose - attrs = self.controller_cls._get_attrs_desc('admin') - for id_dict in parsed_args['ids']: - keys_to_ignore = [] - for key in id_dict: - if key not in attrs: - keys_to_ignore.append(key) - if issubclass(attrs[key]['type'], datetime): - id_dict[key] = dateutil.parser.parse(id_dict[key]) - for key in keys_to_ignore: - del id_dict[key] - - result = list(self.controller.challenge(parsed_args['ids'])) - return result or None, 200 if result else 204 - -api = Api(current_app, prefix=API_ROOT) - -api.add_resource(ArticleNewAPI, '/article', endpoint='article_new.json') -api.add_resource(ArticleAPI, '/article/', endpoint='article.json') -api.add_resource(ArticlesAPI, '/articles', endpoint='articles.json') -api.add_resource(ArticlesChallenge, '/articles/challenge', - endpoint='articles_challenge.json') diff --git a/src/web/views/api/category.py b/src/web/views/api/category.py deleted file mode 100644 index eecfa785..00000000 --- a/src/web/views/api/category.py +++ /dev/null @@ -1,27 +0,0 @@ -from conf import API_ROOT -from flask import current_app -from flask.ext.restful import Api - -from web.controllers.category import CategoryController -from web.views.api.common import (PyAggResourceNew, - PyAggResourceExisting, - PyAggResourceMulti) - - -class CategoryNewAPI(PyAggResourceNew): - controller_cls = CategoryController - - -class CategoryAPI(PyAggResourceExisting): - controller_cls = CategoryController - - -class CategoriesAPI(PyAggResourceMulti): - controller_cls = CategoryController - - -api = Api(current_app, prefix=API_ROOT) -api.add_resource(CategoryNewAPI, '/category', endpoint='category_new.json') -api.add_resource(CategoryAPI, '/category/', - endpoint='category.json') -api.add_resource(CategoriesAPI, '/categories', endpoint='categories.json') diff --git a/src/web/views/api/common.py b/src/web/views/api/common.py deleted file mode 100644 index ace6ba3a..00000000 --- a/src/web/views/api/common.py +++ /dev/null @@ -1,218 +0,0 @@ -"""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 diff --git a/src/web/views/api/feed.py b/src/web/views/api/feed.py deleted file mode 100644 index 774bff5f..00000000 --- a/src/web/views/api/feed.py +++ /dev/null @@ -1,49 +0,0 @@ -from conf import API_ROOT -from flask import current_app -from flask.ext.restful import Api - -from web.views.common import api_permission -from web.controllers.feed import (FeedController, - DEFAULT_MAX_ERROR, - DEFAULT_LIMIT, - DEFAULT_REFRESH_RATE) - -from web.views.api.common import PyAggAbstractResource, \ - PyAggResourceNew, \ - PyAggResourceExisting, \ - PyAggResourceMulti - - -class FeedNewAPI(PyAggResourceNew): - controller_cls = FeedController - - -class FeedAPI(PyAggResourceExisting): - controller_cls = FeedController - - -class FeedsAPI(PyAggResourceMulti): - controller_cls = FeedController - - -class FetchableFeedAPI(PyAggAbstractResource): - controller_cls = FeedController - attrs = {'max_error': {'type': int, 'default': DEFAULT_MAX_ERROR}, - 'limit': {'type': int, 'default': DEFAULT_LIMIT}, - 'refresh_rate': {'type': int, 'default': DEFAULT_REFRESH_RATE}} - - @api_permission.require(http_exception=403) - def get(self): - args = self.reqparse_args(right='read', allow_empty=True) - result = [feed for feed - in self.controller.list_fetchable(**args)] - return result or None, 200 if result else 204 - - -api = Api(current_app, prefix=API_ROOT) - -api.add_resource(FeedNewAPI, '/feed', endpoint='feed_new.json') -api.add_resource(FeedAPI, '/feed/', endpoint='feed.json') -api.add_resource(FeedsAPI, '/feeds', endpoint='feeds.json') -api.add_resource(FetchableFeedAPI, '/feeds/fetchable', - endpoint='fetchable_feed.json') diff --git a/src/web/views/api/v2/__init__.py b/src/web/views/api/v2/__init__.py new file mode 100644 index 00000000..46760261 --- /dev/null +++ b/src/web/views/api/v2/__init__.py @@ -0,0 +1,3 @@ +from web.views.api.v2 import article, feed, category + +__all__ = ['article', 'feed', 'category'] diff --git a/src/web/views/api/v2/article.py b/src/web/views/api/v2/article.py new file mode 100644 index 00000000..71201538 --- /dev/null +++ b/src/web/views/api/v2/article.py @@ -0,0 +1,53 @@ +from conf import API_ROOT +import dateutil.parser +from datetime import datetime +from flask import current_app +from flask.ext.restful import Api + +from web.views.common import api_permission +from web.controllers import ArticleController +from web.views.api.v2.common import (PyAggAbstractResource, + PyAggResourceNew, PyAggResourceExisting, PyAggResourceMulti) + + +class ArticleNewAPI(PyAggResourceNew): + controller_cls = ArticleController + + +class ArticleAPI(PyAggResourceExisting): + controller_cls = ArticleController + + +class ArticlesAPI(PyAggResourceMulti): + controller_cls = ArticleController + + +class ArticlesChallenge(PyAggAbstractResource): + controller_cls = ArticleController + attrs = {'ids': {'type': list, 'default': []}} + + @api_permission.require(http_exception=403) + def get(self): + parsed_args = self.reqparse_args(right='read') + # collecting all attrs for casting purpose + attrs = self.controller_cls._get_attrs_desc('admin') + for id_dict in parsed_args['ids']: + keys_to_ignore = [] + for key in id_dict: + if key not in attrs: + keys_to_ignore.append(key) + if issubclass(attrs[key]['type'], datetime): + id_dict[key] = dateutil.parser.parse(id_dict[key]) + for key in keys_to_ignore: + del id_dict[key] + + result = list(self.controller.challenge(parsed_args['ids'])) + return result or None, 200 if result else 204 + +api = Api(current_app, prefix=API_ROOT) + +api.add_resource(ArticleNewAPI, '/article', endpoint='article_new.json') +api.add_resource(ArticleAPI, '/article/', endpoint='article.json') +api.add_resource(ArticlesAPI, '/articles', endpoint='articles.json') +api.add_resource(ArticlesChallenge, '/articles/challenge', + endpoint='articles_challenge.json') diff --git a/src/web/views/api/v2/category.py b/src/web/views/api/v2/category.py new file mode 100644 index 00000000..21459fc5 --- /dev/null +++ b/src/web/views/api/v2/category.py @@ -0,0 +1,27 @@ +from conf import API_ROOT +from flask import current_app +from flask.ext.restful import Api + +from web.controllers.category import CategoryController +from web.views.api.v2.common import (PyAggResourceNew, + PyAggResourceExisting, + PyAggResourceMulti) + + +class CategoryNewAPI(PyAggResourceNew): + controller_cls = CategoryController + + +class CategoryAPI(PyAggResourceExisting): + controller_cls = CategoryController + + +class CategoriesAPI(PyAggResourceMulti): + controller_cls = CategoryController + + +api = Api(current_app, prefix=API_ROOT) +api.add_resource(CategoryNewAPI, '/category', endpoint='category_new.json') +api.add_resource(CategoryAPI, '/category/', + endpoint='category.json') +api.add_resource(CategoriesAPI, '/categories', endpoint='categories.json') 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 diff --git a/src/web/views/api/v2/feed.py b/src/web/views/api/v2/feed.py new file mode 100644 index 00000000..686dcd76 --- /dev/null +++ b/src/web/views/api/v2/feed.py @@ -0,0 +1,49 @@ +from conf import API_ROOT +from flask import current_app +from flask.ext.restful import Api + +from web.views.common import api_permission +from web.controllers.feed import (FeedController, + DEFAULT_MAX_ERROR, + DEFAULT_LIMIT, + DEFAULT_REFRESH_RATE) + +from web.views.api.v2.common import PyAggAbstractResource, \ + PyAggResourceNew, \ + PyAggResourceExisting, \ + PyAggResourceMulti + + +class FeedNewAPI(PyAggResourceNew): + controller_cls = FeedController + + +class FeedAPI(PyAggResourceExisting): + controller_cls = FeedController + + +class FeedsAPI(PyAggResourceMulti): + controller_cls = FeedController + + +class FetchableFeedAPI(PyAggAbstractResource): + controller_cls = FeedController + attrs = {'max_error': {'type': int, 'default': DEFAULT_MAX_ERROR}, + 'limit': {'type': int, 'default': DEFAULT_LIMIT}, + 'refresh_rate': {'type': int, 'default': DEFAULT_REFRESH_RATE}} + + @api_permission.require(http_exception=403) + def get(self): + args = self.reqparse_args(right='read', allow_empty=True) + result = [feed for feed + in self.controller.list_fetchable(**args)] + return result or None, 200 if result else 204 + + +api = Api(current_app, prefix=API_ROOT) + +api.add_resource(FeedNewAPI, '/feed', endpoint='feed_new.json') +api.add_resource(FeedAPI, '/feed/', endpoint='feed.json') +api.add_resource(FeedsAPI, '/feeds', endpoint='feeds.json') +api.add_resource(FetchableFeedAPI, '/feeds/fetchable', + endpoint='fetchable_feed.json') diff --git a/src/web/views/api/v3/__init__.py b/src/web/views/api/v3/__init__.py new file mode 100644 index 00000000..e69de29b -- cgit