OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2012 Mitch Garnaat http://garnaat.org/ |
| 2 # Copyright (c) 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved |
| 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 # |
| 23 |
| 24 from boto.dynamodb.batch import BatchList |
| 25 from boto.dynamodb.schema import Schema |
| 26 from boto.dynamodb.item import Item |
| 27 from boto.dynamodb import exceptions as dynamodb_exceptions |
| 28 import time |
| 29 |
| 30 class TableBatchGenerator(object): |
| 31 """ |
| 32 A low-level generator used to page through results from |
| 33 batch_get_item operations. |
| 34 |
| 35 :ivar consumed_units: An integer that holds the number of |
| 36 ConsumedCapacityUnits accumulated thus far for this |
| 37 generator. |
| 38 """ |
| 39 |
| 40 def __init__(self, table, keys, attributes_to_get=None, consistent_read=Fals
e): |
| 41 self.table = table |
| 42 self.keys = keys |
| 43 self.consumed_units = 0 |
| 44 self.attributes_to_get = attributes_to_get |
| 45 self.consistent_read = consistent_read |
| 46 |
| 47 def _queue_unprocessed(self, res): |
| 48 if not u'UnprocessedKeys' in res: |
| 49 return |
| 50 if not self.table.name in res[u'UnprocessedKeys']: |
| 51 return |
| 52 |
| 53 keys = res[u'UnprocessedKeys'][self.table.name][u'Keys'] |
| 54 |
| 55 for key in keys: |
| 56 h = key[u'HashKeyElement'] |
| 57 r = key[u'RangeKeyElement'] if u'RangeKeyElement' in key else None |
| 58 self.keys.append((h, r)) |
| 59 |
| 60 def __iter__(self): |
| 61 while self.keys: |
| 62 # Build the next batch |
| 63 batch = BatchList(self.table.layer2) |
| 64 batch.add_batch(self.table, self.keys[:100], self.attributes_to_get) |
| 65 res = batch.submit() |
| 66 |
| 67 # parse the results |
| 68 if not self.table.name in res[u'Responses']: |
| 69 continue |
| 70 self.consumed_units += res[u'Responses'][self.table.name][u'Consumed
CapacityUnits'] |
| 71 for elem in res[u'Responses'][self.table.name][u'Items']: |
| 72 yield elem |
| 73 |
| 74 # re-queue un processed keys |
| 75 self.keys = self.keys[100:] |
| 76 self._queue_unprocessed(res) |
| 77 |
| 78 |
| 79 class Table(object): |
| 80 """ |
| 81 An Amazon DynamoDB table. |
| 82 |
| 83 :ivar name: The name of the table. |
| 84 :ivar create_time: The date and time that the table was created. |
| 85 :ivar status: The current status of the table. One of: |
| 86 'ACTIVE', 'UPDATING', 'DELETING'. |
| 87 :ivar schema: A :class:`boto.dynamodb.schema.Schema` object representing |
| 88 the schema defined for the table. |
| 89 :ivar item_count: The number of items in the table. This value is |
| 90 set only when the Table object is created or refreshed and |
| 91 may not reflect the actual count. |
| 92 :ivar size_bytes: Total size of the specified table, in bytes. |
| 93 Amazon DynamoDB updates this value approximately every six hours. |
| 94 Recent changes might not be reflected in this value. |
| 95 :ivar read_units: The ReadCapacityUnits of the tables |
| 96 Provisioned Throughput. |
| 97 :ivar write_units: The WriteCapacityUnits of the tables |
| 98 Provisioned Throughput. |
| 99 :ivar schema: The Schema object associated with the table. |
| 100 """ |
| 101 |
| 102 def __init__(self, layer2, response): |
| 103 """ |
| 104 |
| 105 :type layer2: :class:`boto.dynamodb.layer2.Layer2` |
| 106 :param layer2: A `Layer2` api object. |
| 107 |
| 108 :type response: dict |
| 109 :param response: The output of |
| 110 `boto.dynamodb.layer1.Layer1.describe_table`. |
| 111 |
| 112 """ |
| 113 self.layer2 = layer2 |
| 114 self._dict = {} |
| 115 self.update_from_response(response) |
| 116 |
| 117 def __repr__(self): |
| 118 return 'Table(%s)' % self.name |
| 119 |
| 120 @property |
| 121 def name(self): |
| 122 return self._dict['TableName'] |
| 123 |
| 124 @property |
| 125 def create_time(self): |
| 126 return self._dict['CreationDateTime'] |
| 127 |
| 128 @property |
| 129 def status(self): |
| 130 return self._dict['TableStatus'] |
| 131 |
| 132 @property |
| 133 def item_count(self): |
| 134 return self._dict.get('ItemCount', 0) |
| 135 |
| 136 @property |
| 137 def size_bytes(self): |
| 138 return self._dict.get('TableSizeBytes', 0) |
| 139 |
| 140 @property |
| 141 def schema(self): |
| 142 return self._schema |
| 143 |
| 144 @property |
| 145 def read_units(self): |
| 146 return self._dict['ProvisionedThroughput']['ReadCapacityUnits'] |
| 147 |
| 148 @property |
| 149 def write_units(self): |
| 150 return self._dict['ProvisionedThroughput']['WriteCapacityUnits'] |
| 151 |
| 152 def update_from_response(self, response): |
| 153 """ |
| 154 Update the state of the Table object based on the response |
| 155 data received from Amazon DynamoDB. |
| 156 """ |
| 157 if 'Table' in response: |
| 158 self._dict.update(response['Table']) |
| 159 elif 'TableDescription' in response: |
| 160 self._dict.update(response['TableDescription']) |
| 161 if 'KeySchema' in self._dict: |
| 162 self._schema = Schema(self._dict['KeySchema']) |
| 163 |
| 164 def refresh(self, wait_for_active=False, retry_seconds=5): |
| 165 """ |
| 166 Refresh all of the fields of the Table object by calling |
| 167 the underlying DescribeTable request. |
| 168 |
| 169 :type wait_for_active: bool |
| 170 :param wait_for_active: If True, this command will not return |
| 171 until the table status, as returned from Amazon DynamoDB, is |
| 172 'ACTIVE'. |
| 173 |
| 174 :type retry_seconds: int |
| 175 :param retry_seconds: If wait_for_active is True, this |
| 176 parameter controls the number of seconds of delay between |
| 177 calls to update_table in Amazon DynamoDB. Default is 5 seconds. |
| 178 """ |
| 179 done = False |
| 180 while not done: |
| 181 response = self.layer2.describe_table(self.name) |
| 182 self.update_from_response(response) |
| 183 if wait_for_active: |
| 184 if self.status == 'ACTIVE': |
| 185 done = True |
| 186 else: |
| 187 time.sleep(retry_seconds) |
| 188 else: |
| 189 done = True |
| 190 |
| 191 def update_throughput(self, read_units, write_units): |
| 192 """ |
| 193 Update the ProvisionedThroughput for the Amazon DynamoDB Table. |
| 194 |
| 195 :type read_units: int |
| 196 :param read_units: The new value for ReadCapacityUnits. |
| 197 |
| 198 :type write_units: int |
| 199 :param write_units: The new value for WriteCapacityUnits. |
| 200 """ |
| 201 self.layer2.update_throughput(self, read_units, write_units) |
| 202 |
| 203 def delete(self): |
| 204 """ |
| 205 Delete this table and all items in it. After calling this |
| 206 the Table objects status attribute will be set to 'DELETING'. |
| 207 """ |
| 208 self.layer2.delete_table(self) |
| 209 |
| 210 def get_item(self, hash_key, range_key=None, |
| 211 attributes_to_get=None, consistent_read=False, |
| 212 item_class=Item): |
| 213 """ |
| 214 Retrieve an existing item from the table. |
| 215 |
| 216 :type hash_key: int|long|float|str|unicode |
| 217 :param hash_key: The HashKey of the requested item. The |
| 218 type of the value must match the type defined in the |
| 219 schema for the table. |
| 220 |
| 221 :type range_key: int|long|float|str|unicode |
| 222 :param range_key: The optional RangeKey of the requested item. |
| 223 The type of the value must match the type defined in the |
| 224 schema for the table. |
| 225 |
| 226 :type attributes_to_get: list |
| 227 :param attributes_to_get: A list of attribute names. |
| 228 If supplied, only the specified attribute names will |
| 229 be returned. Otherwise, all attributes will be returned. |
| 230 |
| 231 :type consistent_read: bool |
| 232 :param consistent_read: If True, a consistent read |
| 233 request is issued. Otherwise, an eventually consistent |
| 234 request is issued. |
| 235 |
| 236 :type item_class: Class |
| 237 :param item_class: Allows you to override the class used |
| 238 to generate the items. This should be a subclass of |
| 239 :class:`boto.dynamodb.item.Item` |
| 240 """ |
| 241 return self.layer2.get_item(self, hash_key, range_key, |
| 242 attributes_to_get, consistent_read, |
| 243 item_class) |
| 244 lookup = get_item |
| 245 |
| 246 def has_item(self, hash_key, range_key=None, consistent_read=False): |
| 247 """ |
| 248 Checks the table to see if the Item with the specified ``hash_key`` |
| 249 exists. This may save a tiny bit of time/bandwidth over a |
| 250 straight :py:meth:`get_item` if you have no intention to touch |
| 251 the data that is returned, since this method specifically tells |
| 252 Amazon not to return anything but the Item's key. |
| 253 |
| 254 :type hash_key: int|long|float|str|unicode |
| 255 :param hash_key: The HashKey of the requested item. The |
| 256 type of the value must match the type defined in the |
| 257 schema for the table. |
| 258 |
| 259 :type range_key: int|long|float|str|unicode |
| 260 :param range_key: The optional RangeKey of the requested item. |
| 261 The type of the value must match the type defined in the |
| 262 schema for the table. |
| 263 |
| 264 :type consistent_read: bool |
| 265 :param consistent_read: If True, a consistent read |
| 266 request is issued. Otherwise, an eventually consistent |
| 267 request is issued. |
| 268 |
| 269 :rtype: bool |
| 270 :returns: ``True`` if the Item exists, ``False`` if not. |
| 271 """ |
| 272 try: |
| 273 # Attempt to get the key. If it can't be found, it'll raise |
| 274 # an exception. |
| 275 self.get_item(hash_key, range_key=range_key, |
| 276 # This minimizes the size of the response body. |
| 277 attributes_to_get=[hash_key], |
| 278 consistent_read=consistent_read) |
| 279 except dynamodb_exceptions.DynamoDBKeyNotFoundError: |
| 280 # Key doesn't exist. |
| 281 return False |
| 282 return True |
| 283 |
| 284 def new_item(self, hash_key=None, range_key=None, attrs=None, |
| 285 item_class=Item): |
| 286 """ |
| 287 Return an new, unsaved Item which can later be PUT to |
| 288 Amazon DynamoDB. |
| 289 |
| 290 This method has explicit (but optional) parameters for |
| 291 the hash_key and range_key values of the item. You can use |
| 292 these explicit parameters when calling the method, such as:: |
| 293 |
| 294 >>> my_item = my_table.new_item(hash_key='a', range_key=1, |
| 295 attrs={'key1': 'val1', 'key2': 'val2'}) |
| 296 >>> my_item |
| 297 {u'bar': 1, u'foo': 'a', 'key1': 'val1', 'key2': 'val2'} |
| 298 |
| 299 Or, if you prefer, you can simply put the hash_key and range_key |
| 300 in the attrs dictionary itself, like this:: |
| 301 |
| 302 >>> attrs = {'foo': 'a', 'bar': 1, 'key1': 'val1', 'key2': 'val2'} |
| 303 >>> my_item = my_table.new_item(attrs=attrs) |
| 304 >>> my_item |
| 305 {u'bar': 1, u'foo': 'a', 'key1': 'val1', 'key2': 'val2'} |
| 306 |
| 307 The effect is the same. |
| 308 |
| 309 .. note: |
| 310 The explicit parameters take priority over the values in |
| 311 the attrs dict. So, if you have a hash_key or range_key |
| 312 in the attrs dict and you also supply either or both using |
| 313 the explicit parameters, the values in the attrs will be |
| 314 ignored. |
| 315 |
| 316 :type hash_key: int|long|float|str|unicode |
| 317 :param hash_key: The HashKey of the new item. The |
| 318 type of the value must match the type defined in the |
| 319 schema for the table. |
| 320 |
| 321 :type range_key: int|long|float|str|unicode |
| 322 :param range_key: The optional RangeKey of the new item. |
| 323 The type of the value must match the type defined in the |
| 324 schema for the table. |
| 325 |
| 326 :type attrs: dict |
| 327 :param attrs: A dictionary of key value pairs used to |
| 328 populate the new item. |
| 329 |
| 330 :type item_class: Class |
| 331 :param item_class: Allows you to override the class used |
| 332 to generate the items. This should be a subclass of |
| 333 :class:`boto.dynamodb.item.Item` |
| 334 """ |
| 335 return item_class(self, hash_key, range_key, attrs) |
| 336 |
| 337 def query(self, hash_key, range_key_condition=None, |
| 338 attributes_to_get=None, request_limit=None, |
| 339 max_results=None, consistent_read=False, |
| 340 scan_index_forward=True, exclusive_start_key=None, |
| 341 item_class=Item): |
| 342 """ |
| 343 Perform a query on the table. |
| 344 |
| 345 :type hash_key: int|long|float|str|unicode |
| 346 :param hash_key: The HashKey of the requested item. The |
| 347 type of the value must match the type defined in the |
| 348 schema for the table. |
| 349 |
| 350 :type range_key_condition: :class:`boto.dynamodb.condition.Condition` |
| 351 :param range_key_condition: A Condition object. |
| 352 Condition object can be one of the following types: |
| 353 |
| 354 EQ|LE|LT|GE|GT|BEGINS_WITH|BETWEEN |
| 355 |
| 356 The only condition which expects or will accept two |
| 357 values is 'BETWEEN', otherwise a single value should |
| 358 be passed to the Condition constructor. |
| 359 |
| 360 :type attributes_to_get: list |
| 361 :param attributes_to_get: A list of attribute names. |
| 362 If supplied, only the specified attribute names will |
| 363 be returned. Otherwise, all attributes will be returned. |
| 364 |
| 365 :type request_limit: int |
| 366 :param request_limit: The maximum number of items to retrieve |
| 367 from Amazon DynamoDB on each request. You may want to set |
| 368 a specific request_limit based on the provisioned throughput |
| 369 of your table. The default behavior is to retrieve as many |
| 370 results as possible per request. |
| 371 |
| 372 :type max_results: int |
| 373 :param max_results: The maximum number of results that will |
| 374 be retrieved from Amazon DynamoDB in total. For example, |
| 375 if you only wanted to see the first 100 results from the |
| 376 query, regardless of how many were actually available, you |
| 377 could set max_results to 100 and the generator returned |
| 378 from the query method will only yeild 100 results max. |
| 379 |
| 380 :type consistent_read: bool |
| 381 :param consistent_read: If True, a consistent read |
| 382 request is issued. Otherwise, an eventually consistent |
| 383 request is issued. |
| 384 |
| 385 :type scan_index_forward: bool |
| 386 :param scan_index_forward: Specified forward or backward |
| 387 traversal of the index. Default is forward (True). |
| 388 |
| 389 :type exclusive_start_key: list or tuple |
| 390 :param exclusive_start_key: Primary key of the item from |
| 391 which to continue an earlier query. This would be |
| 392 provided as the LastEvaluatedKey in that query. |
| 393 |
| 394 :type item_class: Class |
| 395 :param item_class: Allows you to override the class used |
| 396 to generate the items. This should be a subclass of |
| 397 :class:`boto.dynamodb.item.Item` |
| 398 """ |
| 399 return self.layer2.query(self, hash_key, range_key_condition, |
| 400 attributes_to_get, request_limit, |
| 401 max_results, consistent_read, |
| 402 scan_index_forward, exclusive_start_key, |
| 403 item_class=item_class) |
| 404 |
| 405 def scan(self, scan_filter=None, |
| 406 attributes_to_get=None, request_limit=None, max_results=None, |
| 407 count=False, exclusive_start_key=None, item_class=Item): |
| 408 """ |
| 409 Scan through this table, this is a very long |
| 410 and expensive operation, and should be avoided if |
| 411 at all possible. |
| 412 |
| 413 :type scan_filter: A dict |
| 414 :param scan_filter: A dictionary where the key is the |
| 415 attribute name and the value is a |
| 416 :class:`boto.dynamodb.condition.Condition` object. |
| 417 Valid Condition objects include: |
| 418 |
| 419 * EQ - equal (1) |
| 420 * NE - not equal (1) |
| 421 * LE - less than or equal (1) |
| 422 * LT - less than (1) |
| 423 * GE - greater than or equal (1) |
| 424 * GT - greater than (1) |
| 425 * NOT_NULL - attribute exists (0, use None) |
| 426 * NULL - attribute does not exist (0, use None) |
| 427 * CONTAINS - substring or value in list (1) |
| 428 * NOT_CONTAINS - absence of substring or value in list (1) |
| 429 * BEGINS_WITH - substring prefix (1) |
| 430 * IN - exact match in list (N) |
| 431 * BETWEEN - >= first value, <= second value (2) |
| 432 |
| 433 :type attributes_to_get: list |
| 434 :param attributes_to_get: A list of attribute names. |
| 435 If supplied, only the specified attribute names will |
| 436 be returned. Otherwise, all attributes will be returned. |
| 437 |
| 438 :type request_limit: int |
| 439 :param request_limit: The maximum number of items to retrieve |
| 440 from Amazon DynamoDB on each request. You may want to set |
| 441 a specific request_limit based on the provisioned throughput |
| 442 of your table. The default behavior is to retrieve as many |
| 443 results as possible per request. |
| 444 |
| 445 :type max_results: int |
| 446 :param max_results: The maximum number of results that will |
| 447 be retrieved from Amazon DynamoDB in total. For example, |
| 448 if you only wanted to see the first 100 results from the |
| 449 query, regardless of how many were actually available, you |
| 450 could set max_results to 100 and the generator returned |
| 451 from the query method will only yeild 100 results max. |
| 452 |
| 453 :type count: bool |
| 454 :param count: If True, Amazon DynamoDB returns a total |
| 455 number of items for the Scan operation, even if the |
| 456 operation has no matching items for the assigned filter. |
| 457 |
| 458 :type exclusive_start_key: list or tuple |
| 459 :param exclusive_start_key: Primary key of the item from |
| 460 which to continue an earlier query. This would be |
| 461 provided as the LastEvaluatedKey in that query. |
| 462 |
| 463 :type item_class: Class |
| 464 :param item_class: Allows you to override the class used |
| 465 to generate the items. This should be a subclass of |
| 466 :class:`boto.dynamodb.item.Item` |
| 467 |
| 468 :return: A TableGenerator (generator) object which will iterate over all
results |
| 469 :rtype: :class:`boto.dynamodb.layer2.TableGenerator` |
| 470 """ |
| 471 return self.layer2.scan(self, scan_filter, attributes_to_get, |
| 472 request_limit, max_results, count, |
| 473 exclusive_start_key, item_class=item_class) |
| 474 |
| 475 def batch_get_item(self, keys, attributes_to_get=None): |
| 476 """ |
| 477 Return a set of attributes for a multiple items from a single table |
| 478 using their primary keys. This abstraction removes the 100 Items per |
| 479 batch limitations as well as the "UnprocessedKeys" logic. |
| 480 |
| 481 :type keys: list |
| 482 :param keys: A list of scalar or tuple values. Each element in the |
| 483 list represents one Item to retrieve. If the schema for the |
| 484 table has both a HashKey and a RangeKey, each element in the |
| 485 list should be a tuple consisting of (hash_key, range_key). If |
| 486 the schema for the table contains only a HashKey, each element |
| 487 in the list should be a scalar value of the appropriate type |
| 488 for the table schema. NOTE: The maximum number of items that |
| 489 can be retrieved for a single operation is 100. Also, the |
| 490 number of items retrieved is constrained by a 1 MB size limit. |
| 491 |
| 492 :type attributes_to_get: list |
| 493 :param attributes_to_get: A list of attribute names. |
| 494 If supplied, only the specified attribute names will |
| 495 be returned. Otherwise, all attributes will be returned. |
| 496 |
| 497 :return: A TableBatchGenerator (generator) object which will iterate ove
r all results |
| 498 :rtype: :class:`boto.dynamodb.table.TableBatchGenerator` |
| 499 """ |
| 500 return TableBatchGenerator(self, keys, attributes_to_get) |
OLD | NEW |