diff --git a/moto/dynamodb/exceptions.py b/moto/dynamodb/exceptions.py index 90401dbf1..5f7bed4a1 100644 --- a/moto/dynamodb/exceptions.py +++ b/moto/dynamodb/exceptions.py @@ -1,15 +1,24 @@ +import json +from moto.core.exceptions import JsonRESTError from moto.dynamodb.limits import HASH_KEY_MAX_LENGTH, RANGE_KEY_MAX_LENGTH -class InvalidIndexNameError(ValueError): +class DynamodbException(JsonRESTError): pass -class MockValidationException(ValueError): +class MockValidationException(DynamodbException): + error_type = "com.amazonaws.dynamodb.v20111205#ValidationException" + def __init__(self, message): + super().__init__(MockValidationException.error_type, message=message) self.exception_msg = message +class InvalidIndexNameError(MockValidationException): + pass + + class InvalidUpdateExpressionInvalidDocumentPath(MockValidationException): invalid_update_expression_msg = ( "The document path provided in the update expression is invalid for update" @@ -183,19 +192,37 @@ class IncorrectDataType(MockValidationException): super().__init__(self.inc_data_type_msg) -class ConditionalCheckFailed(ValueError): - msg = "The conditional request failed" +class ConditionalCheckFailed(DynamodbException): + error_type = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException" - def __init__(self): - super().__init__(self.msg) + def __init__(self, msg=None): + super().__init__( + ConditionalCheckFailed.error_type, msg or "The conditional request failed" + ) -class TransactionCanceledException(ValueError): +class TransactionCanceledException(DynamodbException): cancel_reason_msg = "Transaction cancelled, please refer cancellation reasons for specific reasons [{}]" + error_type = "com.amazonaws.dynamodb.v20120810#TransactionCanceledException" def __init__(self, errors): - msg = self.cancel_reason_msg.format(", ".join([str(err) for err in errors])) - super().__init__(msg) + msg = self.cancel_reason_msg.format( + ", ".join([str(code) for code, _ in errors]) + ) + super().__init__( + error_type=TransactionCanceledException.error_type, message=msg + ) + reasons = [ + {"Code": code, "Message": message} if code else {"Code": "None"} + for code, message in errors + ] + self.description = json.dumps( + { + "__type": TransactionCanceledException.error_type, + "CancellationReasons": reasons, + "Message": msg, + } + ) class MultipleTransactionsException(MockValidationException): @@ -240,3 +267,46 @@ class TooManyAddClauses(InvalidUpdateExpression): def __init__(self): super().__init__(self.msg) + + +class ResourceNotFoundException(JsonRESTError): + def __init__(self, msg=None): + err = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" + super().__init__(err, msg or "Requested resource not found") + + +class TableNotFoundException(JsonRESTError): + def __init__(self, name): + err = "com.amazonaws.dynamodb.v20111205#TableNotFoundException" + msg = "Table not found: {}".format(name) + super().__init__(err, msg) + + +class SourceTableNotFoundException(JsonRESTError): + def __init__(self, source_table_name): + er = "com.amazonaws.dynamodb.v20111205#SourceTableNotFoundException" + super().__init__(er, "Source table not found: %s" % source_table_name) + + +class BackupNotFoundException(JsonRESTError): + def __init__(self, backup_arn): + er = "com.amazonaws.dynamodb.v20111205#BackupNotFoundException" + super().__init__(er, "Backup not found: %s" % backup_arn) + + +class TableAlreadyExistsException(JsonRESTError): + def __init__(self, target_table_name): + er = "com.amazonaws.dynamodb.v20111205#TableAlreadyExistsException" + super().__init__(er, "Table already exists: %s" % target_table_name) + + +class ResourceInUseException(JsonRESTError): + def __init__(self): + er = "com.amazonaws.dynamodb.v20111205#ResourceInUseException" + super().__init__(er, "Resource in use") + + +class StreamAlreadyEnabledException(JsonRESTError): + def __init__(self): + er = "com.amazonaws.dynamodb.v20111205#ResourceInUseException" + super().__init__(er, "Cannot enable stream") diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py index 7b3efa247..b72b9681f 100644 --- a/moto/dynamodb/models/__init__.py +++ b/moto/dynamodb/models/__init__.py @@ -25,6 +25,14 @@ from moto.dynamodb.exceptions import ( InvalidAttributeTypeError, MultipleTransactionsException, TooManyTransactionsException, + TableNotFoundException, + ResourceNotFoundException, + SourceTableNotFoundException, + TableAlreadyExistsException, + BackupNotFoundException, + ResourceInUseException, + StreamAlreadyEnabledException, + MockValidationException, ) from moto.dynamodb.models.utilities import bytesize from moto.dynamodb.models.dynamo_type import DynamoType @@ -648,7 +656,7 @@ class Table(CloudFormationModel): overwrite=False, ): if self.hash_key_attr not in item_attrs.keys(): - raise KeyError( + raise MockValidationException( "One or more parameter values were invalid: Missing the key " + self.hash_key_attr + " in the item" @@ -656,7 +664,7 @@ class Table(CloudFormationModel): hash_value = DynamoType(item_attrs.get(self.hash_key_attr)) if self.has_range_key: if self.range_key_attr not in item_attrs.keys(): - raise KeyError( + raise MockValidationException( "One or more parameter values were invalid: Missing the key " + self.range_key_attr + " in the item" @@ -725,7 +733,7 @@ class Table(CloudFormationModel): def get_item(self, hash_key, range_key=None, projection_expression=None): if self.has_range_key and not range_key: - raise ValueError( + raise MockValidationException( "Table has a range key, but no range key was passed into get_item" ) try: @@ -780,7 +788,7 @@ class Table(CloudFormationModel): all_indexes = self.all_indexes() indexes_by_name = dict((i.name, i) for i in all_indexes) if index_name not in indexes_by_name: - raise ValueError( + raise MockValidationException( "Invalid index: %s for table: %s. Available indexes are: %s" % (index_name, self.name, ", ".join(indexes_by_name.keys())) ) @@ -791,7 +799,9 @@ class Table(CloudFormationModel): key for key in index.schema if key["KeyType"] == "HASH" ][0] except IndexError: - raise ValueError("Missing Hash Key. KeySchema: %s" % index.name) + raise MockValidationException( + "Missing Hash Key. KeySchema: %s" % index.name + ) try: index_range_key = [ @@ -904,11 +914,13 @@ class Table(CloudFormationModel): def all_indexes(self): return (self.global_indexes or []) + (self.indexes or []) - def get_index(self, index_name, err=None): + def get_index(self, index_name, error_if_not=False): all_indexes = self.all_indexes() indexes_by_name = dict((i.name, i) for i in all_indexes) - if err and index_name not in indexes_by_name: - raise err + if error_if_not and index_name not in indexes_by_name: + raise InvalidIndexNameError( + "The table does not have the specified index: %s" % index_name + ) return indexes_by_name[index_name] def has_idx_items(self, index_name): @@ -938,10 +950,7 @@ class Table(CloudFormationModel): scanned_count = 0 if index_name: - err = InvalidIndexNameError( - "The table does not have the specified index: %s" % index_name - ) - self.get_index(index_name, err) + self.get_index(index_name, error_if_not=True) items = self.has_idx_items(index_name) else: items = self.all_items() @@ -1182,12 +1191,14 @@ class DynamoDBBackend(BaseBackend): def create_table(self, name, **params): if name in self.tables: - return None + raise ResourceInUseException table = Table(name, region=self.region_name, **params) self.tables[name] = table return table def delete_table(self, name): + if name not in self.tables: + raise ResourceNotFoundException return self.tables.pop(name, None) def describe_endpoints(self): @@ -1211,11 +1222,10 @@ class DynamoDBBackend(BaseBackend): ] def list_tags_of_resource(self, table_arn): - required_table = None for table in self.tables: if self.tables[table].table_arn == table_arn: - required_table = self.tables[table] - return required_table.tags + return self.tables[table].tags + raise ResourceNotFoundException def list_tables(self, limit, exclusive_start_table_name): all_tables = list(self.tables.keys()) @@ -1240,7 +1250,7 @@ class DynamoDBBackend(BaseBackend): return tables, None def describe_table(self, name): - table = self.tables[name] + table = self.get_table(name) return table.describe(base_key="Table") def update_table( @@ -1281,7 +1291,7 @@ class DynamoDBBackend(BaseBackend): stream_specification.get("StreamEnabled") or stream_specification.get("StreamViewType") ) and table.latest_stream_label: - raise ValueError("Table already has stream enabled") + raise StreamAlreadyEnabledException table.set_stream_specification(stream_specification) return table @@ -1338,9 +1348,7 @@ class DynamoDBBackend(BaseBackend): expression_attribute_values=None, overwrite=False, ): - table = self.tables.get(table_name) - if not table: - return None + table = self.get_table(table_name) return table.put_item( item_attrs, expected, @@ -1377,22 +1385,37 @@ class DynamoDBBackend(BaseBackend): if table.hash_key_attr not in keys or ( table.has_range_key and table.range_key_attr not in keys ): - raise ValueError( - "Table has a range key, but no range key was passed into get_item" - ) + # "Table has a range key, but no range key was passed into get_item" + raise MockValidationException("Validation Exception") hash_key = DynamoType(keys[table.hash_key_attr]) range_key = ( DynamoType(keys[table.range_key_attr]) if table.has_range_key else None ) return hash_key, range_key + def get_schema(self, table_name, index_name): + table = self.get_table(table_name) + if index_name: + all_indexes = (table.global_indexes or []) + (table.indexes or []) + indexes_by_name = dict((i.name, i) for i in all_indexes) + if index_name not in indexes_by_name: + raise ResourceNotFoundException( + "Invalid index: {} for table: {}. Available indexes are: {}".format( + index_name, table_name, ", ".join(indexes_by_name.keys()) + ) + ) + + return indexes_by_name[index_name].schema + else: + return table.schema + def get_table(self, table_name): + if table_name not in self.tables: + raise ResourceNotFoundException() return self.tables.get(table_name) def get_item(self, table_name, keys, projection_expression=None): table = self.get_table(table_name) - if not table: - raise ValueError("No table found") hash_key, range_key = self.get_keys_value(table, keys) return table.get_item(hash_key, range_key, projection_expression) @@ -1412,9 +1435,7 @@ class DynamoDBBackend(BaseBackend): filter_expression=None, **filter_kwargs, ): - table = self.tables.get(table_name) - if not table: - return None, None + table = self.get_table(table_name) hash_key = DynamoType(hash_key_dict) range_values = [DynamoType(range_value) for range_value in range_value_dicts] @@ -1448,9 +1469,7 @@ class DynamoDBBackend(BaseBackend): index_name, projection_expression, ): - table = self.tables.get(table_name) - if not table: - return None, None, None + table = self.get_table(table_name) scan_filters = {} for key, (comparison_operator, comparison_values) in filters.items(): @@ -1582,8 +1601,6 @@ class DynamoDBBackend(BaseBackend): condition_expression=None, ): table = self.get_table(table_name) - if not table: - return None hash_value, range_value = self.get_keys_value(table, key) item = table.get_item(hash_value, range_value) @@ -1719,25 +1736,31 @@ class DynamoDBBackend(BaseBackend): ) else: raise ValueError - errors.append(None) + errors.append((None, None)) except MultipleTransactionsException: # Rollback to the original state, and reraise the error self.tables = original_table_state raise MultipleTransactionsException() except Exception as e: # noqa: E722 Do not use bare except - errors.append(type(e).__name__) - if any(errors): + errors.append((type(e).__name__, e.message)) + if set(errors) != set([(None, None)]): # Rollback to the original state, and reraise the errors self.tables = original_table_state raise TransactionCanceledException(errors) def describe_continuous_backups(self, table_name): - table = self.get_table(table_name) + try: + table = self.get_table(table_name) + except ResourceNotFoundException: + raise TableNotFoundException(table_name) return table.continuous_backups def update_continuous_backups(self, table_name, point_in_time_spec): - table = self.get_table(table_name) + try: + table = self.get_table(table_name) + except ResourceNotFoundException: + raise TableNotFoundException(table_name) if ( point_in_time_spec["PointInTimeRecoveryEnabled"] @@ -1759,6 +1782,8 @@ class DynamoDBBackend(BaseBackend): return table.continuous_backups def get_backup(self, backup_arn): + if backup_arn not in self.backups: + raise BackupNotFoundException(backup_arn) return self.backups.get(backup_arn) def list_backups(self, table_name): @@ -1768,9 +1793,10 @@ class DynamoDBBackend(BaseBackend): return backups def create_backup(self, table_name, backup_name): - table = self.get_table(table_name) - if table is None: - raise KeyError() + try: + table = self.get_table(table_name) + except ResourceNotFoundException: + raise TableNotFoundException(table_name) backup = Backup(self, backup_name, table) self.backups[backup.arn] = backup return backup @@ -1791,11 +1817,8 @@ class DynamoDBBackend(BaseBackend): def restore_table_from_backup(self, target_table_name, backup_arn): backup = self.get_backup(backup_arn) - if backup is None: - raise KeyError() - existing_table = self.get_table(target_table_name) - if existing_table is not None: - raise ValueError() + if target_table_name in self.tables: + raise TableAlreadyExistsException(target_table_name) new_table = RestoredTable( target_table_name, region=self.region_name, backup=backup ) @@ -1808,12 +1831,12 @@ class DynamoDBBackend(BaseBackend): copy all items from the source without respect to other arguments. """ - source = self.get_table(source_table_name) - if source is None: - raise KeyError() - existing_table = self.get_table(target_table_name) - if existing_table is not None: - raise ValueError() + try: + source = self.get_table(source_table_name) + except ResourceNotFoundException: + raise SourceTableNotFoundException(source_table_name) + if target_table_name in self.tables: + raise TableAlreadyExistsException(target_table_name) new_table = RestoredPITTable( target_table_name, region=self.region_name, source=source ) diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 1d9afe455..264b772f8 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -8,9 +8,9 @@ from functools import wraps from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores, amz_crc32, amzn_request_id from .exceptions import ( - InvalidIndexNameError, MockValidationException, - TransactionCanceledException, + ResourceNotFoundException, + ConditionalCheckFailed, ) from moto.dynamodb.models import dynamodb_backends, dynamo_json_dump @@ -111,13 +111,6 @@ class DynamoHandler(BaseResponse): if match: return match.split(".")[1] - def error(self, type_, message, status=400): - return ( - status, - self.response_headers, - dynamo_json_dump({"__type": type_, "message": message}), - ) - @property def dynamodb_backend(self): """ @@ -165,19 +158,16 @@ class DynamoHandler(BaseResponse): # check billing mode and get the throughput if "BillingMode" in body.keys() and body["BillingMode"] == "PAY_PER_REQUEST": if "ProvisionedThroughput" in body.keys(): - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error( - er, - "ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST", + raise MockValidationException( + "ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST" ) throughput = None billing_mode = "PAY_PER_REQUEST" else: # Provisioned (default billing mode) throughput = body.get("ProvisionedThroughput") if throughput is None: - return self.error( - "ValidationException", - "One or more parameter values were invalid: ReadCapacityUnits and WriteCapacityUnits must both be specified when BillingMode is PROVISIONED", + raise MockValidationException( + "One or more parameter values were invalid: ReadCapacityUnits and WriteCapacityUnits must both be specified when BillingMode is PROVISIONED" ) billing_mode = "PROVISIONED" # getting ServerSideEncryption details @@ -189,16 +179,14 @@ class DynamoHandler(BaseResponse): # getting the indexes global_indexes = body.get("GlobalSecondaryIndexes") if global_indexes == []: - return self.error( - "ValidationException", - "One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty", + raise MockValidationException( + "One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty" ) global_indexes = global_indexes or [] local_secondary_indexes = body.get("LocalSecondaryIndexes") if local_secondary_indexes == []: - return self.error( - "ValidationException", - "One or more parameter values were invalid: List of LocalSecondaryIndexes is empty", + raise MockValidationException( + "One or more parameter values were invalid: List of LocalSecondaryIndexes is empty" ) local_secondary_indexes = local_secondary_indexes or [] # Verify AttributeDefinitions list all @@ -241,76 +229,62 @@ class DynamoHandler(BaseResponse): sse_specification=sse_spec, tags=tags, ) - if table is not None: - return dynamo_json_dump(table.describe()) - else: - er = "com.amazonaws.dynamodb.v20111205#ResourceInUseException" - return self.error(er, "Resource in use") + return dynamo_json_dump(table.describe()) def _throw_attr_error(self, actual_attrs, expected_attrs, indexes): def dump_list(list_): return str(list_).replace("'", "") - er = "com.amazonaws.dynamodb.v20111205#ValidationException" err_head = "One or more parameter values were invalid: " if len(actual_attrs) > len(expected_attrs): if indexes: - return self.error( - er, + raise MockValidationException( err_head + "Some AttributeDefinitions are not used. AttributeDefinitions: " + dump_list(actual_attrs) + ", keys used: " - + dump_list(expected_attrs), + + dump_list(expected_attrs) ) else: - return self.error( - er, + raise MockValidationException( err_head - + "Number of attributes in KeySchema does not exactly match number of attributes defined in AttributeDefinitions", + + "Number of attributes in KeySchema does not exactly match number of attributes defined in AttributeDefinitions" ) elif len(actual_attrs) < len(expected_attrs): if indexes: - return self.error( - er, + raise MockValidationException( err_head + "Some index key attributes are not defined in AttributeDefinitions. Keys: " + dump_list(list(set(expected_attrs) - set(actual_attrs))) + ", AttributeDefinitions: " - + dump_list(actual_attrs), + + dump_list(actual_attrs) ) else: - return self.error( - er, "Invalid KeySchema: Some index key attribute have no definition" + raise MockValidationException( + "Invalid KeySchema: Some index key attribute have no definition" ) else: if indexes: - return self.error( - er, + raise MockValidationException( err_head + "Some index key attributes are not defined in AttributeDefinitions. Keys: " + dump_list(list(set(expected_attrs) - set(actual_attrs))) + ", AttributeDefinitions: " - + dump_list(actual_attrs), + + dump_list(actual_attrs) ) else: - return self.error( - er, + raise MockValidationException( err_head + "Some index key attributes are not defined in AttributeDefinitions. Keys: " + dump_list(expected_attrs) + ", AttributeDefinitions: " - + dump_list(actual_attrs), + + dump_list(actual_attrs) ) def delete_table(self): name = self.body["TableName"] table = self.dynamodb_backend.delete_table(name) - if table is not None: - return dynamo_json_dump(table.describe()) - else: - er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" - return self.error(er, "Requested resource not found") + return dynamo_json_dump(table.describe()) def describe_endpoints(self): response = {"Endpoints": self.dynamodb_backend.describe_endpoints()} @@ -329,26 +303,22 @@ class DynamoHandler(BaseResponse): return "" def list_tags_of_resource(self): - try: - table_arn = self.body["ResourceArn"] - all_tags = self.dynamodb_backend.list_tags_of_resource(table_arn) - all_tag_keys = [tag["Key"] for tag in all_tags] - marker = self.body.get("NextToken") - if marker: - start = all_tag_keys.index(marker) + 1 - else: - start = 0 - max_items = 10 # there is no default, but using 10 to make testing easier - tags_resp = all_tags[start : start + max_items] - next_marker = None - if len(all_tags) > start + max_items: - next_marker = tags_resp[-1]["Key"] - if next_marker: - return json.dumps({"Tags": tags_resp, "NextToken": next_marker}) - return json.dumps({"Tags": tags_resp}) - except AttributeError: - er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" - return self.error(er, "Requested resource not found") + table_arn = self.body["ResourceArn"] + all_tags = self.dynamodb_backend.list_tags_of_resource(table_arn) + all_tag_keys = [tag["Key"] for tag in all_tags] + marker = self.body.get("NextToken") + if marker: + start = all_tag_keys.index(marker) + 1 + else: + start = 0 + max_items = 10 # there is no default, but using 10 to make testing easier + tags_resp = all_tags[start : start + max_items] + next_marker = None + if len(all_tags) > start + max_items: + next_marker = tags_resp[-1]["Key"] + if next_marker: + return json.dumps({"Tags": tags_resp, "NextToken": next_marker}) + return json.dumps({"Tags": tags_resp}) def update_table(self): name = self.body["TableName"] @@ -357,28 +327,20 @@ class DynamoHandler(BaseResponse): throughput = self.body.get("ProvisionedThroughput", None) billing_mode = self.body.get("BillingMode", None) stream_spec = self.body.get("StreamSpecification", None) - try: - table = self.dynamodb_backend.update_table( - name=name, - attr_definitions=attr_definitions, - global_index=global_index, - throughput=throughput, - billing_mode=billing_mode, - stream_spec=stream_spec, - ) - return dynamo_json_dump(table.describe()) - except ValueError: - er = "com.amazonaws.dynamodb.v20111205#ResourceInUseException" - return self.error(er, "Cannot enable stream") + table = self.dynamodb_backend.update_table( + name=name, + attr_definitions=attr_definitions, + global_index=global_index, + throughput=throughput, + billing_mode=billing_mode, + stream_spec=stream_spec, + ) + return dynamo_json_dump(table.describe()) def describe_table(self): name = self.body["TableName"] - try: - table = self.dynamodb_backend.describe_table(name) - return dynamo_json_dump(table) - except KeyError: - er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" - return self.error(er, "Requested resource not found") + table = self.dynamodb_backend.describe_table(name) + return dynamo_json_dump(table) @include_consumed_capacity() def put_item(self): @@ -387,8 +349,7 @@ class DynamoHandler(BaseResponse): return_values = self.body.get("ReturnValues", "NONE") if return_values not in ("ALL_OLD", "NONE"): - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, "Return values set to invalid value") + raise MockValidationException("Return values set to invalid value") if put_has_empty_keys(item, self.dynamodb_backend.get_table(name)): return get_empty_str_error() @@ -415,36 +376,22 @@ class DynamoHandler(BaseResponse): if condition_expression: overwrite = False - try: - result = self.dynamodb_backend.put_item( - name, - item, - expected, - condition_expression, - expression_attribute_names, - expression_attribute_values, - overwrite, - ) - except MockValidationException as mve: - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, mve.exception_msg) - except KeyError as ke: - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, ke.args[0]) - except ValueError as ve: - er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException" - return self.error(er, str(ve)) + result = self.dynamodb_backend.put_item( + name, + item, + expected, + condition_expression, + expression_attribute_names, + expression_attribute_values, + overwrite, + ) - if result: - item_dict = result.to_json() - if return_values == "ALL_OLD": - item_dict["Attributes"] = existing_attributes - else: - item_dict.pop("Attributes", None) - return dynamo_json_dump(item_dict) + item_dict = result.to_json() + if return_values == "ALL_OLD": + item_dict["Attributes"] = existing_attributes else: - er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" - return self.error(er, "Requested resource not found") + item_dict.pop("Attributes", None) + return dynamo_json_dump(item_dict) def batch_write_item(self): table_batches = self.body["RequestItems"] @@ -455,15 +402,10 @@ class DynamoHandler(BaseResponse): request = list(table_request.values())[0] if request_type == "PutRequest": item = request["Item"] - res = self.dynamodb_backend.put_item(table_name, item) - if not res: - return self.error( - "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException", - "Requested resource not found", - ) + self.dynamodb_backend.put_item(table_name, item) elif request_type == "DeleteRequest": keys = request["Key"] - item = self.dynamodb_backend.delete_item(table_name, keys) + self.dynamodb_backend.delete_item(table_name, keys) response = { "ConsumedCapacity": [ @@ -483,36 +425,26 @@ class DynamoHandler(BaseResponse): @include_consumed_capacity(0.5) def get_item(self): name = self.body["TableName"] - table = self.dynamodb_backend.get_table(name) - if table is None: - return self.error( - "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException", - "Requested resource not found", - ) + self.dynamodb_backend.get_table(name) key = self.body["Key"] projection_expression = self.body.get("ProjectionExpression") expression_attribute_names = self.body.get("ExpressionAttributeNames") if expression_attribute_names == {}: if projection_expression is None: - er = "ValidationException" - return self.error( - er, - "ExpressionAttributeNames can only be specified when using expressions", + raise MockValidationException( + "ExpressionAttributeNames can only be specified when using expressions" ) else: - er = "ValidationException" - return self.error(er, "ExpressionAttributeNames must not be empty") + raise MockValidationException( + "ExpressionAttributeNames must not be empty" + ) expression_attribute_names = expression_attribute_names or {} projection_expression = self._adjust_projection_expression( projection_expression, expression_attribute_names ) - try: - item = self.dynamodb_backend.get_item(name, key, projection_expression) - except ValueError: - er = "com.amazon.coral.validate#ValidationException" - return self.error(er, "Validation Exception") + item = self.dynamodb_backend.get_item(name, key, projection_expression) if item: item_dict = item.describe_attrs(attributes=None) return dynamo_json_dump(item_dict) @@ -529,27 +461,26 @@ class DynamoHandler(BaseResponse): # Scenario 1: We're requesting more than a 100 keys from a single table for table_name, table_request in table_batches.items(): if len(table_request["Keys"]) > 100: - return self.error( - "com.amazonaws.dynamodb.v20111205#ValidationException", + raise MockValidationException( "1 validation error detected: Value at 'requestItems." + table_name - + ".member.keys' failed to satisfy constraint: Member must have length less than or equal to 100", + + ".member.keys' failed to satisfy constraint: Member must have length less than or equal to 100" ) # Scenario 2: We're requesting more than a 100 keys across all tables nr_of_keys_across_all_tables = sum( [len(req["Keys"]) for _, req in table_batches.items()] ) if nr_of_keys_across_all_tables > 100: - return self.error( - "com.amazonaws.dynamodb.v20111205#ValidationException", - "Too many items requested for the BatchGetItem call", + raise MockValidationException( + "Too many items requested for the BatchGetItem call" ) for table_name, table_request in table_batches.items(): keys = table_request["Keys"] if self._contains_duplicates(keys): - er = "com.amazon.coral.validate#ValidationException" - return self.error(er, "Provided list of item keys contains duplicates") + raise MockValidationException( + "Provided list of item keys contains duplicates" + ) attributes_to_get = table_request.get("AttributesToGet") projection_expression = table_request.get("ProjectionExpression") expression_attribute_names = table_request.get( @@ -562,15 +493,9 @@ class DynamoHandler(BaseResponse): results["Responses"][table_name] = [] for key in keys: - try: - item = self.dynamodb_backend.get_item( - table_name, key, projection_expression - ) - except ValueError: - return self.error( - "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException", - "Requested resource not found", - ) + item = self.dynamodb_backend.get_item( + table_name, key, projection_expression + ) if item: item_describe = item.describe_attrs(attributes_to_get) results["Responses"][table_name].append(item_describe["Item"]) @@ -607,31 +532,10 @@ class DynamoHandler(BaseResponse): if key_condition_expression: value_alias_map = self.body.get("ExpressionAttributeValues", {}) - table = self.dynamodb_backend.get_table(name) - - # If table does not exist - if table is None: - return self.error( - "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException", - "Requested resource not found", - ) - index_name = self.body.get("IndexName") - if index_name: - all_indexes = (table.global_indexes or []) + (table.indexes or []) - indexes_by_name = dict((i.name, i) for i in all_indexes) - if index_name not in indexes_by_name: - er = "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" - return self.error( - er, - "Invalid index: {} for table: {}. Available indexes are: {}".format( - index_name, name, ", ".join(indexes_by_name.keys()) - ), - ) - - index = indexes_by_name[index_name].schema - else: - index = table.schema + schema = self.dynamodb_backend.get_schema( + table_name=name, index_name=index_name + ) reverse_attribute_lookup = dict( (v, k) for k, v in self.body.get("ExpressionAttributeNames", {}).items() @@ -642,7 +546,7 @@ class DynamoHandler(BaseResponse): " AND ", key_condition_expression, maxsplit=1, flags=re.IGNORECASE ) - index_hash_key = [key for key in index if key["KeyType"] == "HASH"][0] + index_hash_key = [key for key in schema if key["KeyType"] == "HASH"][0] hash_key_var = reverse_attribute_lookup.get( index_hash_key["AttributeName"], index_hash_key["AttributeName"] ) @@ -656,11 +560,10 @@ class DynamoHandler(BaseResponse): (None, None), ) if hash_key_expression is None: - return self.error( - "ValidationException", + raise MockValidationException( "Query condition missed key schema element: {}".format( hash_key_var - ), + ) ) hash_key_expression = hash_key_expression.strip("()") expressions.pop(i) @@ -698,11 +601,10 @@ class DynamoHandler(BaseResponse): "begins_with" ) ] - return self.error( - "com.amazonaws.dynamodb.v20111205#ValidationException", + raise MockValidationException( "Invalid KeyConditionExpression: Invalid function name; function: {}".format( function_used - ), + ) ) else: # [range_key, =, x] @@ -713,14 +615,13 @@ class DynamoHandler(BaseResponse): supplied_range_key, supplied_range_key ) range_keys = [ - k["AttributeName"] for k in index if k["KeyType"] == "RANGE" + k["AttributeName"] for k in schema if k["KeyType"] == "RANGE" ] if supplied_range_key not in range_keys: - return self.error( - "ValidationException", + raise MockValidationException( "Query condition missed key schema element: {}".format( range_keys[0] - ), + ) ) else: hash_key_expression = key_condition_expression.strip("()") @@ -728,10 +629,7 @@ class DynamoHandler(BaseResponse): range_values = [] if not re.search("[^<>]=", hash_key_expression): - return self.error( - "com.amazonaws.dynamodb.v20111205#ValidationException", - "Query key condition not supported", - ) + raise MockValidationException("Query key condition not supported") hash_key_value_alias = hash_key_expression.split("=")[1].strip() # Temporary fix until we get proper KeyConditionExpression function hash_key = value_alias_map.get( @@ -743,9 +641,8 @@ class DynamoHandler(BaseResponse): query_filters = self.body.get("QueryFilter") if not (key_conditions or query_filters): - return self.error( - "com.amazonaws.dynamodb.v20111205#ValidationException", - "Either KeyConditions or QueryFilter should be present", + raise MockValidationException( + "Either KeyConditions or QueryFilter should be present" ) if key_conditions: @@ -759,16 +656,14 @@ class DynamoHandler(BaseResponse): if key not in (hash_key_name, range_key_name): filter_kwargs[key] = value if hash_key_name is None: - er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" - return self.error(er, "Requested resource not found") + raise ResourceNotFoundException hash_key = key_conditions[hash_key_name]["AttributeValueList"][0] if len(key_conditions) == 1: range_comparison = None range_values = [] else: if range_key_name is None and not filter_kwargs: - er = "com.amazon.coral.validate#ValidationException" - return self.error(er, "Validation Exception") + raise MockValidationException("Validation Exception") else: range_condition = key_conditions.get(range_key_name) if range_condition: @@ -798,9 +693,6 @@ class DynamoHandler(BaseResponse): filter_expression=filter_expression, **filter_kwargs ) - if items is None: - er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" - return self.error(er, "Requested resource not found") result = { "Count": len(items), @@ -867,21 +759,8 @@ class DynamoHandler(BaseResponse): index_name, projection_expression, ) - except InvalidIndexNameError as err: - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, str(err)) except ValueError as err: - er = "com.amazonaws.dynamodb.v20111205#ValidationError" - return self.error(er, "Bad Filter Expression: {0}".format(err)) - except Exception as err: - er = "com.amazonaws.dynamodb.v20111205#InternalFailure" - return self.error(er, "Internal error. {0}".format(err)) - - # Items should be a list, at least an empty one. Is None if table does not exist. - # Should really check this at the beginning - if items is None: - er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" - return self.error(er, "Requested resource not found") + raise MockValidationException("Bad Filter Expression: {0}".format(err)) result = { "Count": len(items), @@ -897,14 +776,13 @@ class DynamoHandler(BaseResponse): key = self.body["Key"] return_values = self.body.get("ReturnValues", "NONE") if return_values not in ("ALL_OLD", "NONE"): - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, "Return values set to invalid value") + raise MockValidationException("Return values set to invalid value") - table = self.dynamodb_backend.get_table(name) - if not table: - er = "com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException" - return self.error( - er, "A condition specified in the operation could not be evaluated." + try: + self.dynamodb_backend.get_table(name) + except ResourceNotFoundException: + raise ConditionalCheckFailed( + "A condition specified in the operation could not be evaluated." ) # Attempt to parse simple ConditionExpressions into an Expected @@ -913,19 +791,13 @@ class DynamoHandler(BaseResponse): expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) - try: - item = self.dynamodb_backend.delete_item( - name, - key, - expression_attribute_names, - expression_attribute_values, - condition_expression, - ) - except ValueError: - er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException" - return self.error( - er, "A condition specified in the operation could not be evaluated." - ) + item = self.dynamodb_backend.delete_item( + name, + key, + expression_attribute_names, + expression_attribute_values, + condition_expression, + ) if item and return_values == "ALL_OLD": item_dict = item.to_json() @@ -941,19 +813,11 @@ class DynamoHandler(BaseResponse): update_expression = self.body.get("UpdateExpression", "").strip() attribute_updates = self.body.get("AttributeUpdates") if update_expression and attribute_updates: - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error( - er, - "Can not use both expression and non-expression parameters in the same request: Non-expression parameters: {AttributeUpdates} Expression parameters: {UpdateExpression}", + raise MockValidationException( + "Can not use both expression and non-expression parameters in the same request: Non-expression parameters: {AttributeUpdates} Expression parameters: {UpdateExpression}" ) # We need to copy the item in order to avoid it being modified by the update_item operation - try: - existing_item = copy.deepcopy(self.dynamodb_backend.get_item(name, key)) - except ValueError: - return self.error( - "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException", - "Requested resource not found", - ) + existing_item = copy.deepcopy(self.dynamodb_backend.get_item(name, key)) if existing_item: existing_attributes = existing_item.to_json()["Attributes"] else: @@ -966,8 +830,7 @@ class DynamoHandler(BaseResponse): "UPDATED_OLD", "UPDATED_NEW", ): - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, "Return values set to invalid value") + raise MockValidationException("Return values set to invalid value") if "Expected" in self.body: expected = self.body["Expected"] @@ -980,26 +843,16 @@ class DynamoHandler(BaseResponse): expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) - try: - item = self.dynamodb_backend.update_item( - name, - key, - update_expression=update_expression, - attribute_updates=attribute_updates, - expression_attribute_names=expression_attribute_names, - expression_attribute_values=expression_attribute_values, - expected=expected, - condition_expression=condition_expression, - ) - except MockValidationException as mve: - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, mve.exception_msg) - except ValueError: - er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException" - return self.error(er, "The conditional request failed") - except TypeError: - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, "Validation Exception") + item = self.dynamodb_backend.update_item( + name, + key, + update_expression=update_expression, + attribute_updates=attribute_updates, + expression_attribute_names=expression_attribute_names, + expression_attribute_values=expression_attribute_values, + expected=expected, + condition_expression=condition_expression, + ) item_dict = item.to_json() item_dict["ConsumedCapacity"] = {"TableName": name, "CapacityUnits": 0.5} @@ -1100,7 +953,7 @@ class DynamoHandler(BaseResponse): % TRANSACTION_MAX_ITEMS ) - return self.error("ValidationException", msg) + raise MockValidationException(msg) ret_consumed_capacity = self.body.get("ReturnConsumedCapacity", "NONE") consumed_capacity = dict() @@ -1109,11 +962,7 @@ class DynamoHandler(BaseResponse): table_name = transact_item["Get"]["TableName"] key = transact_item["Get"]["Key"] - try: - item = self.dynamodb_backend.get_item(table_name, key) - except ValueError: - er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" - return self.error(er, "Requested resource not found") + item = self.dynamodb_backend.get_item(table_name, key) if not item: responses.append({}) @@ -1145,26 +994,13 @@ class DynamoHandler(BaseResponse): def transact_write_items(self): transact_items = self.body["TransactItems"] - try: - self.dynamodb_backend.transact_write_items(transact_items) - except TransactionCanceledException as e: - er = "com.amazonaws.dynamodb.v20111205#TransactionCanceledException" - return self.error(er, str(e)) - except MockValidationException as mve: - er = "com.amazonaws.dynamodb.v20111205#ValidationException" - return self.error(er, mve.exception_msg) + self.dynamodb_backend.transact_write_items(transact_items) response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}} return dynamo_json_dump(response) def describe_continuous_backups(self): name = self.body["TableName"] - if self.dynamodb_backend.get_table(name) is None: - return self.error( - "com.amazonaws.dynamodb.v20111205#TableNotFoundException", - "Table not found: {}".format(name), - ) - response = self.dynamodb_backend.describe_continuous_backups(name) return json.dumps({"ContinuousBackupsDescription": response}) @@ -1173,12 +1009,6 @@ class DynamoHandler(BaseResponse): name = self.body["TableName"] point_in_time_spec = self.body["PointInTimeRecoverySpecification"] - if self.dynamodb_backend.get_table(name) is None: - return self.error( - "com.amazonaws.dynamodb.v20111205#TableNotFoundException", - "Table not found: {}".format(name), - ) - response = self.dynamodb_backend.update_continuous_backups( name, point_in_time_spec ) @@ -1196,64 +1026,38 @@ class DynamoHandler(BaseResponse): body = self.body table_name = body.get("TableName") backup_name = body.get("BackupName") - try: - backup = self.dynamodb_backend.create_backup(table_name, backup_name) - response = {"BackupDetails": backup.details} - return dynamo_json_dump(response) - except KeyError: - er = "com.amazonaws.dynamodb.v20111205#TableNotFoundException" - return self.error(er, "Table not found: %s" % table_name) + backup = self.dynamodb_backend.create_backup(table_name, backup_name) + response = {"BackupDetails": backup.details} + return dynamo_json_dump(response) def delete_backup(self): body = self.body backup_arn = body.get("BackupArn") - try: - backup = self.dynamodb_backend.delete_backup(backup_arn) - response = {"BackupDescription": backup.description} - return dynamo_json_dump(response) - except KeyError: - er = "com.amazonaws.dynamodb.v20111205#BackupNotFoundException" - return self.error(er, "Backup not found: %s" % backup_arn) + backup = self.dynamodb_backend.delete_backup(backup_arn) + response = {"BackupDescription": backup.description} + return dynamo_json_dump(response) def describe_backup(self): body = self.body backup_arn = body.get("BackupArn") - try: - backup = self.dynamodb_backend.describe_backup(backup_arn) - response = {"BackupDescription": backup.description} - return dynamo_json_dump(response) - except KeyError: - er = "com.amazonaws.dynamodb.v20111205#BackupNotFoundException" - return self.error(er, "Backup not found: %s" % backup_arn) + backup = self.dynamodb_backend.describe_backup(backup_arn) + response = {"BackupDescription": backup.description} + return dynamo_json_dump(response) def restore_table_from_backup(self): body = self.body target_table_name = body.get("TargetTableName") backup_arn = body.get("BackupArn") - try: - restored_table = self.dynamodb_backend.restore_table_from_backup( - target_table_name, backup_arn - ) - return dynamo_json_dump(restored_table.describe()) - except KeyError: - er = "com.amazonaws.dynamodb.v20111205#BackupNotFoundException" - return self.error(er, "Backup not found: %s" % backup_arn) - except ValueError: - er = "com.amazonaws.dynamodb.v20111205#TableAlreadyExistsException" - return self.error(er, "Table already exists: %s" % target_table_name) + restored_table = self.dynamodb_backend.restore_table_from_backup( + target_table_name, backup_arn + ) + return dynamo_json_dump(restored_table.describe()) def restore_table_to_point_in_time(self): body = self.body target_table_name = body.get("TargetTableName") source_table_name = body.get("SourceTableName") - try: - restored_table = self.dynamodb_backend.restore_table_to_point_in_time( - target_table_name, source_table_name - ) - return dynamo_json_dump(restored_table.describe()) - except KeyError: - er = "com.amazonaws.dynamodb.v20111205#SourceTableNotFoundException" - return self.error(er, "Source table not found: %s" % source_table_name) - except ValueError: - er = "com.amazonaws.dynamodb.v20111205#TableAlreadyExistsException" - return self.error(er, "Table already exists: %s" % target_table_name) + restored_table = self.dynamodb_backend.restore_table_to_point_in_time( + target_table_name, source_table_name + ) + return dynamo_json_dump(restored_table.describe()) diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index 580316c50..75e5515cc 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -6,7 +6,7 @@ import boto3 from boto3.dynamodb.conditions import Attr, Key import re import sure # noqa # pylint: disable=unused-import -from moto import mock_dynamodb +from moto import mock_dynamodb, settings from moto.dynamodb import dynamodb_backends from botocore.exceptions import ClientError @@ -399,14 +399,13 @@ def test_put_item_with_streams(): "Data": {"M": {"Key1": {"S": "Value1"}, "Key2": {"S": "Value2"}}}, } ) - table = dynamodb_backends["us-west-2"].get_table(name) - if not table: - # There is no way to access stream data over the API, so this part can't run in server-tests mode. - return - len(table.stream_shard.items).should.be.equal(1) - stream_record = table.stream_shard.items[0].record - stream_record["eventName"].should.be.equal("INSERT") - stream_record["dynamodb"]["SizeBytes"].should.be.equal(447) + + if not settings.TEST_SERVER_MODE: + table = dynamodb_backends["us-west-2"].get_table(name) + len(table.stream_shard.items).should.be.equal(1) + stream_record = table.stream_shard.items[0].record + stream_record["eventName"].should.be.equal("INSERT") + stream_record["dynamodb"]["SizeBytes"].should.be.equal(447) @mock_dynamodb @@ -1597,12 +1596,9 @@ def test_bad_scan_filter(): table = dynamodb.Table("test1") # Bad expression - try: + with pytest.raises(ClientError) as exc: table.scan(FilterExpression="client test") - except ClientError as err: - err.response["Error"]["Code"].should.equal("ValidationError") - else: - raise RuntimeError("Should have raised ResourceInUseException") + exc.value.response["Error"]["Code"].should.equal("ValidationException") @mock_dynamodb @@ -3870,6 +3866,12 @@ def test_transact_write_items_put_conditional_expressions(): ) # Assert the exception is correct ex.value.response["Error"]["Code"].should.equal("TransactionCanceledException") + reasons = ex.value.response["CancellationReasons"] + reasons.should.have.length_of(5) + reasons.should.contain( + {"Code": "ConditionalCheckFailed", "Message": "The conditional request failed"} + ) + reasons.should.contain({"Code": "None"}) ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) # Assert all are present items = dynamodb.scan(TableName="test-table")["Items"]