| Index: boto/sdb/db/manager/sdbmanager.py
|
| diff --git a/boto/sdb/db/manager/sdbmanager.py b/boto/sdb/db/manager/sdbmanager.py
|
| index 6aac5686f737046e4e40509f0241db6cfd53ffe7..8218f8162c43a40f93984ff458e2c54394f12873 100644
|
| --- a/boto/sdb/db/manager/sdbmanager.py
|
| +++ b/boto/sdb/db/manager/sdbmanager.py
|
| @@ -27,13 +27,15 @@ from boto.sdb.db.key import Key
|
| from boto.sdb.db.model import Model
|
| from boto.sdb.db.blob import Blob
|
| from boto.sdb.db.property import ListProperty, MapProperty
|
| -from datetime import datetime, date
|
| -from boto.exception import SDBPersistenceError
|
| +from datetime import datetime, date, time
|
| +from boto.exception import SDBPersistenceError, S3ResponseError
|
|
|
| ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
|
|
|
| +class TimeDecodeError(Exception):
|
| + pass
|
|
|
| -class SDBConverter:
|
| +class SDBConverter(object):
|
| """
|
| Responsible for converting base Python types to format compatible with underlying
|
| database. For SimpleDB, that means everything needs to be converted to a string
|
| @@ -55,7 +57,9 @@ class SDBConverter:
|
| Key : (self.encode_reference, self.decode_reference),
|
| datetime : (self.encode_datetime, self.decode_datetime),
|
| date : (self.encode_date, self.decode_date),
|
| + time : (self.encode_time, self.decode_time),
|
| Blob: (self.encode_blob, self.decode_blob),
|
| + str: (self.encode_string, self.decode_string),
|
| }
|
|
|
| def encode(self, item_type, value):
|
| @@ -93,6 +97,7 @@ class SDBConverter:
|
| return self.encode_map(prop, values)
|
|
|
| def encode_map(self, prop, value):
|
| + import urllib
|
| if value == None:
|
| return None
|
| if not isinstance(value, dict):
|
| @@ -104,7 +109,7 @@ class SDBConverter:
|
| item_type = Model
|
| encoded_value = self.encode(item_type, value[key])
|
| if encoded_value != None:
|
| - new_value.append('%s:%s' % (key, encoded_value))
|
| + new_value.append('%s:%s' % (urllib.quote(key), encoded_value))
|
| return new_value
|
|
|
| def encode_prop(self, prop, value):
|
| @@ -144,9 +149,11 @@ class SDBConverter:
|
|
|
| def decode_map_element(self, item_type, value):
|
| """Decode a single element for a map"""
|
| + import urllib
|
| key = value
|
| if ":" in value:
|
| key, value = value.split(':',1)
|
| + key = urllib.unquote(key)
|
| if Model in item_type.mro():
|
| value = item_type(id=value)
|
| else:
|
| @@ -270,6 +277,25 @@ class SDBConverter:
|
| except:
|
| return None
|
|
|
| + encode_time = encode_date
|
| +
|
| + def decode_time(self, value):
|
| + """ converts strings in the form of HH:MM:SS.mmmmmm
|
| + (created by datetime.time.isoformat()) to
|
| + datetime.time objects.
|
| +
|
| + Timzone-aware strings ("HH:MM:SS.mmmmmm+HH:MM") won't
|
| + be handled right now and will raise TimeDecodeError.
|
| + """
|
| + if '-' in value or '+' in value:
|
| + # TODO: Handle tzinfo
|
| + raise TimeDecodeError("Can't handle timezone aware objects: %r" % value)
|
| + tmp = value.split('.')
|
| + arg = map(int, tmp[0].split(':'))
|
| + if len(tmp) == 2:
|
| + arg.append(int(tmp[1]))
|
| + return time(*arg)
|
| +
|
| def encode_reference(self, value):
|
| if value in (None, 'None', '', ' '):
|
| return None
|
| @@ -314,7 +340,12 @@ class SDBConverter:
|
| if match:
|
| s3 = self.manager.get_s3_connection()
|
| bucket = s3.get_bucket(match.group(1), validate=False)
|
| - key = bucket.get_key(match.group(2))
|
| + try:
|
| + key = bucket.get_key(match.group(2))
|
| + except S3ResponseError, e:
|
| + if e.reason != "Forbidden":
|
| + raise
|
| + return None
|
| else:
|
| return None
|
| if key:
|
| @@ -322,6 +353,24 @@ class SDBConverter:
|
| else:
|
| return None
|
|
|
| + def encode_string(self, value):
|
| + """Convert ASCII, Latin-1 or UTF-8 to pure Unicode"""
|
| + if not isinstance(value, str): return value
|
| + try:
|
| + return unicode(value, 'utf-8')
|
| + except: # really, this should throw an exception.
|
| + # in the interest of not breaking current
|
| + # systems, however:
|
| + arr = []
|
| + for ch in value:
|
| + arr.append(unichr(ord(ch)))
|
| + return u"".join(arr)
|
| +
|
| + def decode_string(self, value):
|
| + """Decoding a string is really nothing, just
|
| + return the value as-is"""
|
| + return value
|
| +
|
| class SDBManager(object):
|
|
|
| def __init__(self, cls, db_name, db_user, db_passwd,
|
| @@ -357,9 +406,15 @@ class SDBManager(object):
|
| return self._domain
|
|
|
| def _connect(self):
|
| - self._sdb = boto.connect_sdb(aws_access_key_id=self.db_user,
|
| - aws_secret_access_key=self.db_passwd,
|
| - is_secure=self.enable_ssl)
|
| + args = dict(aws_access_key_id=self.db_user,
|
| + aws_secret_access_key=self.db_passwd,
|
| + is_secure=self.enable_ssl)
|
| + try:
|
| + region = [x for x in boto.sdb.regions() if x.endpoint == self.db_host][0]
|
| + args['region'] = region
|
| + except IndexError:
|
| + pass
|
| + self._sdb = boto.connect_sdb(**args)
|
| # This assumes that the domain has already been created
|
| # It's much more efficient to do it this way rather than
|
| # having this make a roundtrip each time to validate.
|
| @@ -486,16 +541,26 @@ class SDBManager(object):
|
| """
|
| import types
|
| query_parts = []
|
| +
|
| order_by_filtered = False
|
| +
|
| if order_by:
|
| if order_by[0] == "-":
|
| order_by_method = "DESC";
|
| order_by = order_by[1:]
|
| else:
|
| order_by_method = "ASC";
|
| +
|
| + if select:
|
| + if order_by and order_by in select:
|
| + order_by_filtered = True
|
| + query_parts.append("(%s)" % select)
|
| +
|
| if isinstance(filters, str) or isinstance(filters, unicode):
|
| - query = "WHERE `__type__` = '%s' AND %s" % (cls.__name__, filters)
|
| - if order_by != None:
|
| + query = "WHERE %s AND `__type__` = '%s'" % (filters, cls.__name__)
|
| + if order_by in ["__id__", "itemName()"]:
|
| + query += " ORDER BY itemName() %s" % order_by_method
|
| + elif order_by != None:
|
| query += " ORDER BY `%s` %s" % (order_by, order_by_method)
|
| return query
|
|
|
| @@ -537,13 +602,14 @@ class SDBManager(object):
|
| query_parts.append(type_query)
|
|
|
| order_by_query = ""
|
| +
|
| if order_by:
|
| if not order_by_filtered:
|
| query_parts.append("`%s` LIKE '%%'" % order_by)
|
| - order_by_query = " ORDER BY `%s` %s" % (order_by, order_by_method)
|
| -
|
| - if select:
|
| - query_parts.append("(%s)" % select)
|
| + if order_by in ["__id__", "itemName()"]:
|
| + order_by_query = " ORDER BY itemName() %s" % order_by_method
|
| + else:
|
| + order_by_query = " ORDER BY `%s` %s" % (order_by, order_by_method)
|
|
|
| if len(query_parts) > 0:
|
| return "WHERE %s %s" % (" AND ".join(query_parts), order_by_query)
|
| @@ -562,7 +628,7 @@ class SDBManager(object):
|
| def query_gql(self, query_string, *args, **kwds):
|
| raise NotImplementedError, "GQL queries not supported in SimpleDB"
|
|
|
| - def save_object(self, obj):
|
| + def save_object(self, obj, expected_value=None):
|
| if not obj.id:
|
| obj.id = str(uuid.uuid4())
|
|
|
| @@ -588,7 +654,14 @@ class SDBManager(object):
|
| raise SDBPersistenceError("Error: %s must be unique!" % property.name)
|
| except(StopIteration):
|
| pass
|
| - self.domain.put_attributes(obj.id, attrs, replace=True)
|
| + # Convert the Expected value to SDB format
|
| + if expected_value:
|
| + prop = obj.find_property(expected_value[0])
|
| + v = expected_value[1]
|
| + if v is not None and not type(v) == bool:
|
| + v = self.encode_value(prop, v)
|
| + expected_value[1] = v
|
| + self.domain.put_attributes(obj.id, attrs, replace=True, expected_value=expected_value)
|
| if len(del_attrs) > 0:
|
| self.domain.delete_attributes(obj.id, del_attrs)
|
| return obj
|
| @@ -597,6 +670,7 @@ class SDBManager(object):
|
| self.domain.delete_attributes(obj.id)
|
|
|
| def set_property(self, prop, obj, name, value):
|
| + setattr(obj, name, value)
|
| value = prop.get_value_for_datastore(obj)
|
| value = self.encode_value(prop, value)
|
| if prop.unique:
|
|
|