Source

flaskext-durus / flask_durus / durus.py

Full commit
from __future__ import absolute_import

from durus.client_storage import ClientStorage
from durus.connection import Connection
from durus.persistent import Persistent, PersistentObject
from durus.persistent_list import PersistentList
from durus.persistent_dict import PersistentDict
import durus.btree 

from UserDict import IterableUserDict

from qp.spec import either, spec, nspec, init, length, add_getters_and_setters
from qp.spec import require, get_spec_problems, specify, identifier_pattern, both
from qp.spec import datetime_with_tz, sequence, instance, mapping, match, get_specs
from qp.keep import Keyed, Stamped, Keep, Counter

from flask import _request_ctx_stack, redirect, url_for

from datetime import datetime

def with_getters_and_setters(klass):
    """
    Add getters and setters to the persistent class
    """
    add_getters_and_setters(klass)
    return klass
    

class Durus(IterableUserDict):
    connections = []
    
    def __init__(self, app=None):
        if app is not None:
            self.app = app
            self.setup(self.app)
        else:
            self.app = None

    @property
    def data(self):
        ctx = _request_ctx_stack.top
        if ctx is not None:
            return self._get_db(ctx).get_root()
    
    def _get_db(self, ctx):
        if not hasattr(ctx, 'durus_db'):
            ctx.durus_db = self.connect()
        return ctx.durus_db


    def setup_app(self, app, browser=None):
        self.app = app
        self.app.config.setdefault('DURUS_PORT', 2972)
        self.app.config.setdefault('DURUS_HOST', 'localhost')
        self.app.teardown_request(self.teardown_request)
        self.app.before_request(self.before_request)
        if browser:
            browsedb_on_connection(self, app, browser)

    def connect(self):
        ##print "connect, try to get from pool"
        try:
            conn = self.connections.pop()
            ##print "existing connection"
        except Exception, e:
            #print "new conection [%s]" % e
            conn = Connection(ClientStorage(port=self.app.config['DURUS_PORT']))
        #print "connection [%s]" % id(conn)
        return conn

    def before_request(self):
        #print "before request"
        ctx = _request_ctx_stack.top
        ctx.durus_db = self.connect()

    def teardown_request(self, exception):
        #print "teardown request, put connection back in the pool"
        ctx = _request_ctx_stack.top
        if hasattr(ctx, 'durus_db'):
            ctx.durus_db.abort()
            self.connections.append(ctx.durus_db)
            #print "back in pool, size [%d]" % len(self.connections)
            del(ctx.durus_db)

    def abort(self):
        ctx = _request_ctx_stack.top
        if ctx is not None:
            return self._get_db(ctx).abort()

    def commit(self):
        ctx = _request_ctx_stack.top
        if ctx is not None:
            # walk list of changed items
            db = self._get_db(ctx) 
            changed = db.changed.values()
            for changed in changed:
                # call commit if definied on item
                print "changed:", changed
                on_commit = getattr(changed, 'on_commit', None)
                if on_commit:
                    print "changed:", changed, "calling on_commit"
                    on_commit()

            return db.commit()

    @property
    def connection(self):
        ctx = _request_ctx_stack.top
        if ctx is not None:
            return self._get_db(ctx)

class BTree(durus.btree.BTree):
    def _with_limit(self, method, *args,  **kws):
        limit = kws.pop('limit', None)
        for idx, item in enumerate(method(self, *args, **kws)):
            if limit == None or idx < limit:
                yield item
            else:
                raise StopIteration()

    def items_backward(self, limit=None):
        """(limit=None) -> generator
        Generate all items in reverse order, getting max limit items.
        """
        return self._with_limit(durus.btree.BTree.items_backward, limit=limit)

    def items_from(self, key, closed=True, limit=None):
        """(key, closed=True, limit=None) -> generator
        If closed is true, generate all items with keys greater than or equal to
        the given key.
        If closed is false, generate all items with keys greater than the
        given key.
        """
        return self._with_limit(durus.btree.BTree.items_from, 
                key, closed=closed, limit=limit)


    def items_backward_from(self, key, closed=False, limit=None):
        """(key, closed=False, limit=None) -> generator
        If closed is true, generate in reverse order all items with keys
        less than or equal to the given key.
        If closed is true, generate in reverse order all items with keys
        less than the given key.
        """
        return self._with_limit(durus.btree.BTree.items_backward_from, 
                key, closed=closed, limit=limit)

    def items_range(self, start, end, closed_start=True, closed_end=False, limit=None):
        """(start, end, closed_start=True, closed_end=False, limit=None) -> generator
        Generate items with keys in the given range, from the start to the end.
        If closed_start is true, include the item with the start key,
        if it is present.
        If closed_end is true, include the item with the end key,
        if it is present.
        """
        return self._with_limit(durus.btree.BTree.items_range, 
                start, end, closed_start=closed_start, closed_end=closed_end, 
                limit=limit)

def browsedb_on_connection(db, app, url_prefix):
    """(db:Durus, app:Flask, url_prefix:str)
    Add a route for a db browser to the app at url_prefix.
    """
    @app.route("/%s" % url_prefix)
    def browseroot():
        """
        Redirect to the root object
        """
        return redirect(url_for('browsedb', oid=0))

    @app.route("/%s/<int:oid>" % url_prefix)
    def browsedb(oid):
        from flask import url_for

        def persistent_vars(obj):
            """() -> dict
            This is like the built-in vars() function, except that it also works
            for PersistentObject instances or other instances that use slots for
            data attributes.
            """
            if hasattr(obj, '__getstate__'):
                return obj.__getstate__() or {}
            elif hasattr(obj, '__dict__'):
                return vars(obj)
            else:
                result = {}
                for name in obj.__slots__:
                    if hasattr(obj, name):
                        result[name] = getattr(obj, name)
                return result

        value = db.connection.get(oid)

        def format(value):
            if isinstance(value, PersistentObject):
                ret = """<a href="%s">%s</a>""" % (url_for("browsedb", oid=value._p_format_oid()), 
                        repr(value).replace('<', '&lt;').replace('>', '&gt;'))
            elif isinstance(value, list):
                ret = '[' + ', '.join([format(v) for v in value]) + ']'
            elif isinstance(value, tuple):
                ret = '(' + ', '.join([format(v) for v in value]) + ')'
            elif isinstance(value, dict):
                ret = '<div style="margin-left:2em">{'
                for k, v in value.items():
                    ret += '<div>' + format(k) + ' : ' + format(v) + '</div>'
                ret += '}</div>'
            else:
                ret = repr(value).replace('<', '&lt;').replace('>','&gt;') 

            return ret

        return """<html><head><title>%s</title></head><body><div>%s</div><div>%s</div></body></html>""" % (repr(value).replace('<', '&lt;').replace('>','&gt;'), repr(value).replace('<', '&lt;').replace('>','&gt;'), format(persistent_vars(value)))

class Index(BTree):
    def _with_limit(self, method, *args,  **kws):
        """
        Overload this so that only the item returned is the object as the last item in the index key.
        """
        for key, value in BTree._with_limit(self, method, *args, **kws):
            yield key[-1]

class IndexedKeyed(Keyed):
    """
    A keyed item that maintains indexes
    """
    KEY_PREFIX = 'key_for_'
    KEY_PREFIX_LEN = len(KEY_PREFIX)
    
    _keep_is = spec(either(None, "IndexedKeep"), "Pointer to the keep this item is in")
    _indexes_is = spec(PersistentDict, "Pointers to the indexes this item exists in")

    def __init__(self):
        print "in init"
        Keyed.__init__(self)
        self._indexes = PersistentDict()
        self._keep = None
    
    def _index(self):
        """()
        Return index keys for this item based on key functions
        """
        for name in dir(self):
            attr = getattr(self, name)
            if name.startswith(self.KEY_PREFIX) and callable(attr):
                for key in attr():
                    if key:
                        # only return a index and key if the key isn't None, ie. no valid value for that key
                        yield name[self.KEY_PREFIX_LEN:], key

    def save(self):
        """()
        Get all index keys and update in the keep
        """
        print "update indexes"
        if self._keep:
            # remove existing index keys
            for name, existing_key in self._indexes.items():
                try:
                    del(self._keep._indexes[name][existing_key])
                    # remove index if empty
                    if not self._keep._indexes[name]:
                        del(self._keep._indexes[name])
                except KeyError:
                    """ no index """
                    # TODO - clean up, should check for index
            
            # add current values
            for name, key in self._index():
                if type(key) == tuple:
                    key = list(key)
                if type(key) != list:
                    key = [key]
                # add item to end of key to ensure uniqueness
                key.append(self)

                key=tuple(key)
                print "key [%s]" % str(key)
                self._keep._indexes.setdefault(name, Index())[key] = None

                # store pointer back into index
                self._indexes[name] = key

class IndexedKeep(Keep):
    """
    A Keep that maintains indexes for quick access to the values.
    """
    
    def __init__(self, value_spec=Keyed, mapping_class=BTree, counter_class=Counter):
        Keep.__init__(self, value_spec, mapping_class, counter_class)
        self._indexes = PersistentDict()

    def add(self, value):
        Keep.add(self, value)
        value._keep = self


    def by(self, name):
        """(name:str) -> BTree
        Get an index into the keep
        """
        return self._indexes.get(name, None)