aboutsummaryrefslogtreecommitdiff
path: root/src/web/views/api/v2
diff options
context:
space:
mode:
authorCédric Bonhomme <cedric@cedricbonhomme.org>2016-04-14 08:14:52 +0200
committerCédric Bonhomme <cedric@cedricbonhomme.org>2016-04-14 08:14:52 +0200
commit728d34d05a47050262322148f1704dc075971863 (patch)
treef4bc2ae7a29a19abd9cdc3b790489c2e4cdf30a1 /src/web/views/api/v2
parentRemoved links between current article and the previous/next articles. (diff)
parentBug fix: arguments in the URL weren't processed. (diff)
downloadnewspipe-728d34d05a47050262322148f1704dc075971863.tar.gz
newspipe-728d34d05a47050262322148f1704dc075971863.tar.bz2
newspipe-728d34d05a47050262322148f1704dc075971863.zip
Merge branch 'new-api'
Diffstat (limited to 'src/web/views/api/v2')
-rw-r--r--src/web/views/api/v2/__init__.py3
-rw-r--r--src/web/views/api/v2/article.py53
-rw-r--r--src/web/views/api/v2/category.py27
-rw-r--r--src/web/views/api/v2/common.py225
-rw-r--r--src/web/views/api/v2/feed.py49
5 files changed, 357 insertions, 0 deletions
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/<int:obj_id>', 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/<int:obj_id>',
+ 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..092b33b0
--- /dev/null
+++ b/src/web/views/api/v2/common.py
@@ -0,0 +1,225 @@
+"""For a given resources, classes in the module intend to create the following
+routes :
+ GET resource/<id>
+ -> to retrieve one
+ POST resource
+ -> to create one
+ PUT resource/<id>
+ -> to update one
+ DELETE resource/<id>
+ -> to delete one
+
+ GET resources
+ -> to retrieve several
+ POST resources
+ -> to create several
+ PUT resources
+ -> to update several
+ DELETE resources
+ -> to delete several
+"""
+import ast
+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)
+ return attrs
+
+
+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
+ """
+ args = {}
+ try:
+ limit = request.json.pop('limit', 10)
+ order_by = request.json.pop('order_by', None)
+ except Exception:
+ attrs = self.reqparse_args(right='read', default=False)
+ for k, v in request.args.items():
+ if k in attrs.keys():
+ if attrs[k]['type'] in [bool, int]:
+ args[k] = ast.literal_eval(v)
+ else:
+ args[k] = v
+ limit = request.args.get('limit', 10)
+ order_by = request.args.get('order_by', 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}]
+ """
+ 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}]]
+ """
+ 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"""
+ 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/<int:obj_id>', endpoint='feed.json')
+api.add_resource(FeedsAPI, '/feeds', endpoint='feeds.json')
+api.add_resource(FetchableFeedAPI, '/feeds/fetchable',
+ endpoint='fetchable_feed.json')
bgstack15