Hi all!
I've modified the session.py file in my latest check-out of webpy.org/
svn (haven't checked for consistency against bazaar). Features are:
- Automatic session start and save
- Only loads session on demand
- Modularised handler functionality
- Session object becomes true Storage dict
- Improving documentation
- ... possibly more? Can't remember.
I've removed the Session class and put it into module-level functions
with an extension to web.ctx.
There is no overhead to apps that don't use sessions. The session is
only started and loaded the first time web.ctx.session is accessed. At
the moment it is inherently extending web.webapi.ThreadedDict which it
probably shouldn't be, but this could be fixed later if the changes
are accepted.
Session save happens in an unloadhook if the web.ctx.session exists.
Handlers are modular and are collected at import by metaclassing
Handler, as are their parameters (now a part of web.config.session
rather than session_parameters and handler_parameters).
Things like web.ctx.session.get('key', default) wasn't working because
of the way the Session class worked. This is now fixed.
There was a lot of abstraction and repetition which wasn't really
needed which has been factored out.
I feel it makes sessions more transparent and easily usable. You just
start using web.ctx.session in your code as a Storage and it all just
works.
I plan on implementing some simple decorators and similar, and making
a small generic authentication class, possibly followed by
authorization with global and granular models.
(In Australia summer holidays are *now* :-) )
I'm still working on it. Tested with Python 2.3 and 2.4 using
FileHandler.
Check it out:
'''
Session module
(part of webapi.py)
'''
# Various system imports
import time, random
import os, errno, re, glob
# SHA Hashing
try: # Python 2.5
from hashlib import sha512 as sha
except ImportError:
from sha import new as sha
# Pickling / serialization
try: # faster C-implementation
import cPickle as pickle
except ImportError:
import pickle
# Web.py
import webapi
from utils import Storage
__all__ = ['start', 'save', 'destroy', 'cleanup']
def _sha_hash(seed):
'''
SHA hash of the specified seed.
'''
return sha(seed).hexdigest()
def _id_generator():
'''
Simple seeded session ID generator.
'''
return _sha_hash('%s %s %s %s' % (
random.random(),
time.time(),
webapi.ctx.ip,
webapi.config.session.id_seed))
webapi.config.session = Storage({
'cookie_name': 'webpy',
'cookie_domain': None,
'id_seed': 'webapi.py',
'id_generator': _id_generator,
'id_regenerate': False,
'timeout': 600,
'max_age': 0,
'same_ip': False,
'handler': 'file',
})
webapi.config.session.__doc__ = '''
Configures session functionality.
Cookie parameters:
`cookie_name`
: (str) name of the session cookie.
`cookie_domain`
: (str) restrict session cookie to this domain.
Identifier parameters:
`id_seed`
: (str) seed for built-in session id generator.
`id_generator`
: (callable) called to generate new session ids (default: built-
in).
`id_regenerate`
: (bool) regenerate id and set a new cookie every request.
Behaviour parameters:
`timeout`
: (int) seconds between a request until the session times out (0 =
disable, default: 600).
`max_age`
: (int) seconds that a session may live for (0 = disable, default:
0).
`same_ip`
: (bool) invalidate the session if the client changes IPs (default:
`False`).
`handler`
: (str) the session data handler to use (Default: `file`).
'''
def start():
'''
Starts or recalls the session.
'''
webapi.ctx.session = Storage()
webapi.ctx.session_id =
webapi.cookies().get(webapi.config.session.cookie_name, False)
webapi.ctx.session_old_id = False
webapi.ctx.session_handler =
handlers[webapi.config.session.handler]()
if webapi.ctx.session_id:
item =
webapi.ctx.session_handler.retreive(webapi.ctx.session_id)
now = time.time()
if item.id != webapi.ctx.session_id:
destroy(cookie=False)
elif webapi.config.session.timeout > 0 and now - item.touched
>= webapi.config.session.timeout:
destroy(cookie=False)
elif webapi.config.session.same_ip and webapi.ctx.ip !=
item.ip.strip():
destroy(cookie=False)
elif webapi.config.session.max_age > 0 and now - item.created
>= webapi.config.session.max_age:
destroy(cookie=False)
else:
webapi.ctx.session.update(item.data)
if webapi.config.session.id_regenerate:
webapi.ctx.session_old_id = webapi.ctx.session_id
webapi.ctx.session_id = webapi.config.session.id_generator()
if not webapi.ctx.session_id:
webapi.ctx.session_id = webapi.config.session.id_generator()
def save():
'''
Saves the current session.
'''
webapi.ctx.session_handler.store(
webapi.ctx.session_id,
webapi.ctx.ip,
webapi.ctx.session,
webapi.ctx.session_old_id)
webapi.setcookie(
webapi.config.session.cookie_name,
webapi.ctx.session_id,
webapi.config.session.timeout,
webapi.config.session.cookie_domain)
def cleanup():
'''
Cleans up expired sessions. Call periodically.
'''
webapi.ctx.session_handler.cleanup(webapi.config.session.timeout)
def destroy(cookie=True):
'''
Remove session data and unset cookies (if `cookie`).
'''
# We have to start the session to get the ID to destroy it
if webapi.ctx.get('session_id', None) == None:
start()
# Remove session data if in the store
if webapi.ctx.session_id:
webapi.ctx.session_handler.remove(webapi.ctx.session_id)
if webapi.ctx.session_old_id:
webapi.ctx.session_handler.remove(webapi.ctx.session_old_id)
# Clear context
webapi.ctx.session_id = False
webapi.ctx.session_old_id = False
webapi.ctx.session = Storage()
# Unset cookie if specified
if cookie:
webapi.setcookie(
webapi.config.session.cookie_name,
'',
(-1) * webapi.config.session.timeout,
webapi.config.session.cookie_domain)
# Dynamic session loading! Hooah!
def __session_get(self):
try:
return self['session']
except KeyError:
start()
return self['session']
# Overload web.ctx.session to dynamically load the session if not
# already started.
webapi.ctx.__class__.session = property(fget=__session_get)
# If the session has been started make sure it's saved.
def __unload():
try:
if webapi.ctx['session']:
save()
except KeyError:
pass
webapi.unloadhooks['__webpy_session'] = __unload
'''
A mapping of handler names to classes used internally.
'''
handlers = {}
class Handler:
'''
Abstract base handler for session data.
'''
class __metahandler(type):
'''
Adds an entry to the handlers dictionary for every Handler
class encountered.
'''
def __init__(klass, name, bases, attrs):
type.__init__(klass, name, bases, attrs)
try:
handlers[klass.name] = klass
except AttributeError:
handlers[name] = klass
__metaclass__ = __metahandler
def __init__(self):
'''
Initialse the session handler.
'''
pass
def store(self, id_, client_ip, data, old_id=False):
'''
Stores a session.
`client_ip`
: (str) IP of the client
`id_`
: (str) session ID
`data`
: (Storage) session data to store
`old_id`
: (str) if the id regenerates after every request
'''
pass
def retreive(self, id_):
'''
Retreive session from the handler in a `Storage`.
Provided keys:
{id:.., ip:.., data:{..}, created:.., touched:..}
'''
pass
def remove(self, id_):
'''
Removes session from the handler.
'''
pass
def cleanup(self, timeout):
'''
Removes all expired sessions from the handler.
'''
pass
class DBHandler(Handler):
'''
Places all session data in a database.
Requires a table with a schema as below:
CREATE TABLE session (
id CHAR(129) UNIQUE NOT NULL,
ip CHAR(16) NOT NULL,
created int NOT NULL,
touched int NOT NULL,
data TEXT
);
'''
name = 'db'
def store(self, id_, client_ip, data, old_id=False):
do_insert = True
if not old_id:
old_id = id_
if len(list(webapi.select(webapi.config.session.db_table,
vars={'id': old_id},
what='id',
where='id = $id'))) == 1:
do_insert = False
webapi.transact()
now = int(time.time())
try:
if do_insert:
webapi.db.insert(webapi.config.session.db_table,
seqname=False, id=id_, ip=client_ip, touched=now,
created=now, data=pickle.dumps(data, 0))
else:
webapi.update(webapi.config.session.db_table,
where='id = $old_id',
vars={'old_id': old_id},
id=id_, ip=client_ip, touched=now,
data=pickle.dumps(data, 0))
webapi.commit()
except Exception, inst:
webapi.rollback()
raise inst
def remove(self, id_):
webapi.transact()
try:
webapi.delete(webapi.config.session.db_table,
vars={'id': id_}, where='id = $id')
webapi.commit()
except Exception, inst:
webapi.rollback()
raise inst
def retreive(self, id_):
try:
tmp = webapi.select(webapi.config.session.db_table,
what='*', where='id = $id',
vars={'id': id_, 'timeout':
webapi.config.session.timeout})
except Exception, inst:
raise inst
try:
result = tmp[0]
result.data = pickle.loads(result.data.encode('ascii'))
return result
except IndexError:
return Storage()
def cleanup(self, timeout):
webapi.transact()
try:
webapi.delete(webapi.config.session.db_table,
where='($now - touched) >= $timeout',
vars={'timeout': timeout, 'now': int(time.time())})
webapi.commit()
except Exception, inst:
webapi.rollback()
raise inst
class FileHandler(Handler):
'''
Places all session data into a directory with one file per
session.
Requires read/write access to a directory `file_dir` in
`webapi.config.session`.
'''
name = 'file'
def __init__(self):
Handler.__init__(self)
# normalize file_dir path
self._path = os.path.abspath(webapi.config.session.file_dir)
def store(self, id_, client_ip, data, old_id=False):
created = False
self._acquire_lock(id_)
if old_id:
self._acquire_lock(old_id)
try:
file_desc = file(self._session_file(old_id), 'rb')
except (IOError, OSError), inst:
if inst.errno != errno.ENOENT:
self._release_lock(old_id)
self._release_lock(id_)
raise inst
else:
result = pickle.load(file_desc)
created = result.created
file_desc.close()
os.unlink(self._session_file(old_id))
self._release_lock(old_id)
if not created:
created = int(time.time())
file_desc = file(self._session_file(id_), 'wb')
box = Storage({'id': id_, 'ip': client_ip, 'data': data,
'created': created})
pickle.dump(box, file_desc, 0)
file_desc.close()
self._release_lock(id_)
def retreive(self, id_):
'''returns Storage'''
self._acquire_lock(id_)
try:
file_desc = file(self._session_file(id_), 'rb')
except (IOError, OSError), inst:
if inst.errno != errno.ENOENT:
raise inst
result = Storage()
else:
result = pickle.load(file_desc)
result.touched = os.fstat(file_desc.fileno()).st_mtime
file_desc.close()
self._release_lock(id_)
return result
def remove(self, id_):
'''removes session'''
self._acquire_lock(id_)
try:
os.unlink(self._session_file(id_))
except (IOError, OSError), inst:
if inst.errno != errno.ENOENT:
raise inst
self._release_lock(id_)
def cleanup(self, timeout):
'''removes all expired sessions'''
files = glob.glob('%s/%s*' % (self._path,
webapi.config.session.file_prefix))
patern = '%s/%s(?P<id>[0-9a-f]{40,128})(?!.lock)' %
(self._path,
webapi.config.session.file_prefix)
compiled = re.compile(patern)
now = time.time()
for file_name in files:
try:
id_ = compiled.match(file_name).group('id')
except AttributeError:
continue
if self._acquire_lock(id_, False):
if now - os.path.getmtime(file_name) > timeout:
os.unlink(file_name)
self._release_lock(id_)
# private methods
def _session_file(self, id_):
'''returns session file name'''
return '%s/%s%s' % (self._path,
webapi.config.session.file_prefix, id_)
def _lock_file(self, id_):
'''returns session lock file name'''
return '%s.lock' % self._session_file(id_)
def _acquire_lock(self, id_, blocking=True):
'''create lock file
if blocking is False, don't loop'''
file_name = self._lock_file(id_)
while True:
try:
file_desc = os.open(file_name, os.O_WRONLY |
os.O_CREAT | os.O_EXCL)
except (IOError, OSError), inst:
if inst.errno != errno.EEXIST:
raise inst
else:
os.close(file_desc)
break
try:
now = time.time()
if now - os.path.getmtime(file_name) > 60:
os.unlink(file_name)
except (IOError, OSError), inst:
if inst.errno != errno.ENOENT:
raise inst
if not blocking:
return False
time.sleep(0.1)
return True
def _release_lock(self, id_):
'''unlink lock file'''
try:
os.unlink(self._lock_file(id_))
except (IOError, OSError), inst:
if inst.errno != errno.ENOENT:
raise inst
webapi.config.session.update({
'file_dir': '/tmp',
'file_prefix': 'webpy.session.',
})
webapi.config.session.__doc__ += '''
`FileHandler` (file) parameters:
`file_dir`
: location to store session files.
`file_prefix`
: prefix for session files.
'''
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"web.py" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at http://groups.google.com/group/webpy?hl=en
-~----------~----~----~----~------~----~------~--~---