Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Heroku #29

Merged
merged 6 commits into from

1 participant

@socketubs
Owner

No description provided.

@socketubs socketubs merged commit 9242f7a into master
@socketubs socketubs deleted the heroku branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 18, 2013
  1. try refactor backends

    socketubs authored
  2. 1 worker

    socketubs authored
  3. without gunicorn

    socketubs authored
  4. update dep

    socketubs authored
  5. fix redis session backend

    socketubs authored
  6. update doc and setup

    socketubs authored
This page is out of date. Refresh to see the latest.
View
2  Procfile
@@ -1 +1 @@
-web: sh heroku.sh && gunicorn 'leselys.wsgi:app("heroku.ini")' --workers 2
+web: sh heroku.sh && leselys serve --config heroku.ini
View
16 README.rst
@@ -3,7 +3,7 @@ Leselys
I'm Leselys, your very elegant RSS reader. No bullshit Android, iPhone apps, just a responsive design for every device.
-Leselys is Heroku ready and easy to install. It can be used with your very own backend. Take a look at the `MongoDB`_ example.
+Leselys is Heroku ready and easy to install. It can be used with your very own storage backend. Take a look at the `MongoDB`_ example.
There is a `demo here`_ (demo/demo).
@@ -21,9 +21,10 @@ Usage
Quick local setup (with sofart backend): ::
- leselys init --config leselys.ini
- leselys adduser --config leselys.ini
- leselys serve --config leselys.ini
+ pip install sofart
+ leselys init --config leselys.ini
+ leselys adduser --config leselys.ini
+ leselys serve --config leselys.ini
Open your browser at ``http://localhost:5000``.
@@ -32,11 +33,12 @@ Import your Google Reader OPML file right now!
Heroku
~~~~~~
-Advanced setup with Gunicorn and MongoDB as backend on Heroku.
-All Heroku dependencies like ``Pymongo`` and ``gunicorn`` are automagically installed with ``heroku.sh``: ::
+Advanced setup with MongoDB for storage and Redis for session on Heroku.
+All Heroku dependencies like ``Pymongo`` and ``redis`` are automagically installed with ``heroku.sh``: ::
heroku create
heroku addons:add mongohq:sandbox
+ heroku addons:add redistogo:nano
git push heroku master
Don't forget to create a Leselys account with ``heroku run "bash heroku.sh && leselys adduser --config heroku.ini"``.
@@ -49,4 +51,4 @@ License is `AGPL3`_. See `LICENSE`_.
.. _demo here: https://leselys.herokuapp.com
.. _MongoDB: https://github.com/socketubs/leselys/blob/master/leselys/backends/_mongodb.py
.. _AGPL3: http://www.gnu.org/licenses/agpl.html
-.. _LICENSE: https://raw.github.com/socketubs/leselys/master/LICENSE
+.. _LICENSE: https://raw.github.com/socketubs/leselys/master/LICENSE
View
8 heroku.sh
@@ -1,6 +1,6 @@
#!/bin/bash
# Installed heroku deps
-pip install gunicorn
+pip install redis
pip install pymongo
# Create heroku configuration file
@@ -11,8 +11,12 @@ host = 0.0.0.0
port = ${PORT}
debug = false
-[backend]
+[storage]
type = mongodb
host = ${MONGOHQ_URL}
database = ${DATABASE}
+
+[session]
+type = redis
+uri = ${REDISTOGO_URL}
EOL
View
20 leselys/accounts.py
@@ -3,13 +3,13 @@
from getpass import getpass
-def get_users(backend):
- return backend.get_users()
+def get_users(storage):
+ return storage.get_users()
-def add_user(backend):
+def add_user(storage):
username = raw_input('Username: ')
- if username in get_users(backend):
+ if username in get_users(storage):
print('User already exists')
exit(1)
@@ -26,22 +26,22 @@ def add_user(backend):
m.update(password1)
password_md5 = m.hexdigest()
- backend.add_user(username, password_md5)
+ storage.add_user(username, password_md5)
print('User added.')
exit(0)
-def del_user(backend):
+def del_user(storage):
username = raw_input('Username: ')
- if username not in get_users(backend):
+ if username not in get_users(storage):
print('User not found.')
exit(1)
- backend.remove_user(username)
+ storage.remove_user(username)
print('User removed.')
exit(0)
-def update_password(backend):
+def update_password(storage):
username = raw_input('Username :')
same_password = False
while not same_password:
@@ -55,6 +55,6 @@ def update_password(backend):
m = hashlib.md5()
m.update(password1)
password_md5 = m.hexdigest()
- backend.set_password(username, password_md5)
+ storage.set_password(username, password_md5)
print('Password updated.')
exit(0)
View
12 leselys/backends/session/__init__.py
@@ -0,0 +1,12 @@
+# coding: utf-8
+import sys
+
+
+def _load_session(session_name):
+ if session_name == "memory":
+ return "memory"
+ session = __import__("leselys.backends.session._%s" % session_name)
+ session = sys.modules["leselys.backends.session._%s" % session_name]
+ if session is None:
+ raise Exception('Failed to load %s session backend' % session_name)
+ return session
View
68 leselys/backends/session/_redis.py
@@ -0,0 +1,68 @@
+# coding: utf-8
+import pickle
+from datetime import timedelta
+from uuid import uuid4
+from redis import Redis
+from werkzeug.datastructures import CallbackDict
+from flask.sessions import SessionInterface, SessionMixin
+
+
+class RedisSession(CallbackDict, SessionMixin):
+
+ def __init__(self, initial=None, sid=None, new=False):
+ def on_update(self):
+ self.modified = True
+ CallbackDict.__init__(self, initial, on_update)
+ self.sid = sid
+ self.new = new
+ self.modified = False
+
+
+class Session(SessionInterface):
+ serializer = pickle
+ session_class = RedisSession
+
+ def __init__(self, address="localhost", port=6379, db=0, uri=None, prefix='session:'):
+ if uri:
+ self.redis = Redis.from_url(uri)
+ else:
+ port = int(port)
+ db = int(db)
+ self.redis = Redis(address, port, db)
+ self.prefix = prefix
+
+ def generate_sid(self):
+ return str(uuid4())
+
+ def get_redis_expiration_time(self, app, session):
+ if session.permanent:
+ return app.permanent_session_lifetime
+ return timedelta(days=1)
+
+ def open_session(self, app, request):
+ sid = request.cookies.get(app.session_cookie_name)
+ if not sid:
+ sid = self.generate_sid()
+ return self.session_class(sid=sid)
+ val = self.redis.get(self.prefix + sid)
+ if val is not None:
+ data = self.serializer.loads(val)
+ return self.session_class(data, sid=sid)
+ return self.session_class(sid=sid, new=True)
+
+ def save_session(self, app, session, response):
+ domain = self.get_cookie_domain(app)
+ if not session:
+ self.redis.delete(self.prefix + session.sid)
+ if session.modified:
+ response.delete_cookie(app.session_cookie_name,
+ domain=domain)
+ return
+ redis_exp = self.get_redis_expiration_time(app, session)
+ cookie_exp = self.get_expiration_time(app, session)
+ val = self.serializer.dumps(dict(session))
+ self.redis.setex(self.prefix + session.sid, val,
+ int(redis_exp.total_seconds()))
+ response.set_cookie(app.session_cookie_name, session.sid,
+ expires=cookie_exp, httponly=True,
+ domain=domain)
View
10 leselys/backends/storage/__init__.py
@@ -0,0 +1,10 @@
+# coding: utf-8
+import sys
+
+
+def _load_storage(storage_name):
+ storage = __import__("leselys.backends.storage._%s" % storage_name)
+ storage = sys.modules["leselys.backends.storage._%s" % storage_name]
+ if storage is None:
+ raise Exception('Failed to load %s storage backend' % storage_name)
+ return storage
View
4 leselys/backends/_mongodb.py → leselys/backends/storage/_mongodb.py
@@ -3,7 +3,7 @@
from bson.objectid import ObjectId
-class Backend(object):
+class Storage(object):
def __init__(self, **kwargs):
self.database = kwargs.get('database') or 'leselys'
@@ -136,4 +136,4 @@ def get_stories(self, feed_id):
for story in self.db.stories.find({'feed_id': feed_id}):
story['_id'] = str(story['_id'])
res.append(story)
- return res
+ return res
View
2  leselys/backends/_sofart.py → leselys/backends/storage/_sofart.py
@@ -2,7 +2,7 @@
from sofart import Database
-class Backend(object):
+class Storage(object):
def __init__(self, **kwargs):
self.path = kwargs['path']
self.mode = kwargs['mode']
View
26 leselys/core.py
@@ -11,11 +11,24 @@ def __init__(self, host, port, debug):
self.host = host
self.port = int(port)
self.debug = debug
- self.backend = None
- self.backend_settings = None
+ self.storage = None
+ self.storage_settings = None
+ self.session = None
+ self.session_settings = None
- def load_backend(self):
- self.backend = self.backend.Backend(**self.backend_settings)
+ self.app = Flask(__name__)
+ self.app.config['SECRET_KEY'] = os.urandom(24)
+ self.signer = TimestampSigner(self.app.config['SECRET_KEY'])
+ self.cache = SimpleCache()
+
+ def load_storage(self):
+ self.storage = self.storage.Storage(**self.storage_settings)
+
+ def load_session(self):
+ if self.session == "memory":
+ return
+ self.session = self.session.Session(**self.session_settings)
+ self.app.session_interface = self.session
def load_wsgi(self):
from leselys.reader import Reader
@@ -34,11 +47,6 @@ def run(self):
from leselys.reader import Reader
self.reader = Reader()
- self.app = Flask(__name__)
- self.app.config['SECRET_KEY'] = os.urandom(24)
- self.signer = TimestampSigner(self.app.config['SECRET_KEY'])
- self.cache = SimpleCache()
-
from leselys import views
from leselys import api
self.app.run(host=self.host, port=int(self.port), debug=self.debug)
View
8 leselys/helpers.py
@@ -58,7 +58,7 @@ def get_dicttime(parsed_date):
# Decorator for webapp
def login_required(f):
- backend = leselys.core.backend
+ storage = leselys.core.storage
signer = leselys.core.signer
@wraps(f)
@@ -72,13 +72,13 @@ def decorated_function(*args, **kwargs):
username = request.cookies.get('username')
password_md5 = request.cookies.get('password')
- if username in backend.get_users():
+ if username in storage.get_users():
try:
password_unsigned = signer.unsign(
password_md5, max_age=15 * 24 * 60 * 60)
except:
return redirect(url_for('login'))
- if password_unsigned == backend.get_password(username):
+ if password_unsigned == storage.get_password(username):
return f(*args, **kwargs)
else:
return redirect(url_for('login'))
@@ -116,4 +116,4 @@ def retrieve_feeds_from_opml(opml_raw):
else:
if outline.type == 'rss':
result.append({'title': outline.text, 'url': outline.xmlUrl})
- return result
+ return result
View
48 leselys/reader.py
@@ -9,16 +9,16 @@
from leselys.helpers import get_datetime
from leselys.helpers import get_dicttime
-backend = leselys.core.backend
+storage = leselys.core.storage
#########################################################################
# Set defaults settings
#########################################################################
-if not backend.get_setting('acceptable_elements'):
- backend.set_setting('acceptable_elements', ["object", "embed", "iframe"])
+if not storage.get_setting('acceptable_elements'):
+ storage.set_setting('acceptable_elements', ["object", "embed", "iframe"])
# Acceptable elements are special tag that you can disable in entries rendering
-acceptable_elements = backend.get_setting('acceptable_elements')
+acceptable_elements = storage.get_setting('acceptable_elements')
for element in acceptable_elements:
feedparser._HTMLSanitizer.acceptable_elements.add(element)
@@ -42,7 +42,7 @@ def __init__(self, feed):
def run(self):
# This feed comes from database
- feed = backend.get_feed_by_title(self.title)
+ feed = storage.get_feed_by_title(self.title)
for entry in self.data:
title = entry['title']
@@ -62,7 +62,7 @@ def run(self):
else:
published = get_dicttime(datetime.datetime.now().timetuple())
- backend.add_story({
+ storage.add_story({
'title': title,
'link': link,
'description': description,
@@ -104,23 +104,23 @@ def run(self):
if remote_update > local_update:
print(':: %s is outdated' % self.feed['title'])
readed = []
- for entry in backend.get_stories(self.feed['_id']):
+ for entry in storage.get_stories(self.feed['_id']):
if entry['read']:
readed.append(entry['title'])
- backend.remove_story(entry['_id'])
+ storage.remove_story(entry['_id'])
retriever = Retriever(self.data)
retriever.start()
retriever.join()
for entry in readed:
- if backend.get_story_by_title(entry):
- entry = backend.get_story_by_title(entry)
+ if storage.get_story_by_title(entry):
+ entry = storage.get_story_by_title(entry)
entry['read'] = True
- backend.update_story(entry['_id'], copy.copy(entry))
+ storage.update_story(entry['_id'], copy.copy(entry))
self.feed['last_update'] = remote_update_raw
- backend.update_feed(self.feed_id, self.feed)
+ storage.update_feed(self.feed_id, self.feed)
#########################################################################
# Reader object
@@ -142,7 +142,7 @@ def add(self, url):
title = feed.feed['title']
- feed_id = backend.get_feed_by_title(title)
+ feed_id = storage.get_feed_by_title(title)
if not feed_id:
if feed.feed.get('updated_parsed'):
feed_update = get_dicttime(feed.feed.updated_parsed)
@@ -155,7 +155,7 @@ def add(self, url):
else:
feed_update = get_dicttime(datetime.datetime.now().timetuple())
- feed_id = backend.add_feed({'url': url,
+ feed_id = storage.add_feed({'url': url,
'title': title,
'last_update': feed_update})
else:
@@ -172,14 +172,14 @@ def add(self, url):
'counter': len(feed['entries'])}
def delete(self, feed_id):
- if not backend.get_feed_by_id(feed_id):
+ if not storage.get_feed_by_id(feed_id):
return {'success': False, "output": "Feed not found"}
- backend.remove_feed(feed_id)
+ storage.remove_feed(feed_id)
return {"success": True, "output": "Feed removed"}
def get(self, feed_id, order_type='normal'):
res = []
- for entry in backend.get_stories(feed_id):
+ for entry in storage.get_stories(feed_id):
res.append({
"title": entry['title'],
"_id": entry['_id'],
@@ -205,7 +205,7 @@ def get(self, feed_id, order_type='normal'):
def get_subscriptions(self):
feeds = []
- for feed in backend.get_feeds():
+ for feed in storage.get_feeds():
feeds.append({'title': feed['title'],
'id': feed['_id'],
'counter': self.get_unread(feed['_id'])
@@ -213,20 +213,20 @@ def get_subscriptions(self):
return feeds
def refresh_all(self):
- for subscription in backend.get_feeds():
+ for subscription in storage.get_feeds():
refresher = Refresher(subscription)
refresher.start()
return []
def get_unread(self, feed_id):
- return len(backend.get_feed_unread(feed_id))
+ return len(storage.get_feed_unread(feed_id))
def read(self, story_id):
"""
Return story content, set it at readed state and give
previous read state for counter
"""
- story = backend.get_story_by_id(story_id)
+ story = storage.get_story_by_id(story_id)
if story['read']:
return {'success': False,
'output': 'Story already readed',
@@ -234,13 +234,13 @@ def read(self, story_id):
# Save read state before update it for javascript counter in UI
story['read'] = True
- backend.update_story(story['_id'], copy.copy(story))
+ storage.update_story(story['_id'], copy.copy(story))
return {'success': True, 'content': story}
def unread(self, story_id):
- story = backend.get_story_by_id(story_id)
+ story = storage.get_story_by_id(story_id)
if not story['read']:
return {'success': False, 'output': 'Story already unreaded'}
story['read'] = False
- backend.update_story(story['_id'], copy.copy(story))
+ storage.update_story(story['_id'], copy.copy(story))
return {'success': True, 'content': story}
View
12 leselys/views.py
@@ -11,7 +11,7 @@
from leselys.helpers import login_required
-backend = leselys.core.backend
+storage = leselys.core.storage
app = leselys.core.app
reader = leselys.core.reader
signer = leselys.core.signer
@@ -36,7 +36,7 @@ def home():
@app.route('/settings')
@login_required
def settings():
- _settings = backend.get_settings()
+ _settings = storage.get_settings()
return render_template('settings.html', settings=_settings)
@@ -50,9 +50,9 @@ def login():
m.update(password)
password_md5 = m.hexdigest()
- if username not in backend.get_users():
+ if username not in storage.get_users():
return render_template('login.html')
- elif backend.get_password(username) != password_md5:
+ elif storage.get_password(username) != password_md5:
return render_template('login.html')
else:
session['logged_in'] = True
@@ -70,13 +70,13 @@ def login():
if request.cookies.get('remember'):
username = request.cookies.get('username')
password_md5 = request.cookies.get('password')
- if username in backend.get_users():
+ if username in storage.get_users():
try:
password_unsigned = signer.unsign(
password_md5, max_age=15 * 24 * 60 * 60)
except:
return render_template('login.html')
- if password_unsigned == backend.get_password(username):
+ if password_unsigned == storage.get_password(username):
return redirect(url_for('home'))
return render_template('login.html')
View
20 leselys/wsgi.py
@@ -4,7 +4,7 @@
import ConfigParser
from leselys import core
-from leselys.backends import _load_backend
+from leselys.backends.storage import _load_storage
def app(config_path):
config = ConfigParser.ConfigParser()
@@ -15,11 +15,11 @@ def app(config_path):
config.read(config_path)
- # Create backend
- backend_settings = {}
- for item in config.items('backend'):
- backend_settings[item[0]] = item[1]
- del backend_settings['type']
+ # Create storage
+ storage_settings = {}
+ for item in config.items('storage'):
+ storage_settings[item[0]] = item[1]
+ del storage_settings['type']
# Flask webserver config
if config.has_section('webserver') and config.get('webserver', 'host'):
@@ -32,10 +32,10 @@ def app(config_path):
else:
core.debug = False
- backend_module = _load_backend(config.get('backend', 'type'))
- core.backend = backend_module
- core.backend_settings = backend_settings
- core.load_backend()
+ storage_module = _load_storage(config.get('storage', 'type'))
+ core.storage = storage_module
+ core.storage_settings = storage_settings
+ core.load_storage()
core.load_wsgi()
app = core.app
View
63 scripts/leselys
@@ -9,7 +9,8 @@ from docopt import docopt
from leselys import core
from leselys import accounts
-from leselys.backends import _load_backend
+from leselys.backends.storage import _load_storage
+from leselys.backends.session import _load_session
doc = """Leselys is a Web Interface for Leselys.
@@ -42,10 +43,16 @@ host = 0.0.0.0
port = 5000
debug = false
-[backend]
+[storage]
type = sofart
path = /tmp/leselys.db
-mode = multi"""
+mode = multi
+
+#[sessions]
+#type = redis
+#host = localhost
+#port = 6379
+#db = 0"""
config.write(config_default)
print('Configuration file created.')
sys.exit(0)
@@ -81,29 +88,43 @@ mode = multi"""
core.debug = core.debug or args.get('--debug')
- if not config.has_section('backend'):
- print('Missing backend section in configuration file')
+ if not config.has_section('storage'):
+ print('Missing storage section in configuration file')
sys.exit(1)
- if not config.get('backend', 'type'):
- print('Missing type setting in backend section in configuration file')
+ if not config.get('storage', 'type'):
+ print('Missing type setting in storage section in configuration file')
sys.exit(1)
-
- # Create backend
- backend_settings = {}
- for item in config.items('backend'):
- backend_settings[item[0]] = item[1]
- del backend_settings['type']
-
- backend_module = _load_backend(config.get('backend', 'type'))
- core.backend = backend_module
- core.backend_settings = backend_settings
- core.load_backend()
+ if not config.has_section('session'):
+ config.add_section('session')
+ config.set('session', 'type', 'memory')
+
+ # Create storage
+ storage_settings = {}
+ for item in config.items('storage'):
+ storage_settings[item[0]] = item[1]
+ del storage_settings['type']
+
+ storage_module = _load_storage(config.get('storage', 'type'))
+ core.storage = storage_module
+ core.storage_settings = storage_settings
+ core.load_storage()
+
+ # Create session
+ session_settings = {}
+ for item in config.items('session'):
+ session_settings[item[0]] = item[1]
+ del session_settings['type']
+
+ session_module = _load_session(config.get('session', 'type'))
+ core.session = session_module
+ core.session_settings = session_settings
+ core.load_session()
if args.get('serve'):
core.run()
if args.get('adduser'):
- accounts.add_user(core.backend)
+ accounts.add_user(core.storage)
if args.get('deluser'):
- accounts.del_user(core.backend)
+ accounts.del_user(core.storage)
if args.get('passwd'):
- accounts.update_password(core.backend)
+ accounts.update_password(core.storage)
View
3  setup.py
@@ -42,8 +42,7 @@ def get_version():
'flask',
'feedparser',
'lxml',
- 'itsdangerous',
- 'sofart'
+ 'itsdangerous'
],
include_package_data=True,
classifiers=(
Something went wrong with that request. Please try again.