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