goa/db.py
changeset 0 b97547f5f1fa
child 447 0e52d72104a6
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """provide replacement classes for gae db module, so that a gae model can be
       
     2 used as base for a cubicweb application by simply replacing ::
       
     3 
       
     4   from google.appengine.ext import db
       
     5 
       
     6 by
       
     7 
       
     8   from cubicweb.goa import db
       
     9 
       
    10 The db.model api should be fully featured by replacement classes, with the
       
    11 following differences:
       
    12 
       
    13 * all methods returning `google.appengine.ext.db.Model` instance(s) will return
       
    14   `cubicweb.goa.db.Model` instance instead (though you should see almost no
       
    15   difference since those instances have the same api)
       
    16   
       
    17 * class methods returning model instance take a `req` as first argument, unless
       
    18   they are called through an instance, representing the current request
       
    19   (accessible through `self.req` on almost all objects)
       
    20   
       
    21 * XXX no instance.<modelname>_set attributes, use instance.reverse_<attr name>
       
    22       instead
       
    23 * XXX reference property always return a list of objects, not the instance
       
    24 * XXX name/collection_name argument of properties constructor are ignored
       
    25 * XXX ListProperty
       
    26 
       
    27 :organization: Logilab
       
    28 :copyright: 2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
    29 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
    30 """
       
    31 __docformat__ = "restructuredtext en"
       
    32 
       
    33 from datetime import datetime
       
    34 from copy import deepcopy
       
    35 
       
    36 from logilab.common.decorators import cached, iclassmethod
       
    37 
       
    38 from cubicweb import RequestSessionMixIn, Binary, entities
       
    39 from cubicweb.rset import ResultSet
       
    40 from cubicweb.common.entity import metaentity
       
    41 from cubicweb.server.utils import crypt_password
       
    42 from cubicweb.goa import use_mx_for_dates, mx2datetime, MODE
       
    43 from cubicweb.goa.dbinit import init_relations
       
    44 
       
    45 from google.appengine.api.datastore import Get, Put, Key, Entity, Query
       
    46 from google.appengine.api.datastore import NormalizeAndTypeCheck, RunInTransaction
       
    47 from google.appengine.api.datastore_types import Text, Blob
       
    48 from google.appengine.api.datastore_errors import BadKeyError
       
    49 
       
    50 # XXX remove this dependancy
       
    51 from google.appengine.ext import db 
       
    52 
       
    53 
       
    54 def rset_from_objs(req, objs, attrs=('eid',), rql=None, args=None):
       
    55     """return a ResultSet instance for list of objects"""
       
    56     if objs is None:
       
    57         objs = ()
       
    58     elif isinstance(objs, Entity):
       
    59         objs = (objs,)
       
    60     if rql is None:
       
    61         rql = 'Any X'
       
    62     rows = []
       
    63     description = []
       
    64     rset = ResultSet(rows, rql, args, description=description)
       
    65     vreg = req.vreg
       
    66     for i, obj in enumerate(objs):
       
    67         line = []
       
    68         linedescr = []
       
    69         eschema = vreg.schema.eschema(obj.kind())
       
    70         for j, attr in enumerate(attrs):
       
    71             if attr == 'eid':
       
    72                 value = obj.key()
       
    73                 obj.row, obj.col = i, j
       
    74                 descr = eschema.type
       
    75                 value = str(value)
       
    76             else:
       
    77                 value = obj[attr]
       
    78                 descr = str(eschema.destination(attr))
       
    79             line.append(value)
       
    80             linedescr.append(descr)            
       
    81         rows.append(line)
       
    82         description.append(linedescr)
       
    83         for j, attr in enumerate(attrs):
       
    84             if attr == 'eid':
       
    85                 entity = vreg.etype_class(eschema.type)(req, rset, i, j)
       
    86                 rset._get_entity_cache_ = {(i, j): entity}        
       
    87     rset.rowcount = len(rows)
       
    88     req.decorate_rset(rset)    
       
    89     return rset
       
    90 
       
    91 
       
    92 def needrequest(wrapped):
       
    93     def wrapper(cls, *args, **kwargs):
       
    94         req = kwargs.pop('req', None)
       
    95         if req is None and args and isinstance(args[0], RequestSessionMixIn):
       
    96             args = list(args)
       
    97             req = args.pop(0)
       
    98         if req is None:
       
    99             req = getattr(cls, 'req', None)
       
   100             if req is None:
       
   101                 raise Exception('either call this method on an instance or '
       
   102                                 'specify the req argument')
       
   103         return wrapped(cls, req, *args, **kwargs)
       
   104     return iclassmethod(wrapper)
       
   105 
       
   106     
       
   107 class gaedbmetaentity(metaentity):
       
   108     """metaclass for goa.db.Model classes: filter entity / db model part,
       
   109     put aside the db model part for later creation of db model class.
       
   110     """
       
   111     def __new__(mcs, name, bases, classdict):
       
   112         if not 'id' in classdict:
       
   113             classdict['id'] = name
       
   114         entitycls = super(gaedbmetaentity, mcs).__new__(mcs, name, bases, classdict)
       
   115         return entitycls
       
   116 
       
   117 
       
   118 TEST_MODELS = {}
       
   119 
       
   120 def extract_dbmodel(entitycls):
       
   121     if MODE == 'test' and entitycls in TEST_MODELS:
       
   122         dbclassdict = TEST_MODELS[entitycls]
       
   123     else:
       
   124         dbclassdict = {}
       
   125         for attr, value in entitycls.__dict__.items():
       
   126             if isinstance(value, db.Property) or isinstance(value, ReferencePropertyStub):
       
   127                 dbclassdict[attr] = value
       
   128                 # don't remove attr from entitycls, this make tests fail, and it's anyway
       
   129                 # overwritten by descriptor at class initialization time
       
   130                 #delattr(entitycls, attr)
       
   131     if MODE == 'test':
       
   132         TEST_MODELS[entitycls] = dbclassdict
       
   133         dbclassdict = deepcopy(dbclassdict)
       
   134         for propname, prop in TEST_MODELS[entitycls].iteritems():
       
   135             if getattr(prop, 'reference_class', None) is db._SELF_REFERENCE:
       
   136                 dbclassdict[propname].reference_class = db._SELF_REFERENCE
       
   137     return dbclassdict
       
   138 
       
   139 
       
   140 class Model(entities.AnyEntity):
       
   141     id = 'Any'
       
   142     __metaclass__ = gaedbmetaentity
       
   143     
       
   144     row = col = 0
       
   145     
       
   146     @classmethod
       
   147     def __initialize__(cls):
       
   148         super(Model, cls).__initialize__()
       
   149         cls._attributes = frozenset(rschema for rschema in cls.e_schema.subject_relations()
       
   150                                     if rschema.is_final())
       
   151     
       
   152     def __init__(self, *args, **kwargs):
       
   153         # db.Model prototype:
       
   154         #   __init__(self, parent=None, key_name=None, **kw)
       
   155         #
       
   156         # Entity prototype:
       
   157         #   __init__(self, req, rset, row=None, col=0)
       
   158         if args and isinstance(args[0], RequestSessionMixIn) or 'req' in kwargs:
       
   159             super(Model, self).__init__(*args, **kwargs)
       
   160             self._gaeinitargs = None
       
   161         else:
       
   162             super(Model, self).__init__(None, None)
       
   163             # if Model instances are given in kwargs, turn them into db model
       
   164             for key, val in kwargs.iteritems():
       
   165                 if key in self.e_schema.subject_relations() and not self.e_schema.schema[key].is_final():
       
   166                     if isinstance(kwargs, (list, tuple)):
       
   167                         val = [isinstance(x, Model) and x._dbmodel or x for x in val]
       
   168                     elif isinstance(val, Model):
       
   169                         val = val._dbmodel
       
   170                     kwargs[key] = val.key()
       
   171             self._gaeinitargs = (args, kwargs)
       
   172             
       
   173     def __repr__(self):
       
   174         return '<ModelEntity %s %s %s at %s>' % (
       
   175             self.e_schema, self.eid, self.keys(), id(self))
       
   176 
       
   177     __getattribute__ = use_mx_for_dates(entities.AnyEntity.__getattribute__)
       
   178 
       
   179     def _cubicweb_to_datastore(self, attr, value):
       
   180         attr = attr[2:] # remove 's_' / 'o_' prefix
       
   181         if attr in self._attributes:
       
   182             tschema = self.e_schema.destination(attr)
       
   183             if tschema in ('Datetime', 'Date', 'Time'):
       
   184                 value = mx2datetime(value, tschema)
       
   185             elif tschema == 'String':
       
   186                 if len(value) > 500:
       
   187                     value = Text(value)                
       
   188             elif tschema == 'Password':
       
   189                 # if value is a Binary instance, this mean we got it
       
   190                 # from a query result and so it is already encrypted
       
   191                 if isinstance(value, Binary):
       
   192                     value = value.getvalue()
       
   193                 else:
       
   194                     value = crypt_password(value)
       
   195             elif tschema == 'Bytes':
       
   196                 if isinstance(value, Binary):
       
   197                     value = value.getvalue()
       
   198                 value = Blob(value)
       
   199         else:
       
   200             value = Key(value)
       
   201         return value
       
   202 
       
   203     def _to_gae_dict(self, convert=True):
       
   204         gaedict = {}
       
   205         for attr, value in self.iteritems():
       
   206             attr = 's_' + attr
       
   207             if value is not None and convert:
       
   208                 value = self._cubicweb_to_datastore(attr, value)
       
   209             gaedict[attr] = value
       
   210         return gaedict
       
   211     
       
   212     def to_gae_model(self):
       
   213         dbmodel = self._dbmodel
       
   214         dbmodel.update(self._to_gae_dict())
       
   215         return dbmodel
       
   216 
       
   217     @property
       
   218     @cached
       
   219     def _dbmodel(self): 
       
   220         if self.has_eid():
       
   221             assert self._gaeinitargs is None
       
   222             try:
       
   223                 return self.req.datastore_get(self.eid)
       
   224             except AttributeError: # self.req is not a server session
       
   225                 return Get(self.eid)
       
   226         self.set_defaults()
       
   227         values = self._to_gae_dict(convert=False)
       
   228         parent = key_name = _app = None
       
   229         if self._gaeinitargs is not None:
       
   230             args, kwargs = self._gaeinitargs
       
   231             args = list(args)
       
   232             if args:
       
   233                 parent = args.pop(0)
       
   234             if args:
       
   235                 key_name = args.pop(0)
       
   236             if args:
       
   237                 _app = args.pop(0)
       
   238             assert not args
       
   239             if 'parent' in kwargs:
       
   240                 assert parent is None
       
   241                 parent = kwargs.pop('parent')
       
   242             if 'key_name' in kwargs:
       
   243                 assert key_name is None
       
   244                 key_name = kwargs.pop('key_name')
       
   245             if '_app' in kwargs:
       
   246                 assert _app is None
       
   247                 _app = kwargs.pop('_app')
       
   248             
       
   249             for key, value in kwargs.iteritems():
       
   250                 if key in self._attributes:
       
   251                     values['s_'+key] = value
       
   252         else:
       
   253             kwargs = None
       
   254         if key_name is None:
       
   255             key_name = self.db_key_name()
       
   256             if key_name is not None:
       
   257                 key_name = 'key_' + key_name
       
   258         for key, value in values.iteritems():
       
   259             if value is None:
       
   260                 continue
       
   261             values[key] = self._cubicweb_to_datastore(key, value)
       
   262         entity = Entity(self.id, parent, _app, key_name)
       
   263         entity.update(values)
       
   264         init_relations(entity, self.e_schema)
       
   265         return entity
       
   266 
       
   267     def db_key_name(self):
       
   268         """override this method to control datastore key name that should be
       
   269         used at entity creation.
       
   270 
       
   271         Note that if this function return something else than None, the returned
       
   272         value will be prefixed by 'key_' to build the actual key name.
       
   273         """
       
   274         return None
       
   275     
       
   276     def metainformation(self):
       
   277         return {'type': self.id, 'source': {'uri': 'system'}, 'extid': None}
       
   278        
       
   279     def view(self, vid, __registry='views', **kwargs):
       
   280         """shortcut to apply a view on this entity"""
       
   281         return self.vreg.render(__registry, vid, self.req, rset=self.rset,
       
   282                                 row=self.row, col=self.col, **kwargs)
       
   283 
       
   284     @classmethod
       
   285     def _rest_attr_info(cls):
       
   286         mainattr, needcheck = super(Model, cls)._rest_attr_info()
       
   287         if needcheck:
       
   288             return 'eid', False
       
   289         return mainattr, needcheck
       
   290     
       
   291     @use_mx_for_dates
       
   292     def get_value(self, name):
       
   293         try:
       
   294             value = self[name]
       
   295         except KeyError:
       
   296             if not self.has_eid():
       
   297                 return None
       
   298             value = self._dbmodel.get('s_'+name)
       
   299             if value is not None:
       
   300                 if isinstance(value, Text):
       
   301                     value = unicode(value)
       
   302                 elif isinstance(value, Blob):
       
   303                     value = Binary(str(value))
       
   304             self[name] = value
       
   305         return value
       
   306 
       
   307     def has_eid(self):
       
   308         if self.eid is None:
       
   309             return False
       
   310         try:
       
   311             Key(self.eid)
       
   312             return True
       
   313         except BadKeyError:
       
   314             return False
       
   315         
       
   316     def complete(self, skip_bytes=True):
       
   317         pass
       
   318 
       
   319     def unrelated(self, rtype, targettype, role='subject', limit=None,
       
   320                   ordermethod=None):
       
   321         # XXX dumb implementation
       
   322         if limit is not None:
       
   323             objs = Query(str(targettype)).Get(limit)
       
   324         else:
       
   325             objs = Query(str(targettype)).Run()
       
   326         return rset_from_objs(self.req, objs, ('eid',),
       
   327                               'Any X WHERE X is %s' % targettype)
       
   328     
       
   329     def key(self):
       
   330         return Key(self.eid)
       
   331 
       
   332     def put(self, req=None):
       
   333         if req is not None and self.req is None:
       
   334             self.req = req
       
   335         dbmodel = self.to_gae_model()
       
   336         key = Put(dbmodel)
       
   337         self.set_eid(str(key))
       
   338         if self.req is not None and self.rset is None:
       
   339             self.rset = rset_from_objs(self.req, dbmodel, ('eid',),
       
   340                                        'Any X WHERE X eid %(x)s', {'x': self.eid})
       
   341             self.row = self.col = 0
       
   342         return dbmodel
       
   343     
       
   344     @needrequest
       
   345     def get(cls, req, keys):
       
   346         # if check if this is a dict.key call
       
   347         if isinstance(cls, Model) and keys in cls._attributes:
       
   348             return super(Model, cls).get(keys)
       
   349         rset = rset_from_objs(req, Get(keys), ('eid',),
       
   350                               'Any X WHERE X eid IN %(x)s', {'x': keys})
       
   351         return list(rset.entities())
       
   352 
       
   353     @needrequest
       
   354     def get_by_id(cls, req, ids, parent=None):
       
   355         if isinstance(parent, Model):
       
   356             parent = parent.key()
       
   357         ids, multiple = NormalizeAndTypeCheck(ids, (int, long))
       
   358         keys = [Key.from_path(cls.kind(), id, parent=parent)
       
   359                 for id in ids]
       
   360         rset = rset_from_objs(req, Get(keys))
       
   361         return list(rset.entities())
       
   362 
       
   363     @classmethod
       
   364     def get_by_key_name(cls, req, key_names, parent=None):
       
   365         if isinstance(parent, Model):
       
   366             parent = parent.key()
       
   367         key_names, multiple = NormalizeAndTypeCheck(key_names, basestring)
       
   368         keys = [Key.from_path(cls.kind(), name, parent=parent)
       
   369                 for name in key_names]
       
   370         rset = rset_from_objs(req, Get(keys))
       
   371         return list(rset.entities())
       
   372 
       
   373     @classmethod
       
   374     def get_or_insert(cls, req, key_name, **kwds):
       
   375         def txn():
       
   376             entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
       
   377             if entity is None:
       
   378                 entity = cls(key_name=key_name, **kwds)
       
   379                 entity.put()
       
   380             return entity
       
   381         return RunInTransaction(txn)
       
   382 
       
   383     @classmethod
       
   384     def all(cls, req):
       
   385         rset = rset_from_objs(req, Query(cls.id).Run())
       
   386         return list(rset.entities())
       
   387 
       
   388     @classmethod
       
   389     def gql(cls, req, query_string, *args, **kwds):
       
   390         raise NotImplementedError('use rql')
       
   391 
       
   392     @classmethod
       
   393     def kind(cls):
       
   394         return self.id
       
   395 
       
   396     @classmethod
       
   397     def properties(cls):
       
   398         raise NotImplementedError('use eschema')
       
   399 
       
   400     def dynamic_properties(self):
       
   401         raise NotImplementedError('use eschema')
       
   402         
       
   403     def is_saved(self):
       
   404         return self.has_eid()
       
   405 
       
   406     def parent(self):
       
   407         parent = self._dbmodel.parent()
       
   408         if not parent is None:
       
   409             rset = rset_from_objs(self.req, (parent,), ('eid',),
       
   410                                   'Any X WHERE X eid %(x)s', {'x': parent.key()})
       
   411             parent = rset.get_entity(0, 0)
       
   412         return parent
       
   413 
       
   414     def parent_key(self):
       
   415         return self.parent().key()
       
   416 
       
   417     def to_xml(self):
       
   418         return self._dbmodel.ToXml()
       
   419 
       
   420 # hijack AnyEntity class
       
   421 entities.AnyEntity = Model
       
   422 
       
   423 BooleanProperty = db.BooleanProperty
       
   424 URLProperty = db.URLProperty
       
   425 DateProperty = db.DateProperty
       
   426 DateTimeProperty = db.DateTimeProperty
       
   427 TimeProperty = db.TimeProperty
       
   428 StringProperty = db.StringProperty
       
   429 TextProperty = db.TextProperty
       
   430 BlobProperty = db.BlobProperty
       
   431 IntegerProperty = db.IntegerProperty
       
   432 FloatProperty = db.FloatProperty
       
   433 ListProperty = db.ListProperty
       
   434 SelfReferenceProperty = db.SelfReferenceProperty 
       
   435 UserProperty = db.UserProperty
       
   436 
       
   437 
       
   438 class ReferencePropertyStub(object):
       
   439     def __init__(self, cls, args, kwargs):
       
   440         self.cls = cls
       
   441         self.args = args
       
   442         self.kwargs = kwargs
       
   443         self.required = False
       
   444         self.__dict__.update(kwargs)
       
   445         self.creation_counter = db.Property.creation_counter
       
   446         db.Property.creation_counter += 1
       
   447 
       
   448     @property
       
   449     def data_type(self):
       
   450         class FakeDataType(object):
       
   451             @staticmethod
       
   452             def kind():
       
   453                 return self.cls.__name__
       
   454         return FakeDataType
       
   455 
       
   456 def ReferenceProperty(cls, *args, **kwargs):
       
   457     if issubclass(cls, db.Model):
       
   458         cls = db.class_for_kind(cls.__name__)
       
   459         return db.ReferenceProperty(cls, *args, **kwargs)
       
   460     return ReferencePropertyStub(cls, args, kwargs)