goa/db.py
changeset 0 b97547f5f1fa
child 447 0e52d72104a6
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/goa/db.py	Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,460 @@
+"""provide replacement classes for gae db module, so that a gae model can be
+used as base for a cubicweb application by simply replacing ::
+
+  from google.appengine.ext import db
+
+by
+
+  from cubicweb.goa import db
+
+The db.model api should be fully featured by replacement classes, with the
+following differences:
+
+* all methods returning `google.appengine.ext.db.Model` instance(s) will return
+  `cubicweb.goa.db.Model` instance instead (though you should see almost no
+  difference since those instances have the same api)
+  
+* class methods returning model instance take a `req` as first argument, unless
+  they are called through an instance, representing the current request
+  (accessible through `self.req` on almost all objects)
+  
+* XXX no instance.<modelname>_set attributes, use instance.reverse_<attr name>
+      instead
+* XXX reference property always return a list of objects, not the instance
+* XXX name/collection_name argument of properties constructor are ignored
+* XXX ListProperty
+
+:organization: Logilab
+:copyright: 2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from datetime import datetime
+from copy import deepcopy
+
+from logilab.common.decorators import cached, iclassmethod
+
+from cubicweb import RequestSessionMixIn, Binary, entities
+from cubicweb.rset import ResultSet
+from cubicweb.common.entity import metaentity
+from cubicweb.server.utils import crypt_password
+from cubicweb.goa import use_mx_for_dates, mx2datetime, MODE
+from cubicweb.goa.dbinit import init_relations
+
+from google.appengine.api.datastore import Get, Put, Key, Entity, Query
+from google.appengine.api.datastore import NormalizeAndTypeCheck, RunInTransaction
+from google.appengine.api.datastore_types import Text, Blob
+from google.appengine.api.datastore_errors import BadKeyError
+
+# XXX remove this dependancy
+from google.appengine.ext import db 
+
+
+def rset_from_objs(req, objs, attrs=('eid',), rql=None, args=None):
+    """return a ResultSet instance for list of objects"""
+    if objs is None:
+        objs = ()
+    elif isinstance(objs, Entity):
+        objs = (objs,)
+    if rql is None:
+        rql = 'Any X'
+    rows = []
+    description = []
+    rset = ResultSet(rows, rql, args, description=description)
+    vreg = req.vreg
+    for i, obj in enumerate(objs):
+        line = []
+        linedescr = []
+        eschema = vreg.schema.eschema(obj.kind())
+        for j, attr in enumerate(attrs):
+            if attr == 'eid':
+                value = obj.key()
+                obj.row, obj.col = i, j
+                descr = eschema.type
+                value = str(value)
+            else:
+                value = obj[attr]
+                descr = str(eschema.destination(attr))
+            line.append(value)
+            linedescr.append(descr)            
+        rows.append(line)
+        description.append(linedescr)
+        for j, attr in enumerate(attrs):
+            if attr == 'eid':
+                entity = vreg.etype_class(eschema.type)(req, rset, i, j)
+                rset._get_entity_cache_ = {(i, j): entity}        
+    rset.rowcount = len(rows)
+    req.decorate_rset(rset)    
+    return rset
+
+
+def needrequest(wrapped):
+    def wrapper(cls, *args, **kwargs):
+        req = kwargs.pop('req', None)
+        if req is None and args and isinstance(args[0], RequestSessionMixIn):
+            args = list(args)
+            req = args.pop(0)
+        if req is None:
+            req = getattr(cls, 'req', None)
+            if req is None:
+                raise Exception('either call this method on an instance or '
+                                'specify the req argument')
+        return wrapped(cls, req, *args, **kwargs)
+    return iclassmethod(wrapper)
+
+    
+class gaedbmetaentity(metaentity):
+    """metaclass for goa.db.Model classes: filter entity / db model part,
+    put aside the db model part for later creation of db model class.
+    """
+    def __new__(mcs, name, bases, classdict):
+        if not 'id' in classdict:
+            classdict['id'] = name
+        entitycls = super(gaedbmetaentity, mcs).__new__(mcs, name, bases, classdict)
+        return entitycls
+
+
+TEST_MODELS = {}
+
+def extract_dbmodel(entitycls):
+    if MODE == 'test' and entitycls in TEST_MODELS:
+        dbclassdict = TEST_MODELS[entitycls]
+    else:
+        dbclassdict = {}
+        for attr, value in entitycls.__dict__.items():
+            if isinstance(value, db.Property) or isinstance(value, ReferencePropertyStub):
+                dbclassdict[attr] = value
+                # don't remove attr from entitycls, this make tests fail, and it's anyway
+                # overwritten by descriptor at class initialization time
+                #delattr(entitycls, attr)
+    if MODE == 'test':
+        TEST_MODELS[entitycls] = dbclassdict
+        dbclassdict = deepcopy(dbclassdict)
+        for propname, prop in TEST_MODELS[entitycls].iteritems():
+            if getattr(prop, 'reference_class', None) is db._SELF_REFERENCE:
+                dbclassdict[propname].reference_class = db._SELF_REFERENCE
+    return dbclassdict
+
+
+class Model(entities.AnyEntity):
+    id = 'Any'
+    __metaclass__ = gaedbmetaentity
+    
+    row = col = 0
+    
+    @classmethod
+    def __initialize__(cls):
+        super(Model, cls).__initialize__()
+        cls._attributes = frozenset(rschema for rschema in cls.e_schema.subject_relations()
+                                    if rschema.is_final())
+    
+    def __init__(self, *args, **kwargs):
+        # db.Model prototype:
+        #   __init__(self, parent=None, key_name=None, **kw)
+        #
+        # Entity prototype:
+        #   __init__(self, req, rset, row=None, col=0)
+        if args and isinstance(args[0], RequestSessionMixIn) or 'req' in kwargs:
+            super(Model, self).__init__(*args, **kwargs)
+            self._gaeinitargs = None
+        else:
+            super(Model, self).__init__(None, None)
+            # if Model instances are given in kwargs, turn them into db model
+            for key, val in kwargs.iteritems():
+                if key in self.e_schema.subject_relations() and not self.e_schema.schema[key].is_final():
+                    if isinstance(kwargs, (list, tuple)):
+                        val = [isinstance(x, Model) and x._dbmodel or x for x in val]
+                    elif isinstance(val, Model):
+                        val = val._dbmodel
+                    kwargs[key] = val.key()
+            self._gaeinitargs = (args, kwargs)
+            
+    def __repr__(self):
+        return '<ModelEntity %s %s %s at %s>' % (
+            self.e_schema, self.eid, self.keys(), id(self))
+
+    __getattribute__ = use_mx_for_dates(entities.AnyEntity.__getattribute__)
+
+    def _cubicweb_to_datastore(self, attr, value):
+        attr = attr[2:] # remove 's_' / 'o_' prefix
+        if attr in self._attributes:
+            tschema = self.e_schema.destination(attr)
+            if tschema in ('Datetime', 'Date', 'Time'):
+                value = mx2datetime(value, tschema)
+            elif tschema == 'String':
+                if len(value) > 500:
+                    value = Text(value)                
+            elif tschema == 'Password':
+                # if value is a Binary instance, this mean we got it
+                # from a query result and so it is already encrypted
+                if isinstance(value, Binary):
+                    value = value.getvalue()
+                else:
+                    value = crypt_password(value)
+            elif tschema == 'Bytes':
+                if isinstance(value, Binary):
+                    value = value.getvalue()
+                value = Blob(value)
+        else:
+            value = Key(value)
+        return value
+
+    def _to_gae_dict(self, convert=True):
+        gaedict = {}
+        for attr, value in self.iteritems():
+            attr = 's_' + attr
+            if value is not None and convert:
+                value = self._cubicweb_to_datastore(attr, value)
+            gaedict[attr] = value
+        return gaedict
+    
+    def to_gae_model(self):
+        dbmodel = self._dbmodel
+        dbmodel.update(self._to_gae_dict())
+        return dbmodel
+
+    @property
+    @cached
+    def _dbmodel(self): 
+        if self.has_eid():
+            assert self._gaeinitargs is None
+            try:
+                return self.req.datastore_get(self.eid)
+            except AttributeError: # self.req is not a server session
+                return Get(self.eid)
+        self.set_defaults()
+        values = self._to_gae_dict(convert=False)
+        parent = key_name = _app = None
+        if self._gaeinitargs is not None:
+            args, kwargs = self._gaeinitargs
+            args = list(args)
+            if args:
+                parent = args.pop(0)
+            if args:
+                key_name = args.pop(0)
+            if args:
+                _app = args.pop(0)
+            assert not args
+            if 'parent' in kwargs:
+                assert parent is None
+                parent = kwargs.pop('parent')
+            if 'key_name' in kwargs:
+                assert key_name is None
+                key_name = kwargs.pop('key_name')
+            if '_app' in kwargs:
+                assert _app is None
+                _app = kwargs.pop('_app')
+            
+            for key, value in kwargs.iteritems():
+                if key in self._attributes:
+                    values['s_'+key] = value
+        else:
+            kwargs = None
+        if key_name is None:
+            key_name = self.db_key_name()
+            if key_name is not None:
+                key_name = 'key_' + key_name
+        for key, value in values.iteritems():
+            if value is None:
+                continue
+            values[key] = self._cubicweb_to_datastore(key, value)
+        entity = Entity(self.id, parent, _app, key_name)
+        entity.update(values)
+        init_relations(entity, self.e_schema)
+        return entity
+
+    def db_key_name(self):
+        """override this method to control datastore key name that should be
+        used at entity creation.
+
+        Note that if this function return something else than None, the returned
+        value will be prefixed by 'key_' to build the actual key name.
+        """
+        return None
+    
+    def metainformation(self):
+        return {'type': self.id, 'source': {'uri': 'system'}, 'extid': None}
+       
+    def view(self, vid, __registry='views', **kwargs):
+        """shortcut to apply a view on this entity"""
+        return self.vreg.render(__registry, vid, self.req, rset=self.rset,
+                                row=self.row, col=self.col, **kwargs)
+
+    @classmethod
+    def _rest_attr_info(cls):
+        mainattr, needcheck = super(Model, cls)._rest_attr_info()
+        if needcheck:
+            return 'eid', False
+        return mainattr, needcheck
+    
+    @use_mx_for_dates
+    def get_value(self, name):
+        try:
+            value = self[name]
+        except KeyError:
+            if not self.has_eid():
+                return None
+            value = self._dbmodel.get('s_'+name)
+            if value is not None:
+                if isinstance(value, Text):
+                    value = unicode(value)
+                elif isinstance(value, Blob):
+                    value = Binary(str(value))
+            self[name] = value
+        return value
+
+    def has_eid(self):
+        if self.eid is None:
+            return False
+        try:
+            Key(self.eid)
+            return True
+        except BadKeyError:
+            return False
+        
+    def complete(self, skip_bytes=True):
+        pass
+
+    def unrelated(self, rtype, targettype, role='subject', limit=None,
+                  ordermethod=None):
+        # XXX dumb implementation
+        if limit is not None:
+            objs = Query(str(targettype)).Get(limit)
+        else:
+            objs = Query(str(targettype)).Run()
+        return rset_from_objs(self.req, objs, ('eid',),
+                              'Any X WHERE X is %s' % targettype)
+    
+    def key(self):
+        return Key(self.eid)
+
+    def put(self, req=None):
+        if req is not None and self.req is None:
+            self.req = req
+        dbmodel = self.to_gae_model()
+        key = Put(dbmodel)
+        self.set_eid(str(key))
+        if self.req is not None and self.rset is None:
+            self.rset = rset_from_objs(self.req, dbmodel, ('eid',),
+                                       'Any X WHERE X eid %(x)s', {'x': self.eid})
+            self.row = self.col = 0
+        return dbmodel
+    
+    @needrequest
+    def get(cls, req, keys):
+        # if check if this is a dict.key call
+        if isinstance(cls, Model) and keys in cls._attributes:
+            return super(Model, cls).get(keys)
+        rset = rset_from_objs(req, Get(keys), ('eid',),
+                              'Any X WHERE X eid IN %(x)s', {'x': keys})
+        return list(rset.entities())
+
+    @needrequest
+    def get_by_id(cls, req, ids, parent=None):
+        if isinstance(parent, Model):
+            parent = parent.key()
+        ids, multiple = NormalizeAndTypeCheck(ids, (int, long))
+        keys = [Key.from_path(cls.kind(), id, parent=parent)
+                for id in ids]
+        rset = rset_from_objs(req, Get(keys))
+        return list(rset.entities())
+
+    @classmethod
+    def get_by_key_name(cls, req, key_names, parent=None):
+        if isinstance(parent, Model):
+            parent = parent.key()
+        key_names, multiple = NormalizeAndTypeCheck(key_names, basestring)
+        keys = [Key.from_path(cls.kind(), name, parent=parent)
+                for name in key_names]
+        rset = rset_from_objs(req, Get(keys))
+        return list(rset.entities())
+
+    @classmethod
+    def get_or_insert(cls, req, key_name, **kwds):
+        def txn():
+            entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
+            if entity is None:
+                entity = cls(key_name=key_name, **kwds)
+                entity.put()
+            return entity
+        return RunInTransaction(txn)
+
+    @classmethod
+    def all(cls, req):
+        rset = rset_from_objs(req, Query(cls.id).Run())
+        return list(rset.entities())
+
+    @classmethod
+    def gql(cls, req, query_string, *args, **kwds):
+        raise NotImplementedError('use rql')
+
+    @classmethod
+    def kind(cls):
+        return self.id
+
+    @classmethod
+    def properties(cls):
+        raise NotImplementedError('use eschema')
+
+    def dynamic_properties(self):
+        raise NotImplementedError('use eschema')
+        
+    def is_saved(self):
+        return self.has_eid()
+
+    def parent(self):
+        parent = self._dbmodel.parent()
+        if not parent is None:
+            rset = rset_from_objs(self.req, (parent,), ('eid',),
+                                  'Any X WHERE X eid %(x)s', {'x': parent.key()})
+            parent = rset.get_entity(0, 0)
+        return parent
+
+    def parent_key(self):
+        return self.parent().key()
+
+    def to_xml(self):
+        return self._dbmodel.ToXml()
+
+# hijack AnyEntity class
+entities.AnyEntity = Model
+
+BooleanProperty = db.BooleanProperty
+URLProperty = db.URLProperty
+DateProperty = db.DateProperty
+DateTimeProperty = db.DateTimeProperty
+TimeProperty = db.TimeProperty
+StringProperty = db.StringProperty
+TextProperty = db.TextProperty
+BlobProperty = db.BlobProperty
+IntegerProperty = db.IntegerProperty
+FloatProperty = db.FloatProperty
+ListProperty = db.ListProperty
+SelfReferenceProperty = db.SelfReferenceProperty 
+UserProperty = db.UserProperty
+
+
+class ReferencePropertyStub(object):
+    def __init__(self, cls, args, kwargs):
+        self.cls = cls
+        self.args = args
+        self.kwargs = kwargs
+        self.required = False
+        self.__dict__.update(kwargs)
+        self.creation_counter = db.Property.creation_counter
+        db.Property.creation_counter += 1
+
+    @property
+    def data_type(self):
+        class FakeDataType(object):
+            @staticmethod
+            def kind():
+                return self.cls.__name__
+        return FakeDataType
+
+def ReferenceProperty(cls, *args, **kwargs):
+    if issubclass(cls, db.Model):
+        cls = db.class_for_kind(cls.__name__)
+        return db.ReferenceProperty(cls, *args, **kwargs)
+    return ReferencePropertyStub(cls, args, kwargs)