From cc2829c95c391043653a3bd543176f3c92f49d72 Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Thu, 15 Jan 2015 17:42:54 +0100 Subject: splitting and refactoring the Restful api part --- pyaggr3g470r/views/api/__init__.py | 31 +++++++++ pyaggr3g470r/views/api/article.py | 125 +++++++++++++++++++++++++++++++++++++ pyaggr3g470r/views/api/common.py | 79 +++++++++++++++++++++++ pyaggr3g470r/views/api/feed.py | 109 ++++++++++++++++++++++++++++++++ 4 files changed, 344 insertions(+) create mode 100644 pyaggr3g470r/views/api/__init__.py create mode 100644 pyaggr3g470r/views/api/article.py create mode 100644 pyaggr3g470r/views/api/common.py create mode 100644 pyaggr3g470r/views/api/feed.py (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/__init__.py b/pyaggr3g470r/views/api/__init__.py new file mode 100644 index 00000000..e11cdd95 --- /dev/null +++ b/pyaggr3g470r/views/api/__init__.py @@ -0,0 +1,31 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# pyAggr3g470r - A Web based news aggregator. +# Copyright (C) 2010-2015 Cédric Bonhomme - http://cedricbonhomme.org/ +# +# For more information : https://bitbucket.org/cedricbonhomme/pyaggr3g470r/ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +__author__ = "Cedric Bonhomme" +__version__ = "$Revision: 0.2 $" +__date__ = "$Date: 2014/06/18 $" +__revision__ = "$Date: 2014/07/05 $" +__copyright__ = "Copyright (c) Cedric Bonhomme" +__license__ = "AGPLv3" + +from pyaggr3g470r.views.api import article, feed + +__all__ = ['article', 'feed'] diff --git a/pyaggr3g470r/views/api/article.py b/pyaggr3g470r/views/api/article.py new file mode 100644 index 00000000..c509b0a8 --- /dev/null +++ b/pyaggr3g470r/views/api/article.py @@ -0,0 +1,125 @@ +import re +import dateutil.parser + +import conf +if not conf.ON_HEROKU: + import pyaggr3g470r.search as fastsearch + +from flask import request, g +from flask.ext.restful import Resource, reqparse + +from pyaggr3g470r import api, db +from pyaggr3g470r.models import Article, Feed +from pyaggr3g470r.views.api.common import authenticate, to_response, \ + PyAggResource + + +class ArticleListAPI(Resource): + """ + Defines a RESTful API for Article elements. + """ + method_decorators = [authenticate, to_response] + + def __init__(self): + self.reqparse = reqparse.RequestParser() + self.reqparse.add_argument('title', type=unicode, location='json') + self.reqparse.add_argument('content', type=unicode, location='json') + self.reqparse.add_argument('link', type=unicode, location='json') + self.reqparse.add_argument('date', type=str, location='json') + self.reqparse.add_argument('feed_id', type=int, location='json') + super(ArticleListAPI, self).__init__() + + def get(self): + """ + Returns a list of articles. + """ + feeds = {feed.id: feed.title for feed in g.user.feeds if feed.enabled} + articles = Article.query.filter(Article.feed_id.in_(feeds.keys()), + Article.user_id == g.user.id) + filter_ = request.args.get('filter_', 'unread') + feed_id = int(request.args.get('feed', 0)) + limit = request.args.get('limit', 1000) + if filter_ != 'all': + articles = articles.filter(Article.readed == (filter_ == 'read')) + if feed_id: + articles = articles.filter(Article.feed_id == feed_id) + + articles = articles.order_by(Article.date.desc()) + if limit != 'all': + limit = int(limit) + articles = articles.limit(limit) + + return {'result': [article.dump() for article in articles]} + + def post(self): + """ + POST method - Create a new article. + """ + args = self.reqparse.parse_args() + article_dict = {} + for k, v in args.iteritems(): + if v != None: + article_dict[k] = v + else: + return {"message": "Missing argument: %s." % (k,)}, 400 + article_date = None + try: + article_date = dateutil.parser.parse(article_dict["date"], dayfirst=True) + except: + try: # trying to clean date field from letters + article_date = dateutil.parser.parse(re.sub('[A-z]', '', article_dict["date"], dayfirst=True)) + except: + return {"message": "Bad format for the date."}, 400 + article = Article(link=article_dict["link"], title=article_dict["title"], + content=article_dict["content"], readed=False, like=False, + date=article_date, user_id=g.user.id, + feed_id=article_dict["feed_id"]) + feed = Feed.query.filter(Feed.id == article_dict["feed_id"], Feed.user_id == g.user.id).first() + feed.articles.append(article) + try: + db.session.commit() + return {"message": "ok"}, 201 + except: + return {"message": "Impossible to create the article."}, 500 + + +class ArticleAPI(PyAggResource): + "Defines a RESTful API for Article elements." + method_decorators = [authenticate, to_response] + db_cls = Article + + def __init__(self): + self.reqparse = reqparse.RequestParser() + self.reqparse.add_argument('like', type=bool, location='json') + self.reqparse.add_argument('readed', type=bool, location= 'json') + super(ArticleAPI, self).__init__() + + def get(self, id): + article = self._get_or_raise(id) + if not article.readed: + article.readed = True + db.session.commit() + return {'result': [article.dump()]} + + def put(self, id): + """ Update an article. It is only possible to update the status + ('like' and 'readed') of an article.""" + args = self.reqparse.parse_args() + article = self._get_or_raise(id) + if 'like' in args: + article.like = args['like'] + if 'readed' in args: + article.readed = args['readed'] + db.session.commit() + + try: + fastsearch.delete_article(g.user.id, article.feed_id, article.id) + except: + pass + return {"message": "ok"} + + +api.add_resource(ArticleListAPI, '/api/v1.0/articles', + endpoint='articles.json') +api.add_resource(ArticleAPI, '/api/v1.0/articles/', + endpoint='article.json') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py new file mode 100644 index 00000000..872c4ae1 --- /dev/null +++ b/pyaggr3g470r/views/api/common.py @@ -0,0 +1,79 @@ +from functools import wraps +from flask import request, g, session, Response, jsonify +from flask.ext.restful import Resource + +from pyaggr3g470r import db +from pyaggr3g470r.models import User + + +class HttpError(Exception): + pass + + +def authenticate(func): + """ + Decorator for the authentication to the web services. + """ + @wraps(func) + def wrapper(*args, **kwargs): + if not getattr(func, 'authenticated', True): + return func(*args, **kwargs) + + # authentication based on the session (already logged on the site) + if 'email' in session or g.user.is_authenticated(): + return func(*args, **kwargs) + + # authentication via HTTP only + auth = request.authorization + try: + email = auth.username + user = User.query.filter(User.email == email).first() + if user and user.check_password(auth.password) and user.activation_key == "": + g.user = user + return func(*args, **kwargs) + except AttributeError: + pass + + return Response('', 401, + {'WWWAuthenticate':'Basic realm="Login Required"'}) + return wrapper + + +def to_response(func): + def wrapper(*args, **kwargs): + try: + res = func(*args, **kwargs) + except HttpError, error: + return Response(*error.args) + if isinstance(res, tuple): + response = jsonify(**res[0]) + if len(res) > 1: + response.status_code = res[1] + return response + return res + return wrapper + + +class PyAggResource(Resource): + db_cls = None + + def _get_or_raise(self, obj_id=None): + if obj_id is None: + raise HttpError({'message': 'No id given'}, 400) + obj = self.db_cls.query.filter(self.db_cls.id == obj_id).first() + if obj is None: + raise HttpError({'message': 'Article not found'}, 404) + if obj.user_id != g.user.id: + raise HttpError({'message': "Unauthorized for %s." + % self.db_cls.__class__.__name__}, 403) + return obj + + def get(self, id=None): + return {'result': [self._get_or_raise(id).dump()]} + + def delete(self, id): + """Delete a feed.""" + feed = self._get_or_raise(id) + db.session.delete(feed) + db.session.commit() + return {"message": "ok"}, 204 diff --git a/pyaggr3g470r/views/api/feed.py b/pyaggr3g470r/views/api/feed.py new file mode 100644 index 00000000..e5937128 --- /dev/null +++ b/pyaggr3g470r/views/api/feed.py @@ -0,0 +1,109 @@ +from flask import g +from flask.ext.restful import Resource, reqparse + +from pyaggr3g470r import api, db +from pyaggr3g470r.models import Feed + +from pyaggr3g470r.views.api.common import authenticate, to_response, \ + PyAggResource + + +class FeedListAPI(Resource): + """ + Defines a RESTful API for Feed elements. + """ + method_decorators = [authenticate, to_response] + + def __init__(self): + self.reqparse = reqparse.RequestParser() + self.reqparse.add_argument('title', + type=unicode, default="", location='json') + self.reqparse.add_argument('description', + type=unicode, default="", location='json') + self.reqparse.add_argument('link', type=unicode, location='json') + self.reqparse.add_argument('site_link', + type=unicode, default="", location='json') + self.reqparse.add_argument('email_notification', + type=bool, default=False, location='json') + self.reqparse.add_argument('enabled', + type=bool, default=True, location='json') + super(FeedListAPI, self).__init__() + + def get(self): + """ + Returns a list of feeds. + """ + return {'result': [{"id": feed.id, + "title": feed.title, + "description": feed.description, + "link": feed.link, + "site_link": feed.site_link, + "email_notification": feed.email_notification, + "enabled": feed.enabled, + "created_date": feed.created_date, + } for feed in g.user.feeds]} + + def post(self): + """ + POST method - Create a new feed. + """ + args = self.reqparse.parse_args() + feed_dict = {} + for k, v in args.iteritems(): + if v != None: + feed_dict[k] = v + else: + return {'message': 'missing argument: %s' % (k,)}, 400 + new_feed = Feed(title=feed_dict["title"], + description=feed_dict["description"], + link=feed_dict["link"], + site_link=feed_dict["site_link"], + email_notification=feed_dict["email_notification"], + enabled=feed_dict["enabled"]) + g.user.feeds.append(new_feed) + try: + db.session.commit() + return {"message": "ok"} + except: + return {'message': 'Impossible to create the feed.'}, 500 + + +class FeedAPI(PyAggResource): + "Defines a RESTful API for Feed elements." + method_decorators = [authenticate, to_response] + db_cls = Feed + + def __init__(self): + self.reqparse = reqparse.RequestParser() + self.reqparse.add_argument('title', type=unicode, location='json') + self.reqparse.add_argument('description', + type=unicode, location='json') + self.reqparse.add_argument('link', type=unicode, location='json') + self.reqparse.add_argument('site_link', type=unicode, location='json') + self.reqparse.add_argument('email_notification', + type=bool, location='json') + self.reqparse.add_argument('enabled', type=bool ,location='json') + super(FeedAPI, self).__init__() + + def put(self, id): + "Update a feed" + args = self.reqparse.parse_args() + feed = self.get_feed_or_raise(id) + if 'title' in args: + feed.title = args['title'] + if 'description' in args: + feed.description = args['description'] + if 'link' in args: + feed.link = args['link'] + if 'site_link' in args: + feed.site_link = args['site_link'] + if 'email_notification' in args: + feed.email_notification = args['email_notification'] + if 'enabled' in args: + feed.enabled = args['enabled'] + db.session.commit() + return {"message": "ok"} + + +api.add_resource(FeedListAPI, '/api/v1.0/feeds', endpoint = 'feeds.json') +api.add_resource(FeedAPI, '/api/v1.0/feeds/', endpoint = 'feed.json') -- cgit From 2849c82255b4b889c7342a0a8fa8a4aecfbe599d Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Sat, 17 Jan 2015 16:50:38 +0100 Subject: a first big refacto of the existing arch --- pyaggr3g470r/views/api/article.py | 40 +++++---------------------------- pyaggr3g470r/views/api/common.py | 47 ++++++++++++++++++--------------------- pyaggr3g470r/views/api/feed.py | 32 ++++++-------------------- 3 files changed, 35 insertions(+), 84 deletions(-) (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/article.py b/pyaggr3g470r/views/api/article.py index c509b0a8..3642cda9 100644 --- a/pyaggr3g470r/views/api/article.py +++ b/pyaggr3g470r/views/api/article.py @@ -1,15 +1,11 @@ import re import dateutil.parser -import conf -if not conf.ON_HEROKU: - import pyaggr3g470r.search as fastsearch - from flask import request, g from flask.ext.restful import Resource, reqparse -from pyaggr3g470r import api, db from pyaggr3g470r.models import Article, Feed +from pyaggr3g470r.controllers import ArticleController from pyaggr3g470r.views.api.common import authenticate, to_response, \ PyAggResource @@ -77,7 +73,7 @@ class ArticleListAPI(Resource): feed = Feed.query.filter(Feed.id == article_dict["feed_id"], Feed.user_id == g.user.id).first() feed.articles.append(article) try: - db.session.commit() + g.db.session.commit() return {"message": "ok"}, 201 except: return {"message": "Impossible to create the article."}, 500 @@ -86,7 +82,8 @@ class ArticleListAPI(Resource): class ArticleAPI(PyAggResource): "Defines a RESTful API for Article elements." method_decorators = [authenticate, to_response] - db_cls = Article + controller_cls = ArticleController + editable_attrs = ['like', 'readed'] def __init__(self): self.reqparse = reqparse.RequestParser() @@ -94,32 +91,7 @@ class ArticleAPI(PyAggResource): self.reqparse.add_argument('readed', type=bool, location= 'json') super(ArticleAPI, self).__init__() - def get(self, id): - article = self._get_or_raise(id) - if not article.readed: - article.readed = True - db.session.commit() - return {'result': [article.dump()]} - - def put(self, id): - """ Update an article. It is only possible to update the status - ('like' and 'readed') of an article.""" - args = self.reqparse.parse_args() - article = self._get_or_raise(id) - if 'like' in args: - article.like = args['like'] - if 'readed' in args: - article.readed = args['readed'] - db.session.commit() - - try: - fastsearch.delete_article(g.user.id, article.feed_id, article.id) - except: - pass - return {"message": "ok"} - -api.add_resource(ArticleListAPI, '/api/v1.0/articles', - endpoint='articles.json') -api.add_resource(ArticleAPI, '/api/v1.0/articles/', +g.api.add_resource(ArticleListAPI, '/articles', endpoint='articles.json') +g.api.add_resource(ArticleAPI, '/articles/', endpoint='article.json') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py index 872c4ae1..a116b9c3 100644 --- a/pyaggr3g470r/views/api/common.py +++ b/pyaggr3g470r/views/api/common.py @@ -2,12 +2,8 @@ from functools import wraps from flask import request, g, session, Response, jsonify from flask.ext.restful import Resource -from pyaggr3g470r import db from pyaggr3g470r.models import User - - -class HttpError(Exception): - pass +from pyaggr3g470r.lib.exceptions import PyAggError def authenticate(func): @@ -43,8 +39,10 @@ def to_response(func): def wrapper(*args, **kwargs): try: res = func(*args, **kwargs) - except HttpError, error: - return Response(*error.args) + except PyAggError, error: + response = jsonify(**error.message) + response.status_code = error.status_code + return response if isinstance(res, tuple): response = jsonify(**res[0]) if len(res) > 1: @@ -55,25 +53,24 @@ def to_response(func): class PyAggResource(Resource): - db_cls = None + method_decorators = [authenticate, to_response] + controller_cls = None + editable_attrs = [] + + def __init__(self, *args, **kwargs): + self.controller = self.controller_cls(g.user.id) + super(PyAggResource, self).__init__(*args, **kwargs) - def _get_or_raise(self, obj_id=None): - if obj_id is None: - raise HttpError({'message': 'No id given'}, 400) - obj = self.db_cls.query.filter(self.db_cls.id == obj_id).first() - if obj is None: - raise HttpError({'message': 'Article not found'}, 404) - if obj.user_id != g.user.id: - raise HttpError({'message': "Unauthorized for %s." - % self.db_cls.__class__.__name__}, 403) - return obj + def get(self, obj_id=None): + return {'result': [self.controller.read(id=obj_id).dump()]} - def get(self, id=None): - return {'result': [self._get_or_raise(id).dump()]} + def put(self, obj_id=None): + args = self.reqparse.parse_args() + new_values = {key: args[key] for key in + set(args).intersection(self.editable_attrs)} + self.controller.update(obj_id, **new_values) + return {"message": "ok"} - def delete(self, id): - """Delete a feed.""" - feed = self._get_or_raise(id) - db.session.delete(feed) - db.session.commit() + def delete(self, obj_id=None): + self.controller.delete(obj_id) return {"message": "ok"}, 204 diff --git a/pyaggr3g470r/views/api/feed.py b/pyaggr3g470r/views/api/feed.py index e5937128..346898b5 100644 --- a/pyaggr3g470r/views/api/feed.py +++ b/pyaggr3g470r/views/api/feed.py @@ -1,7 +1,7 @@ from flask import g from flask.ext.restful import Resource, reqparse -from pyaggr3g470r import api, db +from pyaggr3g470r.controllers import FeedController from pyaggr3g470r.models import Feed from pyaggr3g470r.views.api.common import authenticate, to_response, \ @@ -62,7 +62,7 @@ class FeedListAPI(Resource): enabled=feed_dict["enabled"]) g.user.feeds.append(new_feed) try: - db.session.commit() + g.db.session.commit() return {"message": "ok"} except: return {'message': 'Impossible to create the feed.'}, 500 @@ -70,8 +70,9 @@ class FeedListAPI(Resource): class FeedAPI(PyAggResource): "Defines a RESTful API for Feed elements." - method_decorators = [authenticate, to_response] - db_cls = Feed + controller_cls = FeedController + editable_attrs = ['title', 'description', 'link', 'site_link', + 'email_notification', 'enabled'] def __init__(self): self.reqparse = reqparse.RequestParser() @@ -85,25 +86,6 @@ class FeedAPI(PyAggResource): self.reqparse.add_argument('enabled', type=bool ,location='json') super(FeedAPI, self).__init__() - def put(self, id): - "Update a feed" - args = self.reqparse.parse_args() - feed = self.get_feed_or_raise(id) - if 'title' in args: - feed.title = args['title'] - if 'description' in args: - feed.description = args['description'] - if 'link' in args: - feed.link = args['link'] - if 'site_link' in args: - feed.site_link = args['site_link'] - if 'email_notification' in args: - feed.email_notification = args['email_notification'] - if 'enabled' in args: - feed.enabled = args['enabled'] - db.session.commit() - return {"message": "ok"} - -api.add_resource(FeedListAPI, '/api/v1.0/feeds', endpoint = 'feeds.json') -api.add_resource(FeedAPI, '/api/v1.0/feeds/', endpoint = 'feed.json') +g.api.add_resource(FeedListAPI, '/feeds', endpoint='feeds.json') +g.api.add_resource(FeedAPI, '/feeds/', endpoint='feed.json') -- cgit From 5ce0ce0d57c9d9976a47a120ca6235b84ade236a Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Tue, 20 Jan 2015 17:20:07 +0100 Subject: first implementation of fetchable feeds --- pyaggr3g470r/views/api/common.py | 2 +- pyaggr3g470r/views/api/feed.py | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py index a116b9c3..edf560da 100644 --- a/pyaggr3g470r/views/api/common.py +++ b/pyaggr3g470r/views/api/common.py @@ -62,7 +62,7 @@ class PyAggResource(Resource): super(PyAggResource, self).__init__(*args, **kwargs) def get(self, obj_id=None): - return {'result': [self.controller.read(id=obj_id).dump()]} + return {'result': [self.controller.get(id=obj_id).dump()]} def put(self, obj_id=None): args = self.reqparse.parse_args() diff --git a/pyaggr3g470r/views/api/feed.py b/pyaggr3g470r/views/api/feed.py index 346898b5..94a5a433 100644 --- a/pyaggr3g470r/views/api/feed.py +++ b/pyaggr3g470r/views/api/feed.py @@ -1,7 +1,8 @@ from flask import g from flask.ext.restful import Resource, reqparse -from pyaggr3g470r.controllers import FeedController +from pyaggr3g470r.controllers.feed import FeedController, \ + DEFAULT_MAX_ERROR, DEFAULT_LIMIT from pyaggr3g470r.models import Feed from pyaggr3g470r.views.api.common import authenticate, to_response, \ @@ -72,7 +73,8 @@ class FeedAPI(PyAggResource): "Defines a RESTful API for Feed elements." controller_cls = FeedController editable_attrs = ['title', 'description', 'link', 'site_link', - 'email_notification', 'enabled'] + 'email_notification', 'enabled', 'last_refreshed', + 'last_error', 'error_count'] def __init__(self): self.reqparse = reqparse.RequestParser() @@ -87,5 +89,23 @@ class FeedAPI(PyAggResource): super(FeedAPI, self).__init__() +class FetchableFeedAPI(Resource): + + def __init__(self): + self.reqparse = reqparse.RequestParser() + self.reqparse.add_argument('max_error', type=int, location='json', + default=DEFAULT_MAX_ERROR) + self.reqparse.add_argument('limit', type=int, location='json', + default=DEFAULT_LIMIT) + super(FetchableFeedAPI, self).__init__() + + def get(self): + args = self.reqparse.parse_args() + controller = FeedController(g.user.id) + return {'result': [feed.dump() for feed in controller.list_fetchable( + max_error=args['max_error'], limit=args['limit'])]} + + g.api.add_resource(FeedListAPI, '/feeds', endpoint='feeds.json') g.api.add_resource(FeedAPI, '/feeds/', endpoint='feed.json') +g.api.add_resource(FetchableFeedAPI, '/feeds/fetchable', endpoint='fetchable_feed.json') -- cgit From 4f0ad9e442e64f69d420dea4d737805eefaaf981 Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Wed, 21 Jan 2015 14:07:00 +0100 Subject: continuing refacto --- pyaggr3g470r/views/api/article.py | 107 +++++++-------------------------- pyaggr3g470r/views/api/common.py | 122 +++++++++++++++++++++++++++++++------- pyaggr3g470r/views/api/feed.py | 120 +++++++++++-------------------------- 3 files changed, 161 insertions(+), 188 deletions(-) (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/article.py b/pyaggr3g470r/views/api/article.py index 3642cda9..ebda6247 100644 --- a/pyaggr3g470r/views/api/article.py +++ b/pyaggr3g470r/views/api/article.py @@ -1,97 +1,36 @@ -import re -import dateutil.parser +from flask import g -from flask import request, g -from flask.ext.restful import Resource, reqparse - -from pyaggr3g470r.models import Article, Feed from pyaggr3g470r.controllers import ArticleController -from pyaggr3g470r.views.api.common import authenticate, to_response, \ - PyAggResource - +from pyaggr3g470r.views.api.common import PyAggResourceNew, \ + PyAggResourceExisting, \ + PyAggResourceMulti -class ArticleListAPI(Resource): - """ - Defines a RESTful API for Article elements. - """ - method_decorators = [authenticate, to_response] - def __init__(self): - self.reqparse = reqparse.RequestParser() - self.reqparse.add_argument('title', type=unicode, location='json') - self.reqparse.add_argument('content', type=unicode, location='json') - self.reqparse.add_argument('link', type=unicode, location='json') - self.reqparse.add_argument('date', type=str, location='json') - self.reqparse.add_argument('feed_id', type=int, location='json') - super(ArticleListAPI, self).__init__() +ARTICLE_ATTRS = {'title': {'type': str}, + 'content': {'type': str}, + 'link': {'type': str}, + 'date': {'type': str}, + 'feed_id': {'type': int}, + 'like': {'type': bool}, + 'readed': {'type': bool}} - def get(self): - """ - Returns a list of articles. - """ - feeds = {feed.id: feed.title for feed in g.user.feeds if feed.enabled} - articles = Article.query.filter(Article.feed_id.in_(feeds.keys()), - Article.user_id == g.user.id) - filter_ = request.args.get('filter_', 'unread') - feed_id = int(request.args.get('feed', 0)) - limit = request.args.get('limit', 1000) - if filter_ != 'all': - articles = articles.filter(Article.readed == (filter_ == 'read')) - if feed_id: - articles = articles.filter(Article.feed_id == feed_id) - articles = articles.order_by(Article.date.desc()) - if limit != 'all': - limit = int(limit) - articles = articles.limit(limit) +class ArticleNewAPI(PyAggResourceNew): + controller_cls = ArticleController + attrs = ARTICLE_ATTRS - return {'result': [article.dump() for article in articles]} - def post(self): - """ - POST method - Create a new article. - """ - args = self.reqparse.parse_args() - article_dict = {} - for k, v in args.iteritems(): - if v != None: - article_dict[k] = v - else: - return {"message": "Missing argument: %s." % (k,)}, 400 - article_date = None - try: - article_date = dateutil.parser.parse(article_dict["date"], dayfirst=True) - except: - try: # trying to clean date field from letters - article_date = dateutil.parser.parse(re.sub('[A-z]', '', article_dict["date"], dayfirst=True)) - except: - return {"message": "Bad format for the date."}, 400 - article = Article(link=article_dict["link"], title=article_dict["title"], - content=article_dict["content"], readed=False, like=False, - date=article_date, user_id=g.user.id, - feed_id=article_dict["feed_id"]) - feed = Feed.query.filter(Feed.id == article_dict["feed_id"], Feed.user_id == g.user.id).first() - feed.articles.append(article) - try: - g.db.session.commit() - return {"message": "ok"}, 201 - except: - return {"message": "Impossible to create the article."}, 500 +class ArticleAPI(PyAggResourceExisting): + controller_cls = ArticleController + attrs = ARTICLE_ATTRS -class ArticleAPI(PyAggResource): - "Defines a RESTful API for Article elements." - method_decorators = [authenticate, to_response] +class ArticlesAPI(PyAggResourceMulti): controller_cls = ArticleController - editable_attrs = ['like', 'readed'] - - def __init__(self): - self.reqparse = reqparse.RequestParser() - self.reqparse.add_argument('like', type=bool, location='json') - self.reqparse.add_argument('readed', type=bool, location= 'json') - super(ArticleAPI, self).__init__() + attrs = ARTICLE_ATTRS -g.api.add_resource(ArticleListAPI, '/articles', endpoint='articles.json') -g.api.add_resource(ArticleAPI, '/articles/', - endpoint='article.json') +g.api.add_resource(ArticleNewAPI, '/article', endpoint='article_new.json') +g.api.add_resource(ArticleAPI, '/article/', + endpoint='article.json') +g.api.add_resource(ArticlesAPI, '/articles', endpoint='articles.json') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py index edf560da..c0759c03 100644 --- a/pyaggr3g470r/views/api/common.py +++ b/pyaggr3g470r/views/api/common.py @@ -1,6 +1,8 @@ +import json +import types from functools import wraps -from flask import request, g, session, Response, jsonify -from flask.ext.restful import Resource +from flask import request, g, session, Response +from flask.ext.restful import Resource, reqparse from pyaggr3g470r.models import User from pyaggr3g470r.lib.exceptions import PyAggError @@ -35,42 +37,122 @@ def authenticate(func): return wrapper +def default_handler(obj): + """JSON handler for default query formatting""" + if hasattr(obj, 'isoformat'): + return obj.isoformat() + if hasattr(obj, 'dump'): + return obj.dump() + if isinstance(obj, (set, frozenset, types.GeneratorType)): + return list(obj) + raise TypeError("Object of type %s with value of %r " + "is not JSON serializable" % (type(obj), obj)) + + def to_response(func): def wrapper(*args, **kwargs): try: - res = func(*args, **kwargs) - except PyAggError, error: - response = jsonify(**error.message) + result = func(*args, **kwargs) + except PyAggError as error: + response = Response(json.dumps(result[0], default=default_handler)) response.status_code = error.status_code return response - if isinstance(res, tuple): - response = jsonify(**res[0]) - if len(res) > 1: - response.status_code = res[1] - return response - return res + status_code = 200 + if isinstance(result, tuple): + result, status_code = result + response = Response(json.dumps(result, default=default_handler), + status=status_code) + return response return wrapper -class PyAggResource(Resource): +class PyAggAbstractResource(Resource): method_decorators = [authenticate, to_response] - controller_cls = None - editable_attrs = [] def __init__(self, *args, **kwargs): self.controller = self.controller_cls(g.user.id) - super(PyAggResource, self).__init__(*args, **kwargs) + super(PyAggAbstractResource, self).__init__(*args, **kwargs) + + def reqparse_args(self, strict=False, default=True): + """ + strict: bool + if True will throw 400 error if args are defined and not in request + default: bool + if True, won't return defaults + + """ + parser = reqparse.RequestParser() + for attr_name, attrs in self.attrs.items(): + if not default and attr_name not in request.args: + continue + parser.add_argument(attr_name, location='json', **attrs) + return parser.parse_args(strict=strict) + + +class PyAggResourceNew(PyAggAbstractResource): + + def post(self): + return self.controller.create(**self.reqparse_args()), 201 + + +class PyAggResourceExisting(PyAggAbstractResource): def get(self, obj_id=None): - return {'result': [self.controller.get(id=obj_id).dump()]} + return self.controller.get(id=obj_id).dump() def put(self, obj_id=None): - args = self.reqparse.parse_args() + args = self.reqparse_args() new_values = {key: args[key] for key in - set(args).intersection(self.editable_attrs)} + set(args).intersection(self.attrs)} self.controller.update(obj_id, **new_values) - return {"message": "ok"} def delete(self, obj_id=None): self.controller.delete(obj_id) - return {"message": "ok"}, 204 + return None, 204 + + +class PyAggResourceMulti(PyAggAbstractResource): + + def get(self): + filters = self.reqparse_args(default=False) + return [res.dump() for res in self.controller.read(**filters).all()] + + def post(self): + status = 201 + results = [] + args = [] # FIXME + for arg in args: + try: + results.append(self.controller.create(**arg).id) + except Exception as error: + status = 206 + results.append(error) + return results, status + + def put(self): + status = 200 + results = [] + args = {} # FIXME + for obj_id, attrs in args.items(): + try: + new_values = {key: args[key] for key in + set(attrs).intersection(self.editable_attrs)} + self.controller.update(obj_id, **new_values) + results.append('ok') + except Exception as error: + status = 206 + results.append(error) + return results, status + + def delete(self): + status = 204 + results = [] + obj_ids = [] # FIXME extract some real ids + for obj_id in obj_ids: + try: + self.controller.delete(obj_id) + results.append('ok') + except Exception as error: + status = 206 + results.append(error) + return results, status diff --git a/pyaggr3g470r/views/api/feed.py b/pyaggr3g470r/views/api/feed.py index 94a5a433..e6f74cfd 100644 --- a/pyaggr3g470r/views/api/feed.py +++ b/pyaggr3g470r/views/api/feed.py @@ -1,92 +1,42 @@ +from datetime import datetime from flask import g from flask.ext.restful import Resource, reqparse -from pyaggr3g470r.controllers.feed import FeedController, \ - DEFAULT_MAX_ERROR, DEFAULT_LIMIT -from pyaggr3g470r.models import Feed +from pyaggr3g470r.controllers.feed import FeedController, \ + DEFAULT_MAX_ERROR, DEFAULT_LIMIT -from pyaggr3g470r.views.api.common import authenticate, to_response, \ - PyAggResource +from pyaggr3g470r.views.api.common import PyAggResourceNew, \ + PyAggResourceExisting, \ + PyAggResourceMulti -class FeedListAPI(Resource): - """ - Defines a RESTful API for Feed elements. - """ - method_decorators = [authenticate, to_response] +FEED_ATTRS = {'title': {'type': str}, + 'description': {'type': str}, + 'link': {'type': str}, + 'site_link': {'type': str}, + 'email_notification': {'type': bool, 'default': False}, + 'enabled': {'type': bool, 'default': True}, + 'etag': {'type': str, 'default': None}, + 'last_modified': {'type': datetime}, + 'last_error': {'type': datetime}, + 'error_count': {'type': int, 'default': 0}} - def __init__(self): - self.reqparse = reqparse.RequestParser() - self.reqparse.add_argument('title', - type=unicode, default="", location='json') - self.reqparse.add_argument('description', - type=unicode, default="", location='json') - self.reqparse.add_argument('link', type=unicode, location='json') - self.reqparse.add_argument('site_link', - type=unicode, default="", location='json') - self.reqparse.add_argument('email_notification', - type=bool, default=False, location='json') - self.reqparse.add_argument('enabled', - type=bool, default=True, location='json') - super(FeedListAPI, self).__init__() - def get(self): - """ - Returns a list of feeds. - """ - return {'result': [{"id": feed.id, - "title": feed.title, - "description": feed.description, - "link": feed.link, - "site_link": feed.site_link, - "email_notification": feed.email_notification, - "enabled": feed.enabled, - "created_date": feed.created_date, - } for feed in g.user.feeds]} - - def post(self): - """ - POST method - Create a new feed. - """ - args = self.reqparse.parse_args() - feed_dict = {} - for k, v in args.iteritems(): - if v != None: - feed_dict[k] = v - else: - return {'message': 'missing argument: %s' % (k,)}, 400 - new_feed = Feed(title=feed_dict["title"], - description=feed_dict["description"], - link=feed_dict["link"], - site_link=feed_dict["site_link"], - email_notification=feed_dict["email_notification"], - enabled=feed_dict["enabled"]) - g.user.feeds.append(new_feed) - try: - g.db.session.commit() - return {"message": "ok"} - except: - return {'message': 'Impossible to create the feed.'}, 500 - - -class FeedAPI(PyAggResource): - "Defines a RESTful API for Feed elements." +class FeedNewAPI(PyAggResourceNew): controller_cls = FeedController - editable_attrs = ['title', 'description', 'link', 'site_link', - 'email_notification', 'enabled', 'last_refreshed', - 'last_error', 'error_count'] + attrs = FEED_ATTRS - def __init__(self): - self.reqparse = reqparse.RequestParser() - self.reqparse.add_argument('title', type=unicode, location='json') - self.reqparse.add_argument('description', - type=unicode, location='json') - self.reqparse.add_argument('link', type=unicode, location='json') - self.reqparse.add_argument('site_link', type=unicode, location='json') - self.reqparse.add_argument('email_notification', - type=bool, location='json') - self.reqparse.add_argument('enabled', type=bool ,location='json') - super(FeedAPI, self).__init__() + +class FeedAPI(PyAggResourceExisting): + pass + controller_cls = FeedController + attrs = FEED_ATTRS + + +class FeedsAPI(PyAggResourceMulti): + pass + controller_cls = FeedController + attrs = FEED_ATTRS class FetchableFeedAPI(Resource): @@ -102,10 +52,12 @@ class FetchableFeedAPI(Resource): def get(self): args = self.reqparse.parse_args() controller = FeedController(g.user.id) - return {'result': [feed.dump() for feed in controller.list_fetchable( - max_error=args['max_error'], limit=args['limit'])]} + return [feed for feed in controller.list_fetchable( + max_error=args['max_error'], limit=args['limit'])] -g.api.add_resource(FeedListAPI, '/feeds', endpoint='feeds.json') -g.api.add_resource(FeedAPI, '/feeds/', endpoint='feed.json') -g.api.add_resource(FetchableFeedAPI, '/feeds/fetchable', endpoint='fetchable_feed.json') +g.api.add_resource(FeedNewAPI, '/feed', endpoint='feed_new.json') +g.api.add_resource(FeedAPI, '/feed/', endpoint='feed.json') +g.api.add_resource(FeedsAPI, '/feeds', endpoint='feeds.json') +g.api.add_resource(FetchableFeedAPI, '/feeds/fetchable', + endpoint='fetchable_feed.json') -- cgit From 5572851eca3b2f1bc56aed7232284acc436d2f49 Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Sun, 1 Mar 2015 03:20:12 +0100 Subject: new crawler with cache control and error handling --- pyaggr3g470r/views/api/article.py | 36 ++++++++++++++---- pyaggr3g470r/views/api/common.py | 79 ++++++++++++++++++++------------------- pyaggr3g470r/views/api/feed.py | 37 ++++++++---------- 3 files changed, 86 insertions(+), 66 deletions(-) (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/article.py b/pyaggr3g470r/views/api/article.py index ebda6247..17881412 100644 --- a/pyaggr3g470r/views/api/article.py +++ b/pyaggr3g470r/views/api/article.py @@ -1,36 +1,58 @@ from flask import g +import dateutil.parser from pyaggr3g470r.controllers import ArticleController -from pyaggr3g470r.views.api.common import PyAggResourceNew, \ +from pyaggr3g470r.views.api.common import PyAggAbstractResource,\ + PyAggResourceNew, \ PyAggResourceExisting, \ PyAggResourceMulti -ARTICLE_ATTRS = {'title': {'type': str}, - 'content': {'type': str}, +ARTICLE_ATTRS = {'feed_id': {'type': str}, + 'entry_id': {'type': str}, 'link': {'type': str}, - 'date': {'type': str}, - 'feed_id': {'type': int}, - 'like': {'type': bool}, - 'readed': {'type': bool}} + 'title': {'type': str}, + 'readed': {'type': bool}, 'like': {'type': bool}, + 'content': {'type': str}, + 'date': {'type': str}, 'retrieved_date': {'type': str}} class ArticleNewAPI(PyAggResourceNew): controller_cls = ArticleController attrs = ARTICLE_ATTRS + to_date = ['date', 'retrieved_date'] class ArticleAPI(PyAggResourceExisting): controller_cls = ArticleController attrs = ARTICLE_ATTRS + to_date = ['date', 'retrieved_date'] class ArticlesAPI(PyAggResourceMulti): controller_cls = ArticleController attrs = ARTICLE_ATTRS + to_date = ['date', 'retrieved_date'] + + +class ArticlesChallenge(PyAggAbstractResource): + controller_cls = ArticleController + attrs = {'ids': {'type': list, 'default': []}} + to_date = ['date', 'retrieved_date'] + + def get(self): + parsed_args = self.reqparse_args() + for id_dict in parsed_args['ids']: + for key in self.to_date: + if key in id_dict: + id_dict[key] = dateutil.parser.parse(id_dict[key]) + + return self.controller.challenge(parsed_args['ids']) g.api.add_resource(ArticleNewAPI, '/article', endpoint='article_new.json') g.api.add_resource(ArticleAPI, '/article/', endpoint='article.json') g.api.add_resource(ArticlesAPI, '/articles', endpoint='articles.json') +g.api.add_resource(ArticlesChallenge, '/articles/challenge', + endpoint='articles_challenge.json') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py index c0759c03..a9d35411 100644 --- a/pyaggr3g470r/views/api/common.py +++ b/pyaggr3g470r/views/api/common.py @@ -1,12 +1,16 @@ import json -import types +import logging +import dateutil.parser from functools import wraps from flask import request, g, session, Response from flask.ext.restful import Resource, reqparse +from pyaggr3g470r.lib.utils import default_handler from pyaggr3g470r.models import User from pyaggr3g470r.lib.exceptions import PyAggError +logger = logging.getLogger(__name__) + def authenticate(func): """ @@ -24,55 +28,47 @@ def authenticate(func): # authentication via HTTP only auth = request.authorization try: - email = auth.username - user = User.query.filter(User.email == email).first() - if user and user.check_password(auth.password) and user.activation_key == "": + user = User.query.filter(User.nickname == auth.username).first() + if user and user.check_password(auth.password) \ + and user.activation_key == "": g.user = user - return func(*args, **kwargs) - except AttributeError: - pass - - return Response('', 401, - {'WWWAuthenticate':'Basic realm="Login Required"'}) + except Exception: + return Response('', 401, + {'WWWAuthenticate': + 'Basic realm="Login Required"'}) + return func(*args, **kwargs) return wrapper -def default_handler(obj): - """JSON handler for default query formatting""" - if hasattr(obj, 'isoformat'): - return obj.isoformat() - if hasattr(obj, 'dump'): - return obj.dump() - if isinstance(obj, (set, frozenset, types.GeneratorType)): - return list(obj) - raise TypeError("Object of type %s with value of %r " - "is not JSON serializable" % (type(obj), obj)) - - def to_response(func): def wrapper(*args, **kwargs): + status_code = 200 try: result = func(*args, **kwargs) except PyAggError as error: - response = Response(json.dumps(result[0], default=default_handler)) - response.status_code = error.status_code - return response - status_code = 200 - if isinstance(result, tuple): - result, status_code = result - response = Response(json.dumps(result, default=default_handler), + return Response(json.dumps(error, default=default_handler), status=status_code) - return response + if isinstance(result, Response): + return result + elif isinstance(result, tuple): + result, status_code = result + return Response(json.dumps(result, default=default_handler), + status=status_code) return wrapper class PyAggAbstractResource(Resource): method_decorators = [authenticate, to_response] + attrs = {} + to_date = [] def __init__(self, *args, **kwargs): - self.controller = self.controller_cls(g.user.id) super(PyAggAbstractResource, self).__init__(*args, **kwargs) + @property + def controller(self): + return self.controller_cls(getattr(g.user, 'id', None)) + def reqparse_args(self, strict=False, default=True): """ strict: bool @@ -83,10 +79,17 @@ class PyAggAbstractResource(Resource): """ parser = reqparse.RequestParser() for attr_name, attrs in self.attrs.items(): - if not default and attr_name not in request.args: + if not default and attr_name not in request.json: continue parser.add_argument(attr_name, location='json', **attrs) - return parser.parse_args(strict=strict) + parsed = parser.parse_args(strict=strict) + for field in self.to_date: + if parsed.get(field): + try: + parsed[field] = dateutil.parser.parse(parsed[field]) + except Exception: + logger.exception('failed to parse %r', parsed[field]) + return parsed class PyAggResourceNew(PyAggAbstractResource): @@ -98,13 +101,13 @@ class PyAggResourceNew(PyAggAbstractResource): class PyAggResourceExisting(PyAggAbstractResource): def get(self, obj_id=None): - return self.controller.get(id=obj_id).dump() + return self.controller.get(id=obj_id) def put(self, obj_id=None): - args = self.reqparse_args() + args = self.reqparse_args(default=False) new_values = {key: args[key] for key in set(args).intersection(self.attrs)} - self.controller.update(obj_id, **new_values) + self.controller.update({'id': obj_id}, new_values) def delete(self, obj_id=None): self.controller.delete(obj_id) @@ -115,7 +118,7 @@ class PyAggResourceMulti(PyAggAbstractResource): def get(self): filters = self.reqparse_args(default=False) - return [res.dump() for res in self.controller.read(**filters).all()] + return [res for res in self.controller.read(**filters).all()] def post(self): status = 201 @@ -137,7 +140,7 @@ class PyAggResourceMulti(PyAggAbstractResource): try: new_values = {key: args[key] for key in set(attrs).intersection(self.editable_attrs)} - self.controller.update(obj_id, **new_values) + self.controller.update({'id': obj_id}, new_values) results.append('ok') except Exception as error: status = 206 diff --git a/pyaggr3g470r/views/api/feed.py b/pyaggr3g470r/views/api/feed.py index e6f74cfd..625ad52d 100644 --- a/pyaggr3g470r/views/api/feed.py +++ b/pyaggr3g470r/views/api/feed.py @@ -1,11 +1,10 @@ -from datetime import datetime from flask import g -from flask.ext.restful import Resource, reqparse from pyaggr3g470r.controllers.feed import FeedController, \ DEFAULT_MAX_ERROR, DEFAULT_LIMIT -from pyaggr3g470r.views.api.common import PyAggResourceNew, \ +from pyaggr3g470r.views.api.common import PyAggAbstractResource, \ + PyAggResourceNew, \ PyAggResourceExisting, \ PyAggResourceMulti @@ -16,44 +15,40 @@ FEED_ATTRS = {'title': {'type': str}, 'site_link': {'type': str}, 'email_notification': {'type': bool, 'default': False}, 'enabled': {'type': bool, 'default': True}, - 'etag': {'type': str, 'default': None}, - 'last_modified': {'type': datetime}, - 'last_error': {'type': datetime}, + 'etag': {'type': str, 'default': ''}, + 'last_modified': {'type': str}, + 'last_retreived': {'type': str}, + 'last_error': {'type': str}, 'error_count': {'type': int, 'default': 0}} class FeedNewAPI(PyAggResourceNew): controller_cls = FeedController attrs = FEED_ATTRS + to_date = ['date', 'last_retreived'] class FeedAPI(PyAggResourceExisting): - pass controller_cls = FeedController attrs = FEED_ATTRS + to_date = ['date', 'last_retreived'] class FeedsAPI(PyAggResourceMulti): - pass controller_cls = FeedController attrs = FEED_ATTRS + to_date = ['date', 'last_retreived'] -class FetchableFeedAPI(Resource): - - def __init__(self): - self.reqparse = reqparse.RequestParser() - self.reqparse.add_argument('max_error', type=int, location='json', - default=DEFAULT_MAX_ERROR) - self.reqparse.add_argument('limit', type=int, location='json', - default=DEFAULT_LIMIT) - super(FetchableFeedAPI, self).__init__() +class FetchableFeedAPI(PyAggAbstractResource): + controller_cls = FeedController + to_date = ['date', 'last_retreived'] + attrs = {'max_error': {'type': int, 'default': DEFAULT_MAX_ERROR}, + 'limit': {'type': int, 'default': DEFAULT_LIMIT}} def get(self): - args = self.reqparse.parse_args() - controller = FeedController(g.user.id) - return [feed for feed in controller.list_fetchable( - max_error=args['max_error'], limit=args['limit'])] + return [feed for feed in self.controller.list_fetchable( + **self.reqparse_args())] g.api.add_resource(FeedNewAPI, '/feed', endpoint='feed_new.json') -- cgit From a4fb151ea53d8054cc8e3fb309395c8fa0e23aaf Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Sun, 1 Mar 2015 14:08:02 +0100 Subject: fixing/restoring logging level --- pyaggr3g470r/views/api/common.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py index a9d35411..a136645c 100644 --- a/pyaggr3g470r/views/api/common.py +++ b/pyaggr3g470r/views/api/common.py @@ -2,12 +2,12 @@ import json import logging import dateutil.parser from functools import wraps +from werkzeug.exceptions import Unauthorized from flask import request, g, session, Response from flask.ext.restful import Resource, reqparse from pyaggr3g470r.lib.utils import default_handler from pyaggr3g470r.models import User -from pyaggr3g470r.lib.exceptions import PyAggError logger = logging.getLogger(__name__) @@ -18,36 +18,31 @@ def authenticate(func): """ @wraps(func) def wrapper(*args, **kwargs): + logged_in = False if not getattr(func, 'authenticated', True): - return func(*args, **kwargs) - + logged_in = True # authentication based on the session (already logged on the site) - if 'email' in session or g.user.is_authenticated(): - return func(*args, **kwargs) - - # authentication via HTTP only - auth = request.authorization - try: + elif 'email' in session or g.user.is_authenticated(): + logged_in = True + else: + # authentication via HTTP only + auth = request.authorization user = User.query.filter(User.nickname == auth.username).first() if user and user.check_password(auth.password) \ and user.activation_key == "": g.user = user - except Exception: - return Response('', 401, - {'WWWAuthenticate': - 'Basic realm="Login Required"'}) - return func(*args, **kwargs) + logged_in = True + + if logged_in: + return func(*args, **kwargs) + raise Unauthorized({'WWWAuthenticate': 'Basic realm="Login Required"'}) return wrapper def to_response(func): def wrapper(*args, **kwargs): status_code = 200 - try: - result = func(*args, **kwargs) - except PyAggError as error: - return Response(json.dumps(error, default=default_handler), - status=status_code) + result = func(*args, **kwargs) if isinstance(result, Response): return result elif isinstance(result, tuple): -- cgit From aef485875f45b35103242ff2194448bb61e9fc97 Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Mon, 2 Mar 2015 00:21:51 +0100 Subject: fixing multi get --- pyaggr3g470r/views/api/common.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py index a136645c..e4f80bf7 100644 --- a/pyaggr3g470r/views/api/common.py +++ b/pyaggr3g470r/views/api/common.py @@ -1,6 +1,7 @@ import json import logging import dateutil.parser +from copy import deepcopy from functools import wraps from werkzeug.exceptions import Unauthorized from flask import request, g, session, Response @@ -64,20 +65,24 @@ class PyAggAbstractResource(Resource): def controller(self): return self.controller_cls(getattr(g.user, 'id', None)) - def reqparse_args(self, strict=False, default=True): + def reqparse_args(self, req=None, strict=False, default=True, args=None): """ strict: bool if True will throw 400 error if args are defined and not in request default: bool if True, won't return defaults - """ parser = reqparse.RequestParser() - for attr_name, attrs in self.attrs.items(): - if not default and attr_name not in request.json: + for attr_name, attrs in (args or self.attrs).items(): + if attrs.pop('force_default', False): + parser.add_argument(attr_name, location='json', **attrs) + elif not default and (not request.json + or request.json and attr_name not in request.json): continue - parser.add_argument(attr_name, location='json', **attrs) - parsed = parser.parse_args(strict=strict) + else: + parser.add_argument(attr_name, location='json', **attrs) + parsed = parser.parse_args(strict=strict) if req is None \ + else parser.parse_args(req, strict=strict) for field in self.to_date: if parsed.get(field): try: @@ -112,8 +117,13 @@ class PyAggResourceExisting(PyAggAbstractResource): class PyAggResourceMulti(PyAggAbstractResource): def get(self): - filters = self.reqparse_args(default=False) - return [res for res in self.controller.read(**filters).all()] + args = deepcopy(self.attrs) + args['limit'] = {'type': int, 'default': 10, 'force_default': True} + filters = self.reqparse_args(default=False, strict=False, args=args) + limit = filters.pop('limit', None) + if not limit: + return [res for res in self.controller.read(**filters).all()] + return [res for res in self.controller.read(**filters).limit(limit)] def post(self): status = 201 -- cgit From 631fc8a3ebaf74dc609a445dc0b11b73eb0eab02 Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Tue, 3 Mar 2015 18:12:11 +0100 Subject: adding some docstring --- pyaggr3g470r/views/api/common.py | 51 ++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py index e4f80bf7..c59bb1fc 100644 --- a/pyaggr3g470r/views/api/common.py +++ b/pyaggr3g470r/views/api/common.py @@ -1,3 +1,23 @@ +"""For a given resources, classes in the module intend to create the following +routes : + GET resource/ + -> to retreive one + POST resource + -> to create one + PUT resource/ + -> to update one + DELETE resource/ + -> to delete one + + GET resources + -> to retreive several + POST resources + -> to create several + PUT resources + -> to update several + DELETE resources + -> to delete several +""" import json import logging import dateutil.parser @@ -41,6 +61,8 @@ def authenticate(func): def to_response(func): + """Will cast results of func as a result, and try to extract + a status_code for the Response object""" def wrapper(*args, **kwargs): status_code = 200 result = func(*args, **kwargs) @@ -56,7 +78,7 @@ def to_response(func): class PyAggAbstractResource(Resource): method_decorators = [authenticate, to_response] attrs = {} - to_date = [] + to_date = [] # list of fields to cast to datetime def __init__(self, *args, **kwargs): super(PyAggAbstractResource, self).__init__(*args, **kwargs) @@ -71,6 +93,8 @@ class PyAggAbstractResource(Resource): 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 """ parser = reqparse.RequestParser() for attr_name, attrs in (args or self.attrs).items(): @@ -95,21 +119,25 @@ class PyAggAbstractResource(Resource): class PyAggResourceNew(PyAggAbstractResource): def post(self): + """Create a single new object""" return self.controller.create(**self.reqparse_args()), 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(default=False) new_values = {key: args[key] for key in set(args).intersection(self.attrs)} self.controller.update({'id': obj_id}, new_values) def delete(self, obj_id=None): + """delete a object""" self.controller.delete(obj_id) return None, 204 @@ -117,6 +145,9 @@ class PyAggResourceExisting(PyAggAbstractResource): class PyAggResourceMulti(PyAggAbstractResource): def get(self): + """retreive 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 = deepcopy(self.attrs) args['limit'] = {'type': int, 'default': 10, 'force_default': True} filters = self.reqparse_args(default=False, strict=False, args=args) @@ -126,10 +157,12 @@ class PyAggResourceMulti(PyAggAbstractResource): return [res for res in self.controller.read(**filters).limit(limit)] def post(self): + """creating several objects. payload should be a list of dict. + """ status = 201 results = [] args = [] # FIXME - for arg in args: + for attrs in request.json(): try: results.append(self.controller.create(**arg).id) except Exception as error: @@ -138,10 +171,14 @@ class PyAggResourceMulti(PyAggAbstractResource): return results, status def put(self): + """creating several objects. payload should be: + >>> payload + [[obj_id1, {attr1: val1, attr2: val2}] + [obj_id2, {attr1: val1, attr2: val2}]] + """ status = 200 results = [] - args = {} # FIXME - for obj_id, attrs in args.items(): + for obj_id, attrs in request.json(): try: new_values = {key: args[key] for key in set(attrs).intersection(self.editable_attrs)} @@ -153,10 +190,10 @@ class PyAggResourceMulti(PyAggAbstractResource): return results, status def delete(self): + """will delete several objects, + a list of their ids should be in the payload""" status = 204 - results = [] - obj_ids = [] # FIXME extract some real ids - for obj_id in obj_ids: + for obj_id in request.json(): try: self.controller.delete(obj_id) results.append('ok') -- cgit From a6769fc5f2c84aa079944d28a6b6505c3cc12370 Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Tue, 3 Mar 2015 22:16:24 +0100 Subject: fixing the multi api --- pyaggr3g470r/views/api/common.py | 46 ++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 16 deletions(-) (limited to 'pyaggr3g470r/views/api') diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py index c59bb1fc..4f703712 100644 --- a/pyaggr3g470r/views/api/common.py +++ b/pyaggr3g470r/views/api/common.py @@ -23,7 +23,7 @@ import logging import dateutil.parser from copy import deepcopy from functools import wraps -from werkzeug.exceptions import Unauthorized +from werkzeug.exceptions import Unauthorized, BadRequest from flask import request, g, session, Response from flask.ext.restful import Resource, reqparse @@ -148,26 +148,29 @@ class PyAggResourceMulti(PyAggAbstractResource): """retreive 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 = deepcopy(self.attrs) - args['limit'] = {'type': int, 'default': 10, 'force_default': True} - filters = self.reqparse_args(default=False, strict=False, args=args) - limit = filters.pop('limit', None) + if 'application/json' != request.headers.get('Content-Type'): + raise BadRequest("Content-Type must be application/json") + limit = request.json.pop('limit', 10) if not limit: - return [res for res in self.controller.read(**filters).all()] - return [res for res in self.controller.read(**filters).limit(limit)] + return [res for res in self.controller.read(**request.json).all()] + return [res for res in self.controller.read(**request.json).limit(limit)] def post(self): """creating several objects. payload should be a list of dict. """ + if 'application/json' != request.headers.get('Content-Type'): + raise BadRequest("Content-Type must be application/json") status = 201 results = [] - args = [] # FIXME - for attrs in request.json(): + for attrs in request.json: try: - results.append(self.controller.create(**arg).id) + results.append(self.controller.create(**attrs).id) except Exception as error: status = 206 - results.append(error) + results.append(str(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 def put(self): @@ -176,28 +179,39 @@ class PyAggResourceMulti(PyAggAbstractResource): [[obj_id1, {attr1: val1, attr2: val2}] [obj_id2, {attr1: val1, attr2: val2}]] """ + if 'application/json' != request.headers.get('Content-Type'): + raise BadRequest("Content-Type must be application/json") status = 200 results = [] - for obj_id, attrs in request.json(): + for obj_id, attrs in request.json: try: - new_values = {key: args[key] for key in - set(attrs).intersection(self.editable_attrs)} + new_values = {key: attrs[key] for key in + set(attrs).intersection(self.attrs)} self.controller.update({'id': obj_id}, new_values) results.append('ok') except Exception as error: status = 206 - results.append(error) + results.append(str(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 def delete(self): """will delete several objects, a list of their ids should be in the payload""" + if 'application/json' != request.headers.get('Content-Type'): + raise BadRequest("Content-Type must be application/json") status = 204 - for obj_id in request.json(): + results = [] + 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