Commits

Ben Bangert committed e70b2ff

Applying sqla db backend patch.

  • Participants
  • Parent commits 5a981cc

Comments (0)

Files changed (5)

     pass
 
 try:
+    import beaker.ext.sqla as sqla
+    clsmap['ext:sqla'] = sqla.SQLAlchemyContainer
+except InvalidCacheBackendError:
+    pass
+
+try:
     import beaker.ext.google as google
     clsmap['ext:google'] = google.GoogleContainer
 except (InvalidCacheBackendError, SyntaxError):

beaker/container.py

     return _cls_registry(name, 'Container')
 
 def _cls_registry(name, clsname):
-    if name.startswith('ext:') or name in ['memcached', 'database', 'google']:
+    if name.startswith('ext:') \
+            or name in ['memcached', 'database', 'sqla', 'google']:
         if name.startswith('ext:'):
             name = name[4:]
         modname = "beaker.ext." + name

beaker/ext/sqla.py

+import cPickle
+import logging
+from datetime import datetime
+
+from beaker.container import NamespaceManager, Container
+from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter
+from beaker.synchronization import Synchronizer, _threading
+from beaker.util import verify_directory, SyncDict
+
+try:
+    import sqlalchemy as sa
+except ImportError:
+    raise InvalidCacheBackendError('SQLAlchemy, which is required by this backend, is not installed')
+
+log = logging.getLogger(__name__)
+
+class SQLAlchemyNamespaceManager(NamespaceManager):
+    binds = SyncDict(_threading.Lock(), {})
+    tables = SyncDict(_threading.Lock(), {})
+
+    def __init__(self, namespace, bind, table, data_dir=None, lock_dir=None,
+                 **kwargs):
+        """Create a namespace manager for use with a database table via
+        SQLAlchemy.
+
+        ``bind``
+            SQLAlchemy ``Engine`` or ``Connection`` object
+
+        ``table``
+            SQLAlchemy ``Table`` object in which to store namespace data.
+            This should usually be something created by ``make_cache_table``.
+        """
+        NamespaceManager.__init__(self, namespace, **kwargs)
+
+        if lock_dir is not None:
+            self.lock_dir = lock_dir
+        elif data_dir is None:
+            raise MissingCacheParameter('data_dir or lock_dir is required')
+        else:
+            self.lock_dir = data_dir + '/container_db_lock'
+
+        verify_directory(self.lock_dir)
+
+        self.bind = self.__class__.binds.get(str(bind.url), lambda: bind)
+        self.table = self.__class__.tables.get('%s:%s' % (bind.url, table.name),
+                                               lambda: table)
+        self.hash = {}
+        self._is_new = False
+        self.loaded = False
+
+    def do_acquire_read_lock(self):
+        pass
+
+    def do_release_read_lock(self):
+        pass
+
+    def do_acquire_write_lock(self, wait=True):
+        return True
+
+    def do_release_write_lock(self):
+        pass
+
+    def do_open(self, flags):
+        if self.loaded:
+            self.flags = flags
+            return
+        select = sa.select([self.table.c.data],
+                           (self.table.c.namespace == self.namespace))
+        result = self.bind.execute(select).fetchone()
+        if not result:
+            self._is_new = True
+            self.hash = {}
+        else:
+            self._is_new = False
+            try:
+                self.hash = cPickle.loads(str(result['data']))
+            except (IOError, OSError, EOFError, cPickle.PickleError):
+                log.debug("Couln't load pickle data, creating new storage")
+                self.hash = {}
+                self._is_new = True
+        self.flags = flags
+        self.loaded = True
+
+    def do_close(self):
+        if self.flags is not None and (self.flags == 'c' or self.flags == 'w'):
+            data = cPickle.dumps(self.hash)
+            if self._is_new:
+                insert = self.table.insert()
+                self.bind.execute(insert, namespace=self.namespace, data=data,
+                                  accessed=datetime.now(),
+                                  created=datetime.now())
+                self._is_new = False
+            else:
+                update = self.table.update(self.table.c.namespace == self.namespace)
+                self.bind.execute(update, data=data, accessed=datetime.now())
+        self.flags = None
+
+    def do_remove(self):
+        delete = self.table.delete(self.table.c.namespace == self.namespace)
+        self.bind.execute(delete)
+        self.hash = {}
+        self._is_new = True
+
+    def __getitem__(self, key):
+        return self.hash[key]
+
+    def __contains__(self, key):
+        return self.hash.has_key(key)
+
+    def __setitem__(self, key, value):
+        self.hash[key] = value
+
+    def __delitem__(self, key):
+        del self.hash[key]
+
+    def keys(self):
+        return self.hash.keys()
+
+
+class SQLAlchemyContainer(Container):
+    def do_init(self, data_dir=None, lock_dir=None, **kwargs):
+        self.funclock = None
+
+    def create_namespace(self, namespace, bind, table, **kwargs):
+        return SQLAlchemyNamespaceManager(namespace, bind, table, **kwargs)
+
+    create_namespace = classmethod(create_namespace)
+
+    def lock_createfunc(self, wait=True):
+        if self.funclock is None:
+            identifier = 'sqlalchemycontainer/funclock/%s' \
+                % self.namespacemanager.namespace
+            self.funclock = Synchronizer(identifier, True,
+                                         self.namespacemanager.lock_dir)
+        return self.funclock.acquire_write_lock(wait)
+
+    def unlock_createfunc(self):
+        self.funclock.release_write_lock()
+
+
+def make_cache_table(metadata, table_name='beaker_cache'):
+    """Return a ``Table`` object suitable for storing cached values for the
+    namespace manager.  Do not create the table."""
+    return sa.Table(table_name, metadata,
+                    sa.Column('namespace', sa.String(255), primary_key=True),
+                    sa.Column('accessed', sa.DateTime, nullable=False),
+                    sa.Column('created', sa.DateTime, nullable=False),
+                    sa.Column('data', sa.BLOB(), nullable=False))

beaker/middleware.py

     def __init__(self, wrap_app, config=None, environ_key='beaker.session', **kwargs):
         """Initialize the Session Middleware
         
-        The Session middleware will make a lazy session instance available 
-        every request under the ``environ['beaker.cache']`` key by default. The location in
-        environ can be changed by setting ``environ_key``.
+        The Session middleware will make a lazy session instance available
+        every request under the ``environ['beaker.session']`` key by
+        default. The location in environ can be changed by setting
+        ``environ_key``.
         
         ``config``
-            dict  All settings should be prefixed by 'cache.'. This method of
+            dict  All settings should be prefixed by 'session.'. This method of
             passing variables is intended for Paste and other setups that
             accumulate multiple component settings in a single dictionary. If
             config contains *no cache. prefixed args*, then *all* of the config

tests/test_sqla.py

+# coding: utf-8
+from beaker.cache import clsmap, Cache
+from beaker.middleware import CacheMiddleware
+from nose import SkipTest
+from webtest import TestApp
+
+if 'ext:sqla' not in clsmap:
+    raise SkipTest('SQLAlchemy extension not installed; skipping test')
+
+import sqlalchemy as sa
+from beaker.ext.sqla import make_cache_table
+
+engine = sa.create_engine('sqlite://')
+metadata = sa.MetaData()
+cache_table = make_cache_table(metadata)
+metadata.create_all(engine)
+
+def simple_app(environ, start_response):
+    extra_args = {}
+    clear = False
+    if environ.get('beaker.clear'):
+        clear = True
+    extra_args['type'] = 'ext:sqla'
+    extra_args['bind'] = engine
+    extra_args['table'] = cache_table
+    extra_args['data_dir'] = './cache'
+    cache = environ['beaker.cache'].get_cache('testcache', **extra_args)
+    if clear:
+        cache.clear()
+    try:
+        value = cache.get_value('value')
+    except:
+        value = 0
+    cache.set_value('value', value+1)
+    start_response('200 OK', [('Content-type', 'text/plain')])
+    return ['The current value is: %s' % cache.get_value('value')]
+
+def cache_manager_app(environ, start_response):
+    cm = environ['beaker.cache']
+    cm.get_cache('test')['test_key'] = 'test value'
+
+    start_response('200 OK', [('Content-type', 'text/plain')])
+    yield "test_key is: %s\n" % cm.get_cache('test')['test_key']
+    cm.get_cache('test').clear()
+
+    try:
+        test_value = cm.get_cache('test')['test_key']
+    except KeyError:
+        yield "test_key cleared"
+    else:
+        yield "test_key wasn't cleared, is: %s\n" % \
+            cm.get_cache('test')['test_key']
+
+def make_cache():
+    """Return a ``Cache`` for use by the unit tests."""
+    return Cache('test', data_dir='./cache', bind=engine, table=cache_table,
+                 type='ext:sqla')
+
+def test_has_key():
+    cache = make_cache()
+    o = object()
+    cache.set_value("test", o)
+    assert cache.has_key("test")
+    assert "test" in cache
+    assert not cache.has_key("foo")
+    assert "foo" not in cache
+    cache.remove_value("test")
+    assert not cache.has_key("test")
+
+def test_has_key_multicache():
+    cache = make_cache()
+    o = object()
+    cache.set_value("test", o)
+    assert cache.has_key("test")
+    assert "test" in cache
+    cache = make_cache()
+    assert cache.has_key("test")
+    cache.remove_value('test')
+
+def test_clear():
+    cache = make_cache()
+    o = object()
+    cache.set_value("test", o)
+    assert cache.has_key("test")
+    cache.clear()
+    assert not cache.has_key("test")
+
+def test_unicode_keys():
+    cache = make_cache()
+    o = object()
+    cache.set_value(u'hiŏ', o)
+    assert u'hiŏ' in cache
+    assert u'hŏa' not in cache
+    cache.remove_value(u'hiŏ')
+    assert u'hiŏ' not in cache
+
+def test_increment():
+    app = TestApp(CacheMiddleware(simple_app))
+    res = app.get('/', extra_environ={'beaker.clear': True})
+    assert 'current value is: 1' in res
+    res = app.get('/')
+    assert 'current value is: 2' in res
+    res = app.get('/')
+    assert 'current value is: 3' in res
+
+def test_cache_manager():
+    app = TestApp(CacheMiddleware(cache_manager_app))
+    res = app.get('/')
+    assert 'test_key is: test value' in res
+    assert 'test_key cleared' in res