OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/ |
| 2 # |
| 3 # Permission is hereby granted, free of charge, to any person obtaining a |
| 4 # copy of this software and associated documentation files (the |
| 5 # "Software"), to deal in the Software without restriction, including |
| 6 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 7 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 8 # persons to whom the Software is furnished to do so, subject to the fol- |
| 9 # lowing conditions: |
| 10 # |
| 11 # The above copyright notice and this permission notice shall be included |
| 12 # in all copies or substantial portions of the Software. |
| 13 # |
| 14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 16 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
| 17 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 18 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 19 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 20 # IN THE SOFTWARE. |
| 21 |
| 22 """ |
| 23 Represents an SDB Domain |
| 24 """ |
| 25 from boto.sdb.queryresultset import SelectResultSet |
| 26 |
| 27 class Domain: |
| 28 |
| 29 def __init__(self, connection=None, name=None): |
| 30 self.connection = connection |
| 31 self.name = name |
| 32 self._metadata = None |
| 33 |
| 34 def __repr__(self): |
| 35 return 'Domain:%s' % self.name |
| 36 |
| 37 def __iter__(self): |
| 38 return iter(self.select("SELECT * FROM `%s`" % self.name)) |
| 39 |
| 40 def startElement(self, name, attrs, connection): |
| 41 return None |
| 42 |
| 43 def endElement(self, name, value, connection): |
| 44 if name == 'DomainName': |
| 45 self.name = value |
| 46 else: |
| 47 setattr(self, name, value) |
| 48 |
| 49 def get_metadata(self): |
| 50 if not self._metadata: |
| 51 self._metadata = self.connection.domain_metadata(self) |
| 52 return self._metadata |
| 53 |
| 54 def put_attributes(self, item_name, attributes, |
| 55 replace=True, expected_value=None): |
| 56 """ |
| 57 Store attributes for a given item. |
| 58 |
| 59 :type item_name: string |
| 60 :param item_name: The name of the item whose attributes are being stored
. |
| 61 |
| 62 :type attribute_names: dict or dict-like object |
| 63 :param attribute_names: The name/value pairs to store as attributes |
| 64 |
| 65 :type expected_value: list |
| 66 :param expected_value: If supplied, this is a list or tuple consisting |
| 67 of a single attribute name and expected value. The list can be |
| 68 of the form: |
| 69 |
| 70 * ['name', 'value'] |
| 71 |
| 72 In which case the call will first verify that the attribute |
| 73 "name" of this item has a value of "value". If it does, the delete |
| 74 will proceed, otherwise a ConditionalCheckFailed error will be |
| 75 returned. The list can also be of the form: |
| 76 |
| 77 * ['name', True|False] |
| 78 |
| 79 which will simply check for the existence (True) or non-existence |
| 80 (False) of the attribute. |
| 81 |
| 82 :type replace: bool |
| 83 :param replace: Whether the attribute values passed in will replace |
| 84 existing values or will be added as addition values. |
| 85 Defaults to True. |
| 86 |
| 87 :rtype: bool |
| 88 :return: True if successful |
| 89 """ |
| 90 return self.connection.put_attributes(self, item_name, attributes, |
| 91 replace, expected_value) |
| 92 |
| 93 def batch_put_attributes(self, items, replace=True): |
| 94 """ |
| 95 Store attributes for multiple items. |
| 96 |
| 97 :type items: dict or dict-like object |
| 98 :param items: A dictionary-like object. The keys of the dictionary are |
| 99 the item names and the values are themselves dictionaries |
| 100 of attribute names/values, exactly the same as the |
| 101 attribute_names parameter of the scalar put_attributes |
| 102 call. |
| 103 |
| 104 :type replace: bool |
| 105 :param replace: Whether the attribute values passed in will replace |
| 106 existing values or will be added as addition values. |
| 107 Defaults to True. |
| 108 |
| 109 :rtype: bool |
| 110 :return: True if successful |
| 111 """ |
| 112 return self.connection.batch_put_attributes(self, items, replace) |
| 113 |
| 114 def get_attributes(self, item_name, attribute_name=None, |
| 115 consistent_read=False, item=None): |
| 116 """ |
| 117 Retrieve attributes for a given item. |
| 118 |
| 119 :type item_name: string |
| 120 :param item_name: The name of the item whose attributes are being retrie
ved. |
| 121 |
| 122 :type attribute_names: string or list of strings |
| 123 :param attribute_names: An attribute name or list of attribute names. T
his |
| 124 parameter is optional. If not supplied, all att
ributes |
| 125 will be retrieved for the item. |
| 126 |
| 127 :rtype: :class:`boto.sdb.item.Item` |
| 128 :return: An Item mapping type containing the requested attribute name/va
lues |
| 129 """ |
| 130 return self.connection.get_attributes(self, item_name, attribute_name, |
| 131 consistent_read, item) |
| 132 |
| 133 def delete_attributes(self, item_name, attributes=None, |
| 134 expected_values=None): |
| 135 """ |
| 136 Delete attributes from a given item. |
| 137 |
| 138 :type item_name: string |
| 139 :param item_name: The name of the item whose attributes are being delete
d. |
| 140 |
| 141 :type attributes: dict, list or :class:`boto.sdb.item.Item` |
| 142 :param attributes: Either a list containing attribute names which will c
ause |
| 143 all values associated with that attribute name to be
deleted or |
| 144 a dict or Item containing the attribute names and key
s and list |
| 145 of values to delete as the value. If no value is sup
plied, |
| 146 all attribute name/values for the item will be delete
d. |
| 147 |
| 148 :type expected_value: list |
| 149 :param expected_value: If supplied, this is a list or tuple consisting |
| 150 of a single attribute name and expected value. The list can be of |
| 151 the form: |
| 152 |
| 153 * ['name', 'value'] |
| 154 |
| 155 In which case the call will first verify that the attribute "name" |
| 156 of this item has a value of "value". If it does, the delete |
| 157 will proceed, otherwise a ConditionalCheckFailed error will be |
| 158 returned. The list can also be of the form: |
| 159 |
| 160 * ['name', True|False] |
| 161 |
| 162 which will simply check for the existence (True) or |
| 163 non-existence (False) of the attribute. |
| 164 |
| 165 :rtype: bool |
| 166 :return: True if successful |
| 167 """ |
| 168 return self.connection.delete_attributes(self, item_name, attributes, |
| 169 expected_values) |
| 170 |
| 171 def batch_delete_attributes(self, items): |
| 172 """ |
| 173 Delete multiple items in this domain. |
| 174 |
| 175 :type items: dict or dict-like object |
| 176 :param items: A dictionary-like object. The keys of the dictionary are |
| 177 the item names and the values are either: |
| 178 |
| 179 * dictionaries of attribute names/values, exactly the |
| 180 same as the attribute_names parameter of the scalar |
| 181 put_attributes call. The attribute name/value pairs |
| 182 will only be deleted if they match the name/value |
| 183 pairs passed in. |
| 184 * None which means that all attributes associated |
| 185 with the item should be deleted. |
| 186 |
| 187 :rtype: bool |
| 188 :return: True if successful |
| 189 """ |
| 190 return self.connection.batch_delete_attributes(self, items) |
| 191 |
| 192 def select(self, query='', next_token=None, consistent_read=False, max_items
=None): |
| 193 """ |
| 194 Returns a set of Attributes for item names within domain_name that match
the query. |
| 195 The query must be expressed in using the SELECT style syntax rather than
the |
| 196 original SimpleDB query language. |
| 197 |
| 198 :type query: string |
| 199 :param query: The SimpleDB query to be performed. |
| 200 |
| 201 :rtype: iter |
| 202 :return: An iterator containing the results. This is actually a generat
or |
| 203 function that will iterate across all search results, not just
the |
| 204 first page. |
| 205 """ |
| 206 return SelectResultSet(self, query, max_items=max_items, next_token=next
_token, |
| 207 consistent_read=consistent_read) |
| 208 |
| 209 def get_item(self, item_name, consistent_read=False): |
| 210 """ |
| 211 Retrieves an item from the domain, along with all of its attributes. |
| 212 |
| 213 :param string item_name: The name of the item to retrieve. |
| 214 :rtype: :class:`boto.sdb.item.Item` or ``None`` |
| 215 :keyword bool consistent_read: When set to true, ensures that the most |
| 216 recent data is returned. |
| 217 :return: The requested item, or ``None`` if there was no match found |
| 218 """ |
| 219 item = self.get_attributes(item_name, consistent_read=consistent_read) |
| 220 if item: |
| 221 item.domain = self |
| 222 return item |
| 223 else: |
| 224 return None |
| 225 |
| 226 def new_item(self, item_name): |
| 227 return self.connection.item_cls(self, item_name) |
| 228 |
| 229 def delete_item(self, item): |
| 230 self.delete_attributes(item.name) |
| 231 |
| 232 def to_xml(self, f=None): |
| 233 """Get this domain as an XML DOM Document |
| 234 :param f: Optional File to dump directly to |
| 235 :type f: File or Stream |
| 236 |
| 237 :return: File object where the XML has been dumped to |
| 238 :rtype: file |
| 239 """ |
| 240 if not f: |
| 241 from tempfile import TemporaryFile |
| 242 f = TemporaryFile() |
| 243 print >> f, '<?xml version="1.0" encoding="UTF-8"?>' |
| 244 print >> f, '<Domain id="%s">' % self.name |
| 245 for item in self: |
| 246 print >> f, '\t<Item id="%s">' % item.name |
| 247 for k in item: |
| 248 print >> f, '\t\t<attribute id="%s">' % k |
| 249 values = item[k] |
| 250 if not isinstance(values, list): |
| 251 values = [values] |
| 252 for value in values: |
| 253 print >> f, '\t\t\t<value><![CDATA[', |
| 254 if isinstance(value, unicode): |
| 255 value = value.encode('utf-8', 'replace') |
| 256 else: |
| 257 value = unicode(value, errors='replace').encode('utf-8',
'replace') |
| 258 f.write(value) |
| 259 print >> f, ']]></value>' |
| 260 print >> f, '\t\t</attribute>' |
| 261 print >> f, '\t</Item>' |
| 262 print >> f, '</Domain>' |
| 263 f.flush() |
| 264 f.seek(0) |
| 265 return f |
| 266 |
| 267 |
| 268 def from_xml(self, doc): |
| 269 """Load this domain based on an XML document""" |
| 270 import xml.sax |
| 271 handler = DomainDumpParser(self) |
| 272 xml.sax.parse(doc, handler) |
| 273 return handler |
| 274 |
| 275 def delete(self): |
| 276 """ |
| 277 Delete this domain, and all items under it |
| 278 """ |
| 279 return self.connection.delete_domain(self) |
| 280 |
| 281 |
| 282 class DomainMetaData: |
| 283 |
| 284 def __init__(self, domain=None): |
| 285 self.domain = domain |
| 286 self.item_count = None |
| 287 self.item_names_size = None |
| 288 self.attr_name_count = None |
| 289 self.attr_names_size = None |
| 290 self.attr_value_count = None |
| 291 self.attr_values_size = None |
| 292 |
| 293 def startElement(self, name, attrs, connection): |
| 294 return None |
| 295 |
| 296 def endElement(self, name, value, connection): |
| 297 if name == 'ItemCount': |
| 298 self.item_count = int(value) |
| 299 elif name == 'ItemNamesSizeBytes': |
| 300 self.item_names_size = int(value) |
| 301 elif name == 'AttributeNameCount': |
| 302 self.attr_name_count = int(value) |
| 303 elif name == 'AttributeNamesSizeBytes': |
| 304 self.attr_names_size = int(value) |
| 305 elif name == 'AttributeValueCount': |
| 306 self.attr_value_count = int(value) |
| 307 elif name == 'AttributeValuesSizeBytes': |
| 308 self.attr_values_size = int(value) |
| 309 elif name == 'Timestamp': |
| 310 self.timestamp = value |
| 311 else: |
| 312 setattr(self, name, value) |
| 313 |
| 314 import sys |
| 315 from xml.sax.handler import ContentHandler |
| 316 class DomainDumpParser(ContentHandler): |
| 317 """ |
| 318 SAX parser for a domain that has been dumped |
| 319 """ |
| 320 |
| 321 def __init__(self, domain): |
| 322 self.uploader = UploaderThread(domain) |
| 323 self.item_id = None |
| 324 self.attrs = {} |
| 325 self.attribute = None |
| 326 self.value = "" |
| 327 self.domain = domain |
| 328 |
| 329 def startElement(self, name, attrs): |
| 330 if name == "Item": |
| 331 self.item_id = attrs['id'] |
| 332 self.attrs = {} |
| 333 elif name == "attribute": |
| 334 self.attribute = attrs['id'] |
| 335 elif name == "value": |
| 336 self.value = "" |
| 337 |
| 338 def characters(self, ch): |
| 339 self.value += ch |
| 340 |
| 341 def endElement(self, name): |
| 342 if name == "value": |
| 343 if self.value and self.attribute: |
| 344 value = self.value.strip() |
| 345 attr_name = self.attribute.strip() |
| 346 if attr_name in self.attrs: |
| 347 self.attrs[attr_name].append(value) |
| 348 else: |
| 349 self.attrs[attr_name] = [value] |
| 350 elif name == "Item": |
| 351 self.uploader.items[self.item_id] = self.attrs |
| 352 # Every 20 items we spawn off the uploader |
| 353 if len(self.uploader.items) >= 20: |
| 354 self.uploader.start() |
| 355 self.uploader = UploaderThread(self.domain) |
| 356 elif name == "Domain": |
| 357 # If we're done, spawn off our last Uploader Thread |
| 358 self.uploader.start() |
| 359 |
| 360 from threading import Thread |
| 361 class UploaderThread(Thread): |
| 362 """Uploader Thread""" |
| 363 |
| 364 def __init__(self, domain): |
| 365 self.db = domain |
| 366 self.items = {} |
| 367 Thread.__init__(self) |
| 368 |
| 369 def run(self): |
| 370 try: |
| 371 self.db.batch_put_attributes(self.items) |
| 372 except: |
| 373 print "Exception using batch put, trying regular put instead" |
| 374 for item_name in self.items: |
| 375 self.db.put_attributes(item_name, self.items[item_name]) |
| 376 print ".", |
| 377 sys.stdout.flush() |
OLD | NEW |