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.txt 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  from google.appengine.ext.db import polymodel
     18  import datetime
     19
     20  import pyamf
     21  from pyamf.util import imports
     22  from pyamf.adapters import util
     23
     24
     25 -class ModelStub(object):
     26      """
     27      This class represents a L{db.Model} or L{db.Expando} class as the typed
     28      object is being read from the AMF stream. Once the attributes have been
     29      read from the stream and through the magic of Python, the instance of this
     30      class will be converted into the correct type.
     31
     32      @ivar klass: The referenced class either L{db.Model} or L{db.Expando}.
     33          This is used so we can proxy some of the method calls during decoding.
     34      @type klass: L{db.Model} or L{db.Expando}
     35      @see: L{DataStoreClassAlias.applyAttributes}
     36      """
     37
     38 -    def __init__(self, klass):
     39          self.klass = klass
     40
     41 -    def properties(self):
     42          return self.klass.properties()
     43
     44 -    def dynamic_properties(self):
     45          return []
     46
     47
     48 -class GAEReferenceCollection(dict):
     49      """
     50      This helper class holds a dict of klass to key/objects loaded from the
     51      Datastore.
     52
     53      @since: 0.4.1
     54      """
     55
     56 -    def _getClass(self, klass):
     57          if not issubclass(klass, (db.Model, db.Expando)):
     58              raise TypeError('expected db.Model/db.Expando class, got %s' % (klass,))
     59
     60          if klass not in self.keys():
     61              self[klass] = {}
     62
     63          return self[klass]
     64
     65 -    def getClassKey(self, klass, key):
     66          """
     67          Return an instance based on klass/key.
     68
     69          If an instance cannot be found then L{KeyError} is raised.
     70
     71          @param klass: The class of the instance.
     72          @param key: The key of the instance.
     73          @return: The instance linked to the C{klass}/C{key}.
     74          @rtype: Instance of L{klass}.
     75          """
     76          if not isinstance(key, basestring):
     77              raise TypeError('basestring type expected for test, got %s' % (repr(key),))
     78
     79          d = self._getClass(klass)
     80
     81          return d[key]
     82
     83 -    def addClassKey(self, klass, key, obj):
     84          """
     85          Adds an object to the collection, based on klass and key.
     86
     87          @param klass: The class of the object.
     88          @param key: The datastore key of the object.
     89          @param obj: The loaded instance from the datastore.
     90          """
     91          if not isinstance(key, basestring):
     92              raise TypeError('basestring type expected for test, got %s' % (repr(key),))
     93
     94          d = self._getClass(klass)
     95
     96          d[key] = obj
     97
     98
     99 -class DataStoreClassAlias(pyamf.ClassAlias):
    100      """
    101      This class contains all the business logic to interact with Google's
    102      Datastore API's. Any L{db.Model} or L{db.Expando} classes will use this
    103      class alias for encoding/decoding.
    104
    105      We also add a number of indexes to the encoder context to aggressively
    106      decrease the number of Datastore API's that we need to complete.
    107      """
    108
    109      # The name of the attribute used to represent the key
    110      KEY_ATTR = '_key'
    111
    112 -    def _compile_base_class(self, klass):
    113          if klass in (db.Model, polymodel.PolyModel):
    114              return
    115
    116          pyamf.ClassAlias._compile_base_class(self, klass)
    117
    118 -    def getCustomProperties(self):
    119          props = [self.KEY_ATTR]
    120          self.reference_properties = {}
    121          self.properties = {}
    122          reverse_props = []
    123
    124          for name, prop in self.klass.properties().iteritems():
    125              self.properties[name] = prop
    126
    127              props.append(name)
    128
    129              if isinstance(prop, db.ReferenceProperty):
    130                  self.reference_properties[name] = prop
    131
    132          if issubclass(self.klass, polymodel.PolyModel):
    133              del self.properties['_class']
    134              props.remove('_class')
    135
    136          # check if the property is a defined as a collection_name. These types
    137          # of properties are read-only and the datastore freaks out if you
    138          # attempt to meddle with it. We delete the attribute entirely ..
    139          for name, value in self.klass.__dict__.iteritems():
    140              if isinstance(value, db._ReverseReferenceProperty):
    141                  reverse_props.append(name)
    142
    143          self.static_attrs.update(props)
    144          self.encodable_properties.update(self.properties.keys())
    145          self.decodable_properties.update(self.properties.keys())
    146          self.readonly_attrs.update(reverse_props)
    147
    148          if not self.reference_properties:
    149              self.reference_properties = None
    150
    151          if not self.properties:
    152              self.properties = None
    153
    154 -    def getEncodableAttributes(self, obj, codec=None):
    155          sa, da = pyamf.ClassAlias.getEncodableAttributes(self, obj, codec=codec)
    156
    157          sa[self.KEY_ATTR] = str(obj.key()) if obj.is_saved() else None
    158          gae_objects = getGAEObjects(codec.context) if codec else None
    159
    160          if self.reference_properties and gae_objects:
    161              for name, prop in self.reference_properties.iteritems():
    162                  klass = prop.reference_class
    163                  key = prop.get_value_for_datastore(obj)
    164
    165                  if not key:
    166                      continue
    167
    168                  key = str(key)
    169
    170                  try:
    171                      sa[name] = gae_objects.getClassKey(klass, key)
    172                  except KeyError:
    173                      ref_obj = getattr(obj, name)
    174                      gae_objects.addClassKey(klass, key, ref_obj)
    175                      sa[name] = ref_obj
    176
    177          if da:
    178              for k, v in da.copy().iteritems():
    179                  if k.startswith('_'):
    180                      del da[k]
    181
    182          if not da:
    183              da = {}
    184
    185          for attr in obj.dynamic_properties():
    186              da[attr] = getattr(obj, attr)
    187
    188          if not da:
    189              da = None
    190
    191          return sa, da
    192
    193 -    def createInstance(self, codec=None):
    194          return ModelStub(self.klass)
    195
    196 -    def getDecodableAttributes(self, obj, attrs, codec=None):
    197          try:
    198              key = attrs[self.KEY_ATTR]
    199          except KeyError:
    200              key = attrs[self.KEY_ATTR] = None
    201
    202          attrs = pyamf.ClassAlias.getDecodableAttributes(self, obj, attrs, codec=codec)
    203
    204          del attrs[self.KEY_ATTR]
    205          new_obj = None
    206
    207          # attempt to load the object from the datastore if KEY_ATTR exists.
    208          if key and codec:
    209              new_obj = loadInstanceFromDatastore(self.klass, key, codec)
    210
    211          # clean up the stub
    212          if isinstance(obj, ModelStub) and hasattr(obj, 'klass'):
    213              del obj.klass
    214
    215          if new_obj:
    216              obj.__dict__ = new_obj.__dict__.copy()
    217
    218          obj.__class__ = self.klass
    219          apply_init = True
    220
    221          if self.properties:
    222              for k in [k for k in attrs.keys() if k in self.properties.keys()]:
    223                  prop = self.properties[k]
    224                  v = attrs[k]
    225
    226                  if isinstance(prop, db.FloatProperty) and isinstance(v, (int, long)):
    227                      attrs[k] = float(v)
    228                  elif isinstance(prop, db.ListProperty) and v is None:
    229                      attrs[k] = []
    230                  elif isinstance(v, datetime.datetime):
    231                      # Date/Time Property fields expect specific types of data
    232                      # whereas PyAMF only decodes into datetime.datetime objects.
    233                      if isinstance(prop, db.DateProperty):
    234                          attrs[k] = v.date()
    235                      elif isinstance(prop, db.TimeProperty):
    236                          attrs[k] = v.time()
    237
    238                  if new_obj is None and isinstance(v, ModelStub) and prop.required and k in self.reference_properties:
    239                      apply_init = False
    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          return attrs
    249
    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
    268 -def loadInstanceFromDatastore(klass, key, codec=None):
    269      """
    270      Attempt to load an instance from the datastore, based on C{klass}
    271      and C{key}. We create an index on the codec's context (if it exists)
    272      so we can check that first before accessing the datastore.
    273
    274      @param klass: The class that will be loaded from the datastore.
    275      @type klass: Sub-class of L{db.Model} or L{db.Expando}
    276      @param key: The key which is used to uniquely identify the instance in the
    277          datastore.
    278      @type key: C{str}
    279      @param codec: The codec to reference the C{gae_objects} index. If
    280          supplied,The codec must have have a context attribute.
    281      @type codec: Instance of L{pyamf.BaseEncoder} or L{pyamf.BaseDecoder}
    282      @return: The loaded instance from the datastore.
    283      @rtype: Instance of C{klass}.
    284      @since: 0.4.1
    285      """
    286      if not issubclass(klass, (db.Model, db.Expando)):
    287          raise TypeError('expected db.Model/db.Expando class, got %s' % (klass,))
    288
    289      if not isinstance(key, basestring):
    290          raise TypeError('string expected for key, got %s', (repr(key),))
    291
    292      key = str(key)
    293
    294      if codec is None:
    295          return klass.get(key)
    296
    297      gae_objects = getGAEObjects(codec.context)
    298
    299      try:
    300          return gae_objects.getClassKey(klass, key)
    301      except KeyError:
    302          pass
    303
    304      obj = klass.get(key)
    305      gae_objects.addClassKey(klass, key, obj)
    306
    307      return obj
    308
    309
    310 -def writeGAEObject(self, object, *args, **kwargs):
    311      """
    312      The GAE Datastore creates new instances of objects for each get request.
    313      This is a problem for PyAMF as it uses the id(obj) of the object to do
    314      reference checking.
    315
    316      We could just ignore the problem, but the objects are conceptually the
    317      same so the effort should be made to attempt to resolve references for a
    318      given object graph.
    319
    320      We create a new map on the encoder context object which contains a dict of
    321      C{object.__class__: {key1: object1, key2: object2, .., keyn: objectn}}. We
    322      use the datastore key to do the reference checking.
    323
    324      @since: 0.4.1
    325      """
    326      if not (isinstance(object, db.Model) and object.is_saved()):
    327          self.writeNonGAEObject(object, *args, **kwargs)
    328
    329          return
    330
    331      context = self.context
    332      kls = object.__class__
    333      s = str(object.key())
    334
    335      gae_objects = getGAEObjects(context)
    336
    337      try:
    338          referenced_object = gae_objects.getClassKey(kls, s)
    339      except KeyError:
    340          referenced_object = object
    341          gae_objects.addClassKey(kls, s, object)
    342
    343      self.writeNonGAEObject(referenced_object, *args, **kwargs)
    344
    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.when_imported('pyamf.amf0', install_gae_reference_model_hook)
    365  imports.when_imported('pyamf.amf3', install_gae_reference_model_hook)
    366