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