diff options
-rw-r--r-- | bootstrap.py | 4 | ||||
-rw-r--r-- | documentation/deployment.rst | 3 | ||||
-rw-r--r-- | documentation/web-services.rst | 72 | ||||
-rw-r--r-- | pyaggr3g470r/decorators.py | 13 | ||||
-rwxr-xr-x | pyaggr3g470r/lib/client.py | 16 | ||||
-rw-r--r-- | pyaggr3g470r/lib/crawler.py | 7 | ||||
-rw-r--r-- | pyaggr3g470r/lib/exceptions.py | 13 | ||||
-rw-r--r-- | pyaggr3g470r/search.py | 2 | ||||
-rw-r--r-- | pyaggr3g470r/static/js/articles.js | 8 | ||||
-rw-r--r-- | pyaggr3g470r/templates/layout.html | 10 | ||||
-rw-r--r-- | pyaggr3g470r/views/api/common.py | 33 | ||||
-rw-r--r-- | pyaggr3g470r/views/article.py | 2 | ||||
-rw-r--r-- | pyaggr3g470r/views/feed.py | 6 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rwxr-xr-x | runserver.py | 2 |
15 files changed, 75 insertions, 118 deletions
diff --git a/bootstrap.py b/bootstrap.py index 5d599146..83862640 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -21,14 +21,14 @@ def set_logging(log_path, log_level=logging.DEBUG, logger.addHandler(handler) logger.setLevel(log_level) -set_logging(conf.LOG_PATH) - from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy # Create Flask application application = Flask('pyaggr3g470r') application.debug = conf.WEBSERVER_DEBUG +set_logging(conf.LOG_PATH, log_level=logging.DEBUG if conf.WEBSERVER_DEBUG + else logging.INFO) # Create dummy secrey key so we can use sessions application.config['SECRET_KEY'] = getattr(conf, 'WEBSERVER_SECRET', None) diff --git a/documentation/deployment.rst b/documentation/deployment.rst index d0639c45..d06d55fe 100644 --- a/documentation/deployment.rst +++ b/documentation/deployment.rst @@ -92,6 +92,7 @@ If you want to use PostgreSQL .. code-block:: bash $ sudo apt-get install postgresql postgresql-server-dev-9.3 postgresql-client + $ pip install psycopg2 $ echo "127.0.0.1:5432:aggregator:pgsqluser:pgsqlpwd" > ~/.pgpass $ chmod 700 ~/.pgpass $ sudo -u postgres createuser pgsqluser --no-superuser --createdb --no-createrole @@ -131,7 +132,7 @@ Configuration ============= Configuration (database url, email, proxy, user agent, etc.) is done via the file *conf/conf.cfg*. -Check these configuration before executing *db_create.py*. +Check these configuration before executing *db_create.py*. If you want to use pyAggr3g470r with Tor/Privoxy, you just have to set the value of *http_proxy* (most of the time: *http_proxy = 127.0.0.1:8118**). Else leave the value blank. diff --git a/documentation/web-services.rst b/documentation/web-services.rst index 2d724569..dd2d0125 100644 --- a/documentation/web-services.rst +++ b/documentation/web-services.rst @@ -7,10 +7,11 @@ Articles .. code-block:: python >>> import requests, json - >>> r = requests.get("https://pyaggr3g470r.herokuapp.com/api/v1.0/articles", auth=("your-email", "your-password")) - >>> r.status_code-block - 200 - >>> rjson = json.loads(r.text) + >>> r = requests.get("https://pyaggr3g470r.herokuapp.com/api/v2.0/articles", + ... auth=("your-nickname", "your-password")) + >>> r.status_code + 200 # OK + >>> rjson = r.json() >>> rjson["result"][0]["title"] u'Sponsors required for KDE code sprint in Randa' >>> rjson["result"][0]["date"] @@ -20,15 +21,19 @@ Possible parameters: .. code-block:: bash - $ curl --user your-email:your-password "https://pyaggr3g470r.herokuapp.com/api/v1.0/articles?filter_=unread&feed=24" - $ curl --user your-email:your-password "https://pyaggr3g470r.herokuapp.com/api/v1.0/articles?filter_=read&feed=24&limit=20" - $ curl --user your-email:your-password "https://pyaggr3g470r.herokuapp.com/api/v1.0/articles?filter_=all&feed=24&limit=20" + $ curl --user your-nickname:your-password "https://pyaggr3g470r.herokuapp.com/api/v2.0/articles" -H 'Content-Type: application/json' --data='{"feed": 24}' -Get an article: +Get an article with another way to pass credentials : .. code-block:: bash - $ curl --user your-email:your-password "https://pyaggr3g470r.herokuapp.com/api/v1.0/articles/84566" + $ curl "https://your-nickname:your-password@pyaggr3g470r.herokuapp.com/api/v2.0/article/84566" + +And delete it : + +.. code-block:: bash + + $ curl -XDELETE "https://your-nickname:your-password@pyaggr3g470r.herokuapp.com/api/v2.0/article/84566" Add an article: @@ -36,13 +41,18 @@ Add an article: >>> import requests, json >>> headers = {'Content-type': 'application/json', 'Accept': 'application/json'} - >>> payload = {'link': 'http://blog.cedricbonhomme.org/2014/05/24/sortie-de-pyaggr3g470r-5-3/', 'title': 'Sortie de pyAggr3g470r 5.3', 'content':'La page principale de pyAggr3g470r a été améliorée...', 'date':'06/23/2014 11:42 AM', 'feed_id':'42'} - >>> r = requests.post("https://pyaggr3g470r.herokuapp.com/api/v1.0/articles", headers=headers, auth=("your-email", "your-password"), data=json.dumps(payload)) - >>> print r.content - { - "message": "ok" - } - >>> r = requests.get("https://pyaggr3g470r.herokuapp.com/api/v1.0/articles?feed=42&limit=1", auth=("your-email", "your-password")) + >>> payload = {'link': 'http://blog.cedricbonhomme.org/2014/05/24/sortie-de-pyaggr3g470r-5-3/', + ... 'title': 'Sortie de pyAggr3g470r 5.3', + ... 'content':'La page principale de pyAggr3g470r a été améliorée...', + ... 'date':'2014/06/23T11:42:20 GMT', + ... 'feed_id':'42'} + >>> r = requests.post("https://pyaggr3g470r.herokuapp.com/api/v2.0/article", + ... headers=headers, auth=("your-nickname", "your-password"), data=json.dumps(payload)) + >>> print r.status_code + 201 # Created + >>> r = requests.get("https://pyaggr3g470r.herokuapp.com/api/v2.0/articles", + ... auth=("your-nickname", "your-password") + ... data=json.dumps({'feed_id': 42, 'limit': 1})) >>> print json.loads(r.content)["result"][0]["title"] Sortie de pyAggr3g470r 5.3 @@ -51,30 +61,20 @@ Update an article: .. code-block:: python >>> payload = {"like":True, "readed":False} - >>> r = requests.put("https://pyaggr3g470r.herokuapp.com/api/v1.0/articles/65", headers=headers, auth=("your-email", "your-password"), data=json.dumps(payload)) - >>> print r.content - { - "message": "ok" - } + >>> r = requests.put("https://pyaggr3g470r.herokuapp.com/api/v2.0/article/65", headers=headers, auth=("your-nickname", "your-password"), data=json.dumps(payload)) + >>> print r.status_code + 200 # OK Delete an article: .. code-block:: python - >>> r = requests.delete("https://pyaggr3g470r.herokuapp.com/api/v1.0/articles/84574", auth=("your-email", "your-password")) + >>> r = requests.delete("https://pyaggr3g470r.herokuapp.com/api/v2.0/article/84574", auth=("your-nickname", "your-password")) >>> print r.status_code - 200 - >>> print r.content - { - "message": "ok" - } - >>> r = requests.delete("https://pyaggr3g470r.herokuapp.com/api/v1.0/articles/84574", auth=("your-email", "your-password")) + 204 # deleted - No content + >>> r = requests.delete("https://pyaggr3g470r.herokuapp.com/api/v2.0/article/84574", auth=("your-nickname", "your-password")) >>> print r.status_code - 200 - >>> print r.content - { - "message": "Article not found." - } + 404 # not found Feeds ----- @@ -84,17 +84,17 @@ Add a feed: .. code-block:: python >>> payload = {'link': 'http://blog.cedricbonhomme.org/feed'} - >>> r = requests.post("https://pyaggr3g470r.herokuapp.com/api/v1.0/feeds", headers=headers, auth=("your-email", "your-password"), data=json.dumps(payload)) + >>> r = requests.post("https://pyaggr3g470r.herokuapp.com/api/v2.0/feeds", headers=headers, auth=("your-nickname", "your-password"), data=json.dumps(payload)) Update a feed: .. code-block:: python >>> payload = {"title":"Feed new title", "description":"New description"} - >>> r = requests.put("https://pyaggr3g470r.herokuapp.com/api/v1.0/feeds/42", headers=headers, auth=("your-email", "your-password"), data=json.dumps(payload)) + >>> r = requests.put("https://pyaggr3g470r.herokuapp.com/api/v2.0/feeds/42", headers=headers, auth=("your-nickname", "your-password"), data=json.dumps(payload)) Delete a feed: .. code-block:: python - >>> r = requests.delete("https://pyaggr3g470r.herokuapp.com/api/v1.0/feeds/29", auth=("your-email", "your-password")) + >>> r = requests.delete("https://pyaggr3g470r.herokuapp.com/api/v2.0/feeds/29", auth=("your-nickname", "your-password")) diff --git a/pyaggr3g470r/decorators.py b/pyaggr3g470r/decorators.py index 9bae626d..9e8f9c0b 100644 --- a/pyaggr3g470r/decorators.py +++ b/pyaggr3g470r/decorators.py @@ -9,7 +9,6 @@ from flask.ext.babel import gettext from flask.ext.login import login_required from pyaggr3g470r.models import Feed -from pyaggr3g470r.lib.exceptions import PyAggError def async(f): @@ -43,20 +42,8 @@ def feed_access_required(func): return decorated -def handle_pyagg_error(func): - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except PyAggError as error: - flash(gettext(error.default_message), 'warning') - return redirect(url_for('home')) - return wrapper - - def pyagg_default_decorator(func): @login_required - @handle_pyagg_error @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) diff --git a/pyaggr3g470r/lib/client.py b/pyaggr3g470r/lib/client.py deleted file mode 100755 index 6b2fc9ae..00000000 --- a/pyaggr3g470r/lib/client.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python -import json -import requests -import conf - - -def get_client(email, password): - client = requests.session() - client.get(conf.PLATFORM_URL + 'api/csrf', verify=False, - data=json.dumps({'email': email, - 'password': password})) - return client - - -def get_articles(client): - return client.get(conf.PLATFORM_URL + 'api/v1.0/articles/').json diff --git a/pyaggr3g470r/lib/crawler.py b/pyaggr3g470r/lib/crawler.py index 6697e4c3..de770934 100644 --- a/pyaggr3g470r/lib/crawler.py +++ b/pyaggr3g470r/lib/crawler.py @@ -10,6 +10,7 @@ from requests_futures.sessions import FuturesSession from pyaggr3g470r.lib.utils import default_handler logger = logging.getLogger(__name__) +API_ROOT = "api/v2.0/" def extract_id(entry, keys=[('link', 'link'), @@ -52,7 +53,7 @@ class AbstractCrawler: if data is None: data = {} method = getattr(self.session, method) - return method("%sapi/v1.0/%s" % (self.url, urn), + return method("%s%s%s" % (self.url, API_ROOT, urn), auth=self.auth, data=json.dumps(data, default=default_handler), headers={'Content-Type': 'application/json'}) @@ -193,7 +194,7 @@ class CrawlerScheduler(AbstractCrawler): headers=self.prepare_headers(feed)) future.add_done_callback(FeedCrawler(feed, self.auth).callback) - def run(self): + def run(self, **kwargs): logger.debug('retreving fetchable feed') - future = self.query_pyagg('get', 'feeds/fetchable') + future = self.query_pyagg('get', 'feeds/fetchable', kwargs) future.add_done_callback(self.callback) diff --git a/pyaggr3g470r/lib/exceptions.py b/pyaggr3g470r/lib/exceptions.py deleted file mode 100644 index 30c71a5c..00000000 --- a/pyaggr3g470r/lib/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ -class PyAggError(Exception): - status_code = None - default_message = '' - - -class Forbidden(PyAggError): - status_code = 403 - default_message = 'You do not have the rights to access that resource' - - -class NotFound(PyAggError): - status_code = 404 - default_message = 'Resource was not found' diff --git a/pyaggr3g470r/search.py b/pyaggr3g470r/search.py index 89fa0860..a7f780df 100644 --- a/pyaggr3g470r/search.py +++ b/pyaggr3g470r/search.py @@ -102,7 +102,7 @@ def delete_article(user_id, feed_id, article_id): try: ix = open_dir(indexdir) except (EmptyIndexError, OSError): - raise EmptyIndexError + return writer = ix.writer() document = And([Term("user_id", user_id), Term("feed_id", feed_id), Term("article_id", article_id)]) diff --git a/pyaggr3g470r/static/js/articles.js b/pyaggr3g470r/static/js/articles.js index 51273f3d..312a5cb6 100644 --- a/pyaggr3g470r/static/js/articles.js +++ b/pyaggr3g470r/static/js/articles.js @@ -18,6 +18,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +API_ROOT = 'api/v2.0/' + if (typeof jQuery === 'undefined') { throw new Error('Requires jQuery') } +function ($) { @@ -78,7 +80,7 @@ if (typeof jQuery === 'undefined') { throw new Error('Requires jQuery') } // Encode your data as JSON. data: data, // This is the type of data you're expecting back from the server. - url: "/api/v1.0/articles/"+article_id, + url: API_ROOT + "article/" + article_id, success: function (result) { //console.log(result); }, @@ -114,7 +116,7 @@ if (typeof jQuery === 'undefined') { throw new Error('Requires jQuery') } // Encode your data as JSON. data: data, // This is the type of data you're expecting back from the server. - url: "/api/v1.0/articles/"+article_id, + url: API_ROOT + "article/" + article_id, success: function (result) { //console.log(result); }, @@ -132,7 +134,7 @@ if (typeof jQuery === 'undefined') { throw new Error('Requires jQuery') } // sends the updates to the server $.ajax({ type: 'DELETE', - url: "/api/v1.0/articles/"+article_id, + url: API_ROOT + "article/" + article_id, success: function (result) { //console.log(result); }, diff --git a/pyaggr3g470r/templates/layout.html b/pyaggr3g470r/templates/layout.html index 4dc62350..6b929bf3 100644 --- a/pyaggr3g470r/templates/layout.html +++ b/pyaggr3g470r/templates/layout.html @@ -7,9 +7,9 @@ <meta name="description" content="pyAggr3g470r is a web-based news aggregator." /> <meta name="author" content="" /> <title>{% if head_title %}{{ head_title }} - {% endif %}pyAggr3g470r</title> - <link rel="shortcut icon" href="{{ url_for('.static', filename='img/favicon.png') }}" /> + <link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.png') }}" /> <!-- Bootstrap core CSS --> - <link href="{{ url_for('.static', filename = 'css/bootstrap.css') }}" rel="stylesheet" media="screen" /> + <link href="{{ url_for('static', filename = 'css/bootstrap.css') }}" rel="stylesheet" media="screen" /> <!-- Add custom CSS here --> <style> body { @@ -155,9 +155,9 @@ <!-- Bootstrap core JavaScript --> <!-- Placed at the end of the document so the pages load faster --> - <script src="{{ url_for('.static', filename = 'js/jquery.js') }}"></script> - <script src="{{ url_for('.static', filename = 'js/bootstrap.js') }}"></script> - <script src="{{ url_for('.static', filename = 'js/articles.js') }}"></script> + <script src="{{ url_for('static', filename = 'js/jquery.js') }}"></script> + <script src="{{ url_for('static', filename = 'js/bootstrap.js') }}"></script> + <script src="{{ url_for('static', filename = 'js/articles.js') }}"></script> <script type="text/javascript" class="source"> if (window.location.href.indexOf("filter_=all") > -1){ $("#tab-all").attr('class', "active"); 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('<Authentication required>', 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): diff --git a/pyaggr3g470r/views/article.py b/pyaggr3g470r/views/article.py index 21858a33..66cc0f37 100644 --- a/pyaggr3g470r/views/article.py +++ b/pyaggr3g470r/views/article.py @@ -21,7 +21,7 @@ def articles(feed_id=None, nb_articles=-1): if len(feed.articles.all()) <= nb_articles: nb_articles = -1 if nb_articles == -1: - feed.articles = feed.article.limit(nb_articles) + feed.articles = feed.articles.limit(nb_articles) return render_template('articles.html', feed=feed, nb_articles=nb_articles) diff --git a/pyaggr3g470r/views/feed.py b/pyaggr3g470r/views/feed.py index fa9dc23d..2af502a7 100644 --- a/pyaggr3g470r/views/feed.py +++ b/pyaggr3g470r/views/feed.py @@ -28,15 +28,15 @@ def feed(feed_id=None): top_words = utils.top_words(articles, n=50, size=int(word_size)) tag_cloud = utils.tag_cloud(top_words) - today = datetime.datetime.now() + today = datetime.now() try: last_article = articles[0].date first_article = articles[-1].date delta = last_article - first_article average = round(float(len(articles)) / abs(delta.days), 2) except: - last_article = datetime.datetime.fromtimestamp(0) - first_article = datetime.datetime.fromtimestamp(0) + last_article = datetime.fromtimestamp(0) + first_article = datetime.fromtimestamp(0) delta = last_article - first_article average = 0 elapsed = today - last_article diff --git a/requirements.txt b/requirements.txt index 60869a05..c988f7dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ requests beautifulsoup4 lxml SQLAlchemy -psycopg2 Flask Flask-SQLAlchemy Flask-Login @@ -21,3 +20,4 @@ python-postmark whoosh python-dateutil alembic +requests-futures==0.9.5 diff --git a/runserver.py b/runserver.py index 8d163cd6..5a4aa1fe 100755 --- a/runserver.py +++ b/runserver.py @@ -45,7 +45,7 @@ from flask.ext.restful import Api from flask import g with application.app_context(): - g.api = Api(application, prefix='/api/v1.0') + g.api = Api(application, prefix='/api/v2.0') g.babel = babel g.allowed_file = allowed_file g.db = db |