Lynn Rees avatar Lynn Rees committed eb9badb

[svn]

Comments (0)

Files changed (11)

branches/0.1/ez_setup.py

+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6a9"
+DEFAULT_URL     = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+    'setuptools-0.5a13-py2.3.egg': '85edcf0ef39bab66e130d3f38f578c86',
+    'setuptools-0.5a13-py2.4.egg': 'ede4be600e3890e06d4ee5e0148e092a',
+    'setuptools-0.6a1-py2.3.egg': 'ee819a13b924d9696b0d6ca6d1c5833d',
+    'setuptools-0.6a1-py2.4.egg': '8256b5f1cd9e348ea6877b5ddd56257d',
+    'setuptools-0.6a2-py2.3.egg': 'b98da449da411267c37a738f0ab625ba',
+    'setuptools-0.6a2-py2.4.egg': 'be5b88bc30aed63fdefd2683be135c3b',
+    'setuptools-0.6a3-py2.3.egg': 'ee0e325de78f23aab79d33106dc2a8c8',
+    'setuptools-0.6a3-py2.4.egg': 'd95453d525a456d6c23e7a5eea89a063',
+    'setuptools-0.6a4-py2.3.egg': 'e958cbed4623bbf47dd1f268b99d7784',
+    'setuptools-0.6a4-py2.4.egg': '7f33c3ac2ef1296f0ab4fac1de4767d8',
+    'setuptools-0.6a5-py2.3.egg': '748408389c49bcd2d84f6ae0b01695b1',
+    'setuptools-0.6a5-py2.4.egg': '999bacde623f4284bfb3ea77941d2627',
+    'setuptools-0.6a6-py2.3.egg': '7858139f06ed0600b0d9383f36aca24c',
+    'setuptools-0.6a6-py2.4.egg': 'c10d20d29acebce0dc76219dc578d058',
+    'setuptools-0.6a7-py2.3.egg': 'cfc4125ddb95c07f9500adc5d6abef6f',
+    'setuptools-0.6a7-py2.4.egg': 'c6d62dab4461f71aed943caea89e6f20',
+    'setuptools-0.6a8-py2.3.egg': '2f18eaaa3f544f5543ead4a68f3b2e1a',
+    'setuptools-0.6a8-py2.4.egg': '799018f2894f14c9f8bcb2b34e69b391',
+    'setuptools-0.6a9-py2.3.egg': '8e438ad70438b07b0d8f82cae42b278f',
+    'setuptools-0.6a9-py2.4.egg': '8f6e01fc12fb1cd006dc0d6c04327ec1',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+    if egg_name in md5_data:
+        from md5 import md5
+        digest = md5(data).hexdigest()
+        if digest != md5_data[egg_name]:
+            print >>sys.stderr, (
+                "md5 validation of %s failed!  (Possible download problem?)"
+                % egg_name
+            )
+            sys.exit(2)
+    return data    
+
+
+def use_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    download_delay=15
+):
+    """Automatically find/download setuptools and make it available on sys.path
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end with
+    a '/').  `to_dir` is the directory where setuptools will be downloaded, if
+    it is not already available.  If `download_delay` is specified, it should
+    be the number of seconds that will be paused before initiating a download,
+    should one be required.  If an older version of setuptools is installed,
+    this routine will print a message to ``sys.stderr`` and raise SystemExit in
+    an attempt to abort the calling script.  
+    """
+    try:
+        import setuptools
+        if setuptools.__version__ == '0.0.1':
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
+    except ImportError:
+        egg = download_setuptools(version, download_base, to_dir, download_delay)
+        sys.path.insert(0, egg)
+        import setuptools; setuptools.bootstrap_install_from = egg
+
+    import pkg_resources
+    try:
+        pkg_resources.require("setuptools>="+version)
+
+    except pkg_resources.VersionConflict:
+        # XXX could we install in a subprocess here?
+        print >>sys.stderr, (
+            "The required version of setuptools (>=%s) is not available, and\n"
+            "can't be installed while this script is running. Please install\n"
+            " a more recent version first."
+        ) % version
+        sys.exit(2)
+
+def download_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    delay = 15
+):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download attempt.
+    """
+    import urllib2, shutil
+    egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+    url = download_base + egg_name
+    saveto = os.path.join(to_dir, egg_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            from distutils import log
+            if delay:
+                log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help).  I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+---------------------------------------------------------------------------""",
+                    version, download_base, delay
+                ); from time import sleep; sleep(delay)
+            log.warn("Downloading %s", url)
+            src = urllib2.urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = _validate_md5(egg_name, src.read())
+            dst = open(saveto,"wb"); dst.write(data)
+        finally:
+            if src: src.close()
+            if dst: dst.close()
+    return os.path.realpath(saveto)
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+
+    try:
+        import setuptools
+    except ImportError:
+        import tempfile, shutil
+        tmpdir = tempfile.mkdtemp(prefix="easy_install-")
+        try:
+            egg = download_setuptools(version, to_dir=tmpdir, delay=0)
+            sys.path.insert(0,egg)
+            from setuptools.command.easy_install import main
+            main(list(argv)+[egg])
+        finally:
+            shutil.rmtree(tmpdir)
+    else:
+        if setuptools.__version__ == '0.0.1':
+            # tell the user to uninstall obsolete version
+            use_setuptools(version)
+
+    req = "setuptools>="+version
+    import pkg_resources
+    try:
+        pkg_resources.require(req)
+    except pkg_resources.VersionConflict:
+        try:
+            from setuptools.command.easy_install import main
+        except ImportError:
+            from easy_install import main
+        main(list(argv)+[download_setuptools(delay=0)])
+        sys.exit(0) # try to force an exit
+    else:
+        if argv:
+            from setuptools.command.easy_install import main
+            main(argv)
+        else:
+            print "Setuptools version",version,"or greater has been installed."
+            print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+
+            
+def update_md5(filenames):
+    """Update our built-in md5 registry"""
+
+    import re
+    from md5 import md5
+
+    for name in filenames:
+        base = os.path.basename(name)
+        f = open(name,'rb')       
+        md5_data[base] = md5(f.read()).hexdigest()
+        f.close()
+
+    data = ["    %r: %r,\n" % it for it in md5_data.items()]
+    data.sort()
+    repl = "".join(data)
+
+    import inspect
+    srcfile = inspect.getsourcefile(sys.modules[__name__])
+    f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+    match = re.search("\nmd5_data = {\n([^}]+)}", src)
+    if not match:
+        print >>sys.stderr, "Internal error!"
+        sys.exit(2)
+
+    src = src[:match.start(1)] + repl + src[match.end(1):]
+    f = open(srcfile,'w')
+    f.write(src)
+    f.close()
+
+
+if __name__=='__main__':
+    if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+        update_md5(sys.argv[2:])
+    else:
+        main(sys.argv[1:])
+

branches/0.1/setup.py

+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1.  Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3.  Neither the name of the Portable Site Information Project nor the names
+# of its contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+'''setup - setuptools based setup for wsgistate.'''
+
+import ez_setup
+ez_setup.use_setuptools()
+
+try:
+    from setuptools import setup
+except:
+    from distutils.core import setup
+
+setup(name='wsgistate',
+      version='0.1',
+      description='''Session and caching middleware for WSGI.''',
+      long_description='''flup-compatible session and caching middleware for WSGI.
+      Includes support for thread-safe in-memory, disk-based, database, and memcached caches.''',
+      author='L. C. Rees',
+      author_email='lcrees@gmail.com',
+      license='BSD',
+      packages = ['wsgistate'],
+      zip_safe = True,
+      keywords='WSGI session caching persistence HTTP Web',
+      classifiers=['Development Status :: 3 - Alpha',
+                    'Environment :: Web Environment',
+                    'License :: OSI Approved :: BSD License',
+                    'Natural Language :: English',
+                    'Operating System :: OS Independent',
+                    'Programming Language :: Python',
+                    'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware'])

branches/0.1/wsgistate/__init__.py

+__all__ = ['base', 'db', 'file', 'memory', 'memcached', 'session', 'simple', 'cache']

branches/0.1/wsgistate/base.py

+# Copyright (c) 2005, the Lawrence Journal-World
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of Django nor the names of its contributors may be used
+#       to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Base Cache class.'''
+
+
+class BaseCache(object):
+
+    '''Base Cache class.'''    
+    
+    def __init__(self, *a, **kw):
+        '''Init method.'''
+        super(BaseCache, self).__init__()
+        timeout = kw.get('timeout', 300)
+        try:
+            timeout = int(timeout)
+        except (ValueError, TypeError):
+            timeout = 300
+        self.default_timeout = timeout
+
+    def __getitem__(self, key):
+        '''Fetch a given key from the cache.'''
+        return self.get(key)
+
+    def __setitem__(self, key):
+        '''Set a value in the cache. '''
+        self.set(key, value)
+
+    def __delitem__(self, key):
+        '''Delete a key from the cache.'''
+        self.delete(key) 
+
+    def __contains__(self, key):
+        '''Tell if a given key is in the cache.'''
+        return self.get(key) is not None
+
+    def get(self, key, default=None):
+        '''Fetch a given key from the cache.  If the key does not exist, return
+        default, which itself defaults to None.
+
+        @param key Keyword of item in cache.
+        @param default Default value (default: None)
+        '''
+        raise NotImplementedError()
+
+    def set(self, key, value):
+        '''Set a value in the cache. 
+
+        @param key Keyword of item in cache.
+        @param value Value to be inserted in cache.        
+        '''
+        raise NotImplementedError()
+
+    def delete(self, key):
+        '''Delete a key from the cache, failing silently.
+
+        @param key Keyword of item in cache.
+        '''
+        raise NotImplementedError()
+
+    def get_many(self, keys):
+        '''Fetch a bunch of keys from the cache.  For certain backends
+        (memcached, pgsql) this can be *much* faster when fetching multiple values.
+
+        Returns a dict mapping each key in keys to its value.  If the given
+        key is missing, it will be missing from the response dict.
+
+        @param keys Keywords of items in cache.        
+        '''
+        d = dict()
+        for k in keys:
+            val = self.get(k)
+            if val is not None: d[k] = val
+        return d

branches/0.1/wsgistate/cache.py

+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of psilib nor the names of its contributors may be used
+#       to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''WSGI middleware for caching.'''
+
+import cgi, marshal
+
+
+class WsgiCache(object):
+
+    '''WSGI middleware for caching.'''  
+
+    def __init__(self, app, cache, **kw):
+        '''Init method.'''
+        super(WsgiCache, self).__init__()
+        self.application, self._cache = app, cache
+        # Adds method to cache key
+        self._methidx = kw.get('index_methods', False)
+        # Adds user submitted data (GET, POST) to cache key
+        self._useridx = kw.get('index_user_info', False)
+        # Which HTTP responses by method are cached
+        self._allowed = kw.get('allowed_methods', set(['GET', 'HEAD']))
+        # Responses to user submitted data (GET, POST) is cached
+        self._usersub = kw.get('cache_user_info', False)
+        
+    def __call__(self, environ, start_response):
+        '''Caches responses to HTTP requests.'''
+        key = self._keygen(environ)
+        cached = self._cache.get(key)      
+        # Cache if data not cached
+        try:
+            data = cached['data']
+            start_response(cached['status'], cached['headers'], cached['exc_info'])
+        except TypeError:
+            if self._cacheable(environ):                
+                sr = _CacheResponse(start_response, key, self._cache)
+                data = self.application(environ, sr.cache_start_response)
+                self._cache[key]['data'] = data
+            else:
+                data = self.application(environ, start_response)                
+        return data
+
+    def _keygen(self, environ):
+        '''Generates cache keys.'''
+        # Base of key is alwasy path of request
+        key = environ['PATH_INFO']
+        # Add method name to key if configured that way
+        if self._methidx: key = ''.join([key, environ['REQUEST_METHOD']])
+        # Add marshalled user submitted data to string if configured that way
+        if self._useridx:
+            qdict = cgi.parse(environ['wsgi.input'], environ, False, False)
+            key = ''.join([key, marshal.dumps(qdict)])
+        return key
+    
+    def _cacheable(self, environ):
+        '''Tells if a request should be cached or not.'''
+        # Returns false if method is not to be cached
+        if environ['REQUEST_METHOD'] not in self._allowed: return False
+        # Returns false if requests based on user submissions are not to be cached
+        if self._usersub:
+            if 'QUERY_STRING' in environ: return False
+            if environ['REQUEST_METHOD'] == 'POST': return False
+        return True
+
+
+class _CacheResponse(object):
+
+    def __init__(self, start_response, key, cache):
+        self._start_response = start_response
+        self._cache, self._key = cache, key
+
+    def cache_start_response(self, status, headers, exc_info=None):
+        cachedict = dict((('status', status), ('headers', headers), ('exc_info', exc_info)))
+        self._cache.set(self._key, cachedict)
+        return self._start_response(status, headers, exc_info)
+
+
+def cache(cache, **kw):
+    '''Decorator for simple cache.'''
+    def decorator(application):
+        return WsgiCache(application, cache, **kw)
+    return decorator
+
+
+__all__ = ['WsgiCache', 'cache']   

branches/0.1/wsgistate/db.py

+# Copyright (c) 2005, the Lawrence Journal-World
+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of Django nor the names of its contributors may be used
+#       to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Database cache backend.'''
+
+import time
+from datetime import datetime
+from sqlalchemy import *
+from base import BaseCache
+
+
+class DbCache(BaseCache):     
+
+    '''Database cache backend.'''
+
+    def __init__(self, *a, **kw):
+        '''Init method.'''
+        super(DbCache, self).__init__(self, *a, **kw)
+        # Bind metadata
+        self._metadata = BoundMetaData(a[0])
+        # Make cache
+        self._cache = Table('cache', self._metadata,
+            Column('id', Integer, primary_key=True, nullable=False, unique=True),
+            Column('cache_key', String(60), nullable=False),
+            Column('value', PickleType, nullable=False),
+            Column('expires', DateTime, nullable=False))
+        # Create cache if it does not exist
+        if not self._cache.exists(): self._cache.create()
+        max_entries = kw.get('max_entries', 300)
+        try:
+            self._max_entries = int(max_entries)
+        except (ValueError, TypeError):
+            self._max_entries = 300
+
+    def get(self, key, default=None):
+        '''Fetch a given key from the cache.  If the key does not exist, return
+        default, which itself defaults to None.
+
+        @param key Keyword of item in cache.
+        @param default Default value (default: None)
+        '''
+        row = self._cache.select().execute(cache_key=key).fetchone()
+        if row is None: return default
+        if row.expires < datetime.now().replace(microsecond=0):
+            self.delete(key)
+            return default
+        return row.value
+
+    def set(self, key, val):
+        '''Set a value in the cache.
+
+        @param key Keyword of item in cache.
+        @param value Value to be inserted in cache.        
+        '''
+        
+        timeout = self.default_timeout
+        # Get count
+        num = self._cache.count().execute().fetchone()[0]
+        if num > self._max_entries: self._cull()
+        # Get expiration time
+        exp = datetime.fromtimestamp(time.time() + timeout).replace(microsecond=0)        
+        try:
+            # Update database if key already present
+            if key in self:
+                self._cache.update(self._cache.c.cache_key==key).execute(value=val, expires=exp)
+            # Insert new key if key not present
+            else:            
+                self._cache.insert().execute(cache_key=key, value=val, expires=exp)
+        # To be threadsafe, updates/inserts are allowed to fail silently
+        except: pass
+       
+    def delete(self, key):
+        '''Delete a key from the cache, failing silently.
+
+        @param key Keyword of item in cache.
+        '''
+        self._cache.delete().execute(cache_key=key) 
+
+    def _cull(self):
+        '''Remove items in cache that have timed out.'''
+        now = datetime.now().replace(microsecond=0)
+        self._cache.delete(self._cache.c.expires < now).execute()
+        
+
+__all__ = ['DbCache']

branches/0.1/wsgistate/file.py

+# Copyright (c) 2005, the Lawrence Journal-World
+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of Django nor the names of its contributors may be used
+#       to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''File-based cache backend'''
+
+import os, time, urllib
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+from simple import SimpleCache
+
+
+class FileCache(SimpleCache):
+
+    '''File-based cache backend'''    
+    
+    def __init__(self, *a, **kw):
+        '''Init method.'''
+        super(FileCache, self).__init__(*a, **kw)
+        # Create directory
+        self._dir = a[0]
+        if not os.path.exists(self._dir): self._createdir()
+        # Remove unneeded methods and attributes
+        del self._cache
+        del self._expire_info
+
+    def __contains__(self, key):
+        '''Tell if a given key is in the cache.'''
+        return os.path.exists(self._key_to_file(key))
+
+    def get(self, key, default=None):
+        '''Fetch a given key from the cache.  If the key does not exist, return
+        default, which itself defaults to None.
+
+        @param key Keyword of item in cache.
+        @param default Default value (default: None)
+        '''
+        fname = self._key_to_file(key)
+        try:
+            f = open(fname, 'rb')
+            exp, now = pickle.load(f), time.time()
+            # Remove item if time has expired.
+            if exp < now:
+                f.close()
+                os.remove(fname)
+            else:
+                return pickle.load(f)
+        except (IOError, OSError, EOFError, pickle.PickleError): pass
+        return default
+
+    def set(self, key, value):
+        '''Set a value in the cache.
+
+        @param key Keyword of item in cache.
+        @param value Value to be inserted in cache.        
+        '''
+        fname = self._key_to_file(key)
+        try:
+            filelist = os.listdir(self._dir)
+        except (IOError, OSError):
+            self._createdir()
+            filelist = list()
+        if len(filelist) > self._max_entries: self._cull()
+        try:
+            f = open(fname, 'wb')
+            now = time.time()
+            pickle.dump(now + self.default_timeout, f, 2)
+            pickle.dump(value, f, 2)
+        except (IOError, OSError): pass
+
+    def delete(self, key):
+        '''Delete a key from the cache, failing silently.
+
+        @param key Keyword of item in cache.
+        '''
+        try:
+            os.remove(self._key_to_file(key))
+        except (IOError, OSError): pass
+
+    def _cull(self):
+        '''Remove items in cache that have timed out.'''
+        try:
+            filelist = os.listdir(self._dir)
+        except (IOError, OSError):
+            self._createdir()
+            filelist = list()
+        for fname in filelist:
+            # Remove expired items from cache.
+            try:
+                f = open(fname, 'rb')
+                exp = pickle.load(f)
+                now = time.time()
+                if exp < now:
+                    f.close()
+                    try:
+                        os.remove(os.path.join(self._dir, fname))
+                    except (IOError, OSError): pass
+            except (IOError, OSError, EOFError, pickle.PickleError): pass            
+
+    def _createdir(self):
+        '''Creates the cache directory.'''
+        try:
+            os.makedirs(self._dir)
+        except OSError:
+            raise EnvironmentError("Cache directory '%s' does not exist and could not be created'" % self._dir)
+
+    def _key_to_file(self, key):
+        '''Gives the filesystem path for a key.'''
+        return os.path.join(self._dir, urllib.quote_plus(key))
+
+
+__all__ = ['FileCache']

branches/0.1/wsgistate/memcached.py

+# Copyright (c) 2005, the Lawrence Journal-World
+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of Django nor the names of its contributors may be used
+#       to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Memcached cache backend'''
+
+try:
+    import memcache
+except ImportError:
+    raise ImportError("Memcached cache backend requires the 'memcache' library")
+from base import BaseCache
+
+
+class MemCached(BaseCache):
+
+    '''Memcached cache backend'''    
+    
+    def __init__(self, *a, **kw):
+        super(MemCached, self).__init__(*a, **kw)
+        self._cache = memcache.Client(a[0].split(';'))
+
+    def get(self, key, default=None):
+        '''Fetch a given key from the cache.  If the key does not exist, return
+        default, which itself defaults to None.
+
+        @param key Keyword of item in cache.
+        @param default Default value (default: None)
+        '''
+        val = self._cache.get(key)
+        if val is None:
+            return default
+        else:
+            return val
+
+    def set(self, key, value):
+        '''Set a value in the cache.
+
+        @param key Keyword of item in cache.
+        @param value Value to be inserted in cache.        
+        '''
+        self._cache.set(key, value, self.default_timeout)
+
+    def delete(self, key):
+        '''Delete a key from the cache, failing silently.
+
+        @param key Keyword of item in cache.
+        '''
+        self._cache.delete(key)
+
+    def get_many(self, keys):
+        '''Fetch a bunch of keys from the cache.
+
+        Returns a dict mapping each key in keys to its value.  If the given
+        key is missing, it will be missing from the response dict.
+
+        @param keys Keywords of items in cache.        
+        '''
+        return self._cache.get_multi(keys)
+
+    def _cull(self):
+        '''Stub.'''
+        pass
+       
+
+__all__ = ['MemCached']

branches/0.1/wsgistate/memory.py

+# Copyright (c) 2005, the Lawrence Journal-World
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of Django nor the names of its contributors may be used
+#       to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Thread-safe in-memory cache backend.'''
+
+import copy, time
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+from simple import SimpleCache
+
+
+class MemoryCache(SimpleCache):
+
+    '''Thread-safe in-memory cache backend.'''    
+
+    def __init__(self, *a, **kw):
+        '''Init method.'''
+        super(MemoryCache, self).__init__(*a, **kw)
+        self._lock = threading.Condition()
+        
+    def get(self, key, default=None):
+        '''Fetch a given key from the cache.  If the key does not exist, return
+        default, which itself defaults to None.
+
+        @param key Keyword of item in cache.
+        @param default Default value (default: None)
+        '''
+        self._lock.acquire()
+        try:
+            now, exp = time.time(), self._expire_info.get(key)
+            if exp is None:
+                return default
+            # Return default value if item expired
+            elif exp < now:
+                self.delete(key)
+                return default
+            else:
+                return copy.deepcopy(self._cache[key])
+        finally:
+            self._lock.release()
+
+    def set(self, key, value):
+        '''Set a value in the cache.  
+
+        @param key Keyword of item in cache.
+        @param value Value to be inserted in cache.        
+        '''
+        self._lock.acquire()
+        try:
+            super(MemoryCache, self).set(key, value)
+        finally:
+            self._lock.release()
+
+    def delete(self, key):
+        '''Delete a key from the cache, failing silently.
+
+        @param key Keyword of item in cache.
+        '''
+        self._lock.acquire()
+        try:
+            super(MemoryCache, self).delete(key)
+        finally:
+            self._lock.release()
+        
+
+__all__ = ['MemoryCache']

branches/0.1/wsgistate/session.py

+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# Copyright (c) 2005, the Lawrence Journal-World
+# Copyright (c) 2006 L. C. Rees
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+
+import os, string, time, weakref, atexit, cgi, urlparse, md5, random, sys
+from Cookie import SimpleCookie
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+
+def _shutdown(ref):
+    cache = ref()
+    if cache is not None: cache.shutdown()
+    
+def _addclose(appiter, closefunc):
+    '''
+    Wraps an iterator so that its close() method calls closefunc. Respects
+    the existence of __len__ and the iterator's own close() method.
+
+    Need to use metaclass magic because __len__ and next are not
+    recognized unless they're part of the class. (Can't assign at
+    __init__ time.)
+    '''
+
+    class MetaIterWrapper(type):
+        def __init__(cls, name, bases, clsdict):
+            super(MetaIterWrapper, cls).__init__(name, bases, clsdict)
+            if hasattr(appiter, '__len__'): cls.__len__ = appiter.__len__
+            cls.next = iter(appiter).next
+            if hasattr(appiter, 'close'):
+                def _close(self):
+                    appiter.close()
+                    closefunc()
+                cls.close = _close
+            else:
+                cls.close = closefunc
+
+    class IterWrapper(object):
+        __metaclass__ = MetaIterWrapper
+        def __iter__(self):
+            return self
+
+    return IterWrapper()
+
+
+class SessionCache(object):
+    
+    '''Abstract base class for session stores. You first acquire a session by
+    calling create() or checkout(). After using the session,
+    you must call checkin(). You must not keep references to sessions
+    outside of a check in/check out block. Always obtain a fresh reference.
+
+    After timeout minutes of inactivity, sessions are deleted.
+    '''
+    
+    # Would be nice if len(idchars) were some power of 2.
+    idchars = '-_'.join([string.digits, string.ascii_letters])  
+    length = 32
+
+    def __init__(self, cache, **kw):
+        super(SessionCache, self).__init__()
+        self._lock = threading.Condition()
+        self._checkedout, self._closed, self._cache = dict(), False, cache
+        # Sets if session id is random on every access or not
+        self._random = kw.get('random', False)
+        self._secret = ''.join(self.idchars[ord(c) % len(self.idchars)]
+            for c in os.urandom(self.length))
+        # Ensure shutdown is called.
+        atexit.register(_shutdown, weakref.ref(self))
+
+    def __del__(self):
+        self.shutdown()        
+
+    # Public interface.
+
+    def create(self):
+        '''Create a new session with a unique identifier.
+        
+        The newly-created session should eventually be released by
+        a call to checkin().            
+        '''
+        assert not self._closed
+        self._lock.acquire()
+        try:
+            sid, sess = self.newid(), dict()
+            self._cache.set(sid, sess)                
+            assert sid not in self._checkedout            
+            self._checkedout[sid] = sess
+            return sid, sess
+        finally:
+            self._lock.release()
+
+    def checkout(self, sid):
+        '''Checks out a session for use. Returns the session if it exists,
+        otherwise returns None. If this call succeeds, the session
+        will be touch()'ed and locked from use by other processes.
+        Therefore, it should eventually be released by a call to
+        checkin().
+
+        @param sid Session id        
+        '''
+        assert not self._closed
+        self._lock.acquire()
+        try:
+            # If we know it's already checked out, block.
+            while sid in self._checkedout: self._lock.wait()
+            sess = self._cache.get(sid)
+            if sess is not None:
+                assert sid not in self._checkedout
+                # Randomize session id if set and remove old session id
+                if self._random:
+                    self._cache.delete(sid)
+                    sid = self.newid()
+                # Put in checkout
+                self._checkedout[sid] = sess
+                return sid, sess
+            else:
+                return None, None
+        finally:
+            self._lock.release()
+
+    def checkin(self, sid, session):
+        '''Returns the session for use by other threads/processes.
+
+        @param sid Session id
+        @param session Session dictionary
+        '''
+        assert not self._closed
+        if session is None: return
+        self._lock.acquire()
+        try:
+            assert sid in self._checkedout
+            del self._checkedout[sid]
+            self._cache.set(sid, session)
+            self._lock.notify()
+        finally:            
+            self._lock.release()
+
+    def shutdown(self):
+        '''Clean up outstanding sessions.'''
+        self._lock.acquire()
+        try:
+            if not self._closed:
+                # Save or delete any sessions that are still out there.                
+                for sid, sess in self._checkedout.iteritems():
+                    self._cache.set(sid, session)
+                self._cache._cull()                
+                self._checkedout.clear()
+                self._closed = True
+        finally:
+            self._lock.release()
+
+    # Utilities
+
+    def newid(self):
+        "Returns session key that isn't being used."
+        sid = None
+        for num in xrange(10000):
+            sid = md5.new(str(random.randint(0, sys.maxint - 1)) +
+                str(random.randint(0, sys.maxint - 1)) + self._secret).hexdigest()
+            if sid not in self._cache: break
+        return sid
+            
+
+# SessionMiddleware stuff.
+
+class SessionService(object):
+
+    '''WSGI extension API passed to applications as
+    environ['com.saddi.service.session'].
+
+    Public API: (assume service = environ['com.saddi.service.session'])
+      service.session - Returns the Session associated with the client.
+      service.current - True if the client is currently associated with
+        a Session.
+      service.new - True if the Session was created in this
+        transaction.
+      service.expired - True if the client is associated with a
+        non-existent Session.
+      service.inurl - True if the Session ID should be encoded in
+        the URL. (read/write)
+      service.seturl(url) - Returns url encoded with Session ID (if
+        necessary).
+    '''
+
+    _expiredid = 'expired session'
+
+    def __init__(self, cache, environ, **kw):
+        self._cache, self._cookiename, self._fieldname = cache, cname, fname
+        self._cookiename = kw.get('cookiename', '_SID_')
+        self._fieldname = kw.get('fieldname', '_SID_')
+        self._path = kw.get('path', '/')
+        self._session, self._sid, self._csid = None, None, None
+        self._newsession, self._expired, self.inurl = False, False, False
+        if __debug__: self._closed = False
+        self.get(environ)
+
+    def _fromcookie(self, environ):
+        '''Attempt to load the associated session using the identifier from
+        the cookie.
+        '''
+        cookie = SimpleCookie(environ.get('HTTP_COOKIE'))
+        morsel = cookie.get(self._cookiename, None)
+        if morsel is not None:
+            self._sid, self._session = self._cache.checkout(morsel.value)
+            self._expired, self._csid = self._session is None, morsel.value
+
+    def _fromquery(self, environ):
+        '''
+        Attempt to load the associated session using the identifier from
+        the query string.
+        '''
+        qs = cgi.parse_qsl(environ.get('QUERY_STRING', ''))
+        for name, value in qs:
+            if name == self._fieldname:
+                self._sid, self._session = self._cache.checkout(value)
+                self._expired, self._csid = self._session is None, value
+                self.inurl = True
+                break
+        
+    def get(self, environ):
+        '''Attempt to associate with an existing Session.'''
+        # Try cookie first.
+        self._fromcookie(environ)
+        # Next, try query string.
+        if self._session is None: self._fromquery(environ)
+
+    def _addcookie(self):
+        '''Returns True if the session cookie should be added to the header
+        (if not encoding the session ID in the URL). The cookie is added if
+        one of these three conditions are true: a) the session was just
+        created, b) the session is no longer valid, or c) the client is
+        associated with a non-existent session.
+        '''
+        return self._newsession or \
+               (self._session is not None) or \
+               (self._session is None and self._expired)
+        
+    def setcookie(self, headers):
+        '''Adds Set-Cookie header if needed.'''
+        if not self.inurl:           
+            expire = False
+            # Reassign cookie if session id is randomized
+            if self._csid != self._sid or self._addcookie():                
+                if self._sid is None: sid, expire = self._expiredid, True
+                cookie, name = SimpleCookie(), self._cookiename
+                cookie[name], cookie[name]['path'] = self._sid, self._path
+                if expire:
+                    # Expire cookie
+                    cookie[name]['expires'] = -365*24*60*60
+                    cookie[name]['max-age'] = 0
+                headers.append(('Set-Cookie', cookie[name].OutputString()))
+    
+    def close(self):
+        '''Checks session back into session cache.'''
+        if self._session is None: return
+        # Check the session back in and get rid of our reference.
+        sid = self._cache.checkin(self._sid, self._session)
+        self._session = None
+        if __debug__: self._closed = True
+
+    # Public API
+
+    @property
+    def session(self):
+        '''Returns the Session object associated with this client.'''
+        assert not self._closed
+        if self._session is None:
+            self._sid, self._session = self._cache.create()
+            self._newsession = True
+        assert self._session is not None
+        return self._session
+
+    @property
+    def current(self):
+        '''True if a Session currently exists for this client'''
+        assert not self._closed
+        return self._session is not None
+
+    @property
+    def new(self):
+        '''True if the Session was created in this transaction.'''
+        assert not self._closed
+        return self._newsession
+
+    @property    
+    def expired(self):
+        '''True if the client was associated with a non-existent Session'''
+        assert not self._closed
+        return self._expired
+
+    # Utilities
+
+    def seturl(self, url):
+        '''Encodes session ID in URL, if necessary.'''
+        assert not self._closed
+        if not self.inurl or self._session is None: return url
+        u = list(urlparse.urlsplit(url))
+        q = '%s=%s' % (self._fieldname, self.close())
+        if u[3]:
+            u[3] = q + '&' + u[3]
+        else:
+            u[3] = q
+        return urlparse.urlunsplit(u)
+
+
+class SessionMiddleware(object):
+
+    '''WSGI middleware that adds a session service. A SessionService instance
+    is passed to the application in environ['com.saddi.service.session'].
+    A references to this instance should not be saved. (A new instance is
+    instantiated with every call to the application.)
+    '''
+
+    def __init__(self, cache, application, **kw):
+        self._cache, self._application = cache, application
+        self._sessionkey = kw.get('sessionkey', 'com.saddi.service.session')
+        self._kw = kw
+
+    def __call__(self, environ, start_response):
+        service = SessionService(self._cache, environ, **self._kw)
+        environ[self._sessionkey] = service        
+        def my_start_response(status, headers, exc_info=None):
+            service.setcookie(headers)
+            return start_response(status, headers, exc_info)
+        try:
+            result = self._application(environ, my_start_response)
+        except:
+            # If anything goes wrong, ensure the session is checked back in.
+            service.close()
+        # The iterator must be unconditionally wrapped, just in case it
+        # is a generator. (In which case, we may not know that a Session
+        # has been checked out until completion of the first iteration.)
+        return _addclose(result, service.close)
+
+
+__all__ = ['Session', 'SessionCache', 'SessionMiddleware']

branches/0.1/wsgistate/simple.py

+# Copyright (c) 2005, the Lawrence Journal-World
+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of Django nor the names of its contributors may be used
+#       to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Single-process in-memory cache backend.'''
+
+import time
+from base import BaseCache
+
+
+class SimpleCache(BaseCache):
+
+    '''Single-process in-memory cache backend.'''    
+    
+    def __init__(self, *a, **kw):
+        '''Init method.'''
+        super(SimpleCache, self).__init__(*a, **kw)
+        self._cache, self._expire_info = dict(), dict()
+        max_entries = kw.get('max_entries', 300)
+        try:
+            self._max_entries = int(max_entries)
+        except (ValueError, TypeError):
+            self._max_entries = 300
+
+    def get(self, key, default=None):
+        '''Fetch a given key from the cache.  If the key does not exist, return
+        default, which itself defaults to None.
+
+        @param key Keyword of item in cache.
+        @param default Default value (default: None)
+        '''
+        now, exp = time.time(), self._expire_info.get(key)
+        if exp is None:
+            return default
+        # Delete if item timed out and return default.
+        elif exp < now:
+            self.delete(key)
+            return default
+        else:
+            return self._cache[key]
+
+    def set(self, key, value):
+        '''Set a value in the cache.
+
+        @param key Keyword of item in cache.
+        @param value Value to be inserted in cache.        
+        '''
+        # Cull timed out values if over max # of entries
+        if len(self._cache) >= self._max_entries: self._cull()
+        self._cache[key] = value
+        # Set timeout
+        self._expire_info[key] = time.time() + self.default_timeout
+
+    def delete(self, key):
+        '''Delete a key from the cache, failing silently.
+
+        @param key Keyword of item in cache.
+        '''
+        try:
+            del self._cache[key]
+        except KeyError: pass
+        try:
+            del self._expire_info[key]
+        except KeyError: pass
+
+    def _cull(self):
+        '''Remove items in cache that have timed out.'''
+        now = time.time()
+        for key, exp in self._expire_info.iteritems():
+            if exp < now: self.delete(key)
+
+
+__all__ = ['SimpleCache']
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.