Package pyamf :: Package adapters :: Module _google_appengine_ext_db

[hide private]
[frames] | no frames]

# Source Code for Module pyamf.adapters._google_appengine_ext_db

      1  # Copyright (c) 2007-2009 The PyAMF Project.
      2  # See LICENSE for details.
      3
      4  """
      5  Google App Engine adapter module.
      6
      7  Sets up basic type mapping and class mappings for using the Datastore API
      8  in Google App Engine.
      9
     10  @see: U{Datastore API on Google App Engine (external)
     11  <http://code.google.com/appengine/docs/datastore>}
     12
     13  @since: 0.3.1
     14  """
     15
     16  from google.appengine.ext import db
     17  import datetime
     18
     19  import pyamf
     20  from pyamf.util import imports
     21  from pyamf.adapters import util
     22
     23 -class ModelStub(object):
     24      """
     25      This class represents a L{db.Model} or L{db.Expando} class as the typed
     26      object is being read from the AMF stream. Once the attributes have been
     27      read from the stream and through the magic of Python, the instance of this
     28      class will be converted into the correct type.
     29
     30      @ivar klass: The referenced class either L{db.Model} or L{db.Expando}.
     31          This is used so we can proxy some of the method calls during decoding.
     32      @type klass: L{db.Model} or L{db.Expando}
     33      @see: L{DataStoreClassAlias.applyAttributes}
     34      """
     35
     36 -    def __init__(self, klass):
     37          self.klass = klass
     38
     39 -    def properties(self):
     40          return self.klass.properties()
     41
     42 -    def dynamic_properties(self):
     43          return []
     44
     45 -class GAEReferenceCollection(dict):
     46      """
     47      This helper class holds a dict of klass to key/objects loaded from the
     48      Datastore.
     49
     50      @since: 0.4.1
     51      """
     52
     53 -    def _getClass(self, klass):
     54          if not issubclass(klass, (db.Model, db.Expando)):
     55              raise TypeError('expected db.Model/db.Expando class, got %s' % (klass,))
     56
     57          if klass not in self.keys():
     58              self[klass] = {}
     59
     60          return self[klass]
     61
     62 -    def getClassKey(self, klass, key):
     63          """
     64          Return an instance based on klass/key.
     65
     66          If an instance cannot be found then L{KeyError} is raised.
     67
     68          @param klass: The class of the instance.
     69          @param key: The key of the instance.
     70          @return: The instance linked to the C{klass}/C{key}.
     71          @rtype: Instance of L{klass}.
     72          """
     73          if not isinstance(key, basestring):
     74              raise TypeError('basestring type expected for test, got %s' % (repr(key),))
     75
     76          d = self._getClass(klass)
     77
     78          return d[key]
     79
     80 -    def addClassKey(self, klass, key, obj):
     81          """
     82          Adds an object to the collection, based on klass and key.
     83
     84          @param klass: The class of the object.
     85          @param key: The datastore key of the object.
     86          @param obj: The loaded instance from the datastore.
     87          """
     88          if not isinstance(key, basestring):
     89              raise TypeError('basestring type expected for test, got %s' % (repr(key),))
     90
     91          d = self._getClass(klass)
     92
     93          d[key] = obj
     94
     95 -class DataStoreClassAlias(pyamf.ClassAlias):
     96      """
     97      This class contains all the business logic to interact with Google's
     98      Datastore API's. Any L{db.Model} or L{db.Expando} classes will use this
     99      class alias for encoding/decoding.
    100
    101      We also add a number of indexes to the encoder context to aggressively
    102      decrease the number of Datastore API's that we need to complete.
    103      """
    104
    105      # The name of the attribute used to represent the key
    106      KEY_ATTR = '_key'
    107
    108 -    def getAttrs(self, obj, codec=None):
    109          """
    110          @since: 0.4
    111          """
    112          if not hasattr(self, 'static_attrs'):
    113              self.static_attrs = obj.properties().keys()
    114              self.static_attrs.insert(0, DataStoreClassAlias.KEY_ATTR)
    115
    116              for k, v in self.klass.__dict__.iteritems():
    117                  if isinstance(v, property):
    118                      self.static_attrs.append(k)
    119
    120          dynamic_attrs = obj.dynamic_properties()
    121
    122          for k, v in obj.__dict__.iteritems():
    123              if k.startswith('_'):
    124                  continue
    125
    126              if k not in self.static_attrs:
    127                  dynamic_attrs.append(k)
    128
    129          return self.static_attrs, dynamic_attrs
    130
    131 -    def getAttributes(self, obj, codec=None):
    132          static_attrs = {}
    133          dynamic_attrs = {}
    134          static_attrs_names, dynamic_attrs_names = self.getAttrs(obj, codec=codec)
    135          gae_objects = None
    136
    137          if codec is not None:
    138              gae_objects = getGAEObjects(codec.context)
    139
    140          key = str(obj.key()) if obj.is_saved() else None
    141
    142          static_attrs[DataStoreClassAlias.KEY_ATTR] = key
    143          kd = self.klass.__dict__
    144
    145          for a in static_attrs_names:
    146              if a == DataStoreClassAlias.KEY_ATTR:
    147                  continue
    148
    149              try:
    150                  prop = kd[a]
    151              except KeyError:
    152                  prop = None
    153
    154              if isinstance(prop, db.ReferenceProperty):
    155                  if gae_objects is not None:
    156                      klass = prop.reference_class
    157                      key = prop.get_value_for_datastore(obj)
    158
    159                      if key is not None:
    160                          key = str(key)
    161
    162                          try:
    163                              static_attrs[a] = gae_objects.getClassKey(klass, key)
    164                          except KeyError:
    165                              ref_obj = getattr(obj, a)
    166                              gae_objects.addClassKey(klass, key, ref_obj)
    167                              static_attrs[a] = ref_obj
    168
    169                          continue
    170
    171              static_attrs[a] = getattr(obj, a)
    172
    173          for a in dynamic_attrs_names:
    174              dynamic_attrs[a] = getattr(obj, a)
    175
    176          return static_attrs, dynamic_attrs
    177
    178 -    def createInstance(self, codec=None):
    179          return ModelStub(self.klass)
    180
    181 -    def applyAttributes(self, obj, attrs, codec=None):
    182          new_obj = None
    183
    184          # attempt to load the object from the datastore if KEY_ATTR exists.
    185          if DataStoreClassAlias.KEY_ATTR in attrs.keys():
    186              if attrs[DataStoreClassAlias.KEY_ATTR] is not None:
    187                  key = attrs[DataStoreClassAlias.KEY_ATTR]
    188                  new_obj = loadInstanceFromDatastore(self.klass, key, codec)
    189
    190              del attrs[DataStoreClassAlias.KEY_ATTR]
    191
    192          properties = self.klass.properties()
    193          p_keys = properties.keys()
    194          apply_init = True
    195          sa, da = self.getAttrs(obj)
    196
    197          # clean up the stub
    198          if isinstance(obj, ModelStub) and hasattr(obj, 'klass'):
    199              del obj.klass
    200
    201          if new_obj is not None:
    202              obj.__dict__ = new_obj.__dict__.copy()
    203
    204          obj.__class__ = self.klass
    205          kd = self.klass.__dict__
    206
    207          for k, v in attrs.copy().iteritems():
    208              if k in p_keys:
    209                  prop = properties[k]
    210
    211                  if k not in sa:
    212                      del attrs[k]
    213                      continue
    214
    215                  if isinstance(prop, db.ListProperty) and v is None:
    216                      attrs[k] = []
    217                  elif isinstance(v, datetime.datetime):
    218                      # Date/Time Property fields expect specific types of data
    219                      # whereas PyAMF only decodes into datetime.datetime objects.
    220                      if isinstance(prop, db.DateProperty):
    221                          attrs[k] = v.date()
    222                      elif isinstance(prop, db.TimeProperty):
    223                          attrs[k] = v.time()
    224
    225                  if new_obj is None and isinstance(v, ModelStub) and (k in kd and isinstance(kd[k], db.ReferenceProperty) and kd[k].required is True):
    226                      apply_init = False
    227                      del attrs[k]
    228                      continue
    229              elif k in kd:
    230                  kp = kd[k]
    231
    232                  # check if the property is a defined as a collection_name.
    233                  # These types of properties are read-only and the datastore
    234                  # freaks out if you attempt to meddle with it. We delete the
    235                  # attribute entirely ..
    236                  if isinstance(kp, db._ReverseReferenceProperty):
    237                      del attrs[k]
    238                  elif isinstance(kp, property):
    239                      if kp.fset is None:
    240                          del attrs[k]
    241
    242          # If the object does not exist in the datastore, we must fire the
    243          # class constructor. This sets internal attributes that pyamf has
    244          # no business messing with ..
    245          if new_obj is None and apply_init is True:
    246              obj.__init__(**attrs)
    247
    248          for k, v in attrs.iteritems():
    249              setattr(obj, k, v)
    250
    251 -def getGAEObjects(context):
    252      """
    253      Returns a reference to the C{gae_objects} on the context. If it doesn't
    254      exist then it is created.
    255
    256      @param context: The context to load the C{gae_objects} index from.
    257      @type context: Instance of L{pyamf.BaseContext}
    258      @return: The C{gae_objects} index reference.
    259      @rtype: Instance of L{GAEReferenceCollection}
    260      @since: 0.4.1
    261      """
    262      if not hasattr(context, 'gae_objects'):
    263          context.gae_objects = GAEReferenceCollection()
    264
    265      return context.gae_objects
    266
    267 -def loadInstanceFromDatastore(klass, key, codec=None):
    268      """
    269      Attempt to load an instance from the datastore, based on C{klass}
    270      and C{key}. We create an index on the codec's context (if it exists)
    271      so we can check that first before accessing the datastore.
    272
    273      @param klass: The class that will be loaded from the datastore.
    274      @type klass: Sub-class of L{db.Model} or L{db.Expando}
    275      @param key: The key which is used to uniquely identify the instance in the
    276          datastore.
    277      @type key: C{str}
    278      @param codec: The codec to reference the C{gae_objects} index. If
    279          supplied,The codec must have have a context attribute.
    280      @type codec: Instance of L{pyamf.BaseEncoder} or L{pyamf.BaseDecoder}
    281      @return: The loaded instance from the datastore.
    282      @rtype: Instance of C{klass}.
    283      @since: 0.4.1
    284      """
    285      if not issubclass(klass, (db.Model, db.Expando)):
    286          raise TypeError('expected db.Model/db.Expando class, got %s' % (klass,))
    287
    288      if not isinstance(key, basestring):
    289          raise TypeError('string expected for key, got %s', (repr(key),))
    290
    291      key = str(key)
    292
    293      if codec is None:
    294          return klass.get(key)
    295
    296      gae_objects = getGAEObjects(codec.context)
    297
    298      try:
    299          return gae_objects.getClassKey(klass, key)
    300      except KeyError:
    301          pass
    302
    303      obj = klass.get(key)
    304      gae_objects.addClassKey(klass, key, obj)
    305
    306      return obj
    307
    308 -def writeGAEObject(self, object, *args, **kwargs):
    309      """
    310      The GAE Datastore creates new instances of objects for each get request.
    311      This is a problem for PyAMF as it uses the id(obj) of the object to do
    312      reference checking.
    313
    314      We could just ignore the problem, but the objects are conceptually the
    315      same so the effort should be made to attempt to resolve references for a
    316      given object graph.
    317
    318      We create a new map on the encoder context object which contains a dict of
    319      C{object.__class__: {key1: object1, key2: object2, .., keyn: objectn}}. We
    320      use the datastore key to do the reference checking.
    321
    322      @since: 0.4.1
    323      """
    324      if not (isinstance(object, db.Model) and object.is_saved()):
    325          self.writeNonGAEObject(object, *args, **kwargs)
    326
    327          return
    328
    329      context = self.context
    330      kls = object.__class__
    331      s = str(object.key())
    332
    333      gae_objects = getGAEObjects(context)
    334      referenced_object = None
    335
    336      try:
    337          referenced_object = gae_objects.getClassKey(kls, s)
    338      except KeyError:
    339          gae_objects.addClassKey(kls, s, object)
    340          self.writeNonGAEObject(object, *args, **kwargs)
    341
    342          return
    343
    344      self.writeNonGAEObject(referenced_object, *args, **kwargs)
    345
    346 -def install_gae_reference_model_hook(mod):
    347      """
    348      Called when L{pyamf.amf0} or L{pyamf.amf3} are imported. Attaches the
    349      L{writeGAEObject} method to the C{Encoder} class in that module.
    350
    351      @param mod: The module imported.
    352      @since: 0.4.1
    353      """
    354      if not hasattr(mod.Encoder, 'writeNonGAEObject'):
    355          mod.Encoder.writeNonGAEObject = mod.Encoder.writeObject
    356          mod.Encoder.writeObject = writeGAEObject
    357
    358  # initialise the module here: hook into pyamf
    359
    360  pyamf.add_type(db.Query, util.to_list)
    361  pyamf.register_alias_type(DataStoreClassAlias, db.Model, db.Expando)
    362
    363  # hook the L{writeGAEObject} method to the Encoder class on import
    364  imports.whenImported('pyamf.amf0', install_gae_reference_model_hook)
    365  imports.whenImported('pyamf.amf3', install_gae_reference_model_hook)
    366