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. 21import xml.sax 22import threading 23import boto 24from boto import handler 25from boto.connection import AWSQueryConnection 26from boto.sdb.domain import Domain, DomainMetaData 27from boto.sdb.item import Item 28from boto.sdb.regioninfo import SDBRegionInfo 29from boto.exception import SDBResponseError 30 31class ItemThread(threading.Thread): 32 """ 33 A threaded :class:`Item <boto.sdb.item.Item>` retriever utility class. 34 Retrieved :class:`Item <boto.sdb.item.Item>` objects are stored in the 35 ``items`` instance variable after :py:meth:`run() <run>` is called. 36 37 .. tip:: The item retrieval will not start until 38 the :func:`run() <boto.sdb.connection.ItemThread.run>` method is called. 39 """ 40 def __init__(self, name, domain_name, item_names): 41 """ 42 :param str name: A thread name. Used for identification. 43 :param str domain_name: The name of a SimpleDB 44 :class:`Domain <boto.sdb.domain.Domain>` 45 :type item_names: string or list of strings 46 :param item_names: The name(s) of the items to retrieve from the specified 47 :class:`Domain <boto.sdb.domain.Domain>`. 48 :ivar list items: A list of items retrieved. Starts as empty list. 49 """ 50 super(ItemThread, self).__init__(name=name) 51 #print 'starting %s with %d items' % (name, len(item_names)) 52 self.domain_name = domain_name 53 self.conn = SDBConnection() 54 self.item_names = item_names 55 self.items = [] 56 57 def run(self): 58 """ 59 Start the threaded retrieval of items. Populates the 60 ``items`` list with :class:`Item <boto.sdb.item.Item>` objects. 61 """ 62 for item_name in self.item_names: 63 item = self.conn.get_attributes(self.domain_name, item_name) 64 self.items.append(item) 65 66#boto.set_stream_logger('sdb') 67 68class SDBConnection(AWSQueryConnection): 69 """ 70 This class serves as a gateway to your SimpleDB region (defaults to 71 us-east-1). Methods within allow access to SimpleDB 72 :class:`Domain <boto.sdb.domain.Domain>` objects and their associated 73 :class:`Item <boto.sdb.item.Item>` objects. 74 75 .. tip:: 76 While you may instantiate this class directly, it may be easier to 77 go through :py:func:`boto.connect_sdb`. 78 """ 79 DefaultRegionName = 'us-east-1' 80 DefaultRegionEndpoint = 'sdb.us-east-1.amazonaws.com' 81 APIVersion = '2009-04-15' 82 ResponseError = SDBResponseError 83 84 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, 85 is_secure=True, port=None, proxy=None, proxy_port=None, 86 proxy_user=None, proxy_pass=None, debug=0, 87 https_connection_factory=None, region=None, path='/', 88 converter=None, security_token=None, validate_certs=True, 89 profile_name=None): 90 """ 91 For any keywords that aren't documented, refer to the parent class, 92 :py:class:`boto.connection.AWSAuthConnection`. You can avoid having 93 to worry about these keyword arguments by instantiating these objects 94 via :py:func:`boto.connect_sdb`. 95 96 :type region: :class:`boto.sdb.regioninfo.SDBRegionInfo` 97 :keyword region: Explicitly specify a region. Defaults to ``us-east-1`` 98 if not specified. You may also specify the region in your ``boto.cfg``: 99 100 .. code-block:: cfg 101 102 [SDB] 103 region = eu-west-1 104 105 """ 106 if not region: 107 region_name = boto.config.get('SDB', 'region', self.DefaultRegionName) 108 for reg in boto.sdb.regions(): 109 if reg.name == region_name: 110 region = reg 111 break 112 113 self.region = region 114 super(SDBConnection, self).__init__(aws_access_key_id, 115 aws_secret_access_key, 116 is_secure, port, proxy, 117 proxy_port, proxy_user, proxy_pass, 118 self.region.endpoint, debug, 119 https_connection_factory, path, 120 security_token=security_token, 121 validate_certs=validate_certs, 122 profile_name=profile_name) 123 self.box_usage = 0.0 124 self.converter = converter 125 self.item_cls = Item 126 127 def _required_auth_capability(self): 128 return ['sdb'] 129 130 def set_item_cls(self, cls): 131 """ 132 While the default item class is :py:class:`boto.sdb.item.Item`, this 133 default may be overridden. Use this method to change a connection's 134 item class. 135 136 :param object cls: The new class to set as this connection's item 137 class. See the default item class for inspiration as to what your 138 replacement should/could look like. 139 """ 140 self.item_cls = cls 141 142 def _build_name_value_list(self, params, attributes, replace=False, 143 label='Attribute'): 144 keys = sorted(attributes.keys()) 145 i = 1 146 for key in keys: 147 value = attributes[key] 148 if isinstance(value, list): 149 for v in value: 150 params['%s.%d.Name' % (label, i)] = key 151 if self.converter: 152 v = self.converter.encode(v) 153 params['%s.%d.Value' % (label, i)] = v 154 if replace: 155 params['%s.%d.Replace' % (label, i)] = 'true' 156 i += 1 157 else: 158 params['%s.%d.Name' % (label, i)] = key 159 if self.converter: 160 value = self.converter.encode(value) 161 params['%s.%d.Value' % (label, i)] = value 162 if replace: 163 params['%s.%d.Replace' % (label, i)] = 'true' 164 i += 1 165 166 def _build_expected_value(self, params, expected_value): 167 params['Expected.1.Name'] = expected_value[0] 168 if expected_value[1] is True: 169 params['Expected.1.Exists'] = 'true' 170 elif expected_value[1] is False: 171 params['Expected.1.Exists'] = 'false' 172 else: 173 params['Expected.1.Value'] = expected_value[1] 174 175 def _build_batch_list(self, params, items, replace=False): 176 item_names = items.keys() 177 i = 0 178 for item_name in item_names: 179 params['Item.%d.ItemName' % i] = item_name 180 j = 0 181 item = items[item_name] 182 if item is not None: 183 attr_names = item.keys() 184 for attr_name in attr_names: 185 value = item[attr_name] 186 if isinstance(value, list): 187 for v in value: 188 if self.converter: 189 v = self.converter.encode(v) 190 params['Item.%d.Attribute.%d.Name' % (i, j)] = attr_name 191 params['Item.%d.Attribute.%d.Value' % (i, j)] = v 192 if replace: 193 params['Item.%d.Attribute.%d.Replace' % (i, j)] = 'true' 194 j += 1 195 else: 196 params['Item.%d.Attribute.%d.Name' % (i, j)] = attr_name 197 if self.converter: 198 value = self.converter.encode(value) 199 params['Item.%d.Attribute.%d.Value' % (i, j)] = value 200 if replace: 201 params['Item.%d.Attribute.%d.Replace' % (i, j)] = 'true' 202 j += 1 203 i += 1 204 205 def _build_name_list(self, params, attribute_names): 206 i = 1 207 attribute_names.sort() 208 for name in attribute_names: 209 params['Attribute.%d.Name' % i] = name 210 i += 1 211 212 def get_usage(self): 213 """ 214 Returns the BoxUsage (in USD) accumulated on this specific SDBConnection 215 instance. 216 217 .. tip:: This can be out of date, and should only be treated as a 218 rough estimate. Also note that this estimate only applies to the 219 requests made on this specific connection instance. It is by 220 no means an account-wide estimate. 221 222 :rtype: float 223 :return: The accumulated BoxUsage of all requests made on the connection. 224 """ 225 return self.box_usage 226 227 def print_usage(self): 228 """ 229 Print the BoxUsage and approximate costs of all requests made on 230 this specific SDBConnection instance. 231 232 .. tip:: This can be out of date, and should only be treated as a 233 rough estimate. Also note that this estimate only applies to the 234 requests made on this specific connection instance. It is by 235 no means an account-wide estimate. 236 """ 237 print('Total Usage: %f compute seconds' % self.box_usage) 238 cost = self.box_usage * 0.14 239 print('Approximate Cost: $%f' % cost) 240 241 def get_domain(self, domain_name, validate=True): 242 """ 243 Retrieves a :py:class:`boto.sdb.domain.Domain` object whose name 244 matches ``domain_name``. 245 246 :param str domain_name: The name of the domain to retrieve 247 :keyword bool validate: When ``True``, check to see if the domain 248 actually exists. If ``False``, blindly return a 249 :py:class:`Domain <boto.sdb.domain.Domain>` object with the 250 specified name set. 251 252 :raises: 253 :py:class:`boto.exception.SDBResponseError` if ``validate`` is 254 ``True`` and no match could be found. 255 256 :rtype: :py:class:`boto.sdb.domain.Domain` 257 :return: The requested domain 258 """ 259 domain = Domain(self, domain_name) 260 if validate: 261 self.select(domain, """select * from `%s` limit 1""" % domain_name) 262 return domain 263 264 def lookup(self, domain_name, validate=True): 265 """ 266 Lookup an existing SimpleDB domain. This differs from 267 :py:meth:`get_domain` in that ``None`` is returned if ``validate`` is 268 ``True`` and no match was found (instead of raising an exception). 269 270 :param str domain_name: The name of the domain to retrieve 271 272 :param bool validate: If ``True``, a ``None`` value will be returned 273 if the specified domain can't be found. If ``False``, a 274 :py:class:`Domain <boto.sdb.domain.Domain>` object will be dumbly 275 returned, regardless of whether it actually exists. 276 277 :rtype: :class:`boto.sdb.domain.Domain` object or ``None`` 278 :return: The Domain object or ``None`` if the domain does not exist. 279 """ 280 try: 281 domain = self.get_domain(domain_name, validate) 282 except: 283 domain = None 284 return domain 285 286 def get_all_domains(self, max_domains=None, next_token=None): 287 """ 288 Returns a :py:class:`boto.resultset.ResultSet` containing 289 all :py:class:`boto.sdb.domain.Domain` objects associated with 290 this connection's Access Key ID. 291 292 :keyword int max_domains: Limit the returned 293 :py:class:`ResultSet <boto.resultset.ResultSet>` to the specified 294 number of members. 295 :keyword str next_token: A token string that was returned in an 296 earlier call to this method as the ``next_token`` attribute 297 on the returned :py:class:`ResultSet <boto.resultset.ResultSet>` 298 object. This attribute is set if there are more than Domains than 299 the value specified in the ``max_domains`` keyword. Pass the 300 ``next_token`` value from you earlier query in this keyword to 301 get the next 'page' of domains. 302 """ 303 params = {} 304 if max_domains: 305 params['MaxNumberOfDomains'] = max_domains 306 if next_token: 307 params['NextToken'] = next_token 308 return self.get_list('ListDomains', params, [('DomainName', Domain)]) 309 310 def create_domain(self, domain_name): 311 """ 312 Create a SimpleDB domain. 313 314 :type domain_name: string 315 :param domain_name: The name of the new domain 316 317 :rtype: :class:`boto.sdb.domain.Domain` object 318 :return: The newly created domain 319 """ 320 params = {'DomainName': domain_name} 321 d = self.get_object('CreateDomain', params, Domain) 322 d.name = domain_name 323 return d 324 325 def get_domain_and_name(self, domain_or_name): 326 """ 327 Given a ``str`` or :class:`boto.sdb.domain.Domain`, return a 328 ``tuple`` with the following members (in order): 329 330 * In instance of :class:`boto.sdb.domain.Domain` for the requested 331 domain 332 * The domain's name as a ``str`` 333 334 :type domain_or_name: ``str`` or :class:`boto.sdb.domain.Domain` 335 :param domain_or_name: The domain or domain name to get the domain 336 and name for. 337 338 :raises: :class:`boto.exception.SDBResponseError` when an invalid 339 domain name is specified. 340 341 :rtype: tuple 342 :return: A ``tuple`` with contents outlined as per above. 343 """ 344 if (isinstance(domain_or_name, Domain)): 345 return (domain_or_name, domain_or_name.name) 346 else: 347 return (self.get_domain(domain_or_name), domain_or_name) 348 349 def delete_domain(self, domain_or_name): 350 """ 351 Delete a SimpleDB domain. 352 353 .. caution:: This will delete the domain and all items within the domain. 354 355 :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object. 356 :param domain_or_name: Either the name of a domain or a Domain object 357 358 :rtype: bool 359 :return: True if successful 360 361 """ 362 domain, domain_name = self.get_domain_and_name(domain_or_name) 363 params = {'DomainName': domain_name} 364 return self.get_status('DeleteDomain', params) 365 366 def domain_metadata(self, domain_or_name): 367 """ 368 Get the Metadata for a SimpleDB domain. 369 370 :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object. 371 :param domain_or_name: Either the name of a domain or a Domain object 372 373 :rtype: :class:`boto.sdb.domain.DomainMetaData` object 374 :return: The newly created domain metadata object 375 """ 376 domain, domain_name = self.get_domain_and_name(domain_or_name) 377 params = {'DomainName': domain_name} 378 d = self.get_object('DomainMetadata', params, DomainMetaData) 379 d.domain = domain 380 return d 381 382 def put_attributes(self, domain_or_name, item_name, attributes, 383 replace=True, expected_value=None): 384 """ 385 Store attributes for a given item in a domain. 386 387 :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object. 388 :param domain_or_name: Either the name of a domain or a Domain object 389 390 :type item_name: string 391 :param item_name: The name of the item whose attributes are being 392 stored. 393 394 :type attribute_names: dict or dict-like object 395 :param attribute_names: The name/value pairs to store as attributes 396 397 :type expected_value: list 398 :param expected_value: If supplied, this is a list or tuple consisting 399 of a single attribute name and expected value. The list can be 400 of the form: 401 402 * ['name', 'value'] 403 404 In which case the call will first verify that the attribute "name" 405 of this item has a value of "value". If it does, the delete 406 will proceed, otherwise a ConditionalCheckFailed error will be 407 returned. The list can also be of the form: 408 409 * ['name', True|False] 410 411 which will simply check for the existence (True) or 412 non-existence (False) of the attribute. 413 414 :type replace: bool 415 :param replace: Whether the attribute values passed in will replace 416 existing values or will be added as addition values. 417 Defaults to True. 418 419 :rtype: bool 420 :return: True if successful 421 """ 422 domain, domain_name = self.get_domain_and_name(domain_or_name) 423 params = {'DomainName': domain_name, 424 'ItemName': item_name} 425 self._build_name_value_list(params, attributes, replace) 426 if expected_value: 427 self._build_expected_value(params, expected_value) 428 return self.get_status('PutAttributes', params) 429 430 def batch_put_attributes(self, domain_or_name, items, replace=True): 431 """ 432 Store attributes for multiple items in a domain. 433 434 :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object. 435 :param domain_or_name: Either the name of a domain or a Domain object 436 437 :type items: dict or dict-like object 438 :param items: A dictionary-like object. The keys of the dictionary are 439 the item names and the values are themselves dictionaries 440 of attribute names/values, exactly the same as the 441 attribute_names parameter of the scalar put_attributes 442 call. 443 444 :type replace: bool 445 :param replace: Whether the attribute values passed in will replace 446 existing values or will be added as addition values. 447 Defaults to True. 448 449 :rtype: bool 450 :return: True if successful 451 """ 452 domain, domain_name = self.get_domain_and_name(domain_or_name) 453 params = {'DomainName': domain_name} 454 self._build_batch_list(params, items, replace) 455 return self.get_status('BatchPutAttributes', params, verb='POST') 456 457 def get_attributes(self, domain_or_name, item_name, attribute_names=None, 458 consistent_read=False, item=None): 459 """ 460 Retrieve attributes for a given item in a domain. 461 462 :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object. 463 :param domain_or_name: Either the name of a domain or a Domain object 464 465 :type item_name: string 466 :param item_name: The name of the item whose attributes are 467 being retrieved. 468 469 :type attribute_names: string or list of strings 470 :param attribute_names: An attribute name or list of attribute names. 471 This parameter is optional. If not supplied, all attributes will 472 be retrieved for the item. 473 474 :type consistent_read: bool 475 :param consistent_read: When set to true, ensures that the most recent 476 data is returned. 477 478 :type item: :class:`boto.sdb.item.Item` 479 :keyword item: Instead of instantiating a new Item object, you may 480 specify one to update. 481 482 :rtype: :class:`boto.sdb.item.Item` 483 :return: An Item with the requested attribute name/values set on it 484 """ 485 domain, domain_name = self.get_domain_and_name(domain_or_name) 486 params = {'DomainName': domain_name, 487 'ItemName': item_name} 488 if consistent_read: 489 params['ConsistentRead'] = 'true' 490 if attribute_names: 491 if not isinstance(attribute_names, list): 492 attribute_names = [attribute_names] 493 self.build_list_params(params, attribute_names, 'AttributeName') 494 response = self.make_request('GetAttributes', params) 495 body = response.read() 496 if response.status == 200: 497 if item is None: 498 item = self.item_cls(domain, item_name) 499 h = handler.XmlHandler(item, self) 500 xml.sax.parseString(body, h) 501 return item 502 else: 503 raise SDBResponseError(response.status, response.reason, body) 504 505 def delete_attributes(self, domain_or_name, item_name, attr_names=None, 506 expected_value=None): 507 """ 508 Delete attributes from a given item in a domain. 509 510 :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object. 511 :param domain_or_name: Either the name of a domain or a Domain object 512 513 :type item_name: string 514 :param item_name: The name of the item whose attributes are being 515 deleted. 516 517 :type attributes: dict, list or :class:`boto.sdb.item.Item` 518 :param attributes: Either a list containing attribute names which 519 will cause all values associated with that attribute 520 name to be deleted or a dict or Item containing the 521 attribute names and keys and list of values to 522 delete as the value. If no value is supplied, 523 all attribute name/values for the item will be 524 deleted. 525 526 :type expected_value: list 527 :param expected_value: If supplied, this is a list or tuple consisting 528 of a single attribute name and expected value. The list can be 529 of the form: 530 531 * ['name', 'value'] 532 533 In which case the call will first verify that the attribute "name" 534 of this item has a value of "value". If it does, the delete 535 will proceed, otherwise a ConditionalCheckFailed error will be 536 returned. The list can also be of the form: 537 538 * ['name', True|False] 539 540 which will simply check for the existence (True) or 541 non-existence (False) of the attribute. 542 543 :rtype: bool 544 :return: True if successful 545 """ 546 domain, domain_name = self.get_domain_and_name(domain_or_name) 547 params = {'DomainName': domain_name, 548 'ItemName': item_name} 549 if attr_names: 550 if isinstance(attr_names, list): 551 self._build_name_list(params, attr_names) 552 elif isinstance(attr_names, dict) or isinstance(attr_names, self.item_cls): 553 self._build_name_value_list(params, attr_names) 554 if expected_value: 555 self._build_expected_value(params, expected_value) 556 return self.get_status('DeleteAttributes', params) 557 558 def batch_delete_attributes(self, domain_or_name, items): 559 """ 560 Delete multiple items in a domain. 561 562 :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object. 563 :param domain_or_name: Either the name of a domain or a Domain object 564 565 :type items: dict or dict-like object 566 :param items: A dictionary-like object. The keys of the dictionary are 567 the item names and the values are either: 568 569 * dictionaries of attribute names/values, exactly the 570 same as the attribute_names parameter of the scalar 571 put_attributes call. The attribute name/value pairs 572 will only be deleted if they match the name/value 573 pairs passed in. 574 * None which means that all attributes associated 575 with the item should be deleted. 576 577 :return: True if successful 578 """ 579 domain, domain_name = self.get_domain_and_name(domain_or_name) 580 params = {'DomainName': domain_name} 581 self._build_batch_list(params, items, False) 582 return self.get_status('BatchDeleteAttributes', params, verb='POST') 583 584 def select(self, domain_or_name, query='', next_token=None, 585 consistent_read=False): 586 """ 587 Returns a set of Attributes for item names within domain_name that 588 match the query. The query must be expressed in using the SELECT 589 style syntax rather than the original SimpleDB query language. 590 Even though the select request does not require a domain object, 591 a domain object must be passed into this method so the Item objects 592 returned can point to the appropriate domain. 593 594 :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object 595 :param domain_or_name: Either the name of a domain or a Domain object 596 597 :type query: string 598 :param query: The SimpleDB query to be performed. 599 600 :type consistent_read: bool 601 :param consistent_read: When set to true, ensures that the most recent 602 data is returned. 603 604 :rtype: ResultSet 605 :return: An iterator containing the results. 606 """ 607 domain, domain_name = self.get_domain_and_name(domain_or_name) 608 params = {'SelectExpression': query} 609 if consistent_read: 610 params['ConsistentRead'] = 'true' 611 if next_token: 612 params['NextToken'] = next_token 613 try: 614 return self.get_list('Select', params, [('Item', self.item_cls)], 615 parent=domain) 616 except SDBResponseError as e: 617 e.body = "Query: %s\n%s" % (query, e.body) 618 raise e 619