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