Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1)

Side by Side Diff: third_party/gsutil/boto/sdb/db/manager/sdbmanager.py

Issue 12042069: Scripts to download files from google storage based on sha1 sums (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: Removed gsutil/tests and gsutil/docs Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright (c) 2006,2007,2008 Mitch Garnaat http://garnaat.org/
2 # Copyright (c) 2010 Chris Moyer http://coredumped.org/
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining a
5 # copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish, dis-
8 # tribute, sublicense, and/or sell copies of the Software, and to permit
9 # persons to whom the Software is furnished to do so, subject to the fol-
10 # lowing conditions:
11 #
12 # The above copyright notice and this permission notice shall be included
13 # in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 # IN THE SOFTWARE.
22 import boto
23 import re
24 from boto.utils import find_class
25 import uuid
26 from boto.sdb.db.key import Key
27 from boto.sdb.db.model import Model
28 from boto.sdb.db.blob import Blob
29 from boto.sdb.db.property import ListProperty, MapProperty
30 from datetime import datetime, date, time
31 from boto.exception import SDBPersistenceError, S3ResponseError
32
33 ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
34
35
36 class TimeDecodeError(Exception):
37 pass
38
39
40 class SDBConverter(object):
41 """
42 Responsible for converting base Python types to format compatible
43 with underlying database. For SimpleDB, that means everything
44 needs to be converted to a string when stored in SimpleDB and from
45 a string when retrieved.
46
47 To convert a value, pass it to the encode or decode method. The
48 encode method will take a Python native value and convert to DB
49 format. The decode method will take a DB format value and convert
50 it to Python native format. To find the appropriate method to
51 call, the generic encode/decode methods will look for the
52 type-specific method by searching for a method
53 called"encode_<type name>" or "decode_<type name>".
54 """
55 def __init__(self, manager):
56 self.manager = manager
57 self.type_map = {bool: (self.encode_bool, self.decode_bool),
58 int: (self.encode_int, self.decode_int),
59 long: (self.encode_long, self.decode_long),
60 float: (self.encode_float, self.decode_float),
61 Model: (self.encode_reference, self.decode_reference),
62 Key: (self.encode_reference, self.decode_reference),
63 datetime: (self.encode_datetime, self.decode_datetime),
64 date: (self.encode_date, self.decode_date),
65 time: (self.encode_time, self.decode_time),
66 Blob: (self.encode_blob, self.decode_blob),
67 str: (self.encode_string, self.decode_string),
68 }
69
70 def encode(self, item_type, value):
71 try:
72 if Model in item_type.mro():
73 item_type = Model
74 except:
75 pass
76 if item_type in self.type_map:
77 encode = self.type_map[item_type][0]
78 return encode(value)
79 return value
80
81 def decode(self, item_type, value):
82 if item_type in self.type_map:
83 decode = self.type_map[item_type][1]
84 return decode(value)
85 return value
86
87 def encode_list(self, prop, value):
88 if value in (None, []):
89 return []
90 if not isinstance(value, list):
91 # This is a little trick to avoid encoding when it's just a single v alue,
92 # since that most likely means it's from a query
93 item_type = getattr(prop, "item_type")
94 return self.encode(item_type, value)
95 # Just enumerate(value) won't work here because
96 # we need to add in some zero padding
97 # We support lists up to 1,000 attributes, since
98 # SDB technically only supports 1024 attributes anyway
99 values = {}
100 for k, v in enumerate(value):
101 values["%03d" % k] = v
102 return self.encode_map(prop, values)
103
104 def encode_map(self, prop, value):
105 import urllib
106 if value == None:
107 return None
108 if not isinstance(value, dict):
109 raise ValueError('Expected a dict value, got %s' % type(value))
110 new_value = []
111 for key in value:
112 item_type = getattr(prop, "item_type")
113 if Model in item_type.mro():
114 item_type = Model
115 encoded_value = self.encode(item_type, value[key])
116 if encoded_value != None:
117 new_value.append('%s:%s' % (urllib.quote(key), encoded_value))
118 return new_value
119
120 def encode_prop(self, prop, value):
121 if isinstance(prop, ListProperty):
122 return self.encode_list(prop, value)
123 elif isinstance(prop, MapProperty):
124 return self.encode_map(prop, value)
125 else:
126 return self.encode(prop.data_type, value)
127
128 def decode_list(self, prop, value):
129 if not isinstance(value, list):
130 value = [value]
131 if hasattr(prop, 'item_type'):
132 item_type = getattr(prop, "item_type")
133 dec_val = {}
134 for val in value:
135 if val != None:
136 k, v = self.decode_map_element(item_type, val)
137 try:
138 k = int(k)
139 except:
140 k = v
141 dec_val[k] = v
142 value = dec_val.values()
143 return value
144
145 def decode_map(self, prop, value):
146 if not isinstance(value, list):
147 value = [value]
148 ret_value = {}
149 item_type = getattr(prop, "item_type")
150 for val in value:
151 k, v = self.decode_map_element(item_type, val)
152 ret_value[k] = v
153 return ret_value
154
155 def decode_map_element(self, item_type, value):
156 """Decode a single element for a map"""
157 import urllib
158 key = value
159 if ":" in value:
160 key, value = value.split(':', 1)
161 key = urllib.unquote(key)
162 if Model in item_type.mro():
163 value = item_type(id=value)
164 else:
165 value = self.decode(item_type, value)
166 return (key, value)
167
168 def decode_prop(self, prop, value):
169 if isinstance(prop, ListProperty):
170 return self.decode_list(prop, value)
171 elif isinstance(prop, MapProperty):
172 return self.decode_map(prop, value)
173 else:
174 return self.decode(prop.data_type, value)
175
176 def encode_int(self, value):
177 value = int(value)
178 value += 2147483648
179 return '%010d' % value
180
181 def decode_int(self, value):
182 try:
183 value = int(value)
184 except:
185 boto.log.error("Error, %s is not an integer" % value)
186 value = 0
187 value = int(value)
188 value -= 2147483648
189 return int(value)
190
191 def encode_long(self, value):
192 value = long(value)
193 value += 9223372036854775808
194 return '%020d' % value
195
196 def decode_long(self, value):
197 value = long(value)
198 value -= 9223372036854775808
199 return value
200
201 def encode_bool(self, value):
202 if value == True or str(value).lower() in ("true", "yes"):
203 return 'true'
204 else:
205 return 'false'
206
207 def decode_bool(self, value):
208 if value.lower() == 'true':
209 return True
210 else:
211 return False
212
213 def encode_float(self, value):
214 """
215 See http://tools.ietf.org/html/draft-wood-ldapext-float-00.
216 """
217 s = '%e' % value
218 l = s.split('e')
219 mantissa = l[0].ljust(18, '0')
220 exponent = l[1]
221 if value == 0.0:
222 case = '3'
223 exponent = '000'
224 elif mantissa[0] != '-' and exponent[0] == '+':
225 case = '5'
226 exponent = exponent[1:].rjust(3, '0')
227 elif mantissa[0] != '-' and exponent[0] == '-':
228 case = '4'
229 exponent = 999 + int(exponent)
230 exponent = '%03d' % exponent
231 elif mantissa[0] == '-' and exponent[0] == '-':
232 case = '2'
233 mantissa = '%f' % (10 + float(mantissa))
234 mantissa = mantissa.ljust(18, '0')
235 exponent = exponent[1:].rjust(3, '0')
236 else:
237 case = '1'
238 mantissa = '%f' % (10 + float(mantissa))
239 mantissa = mantissa.ljust(18, '0')
240 exponent = 999 - int(exponent)
241 exponent = '%03d' % exponent
242 return '%s %s %s' % (case, exponent, mantissa)
243
244 def decode_float(self, value):
245 case = value[0]
246 exponent = value[2:5]
247 mantissa = value[6:]
248 if case == '3':
249 return 0.0
250 elif case == '5':
251 pass
252 elif case == '4':
253 exponent = '%03d' % (int(exponent) - 999)
254 elif case == '2':
255 mantissa = '%f' % (float(mantissa) - 10)
256 exponent = '-' + exponent
257 else:
258 mantissa = '%f' % (float(mantissa) - 10)
259 exponent = '%03d' % abs((int(exponent) - 999))
260 return float(mantissa + 'e' + exponent)
261
262 def encode_datetime(self, value):
263 if isinstance(value, str) or isinstance(value, unicode):
264 return value
265 if isinstance(value, datetime):
266 return value.strftime(ISO8601)
267 else:
268 return value.isoformat()
269
270 def decode_datetime(self, value):
271 """Handles both Dates and DateTime objects"""
272 if value is None:
273 return value
274 try:
275 if "T" in value:
276 if "." in value:
277 # Handle true "isoformat()" dates, which may have a microsec ond on at the end of them
278 return datetime.strptime(value.split(".")[0], "%Y-%m-%dT%H:% M:%S")
279 else:
280 return datetime.strptime(value, ISO8601)
281 else:
282 value = value.split("-")
283 return date(int(value[0]), int(value[1]), int(value[2]))
284 except Exception, e:
285 return None
286
287 def encode_date(self, value):
288 if isinstance(value, str) or isinstance(value, unicode):
289 return value
290 return value.isoformat()
291
292 def decode_date(self, value):
293 try:
294 value = value.split("-")
295 return date(int(value[0]), int(value[1]), int(value[2]))
296 except:
297 return None
298
299 encode_time = encode_date
300
301 def decode_time(self, value):
302 """ converts strings in the form of HH:MM:SS.mmmmmm
303 (created by datetime.time.isoformat()) to
304 datetime.time objects.
305
306 Timzone-aware strings ("HH:MM:SS.mmmmmm+HH:MM") won't
307 be handled right now and will raise TimeDecodeError.
308 """
309 if '-' in value or '+' in value:
310 # TODO: Handle tzinfo
311 raise TimeDecodeError("Can't handle timezone aware objects: %r" % va lue)
312 tmp = value.split('.')
313 arg = map(int, tmp[0].split(':'))
314 if len(tmp) == 2:
315 arg.append(int(tmp[1]))
316 return time(*arg)
317
318 def encode_reference(self, value):
319 if value in (None, 'None', '', ' '):
320 return None
321 if isinstance(value, str) or isinstance(value, unicode):
322 return value
323 else:
324 return value.id
325
326 def decode_reference(self, value):
327 if not value or value == "None":
328 return None
329 return value
330
331 def encode_blob(self, value):
332 if not value:
333 return None
334 if isinstance(value, str):
335 return value
336
337 if not value.id:
338 bucket = self.manager.get_blob_bucket()
339 key = bucket.new_key(str(uuid.uuid4()))
340 value.id = "s3://%s/%s" % (key.bucket.name, key.name)
341 else:
342 match = re.match("^s3:\/\/([^\/]*)\/(.*)$", value.id)
343 if match:
344 s3 = self.manager.get_s3_connection()
345 bucket = s3.get_bucket(match.group(1), validate=False)
346 key = bucket.get_key(match.group(2))
347 else:
348 raise SDBPersistenceError("Invalid Blob ID: %s" % value.id)
349
350 if value.value != None:
351 key.set_contents_from_string(value.value)
352 return value.id
353
354 def decode_blob(self, value):
355 if not value:
356 return None
357 match = re.match("^s3:\/\/([^\/]*)\/(.*)$", value)
358 if match:
359 s3 = self.manager.get_s3_connection()
360 bucket = s3.get_bucket(match.group(1), validate=False)
361 try:
362 key = bucket.get_key(match.group(2))
363 except S3ResponseError, e:
364 if e.reason != "Forbidden":
365 raise
366 return None
367 else:
368 return None
369 if key:
370 return Blob(file=key, id="s3://%s/%s" % (key.bucket.name, key.name))
371 else:
372 return None
373
374 def encode_string(self, value):
375 """Convert ASCII, Latin-1 or UTF-8 to pure Unicode"""
376 if not isinstance(value, str):
377 return value
378 try:
379 return unicode(value, 'utf-8')
380 except:
381 # really, this should throw an exception.
382 # in the interest of not breaking current
383 # systems, however:
384 arr = []
385 for ch in value:
386 arr.append(unichr(ord(ch)))
387 return u"".join(arr)
388
389 def decode_string(self, value):
390 """Decoding a string is really nothing, just
391 return the value as-is"""
392 return value
393
394
395 class SDBManager(object):
396
397 def __init__(self, cls, db_name, db_user, db_passwd,
398 db_host, db_port, db_table, ddl_dir, enable_ssl,
399 consistent=None):
400 self.cls = cls
401 self.db_name = db_name
402 self.db_user = db_user
403 self.db_passwd = db_passwd
404 self.db_host = db_host
405 self.db_port = db_port
406 self.db_table = db_table
407 self.ddl_dir = ddl_dir
408 self.enable_ssl = enable_ssl
409 self.s3 = None
410 self.bucket = None
411 self.converter = SDBConverter(self)
412 self._sdb = None
413 self._domain = None
414 if consistent == None and hasattr(cls, "__consistent__"):
415 consistent = cls.__consistent__
416 self.consistent = consistent
417
418 @property
419 def sdb(self):
420 if self._sdb is None:
421 self._connect()
422 return self._sdb
423
424 @property
425 def domain(self):
426 if self._domain is None:
427 self._connect()
428 return self._domain
429
430 def _connect(self):
431 args = dict(aws_access_key_id=self.db_user,
432 aws_secret_access_key=self.db_passwd,
433 is_secure=self.enable_ssl)
434 try:
435 region = [x for x in boto.sdb.regions() if x.endpoint == self.db_hos t][0]
436 args['region'] = region
437 except IndexError:
438 pass
439 self._sdb = boto.connect_sdb(**args)
440 # This assumes that the domain has already been created
441 # It's much more efficient to do it this way rather than
442 # having this make a roundtrip each time to validate.
443 # The downside is that if the domain doesn't exist, it breaks
444 self._domain = self._sdb.lookup(self.db_name, validate=False)
445 if not self._domain:
446 self._domain = self._sdb.create_domain(self.db_name)
447
448 def _object_lister(self, cls, query_lister):
449 for item in query_lister:
450 obj = self.get_object(cls, item.name, item)
451 if obj:
452 yield obj
453
454 def encode_value(self, prop, value):
455 if value == None:
456 return None
457 if not prop:
458 return str(value)
459 return self.converter.encode_prop(prop, value)
460
461 def decode_value(self, prop, value):
462 return self.converter.decode_prop(prop, value)
463
464 def get_s3_connection(self):
465 if not self.s3:
466 self.s3 = boto.connect_s3(self.db_user, self.db_passwd)
467 return self.s3
468
469 def get_blob_bucket(self, bucket_name=None):
470 s3 = self.get_s3_connection()
471 bucket_name = "%s-%s" % (s3.aws_access_key_id, self.domain.name)
472 bucket_name = bucket_name.lower()
473 try:
474 self.bucket = s3.get_bucket(bucket_name)
475 except:
476 self.bucket = s3.create_bucket(bucket_name)
477 return self.bucket
478
479 def load_object(self, obj):
480 if not obj._loaded:
481 a = self.domain.get_attributes(obj.id, consistent_read=self.consiste nt)
482 if '__type__' in a:
483 for prop in obj.properties(hidden=False):
484 if prop.name in a:
485 value = self.decode_value(prop, a[prop.name])
486 value = prop.make_value_from_datastore(value)
487 try:
488 setattr(obj, prop.name, value)
489 except Exception, e:
490 boto.log.exception(e)
491 obj._loaded = True
492
493 def get_object(self, cls, id, a=None):
494 obj = None
495 if not a:
496 a = self.domain.get_attributes(id, consistent_read=self.consistent)
497 if '__type__' in a:
498 if not cls or a['__type__'] != cls.__name__:
499 cls = find_class(a['__module__'], a['__type__'])
500 if cls:
501 params = {}
502 for prop in cls.properties(hidden=False):
503 if prop.name in a:
504 value = self.decode_value(prop, a[prop.name])
505 value = prop.make_value_from_datastore(value)
506 params[prop.name] = value
507 obj = cls(id, **params)
508 obj._loaded = True
509 else:
510 s = '(%s) class %s.%s not found' % (id, a['__module__'], a['__ty pe__'])
511 boto.log.info('sdbmanager: %s' % s)
512 return obj
513
514 def get_object_from_id(self, id):
515 return self.get_object(None, id)
516
517 def query(self, query):
518 query_str = "select * from `%s` %s" % (self.domain.name, self._build_fil ter_part(query.model_class, query.filters, query.sort_by, query.select))
519 if query.limit:
520 query_str += " limit %s" % query.limit
521 rs = self.domain.select(query_str, max_items=query.limit, next_token = q uery.next_token)
522 query.rs = rs
523 return self._object_lister(query.model_class, rs)
524
525 def count(self, cls, filters, quick=True, sort_by=None, select=None):
526 """
527 Get the number of results that would
528 be returned in this query
529 """
530 query = "select count(*) from `%s` %s" % (self.domain.name, self._build_ filter_part(cls, filters, sort_by, select))
531 count = 0
532 for row in self.domain.select(query):
533 count += int(row['Count'])
534 if quick:
535 return count
536 return count
537
538 def _build_filter(self, property, name, op, val):
539 if name == "__id__":
540 name = 'itemName()'
541 if name != "itemName()":
542 name = '`%s`' % name
543 if val == None:
544 if op in ('is', '='):
545 return "%(name)s is null" % {"name": name}
546 elif op in ('is not', '!='):
547 return "%s is not null" % name
548 else:
549 val = ""
550 if property.__class__ == ListProperty:
551 if op in ("is", "="):
552 op = "like"
553 elif op in ("!=", "not"):
554 op = "not like"
555 if not(op in ["like", "not like"] and val.startswith("%")):
556 val = "%%:%s" % val
557 return "%s %s '%s'" % (name, op, val.replace("'", "''"))
558
559 def _build_filter_part(self, cls, filters, order_by=None, select=None):
560 """
561 Build the filter part
562 """
563 import types
564 query_parts = []
565
566 order_by_filtered = False
567
568 if order_by:
569 if order_by[0] == "-":
570 order_by_method = "DESC"
571 order_by = order_by[1:]
572 else:
573 order_by_method = "ASC"
574
575 if select:
576 if order_by and order_by in select:
577 order_by_filtered = True
578 query_parts.append("(%s)" % select)
579
580 if isinstance(filters, str) or isinstance(filters, unicode):
581 query = "WHERE %s AND `__type__` = '%s'" % (filters, cls.__name__)
582 if order_by in ["__id__", "itemName()"]:
583 query += " ORDER BY itemName() %s" % order_by_method
584 elif order_by != None:
585 query += " ORDER BY `%s` %s" % (order_by, order_by_method)
586 return query
587
588 for filter in filters:
589 filter_parts = []
590 filter_props = filter[0]
591 if not isinstance(filter_props, list):
592 filter_props = [filter_props]
593 for filter_prop in filter_props:
594 (name, op) = filter_prop.strip().split(" ", 1)
595 value = filter[1]
596 property = cls.find_property(name)
597 if name == order_by:
598 order_by_filtered = True
599 if types.TypeType(value) == types.ListType:
600 filter_parts_sub = []
601 for val in value:
602 val = self.encode_value(property, val)
603 if isinstance(val, list):
604 for v in val:
605 filter_parts_sub.append(self._build_filter(prope rty, name, op, v))
606 else:
607 filter_parts_sub.append(self._build_filter(property, name, op, val))
608 filter_parts.append("(%s)" % (" OR ".join(filter_parts_sub)) )
609 else:
610 val = self.encode_value(property, value)
611 if isinstance(val, list):
612 for v in val:
613 filter_parts.append(self._build_filter(property, nam e, op, v))
614 else:
615 filter_parts.append(self._build_filter(property, name, o p, val))
616 query_parts.append("(%s)" % (" or ".join(filter_parts)))
617
618
619 type_query = "(`__type__` = '%s'" % cls.__name__
620 for subclass in self._get_all_decendents(cls).keys():
621 type_query += " or `__type__` = '%s'" % subclass
622 type_query += ")"
623 query_parts.append(type_query)
624
625 order_by_query = ""
626
627 if order_by:
628 if not order_by_filtered:
629 query_parts.append("`%s` LIKE '%%'" % order_by)
630 if order_by in ["__id__", "itemName()"]:
631 order_by_query = " ORDER BY itemName() %s" % order_by_method
632 else:
633 order_by_query = " ORDER BY `%s` %s" % (order_by, order_by_metho d)
634
635 if len(query_parts) > 0:
636 return "WHERE %s %s" % (" AND ".join(query_parts), order_by_query)
637 else:
638 return ""
639
640
641 def _get_all_decendents(self, cls):
642 """Get all decendents for a given class"""
643 decendents = {}
644 for sc in cls.__sub_classes__:
645 decendents[sc.__name__] = sc
646 decendents.update(self._get_all_decendents(sc))
647 return decendents
648
649 def query_gql(self, query_string, *args, **kwds):
650 raise NotImplementedError("GQL queries not supported in SimpleDB")
651
652 def save_object(self, obj, expected_value=None):
653 if not obj.id:
654 obj.id = str(uuid.uuid4())
655
656 attrs = {'__type__': obj.__class__.__name__,
657 '__module__': obj.__class__.__module__,
658 '__lineage__': obj.get_lineage()}
659 del_attrs = []
660 for property in obj.properties(hidden=False):
661 value = property.get_value_for_datastore(obj)
662 if value is not None:
663 value = self.encode_value(property, value)
664 if value == []:
665 value = None
666 if value == None:
667 del_attrs.append(property.name)
668 continue
669 attrs[property.name] = value
670 if property.unique:
671 try:
672 args = {property.name: value}
673 obj2 = obj.find(**args).next()
674 if obj2.id != obj.id:
675 raise SDBPersistenceError("Error: %s must be unique!" % property.name)
676 except(StopIteration):
677 pass
678 # Convert the Expected value to SDB format
679 if expected_value:
680 prop = obj.find_property(expected_value[0])
681 v = expected_value[1]
682 if v is not None and not isinstance(v, bool):
683 v = self.encode_value(prop, v)
684 expected_value[1] = v
685 self.domain.put_attributes(obj.id, attrs, replace=True, expected_value=e xpected_value)
686 if len(del_attrs) > 0:
687 self.domain.delete_attributes(obj.id, del_attrs)
688 return obj
689
690 def delete_object(self, obj):
691 self.domain.delete_attributes(obj.id)
692
693 def set_property(self, prop, obj, name, value):
694 setattr(obj, name, value)
695 value = prop.get_value_for_datastore(obj)
696 value = self.encode_value(prop, value)
697 if prop.unique:
698 try:
699 args = {prop.name: value}
700 obj2 = obj.find(**args).next()
701 if obj2.id != obj.id:
702 raise SDBPersistenceError("Error: %s must be unique!" % prop .name)
703 except(StopIteration):
704 pass
705 self.domain.put_attributes(obj.id, {name: value}, replace=True)
706
707 def get_property(self, prop, obj, name):
708 a = self.domain.get_attributes(obj.id, consistent_read=self.consistent)
709
710 # try to get the attribute value from SDB
711 if name in a:
712 value = self.decode_value(prop, a[name])
713 value = prop.make_value_from_datastore(value)
714 setattr(obj, prop.name, value)
715 return value
716 raise AttributeError('%s not found' % name)
717
718 def set_key_value(self, obj, name, value):
719 self.domain.put_attributes(obj.id, {name: value}, replace=True)
720
721 def delete_key_value(self, obj, name):
722 self.domain.delete_attributes(obj.id, name)
723
724 def get_key_value(self, obj, name):
725 a = self.domain.get_attributes(obj.id, name, consistent_read=self.consis tent)
726 if name in a:
727 return a[name]
728 else:
729 return None
730
731 def get_raw_item(self, obj):
732 return self.domain.get_item(obj.id)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698