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 import datetime
18
19 import pyamf
20 from pyamf.util import imports
21 from pyamf.adapters import util
22
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
38
41
44
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
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
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
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
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
106 KEY_ATTR = '_key'
107
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
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
180
182 new_obj = None
183
184
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
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
219
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
233
234
235
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
243
244
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
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
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
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
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.whenImported('pyamf.amf0', install_gae_reference_model_hook)
365 imports.whenImported('pyamf.amf3', install_gae_reference_model_hook)
366