1
2
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
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
40
43
46
47
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
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
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
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
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
110 KEY_ATTR = '_key'
111
117
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
137
138
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
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
195
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
208 if key and codec:
209 new_obj = loadInstanceFromDatastore(self.klass, key, codec)
210
211
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
232
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
243
244
245 if new_obj is None and apply_init is True:
246 obj.__init__(**attrs)
247
248 return attrs
249
250
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
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
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
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
359
360 pyamf.add_type(db.Query, util.to_list)
361 pyamf.register_alias_type(DataStoreClassAlias, db.Model, db.Expando)
362
363
364 imports.when_imported('pyamf.amf0', install_gae_reference_model_hook)
365 imports.when_imported('pyamf.amf3', install_gae_reference_model_hook)
366