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