This is based on the snippet at http://flask.pocoo.org/snippets/75/ .
Simple but powerful implementation of server-side sessions using sqlite database which comes bundled with python:
import os, errno, sqlite3
from uuid import uuid4
from cPickle import dumps, loads
from collections import MutableMapping
from flask.sessions import SessionInterface, SessionMixin
class SqliteSession(MutableMapping, SessionMixin):
_create_sql = (
'CREATE TABLE IF NOT EXISTS session '
'('
' key TEXT PRIMARY KEY,'
' val BLOB'
')'
)
_get_sql = 'SELECT val FROM session WHERE key = ?'
_set_sql = 'REPLACE INTO session (key, val) VALUES (?, ?)'
_del_sql = 'DELETE FROM session WHERE key = ?'
_ite_sql = 'SELECT key FROM session'
_len_sql = 'SELECT COUNT(*) FROM session'
def __init__(self, directory, sid, *args, **kwargs):
self.path = os.path.join(directory, sid)
self.directory = directory
self.sid = sid
self.modified = False
self.conn = None
if not os.path.exists(self.path):
with self._get_conn() as conn:
conn.execute(self._create_sql)
self.new = True
def __getitem__(self, key):
key = dumps(key, 0)
rv = None
with self._get_conn() as conn:
for row in conn.execute(self._get_sql, (key,)):
rv = loads(str(row[0]))
break
if rv is None:
raise KeyError('Key not in this session')
return rv
def __setitem__(self, key, value):
key = dumps(key, 0)
value = buffer(dumps(value, 2))
with self._get_conn() as conn:
conn.execute(self._set_sql, (key, value))
self.modified = True
def __delitem__(self, key):
key = dumps(key, 0)
with self._get_conn() as conn:
conn.execute(self._del_sql, (key,))
self.modified = True
def __iter__(self):
with self._get_conn() as conn:
for row in conn.execute(self._ite_sql):
yield loads(str(row[0]))
def __len__(self):
with self._get_conn() as conn:
for row in conn.execute(self._len_sql):
return row[0]
def _get_conn(self):
if not self.conn:
self.conn = sqlite3.Connection(self.path)
return self.conn
# These proxy classes are needed in order
# for this session implementation to work properly.
# That is because sometimes flask will chain method calls
# with session'setdefault' calls.
# Eg: session.setdefault('_flashes', []).append(1)
# With these proxies, the changes made by chained
# method calls will be persisted back to the sqlite
# database.
class CallableAttributeProxy(object):
def __init__(self, session, key, obj, attr):
self.session = session
self.key = key
self.obj = obj
self.attr = attr
def __call__(self, *args, **kwargs):
rv = self.attr(*args, **kwargs)
self.session[self.key] = self.obj
return rv
class PersistedObjectProxy(object):
def __init__(self, session, key, obj):
self.session = session
self.key = key
self.obj = obj
def __getattr__(self, name):
attr = getattr(self.obj, name)
if callable(attr):
return SqliteSession.CallableAttributeProxy(
self.session, self.key, self.obj, attr)
return attr
def setdefault(self, key, value):
if key not in self:
self[key] = value
self.modified = True
return SqliteSession.PersistedObjectProxy(
self, key, self[key])
class SqliteSessionInterface(SessionInterface):
def __init__(self, directory):
directory = os.path.abspath(directory)
if not os.path.exists(directory):
os.mkdir(directory)
self.directory = directory
def open_session(self, app, request):
sid = request.cookies.get(app.session_cookie_name)
if not sid:
sid = str(uuid4())
rv = SqliteSession(self.directory, sid)
return rv
def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
if not session:
try:
os.unlink(session.path)
except OSError, e:
if e.errno != errno.ENOENT:
raise
if session.modified:
response.delete_cookie(app.session_cookie_name,
domain=domain)
return
cookie_exp = self.get_expiration_time(app, session)
response.set_cookie(app.session_cookie_name, session.sid,
expires=cookie_exp, httponly=True, domain=domain)
An example on how to use it in a ubuntu production server:
# Use shared memory (tmpfs) for maximum scalability
# It is possible to use a NFS directory.
# Recent NFS implementions have good fcntl support
# which is the locking mechanism sqlite uses.
path = '/run/shm/app_session'
if not os.path.exists(path):
os.mkdir(path)
os.chmod(path, int('700', 8))
app.session_interface = SqliteSessionInterface(path)
This snippet by Thiago Arruda can be used freely for anything you like. Consider it public domain.
Comments
One DB per session? by Simon Sapin on 2012-03-03 @ 21:12
The idea is nice, but the implementation looks weird. Apparently, this snippet creates one SQLite DB file per session/user and one row for each key/value pair in the session dict. (Values serialized with pickle.)
Wouldn’t it be better to have a single database and one row per user? (And serialize the whole session dict at once with pickle.)
Concurrency by Thiago Arruda on 2012-03-04 @ 10:39
Since SQLite is a single-file database, writes to the database are serialized(It allows multiple simultaneous readers, but only one writer at a time). Since every session will have it's own thread/process, using a single file will eventually lead to scalability problems.
? by Simon Sapin on 2012-03-04 @ 11:10
But then what’s the point of using SQLite vs. plain pickle files? You still have the sames issues in choosing a filename (the tempfile module could help here.)
Single Database vs Multiple Databases by Adrian Lopez on 2012-03-04 @ 19:29
Is there any reason not to use separate databases? There's no need or reason to lock unrelated sessions, so why put everything together in a single database? Considering the disadvantages of allowing only one session to be written at a time, one should first have a very good reason for doing it differently than in the snippet.
Expired Sessions by Adrian Lopez on 2012-03-04 @ 19:36
Unless I missed it, there appears to be no mechanism here to delete expired sessions. Is the assumption here that an external process will take care of deleting expired sessions from the server?
SQLite vs Plain pickle by Thiago Arruda on 2012-03-06 @ 10:39
Simon, if you use pickled files, there are three possible problems that I can think of:
1 - Since it is possible for multiple concurrent requests to access/modify the same session, there can be race conditions when writing back the session data(eg: request1 reads the session file; request2 reads session file; request1 adds a key to the session; request2 adds a another key to the session; session1 writes back the session file; request2 writes back the session file; Result: request2 overwrites the changes made by request1). With the current implementation there will be no such problems.
2 - Every request will make a new system call to read the file along with executing the pickling/unpickling code(affecting scalability for highly accessed websites). With the current implementation, you can have requests that don't access sessions, without context switches(the impact will be even greater if the file read from disk) and without pickles/unpickles.
3 - You will load the session in memory for every request, and it can lead to scalability problems since one of the use cases of server side sessions is the possibility of storing a large amount of data. With the current implementation, you can store gigabytes in the session, and it won't affect the speed of requests that don't need that data.
Expired Sessions by Thiago Arruda on 2012-03-06 @ 10:41
Adrian, the code
will delete the session file all keys are removed from the session.
Unlink by Adrian Lopez on 2012-03-07 @ 02:24
os.unlink(session.path) will delete the file belonging to a known session, but what about sessions whose cookies have expired?
Non standard behavior by Mum Pitz on 2013-03-13 @ 15:35
With the above implementation the following does not work: <code> session['key'] = {} session['key']['subkey'] = value </code>
Does someone know why ?
A workaround is: <code> temp = {} temp['subkey'] = value session['key'] = temp </code>