Source

flaskext-durus / flaskext / 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 .spec import either, spec, nspec, init, length, add_getters_and_setters
from .spec import require, get_spec_problems, specify, identifier_pattern, both
from .spec import datetime_with_tz, sequence, instance, mapping, match, get_specs
from .keep import Keyed, Stamped, Keep, Counter

from flask import _request_ctx_stack, redirect, url_for

from datetime import datetime
import pytz
epoch = datetime(1970,1,1, tzinfo=pytz.utc)

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
        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) 
            for changed in db.changed.itervalues():
                print "changed:", changed
            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 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()
        self.create_indexes()

    def add(self, value):
        Keep.add(self, value)
        # update indexes if defined.
        value._indexes = PeristentDict()

    def create_indexes(self):
        """()
        Create all indexes bases on callables on the value_spec class that start with 'key_for'
        """
        for attr in self.value_spec.__dict__.keys():
            if attr.startswith('key_for_') and callable(getattr(self.value_spec, attr)):
                # index key
                print attr
                self._indexes.setdefault(attr.split('key_for_')[1], BTree())

    def update_indexes(self):
        """()
        Rebuild all indexes defined on this Keep.
        """
        for name, index in self._indexes.items():
            print "update index:", name
            index.clear()
            for item in self.mapping.itervalues():
                key = getattr(item, "key_for_%s" % name)()
                index[(key, item.key)] = item
                if not hasattr(item, '_indexes'):
                    item._indexes = PersistentDict()
                item._indexes[name] = (key, item.key)