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 import time |
| 24 from binascii import crc32 |
| 25 |
| 26 import boto |
| 27 from boto.connection import AWSAuthConnection |
| 28 from boto.exception import DynamoDBResponseError |
| 29 from boto.provider import Provider |
| 30 from boto.dynamodb import exceptions as dynamodb_exceptions |
| 31 from boto.compat import json |
| 32 |
| 33 # |
| 34 # To get full debug output, uncomment the following line and set the |
| 35 # value of Debug to be 2 |
| 36 # |
| 37 #boto.set_stream_logger('dynamodb') |
| 38 Debug = 0 |
| 39 |
| 40 |
| 41 class Layer1(AWSAuthConnection): |
| 42 """ |
| 43 This is the lowest-level interface to DynamoDB. Methods at this |
| 44 layer map directly to API requests and parameters to the methods |
| 45 are either simple, scalar values or they are the Python equivalent |
| 46 of the JSON input as defined in the DynamoDB Developer's Guide. |
| 47 All responses are direct decoding of the JSON response bodies to |
| 48 Python data structures via the json or simplejson modules. |
| 49 |
| 50 :ivar throughput_exceeded_events: An integer variable that |
| 51 keeps a running total of the number of ThroughputExceeded |
| 52 responses this connection has received from Amazon DynamoDB. |
| 53 """ |
| 54 |
| 55 DefaultRegionName = 'us-east-1' |
| 56 """The default region name for DynamoDB API.""" |
| 57 |
| 58 ServiceName = 'DynamoDB' |
| 59 """The name of the Service""" |
| 60 |
| 61 Version = '20111205' |
| 62 """DynamoDB API version.""" |
| 63 |
| 64 ThruputError = "ProvisionedThroughputExceededException" |
| 65 """The error response returned when provisioned throughput is exceeded""" |
| 66 |
| 67 SessionExpiredError = 'com.amazon.coral.service#ExpiredTokenException' |
| 68 """The error response returned when session token has expired""" |
| 69 |
| 70 ConditionalCheckFailedError = 'ConditionalCheckFailedException' |
| 71 """The error response returned when a conditional check fails""" |
| 72 |
| 73 ValidationError = 'ValidationException' |
| 74 """The error response returned when an item is invalid in some way""" |
| 75 |
| 76 ResponseError = DynamoDBResponseError |
| 77 |
| 78 NumberRetries = 10 |
| 79 """The number of times an error is retried.""" |
| 80 |
| 81 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, |
| 82 is_secure=True, port=None, proxy=None, proxy_port=None, |
| 83 debug=0, security_token=None, region=None, |
| 84 validate_certs=True, validate_checksums=True): |
| 85 if not region: |
| 86 region_name = boto.config.get('DynamoDB', 'region', |
| 87 self.DefaultRegionName) |
| 88 for reg in boto.dynamodb.regions(): |
| 89 if reg.name == region_name: |
| 90 region = reg |
| 91 break |
| 92 |
| 93 self.region = region |
| 94 AWSAuthConnection.__init__(self, self.region.endpoint, |
| 95 aws_access_key_id, |
| 96 aws_secret_access_key, |
| 97 is_secure, port, proxy, proxy_port, |
| 98 debug=debug, security_token=security_token, |
| 99 validate_certs=validate_certs) |
| 100 self.throughput_exceeded_events = 0 |
| 101 self._validate_checksums = boto.config.getbool( |
| 102 'DynamoDB', 'validate_checksums', validate_checksums) |
| 103 |
| 104 def _get_session_token(self): |
| 105 self.provider = Provider(self._provider_type) |
| 106 self._auth_handler.update_provider(self.provider) |
| 107 |
| 108 def _required_auth_capability(self): |
| 109 return ['hmac-v4'] |
| 110 |
| 111 def make_request(self, action, body='', object_hook=None): |
| 112 """ |
| 113 :raises: ``DynamoDBExpiredTokenError`` if the security token expires. |
| 114 """ |
| 115 headers = {'X-Amz-Target': '%s_%s.%s' % (self.ServiceName, |
| 116 self.Version, action), |
| 117 'Host': self.region.endpoint, |
| 118 'Content-Type': 'application/x-amz-json-1.0', |
| 119 'Content-Length': str(len(body))} |
| 120 http_request = self.build_base_http_request('POST', '/', '/', |
| 121 {}, headers, body, None) |
| 122 start = time.time() |
| 123 response = self._mexe(http_request, sender=None, |
| 124 override_num_retries=self.NumberRetries, |
| 125 retry_handler=self._retry_handler) |
| 126 elapsed = (time.time() - start) * 1000 |
| 127 request_id = response.getheader('x-amzn-RequestId') |
| 128 boto.log.debug('RequestId: %s' % request_id) |
| 129 boto.perflog.debug('%s: id=%s time=%sms', |
| 130 headers['X-Amz-Target'], request_id, int(elapsed)) |
| 131 response_body = response.read() |
| 132 boto.log.debug(response_body) |
| 133 return json.loads(response_body, object_hook=object_hook) |
| 134 |
| 135 def _retry_handler(self, response, i, next_sleep): |
| 136 status = None |
| 137 if response.status == 400: |
| 138 response_body = response.read() |
| 139 boto.log.debug(response_body) |
| 140 data = json.loads(response_body) |
| 141 if self.ThruputError in data.get('__type'): |
| 142 self.throughput_exceeded_events += 1 |
| 143 msg = "%s, retry attempt %s" % (self.ThruputError, i) |
| 144 next_sleep = self._exponential_time(i) |
| 145 i += 1 |
| 146 status = (msg, i, next_sleep) |
| 147 elif self.SessionExpiredError in data.get('__type'): |
| 148 msg = 'Renewing Session Token' |
| 149 self._get_session_token() |
| 150 status = (msg, i + self.num_retries - 1, 0) |
| 151 elif self.ConditionalCheckFailedError in data.get('__type'): |
| 152 raise dynamodb_exceptions.DynamoDBConditionalCheckFailedError( |
| 153 response.status, response.reason, data) |
| 154 elif self.ValidationError in data.get('__type'): |
| 155 raise dynamodb_exceptions.DynamoDBValidationError( |
| 156 response.status, response.reason, data) |
| 157 else: |
| 158 raise self.ResponseError(response.status, response.reason, |
| 159 data) |
| 160 expected_crc32 = response.getheader('x-amz-crc32') |
| 161 if self._validate_checksums and expected_crc32 is not None: |
| 162 boto.log.debug('Validating crc32 checksum for body: %s', |
| 163 response.read()) |
| 164 actual_crc32 = crc32(response.read()) & 0xffffffff |
| 165 expected_crc32 = int(expected_crc32) |
| 166 if actual_crc32 != expected_crc32: |
| 167 msg = ("The calculated checksum %s did not match the expected " |
| 168 "checksum %s" % (actual_crc32, expected_crc32)) |
| 169 status = (msg, i + 1, self._exponential_time(i)) |
| 170 return status |
| 171 |
| 172 def _exponential_time(self, i): |
| 173 if i == 0: |
| 174 next_sleep = 0 |
| 175 else: |
| 176 next_sleep = 0.05 * (2 ** i) |
| 177 return next_sleep |
| 178 |
| 179 def list_tables(self, limit=None, start_table=None): |
| 180 """ |
| 181 Returns a dictionary of results. The dictionary contains |
| 182 a **TableNames** key whose value is a list of the table names. |
| 183 The dictionary could also contain a **LastEvaluatedTableName** |
| 184 key whose value would be the last table name returned if |
| 185 the complete list of table names was not returned. This |
| 186 value would then be passed as the ``start_table`` parameter on |
| 187 a subsequent call to this method. |
| 188 |
| 189 :type limit: int |
| 190 :param limit: The maximum number of tables to return. |
| 191 |
| 192 :type start_table: str |
| 193 :param start_table: The name of the table that starts the |
| 194 list. If you ran a previous list_tables and not |
| 195 all results were returned, the response dict would |
| 196 include a LastEvaluatedTableName attribute. Use |
| 197 that value here to continue the listing. |
| 198 """ |
| 199 data = {} |
| 200 if limit: |
| 201 data['Limit'] = limit |
| 202 if start_table: |
| 203 data['ExclusiveStartTableName'] = start_table |
| 204 json_input = json.dumps(data) |
| 205 return self.make_request('ListTables', json_input) |
| 206 |
| 207 def describe_table(self, table_name): |
| 208 """ |
| 209 Returns information about the table including current |
| 210 state of the table, primary key schema and when the |
| 211 table was created. |
| 212 |
| 213 :type table_name: str |
| 214 :param table_name: The name of the table to describe. |
| 215 """ |
| 216 data = {'TableName': table_name} |
| 217 json_input = json.dumps(data) |
| 218 return self.make_request('DescribeTable', json_input) |
| 219 |
| 220 def create_table(self, table_name, schema, provisioned_throughput): |
| 221 """ |
| 222 Add a new table to your account. The table name must be unique |
| 223 among those associated with the account issuing the request. |
| 224 This request triggers an asynchronous workflow to begin creating |
| 225 the table. When the workflow is complete, the state of the |
| 226 table will be ACTIVE. |
| 227 |
| 228 :type table_name: str |
| 229 :param table_name: The name of the table to create. |
| 230 |
| 231 :type schema: dict |
| 232 :param schema: A Python version of the KeySchema data structure |
| 233 as defined by DynamoDB |
| 234 |
| 235 :type provisioned_throughput: dict |
| 236 :param provisioned_throughput: A Python version of the |
| 237 ProvisionedThroughput data structure defined by |
| 238 DynamoDB. |
| 239 """ |
| 240 data = {'TableName': table_name, |
| 241 'KeySchema': schema, |
| 242 'ProvisionedThroughput': provisioned_throughput} |
| 243 json_input = json.dumps(data) |
| 244 response_dict = self.make_request('CreateTable', json_input) |
| 245 return response_dict |
| 246 |
| 247 def update_table(self, table_name, provisioned_throughput): |
| 248 """ |
| 249 Updates the provisioned throughput for a given table. |
| 250 |
| 251 :type table_name: str |
| 252 :param table_name: The name of the table to update. |
| 253 |
| 254 :type provisioned_throughput: dict |
| 255 :param provisioned_throughput: A Python version of the |
| 256 ProvisionedThroughput data structure defined by |
| 257 DynamoDB. |
| 258 """ |
| 259 data = {'TableName': table_name, |
| 260 'ProvisionedThroughput': provisioned_throughput} |
| 261 json_input = json.dumps(data) |
| 262 return self.make_request('UpdateTable', json_input) |
| 263 |
| 264 def delete_table(self, table_name): |
| 265 """ |
| 266 Deletes the table and all of it's data. After this request |
| 267 the table will be in the DELETING state until DynamoDB |
| 268 completes the delete operation. |
| 269 |
| 270 :type table_name: str |
| 271 :param table_name: The name of the table to delete. |
| 272 """ |
| 273 data = {'TableName': table_name} |
| 274 json_input = json.dumps(data) |
| 275 return self.make_request('DeleteTable', json_input) |
| 276 |
| 277 def get_item(self, table_name, key, attributes_to_get=None, |
| 278 consistent_read=False, object_hook=None): |
| 279 """ |
| 280 Return a set of attributes for an item that matches |
| 281 the supplied key. |
| 282 |
| 283 :type table_name: str |
| 284 :param table_name: The name of the table containing the item. |
| 285 |
| 286 :type key: dict |
| 287 :param key: A Python version of the Key data structure |
| 288 defined by DynamoDB. |
| 289 |
| 290 :type attributes_to_get: list |
| 291 :param attributes_to_get: A list of attribute names. |
| 292 If supplied, only the specified attribute names will |
| 293 be returned. Otherwise, all attributes will be returned. |
| 294 |
| 295 :type consistent_read: bool |
| 296 :param consistent_read: If True, a consistent read |
| 297 request is issued. Otherwise, an eventually consistent |
| 298 request is issued. |
| 299 """ |
| 300 data = {'TableName': table_name, |
| 301 'Key': key} |
| 302 if attributes_to_get: |
| 303 data['AttributesToGet'] = attributes_to_get |
| 304 if consistent_read: |
| 305 data['ConsistentRead'] = True |
| 306 json_input = json.dumps(data) |
| 307 response = self.make_request('GetItem', json_input, |
| 308 object_hook=object_hook) |
| 309 if 'Item' not in response: |
| 310 raise dynamodb_exceptions.DynamoDBKeyNotFoundError( |
| 311 "Key does not exist." |
| 312 ) |
| 313 return response |
| 314 |
| 315 def batch_get_item(self, request_items, object_hook=None): |
| 316 """ |
| 317 Return a set of attributes for a multiple items in |
| 318 multiple tables using their primary keys. |
| 319 |
| 320 :type request_items: dict |
| 321 :param request_items: A Python version of the RequestItems |
| 322 data structure defined by DynamoDB. |
| 323 """ |
| 324 # If the list is empty, return empty response |
| 325 if not request_items: |
| 326 return {} |
| 327 data = {'RequestItems': request_items} |
| 328 json_input = json.dumps(data) |
| 329 return self.make_request('BatchGetItem', json_input, |
| 330 object_hook=object_hook) |
| 331 |
| 332 def batch_write_item(self, request_items, object_hook=None): |
| 333 """ |
| 334 This operation enables you to put or delete several items |
| 335 across multiple tables in a single API call. |
| 336 |
| 337 :type request_items: dict |
| 338 :param request_items: A Python version of the RequestItems |
| 339 data structure defined by DynamoDB. |
| 340 """ |
| 341 data = {'RequestItems': request_items} |
| 342 json_input = json.dumps(data) |
| 343 return self.make_request('BatchWriteItem', json_input, |
| 344 object_hook=object_hook) |
| 345 |
| 346 def put_item(self, table_name, item, |
| 347 expected=None, return_values=None, |
| 348 object_hook=None): |
| 349 """ |
| 350 Create a new item or replace an old item with a new |
| 351 item (including all attributes). If an item already |
| 352 exists in the specified table with the same primary |
| 353 key, the new item will completely replace the old item. |
| 354 You can perform a conditional put by specifying an |
| 355 expected rule. |
| 356 |
| 357 :type table_name: str |
| 358 :param table_name: The name of the table in which to put the item. |
| 359 |
| 360 :type item: dict |
| 361 :param item: A Python version of the Item data structure |
| 362 defined by DynamoDB. |
| 363 |
| 364 :type expected: dict |
| 365 :param expected: A Python version of the Expected |
| 366 data structure defined by DynamoDB. |
| 367 |
| 368 :type return_values: str |
| 369 :param return_values: Controls the return of attribute |
| 370 name-value pairs before then were changed. Possible |
| 371 values are: None or 'ALL_OLD'. If 'ALL_OLD' is |
| 372 specified and the item is overwritten, the content |
| 373 of the old item is returned. |
| 374 """ |
| 375 data = {'TableName': table_name, |
| 376 'Item': item} |
| 377 if expected: |
| 378 data['Expected'] = expected |
| 379 if return_values: |
| 380 data['ReturnValues'] = return_values |
| 381 json_input = json.dumps(data) |
| 382 return self.make_request('PutItem', json_input, |
| 383 object_hook=object_hook) |
| 384 |
| 385 def update_item(self, table_name, key, attribute_updates, |
| 386 expected=None, return_values=None, |
| 387 object_hook=None): |
| 388 """ |
| 389 Edits an existing item's attributes. You can perform a conditional |
| 390 update (insert a new attribute name-value pair if it doesn't exist, |
| 391 or replace an existing name-value pair if it has certain expected |
| 392 attribute values). |
| 393 |
| 394 :type table_name: str |
| 395 :param table_name: The name of the table. |
| 396 |
| 397 :type key: dict |
| 398 :param key: A Python version of the Key data structure |
| 399 defined by DynamoDB which identifies the item to be updated. |
| 400 |
| 401 :type attribute_updates: dict |
| 402 :param attribute_updates: A Python version of the AttributeUpdates |
| 403 data structure defined by DynamoDB. |
| 404 |
| 405 :type expected: dict |
| 406 :param expected: A Python version of the Expected |
| 407 data structure defined by DynamoDB. |
| 408 |
| 409 :type return_values: str |
| 410 :param return_values: Controls the return of attribute |
| 411 name-value pairs before then were changed. Possible |
| 412 values are: None or 'ALL_OLD'. If 'ALL_OLD' is |
| 413 specified and the item is overwritten, the content |
| 414 of the old item is returned. |
| 415 """ |
| 416 data = {'TableName': table_name, |
| 417 'Key': key, |
| 418 'AttributeUpdates': attribute_updates} |
| 419 if expected: |
| 420 data['Expected'] = expected |
| 421 if return_values: |
| 422 data['ReturnValues'] = return_values |
| 423 json_input = json.dumps(data) |
| 424 return self.make_request('UpdateItem', json_input, |
| 425 object_hook=object_hook) |
| 426 |
| 427 def delete_item(self, table_name, key, |
| 428 expected=None, return_values=None, |
| 429 object_hook=None): |
| 430 """ |
| 431 Delete an item and all of it's attributes by primary key. |
| 432 You can perform a conditional delete by specifying an |
| 433 expected rule. |
| 434 |
| 435 :type table_name: str |
| 436 :param table_name: The name of the table containing the item. |
| 437 |
| 438 :type key: dict |
| 439 :param key: A Python version of the Key data structure |
| 440 defined by DynamoDB. |
| 441 |
| 442 :type expected: dict |
| 443 :param expected: A Python version of the Expected |
| 444 data structure defined by DynamoDB. |
| 445 |
| 446 :type return_values: str |
| 447 :param return_values: Controls the return of attribute |
| 448 name-value pairs before then were changed. Possible |
| 449 values are: None or 'ALL_OLD'. If 'ALL_OLD' is |
| 450 specified and the item is overwritten, the content |
| 451 of the old item is returned. |
| 452 """ |
| 453 data = {'TableName': table_name, |
| 454 'Key': key} |
| 455 if expected: |
| 456 data['Expected'] = expected |
| 457 if return_values: |
| 458 data['ReturnValues'] = return_values |
| 459 json_input = json.dumps(data) |
| 460 return self.make_request('DeleteItem', json_input, |
| 461 object_hook=object_hook) |
| 462 |
| 463 def query(self, table_name, hash_key_value, range_key_conditions=None, |
| 464 attributes_to_get=None, limit=None, consistent_read=False, |
| 465 scan_index_forward=True, exclusive_start_key=None, |
| 466 object_hook=None): |
| 467 """ |
| 468 Perform a query of DynamoDB. This version is currently punting |
| 469 and expecting you to provide a full and correct JSON body |
| 470 which is passed as is to DynamoDB. |
| 471 |
| 472 :type table_name: str |
| 473 :param table_name: The name of the table to query. |
| 474 |
| 475 :type hash_key_value: dict |
| 476 :param key: A DynamoDB-style HashKeyValue. |
| 477 |
| 478 :type range_key_conditions: dict |
| 479 :param range_key_conditions: A Python version of the |
| 480 RangeKeyConditions data structure. |
| 481 |
| 482 :type attributes_to_get: list |
| 483 :param attributes_to_get: A list of attribute names. |
| 484 If supplied, only the specified attribute names will |
| 485 be returned. Otherwise, all attributes will be returned. |
| 486 |
| 487 :type limit: int |
| 488 :param limit: The maximum number of items to return. |
| 489 |
| 490 :type consistent_read: bool |
| 491 :param consistent_read: If True, a consistent read |
| 492 request is issued. Otherwise, an eventually consistent |
| 493 request is issued. |
| 494 |
| 495 :type scan_index_forward: bool |
| 496 :param scan_index_forward: Specified forward or backward |
| 497 traversal of the index. Default is forward (True). |
| 498 |
| 499 :type exclusive_start_key: list or tuple |
| 500 :param exclusive_start_key: Primary key of the item from |
| 501 which to continue an earlier query. This would be |
| 502 provided as the LastEvaluatedKey in that query. |
| 503 """ |
| 504 data = {'TableName': table_name, |
| 505 'HashKeyValue': hash_key_value} |
| 506 if range_key_conditions: |
| 507 data['RangeKeyCondition'] = range_key_conditions |
| 508 if attributes_to_get: |
| 509 data['AttributesToGet'] = attributes_to_get |
| 510 if limit: |
| 511 data['Limit'] = limit |
| 512 if consistent_read: |
| 513 data['ConsistentRead'] = True |
| 514 if scan_index_forward: |
| 515 data['ScanIndexForward'] = True |
| 516 else: |
| 517 data['ScanIndexForward'] = False |
| 518 if exclusive_start_key: |
| 519 data['ExclusiveStartKey'] = exclusive_start_key |
| 520 json_input = json.dumps(data) |
| 521 return self.make_request('Query', json_input, |
| 522 object_hook=object_hook) |
| 523 |
| 524 def scan(self, table_name, scan_filter=None, |
| 525 attributes_to_get=None, limit=None, |
| 526 count=False, exclusive_start_key=None, |
| 527 object_hook=None): |
| 528 """ |
| 529 Perform a scan of DynamoDB. This version is currently punting |
| 530 and expecting you to provide a full and correct JSON body |
| 531 which is passed as is to DynamoDB. |
| 532 |
| 533 :type table_name: str |
| 534 :param table_name: The name of the table to scan. |
| 535 |
| 536 :type scan_filter: dict |
| 537 :param scan_filter: A Python version of the |
| 538 ScanFilter data structure. |
| 539 |
| 540 :type attributes_to_get: list |
| 541 :param attributes_to_get: A list of attribute names. |
| 542 If supplied, only the specified attribute names will |
| 543 be returned. Otherwise, all attributes will be returned. |
| 544 |
| 545 :type limit: int |
| 546 :param limit: The maximum number of items to return. |
| 547 |
| 548 :type count: bool |
| 549 :param count: If True, Amazon DynamoDB returns a total |
| 550 number of items for the Scan operation, even if the |
| 551 operation has no matching items for the assigned filter. |
| 552 |
| 553 :type exclusive_start_key: list or tuple |
| 554 :param exclusive_start_key: Primary key of the item from |
| 555 which to continue an earlier query. This would be |
| 556 provided as the LastEvaluatedKey in that query. |
| 557 """ |
| 558 data = {'TableName': table_name} |
| 559 if scan_filter: |
| 560 data['ScanFilter'] = scan_filter |
| 561 if attributes_to_get: |
| 562 data['AttributesToGet'] = attributes_to_get |
| 563 if limit: |
| 564 data['Limit'] = limit |
| 565 if count: |
| 566 data['Count'] = True |
| 567 if exclusive_start_key: |
| 568 data['ExclusiveStartKey'] = exclusive_start_key |
| 569 json_input = json.dumps(data) |
| 570 return self.make_request('Scan', json_input, object_hook=object_hook) |
OLD | NEW |