DynamoDB - extend TransactWriteItems error handling (#5012)

This commit is contained in:
Bert Blommers 2022-04-09 21:45:25 +00:00 committed by GitHub
parent a0eb48d588
commit f8f6f6f1ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 320 additions and 421 deletions

View File

@ -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 from moto.dynamodb.limits import HASH_KEY_MAX_LENGTH, RANGE_KEY_MAX_LENGTH
class InvalidIndexNameError(ValueError): class DynamodbException(JsonRESTError):
pass pass
class MockValidationException(ValueError): class MockValidationException(DynamodbException):
error_type = "com.amazonaws.dynamodb.v20111205#ValidationException"
def __init__(self, message): def __init__(self, message):
super().__init__(MockValidationException.error_type, message=message)
self.exception_msg = message self.exception_msg = message
class InvalidIndexNameError(MockValidationException):
pass
class InvalidUpdateExpressionInvalidDocumentPath(MockValidationException): class InvalidUpdateExpressionInvalidDocumentPath(MockValidationException):
invalid_update_expression_msg = ( invalid_update_expression_msg = (
"The document path provided in the update expression is invalid for update" "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) super().__init__(self.inc_data_type_msg)
class ConditionalCheckFailed(ValueError): class ConditionalCheckFailed(DynamodbException):
msg = "The conditional request failed" error_type = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException"
def __init__(self): def __init__(self, msg=None):
super().__init__(self.msg) 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 [{}]" cancel_reason_msg = "Transaction cancelled, please refer cancellation reasons for specific reasons [{}]"
error_type = "com.amazonaws.dynamodb.v20120810#TransactionCanceledException"
def __init__(self, errors): def __init__(self, errors):
msg = self.cancel_reason_msg.format(", ".join([str(err) for err in errors])) msg = self.cancel_reason_msg.format(
super().__init__(msg) ", ".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): class MultipleTransactionsException(MockValidationException):
@ -240,3 +267,46 @@ class TooManyAddClauses(InvalidUpdateExpression):
def __init__(self): def __init__(self):
super().__init__(self.msg) 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")

View File

@ -25,6 +25,14 @@ from moto.dynamodb.exceptions import (
InvalidAttributeTypeError, InvalidAttributeTypeError,
MultipleTransactionsException, MultipleTransactionsException,
TooManyTransactionsException, TooManyTransactionsException,
TableNotFoundException,
ResourceNotFoundException,
SourceTableNotFoundException,
TableAlreadyExistsException,
BackupNotFoundException,
ResourceInUseException,
StreamAlreadyEnabledException,
MockValidationException,
) )
from moto.dynamodb.models.utilities import bytesize from moto.dynamodb.models.utilities import bytesize
from moto.dynamodb.models.dynamo_type import DynamoType from moto.dynamodb.models.dynamo_type import DynamoType
@ -648,7 +656,7 @@ class Table(CloudFormationModel):
overwrite=False, overwrite=False,
): ):
if self.hash_key_attr not in item_attrs.keys(): if self.hash_key_attr not in item_attrs.keys():
raise KeyError( raise MockValidationException(
"One or more parameter values were invalid: Missing the key " "One or more parameter values were invalid: Missing the key "
+ self.hash_key_attr + self.hash_key_attr
+ " in the item" + " in the item"
@ -656,7 +664,7 @@ class Table(CloudFormationModel):
hash_value = DynamoType(item_attrs.get(self.hash_key_attr)) hash_value = DynamoType(item_attrs.get(self.hash_key_attr))
if self.has_range_key: if self.has_range_key:
if self.range_key_attr not in item_attrs.keys(): if self.range_key_attr not in item_attrs.keys():
raise KeyError( raise MockValidationException(
"One or more parameter values were invalid: Missing the key " "One or more parameter values were invalid: Missing the key "
+ self.range_key_attr + self.range_key_attr
+ " in the item" + " in the item"
@ -725,7 +733,7 @@ class Table(CloudFormationModel):
def get_item(self, hash_key, range_key=None, projection_expression=None): def get_item(self, hash_key, range_key=None, projection_expression=None):
if self.has_range_key and not range_key: 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" "Table has a range key, but no range key was passed into get_item"
) )
try: try:
@ -780,7 +788,7 @@ class Table(CloudFormationModel):
all_indexes = self.all_indexes() all_indexes = self.all_indexes()
indexes_by_name = dict((i.name, i) for i in all_indexes) indexes_by_name = dict((i.name, i) for i in all_indexes)
if index_name not in indexes_by_name: if index_name not in indexes_by_name:
raise ValueError( raise MockValidationException(
"Invalid index: %s for table: %s. Available indexes are: %s" "Invalid index: %s for table: %s. Available indexes are: %s"
% (index_name, self.name, ", ".join(indexes_by_name.keys())) % (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" key for key in index.schema if key["KeyType"] == "HASH"
][0] ][0]
except IndexError: except IndexError:
raise ValueError("Missing Hash Key. KeySchema: %s" % index.name) raise MockValidationException(
"Missing Hash Key. KeySchema: %s" % index.name
)
try: try:
index_range_key = [ index_range_key = [
@ -904,11 +914,13 @@ class Table(CloudFormationModel):
def all_indexes(self): def all_indexes(self):
return (self.global_indexes or []) + (self.indexes or []) 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() all_indexes = self.all_indexes()
indexes_by_name = dict((i.name, i) for i in all_indexes) indexes_by_name = dict((i.name, i) for i in all_indexes)
if err and index_name not in indexes_by_name: if error_if_not and index_name not in indexes_by_name:
raise err raise InvalidIndexNameError(
"The table does not have the specified index: %s" % index_name
)
return indexes_by_name[index_name] return indexes_by_name[index_name]
def has_idx_items(self, index_name): def has_idx_items(self, index_name):
@ -938,10 +950,7 @@ class Table(CloudFormationModel):
scanned_count = 0 scanned_count = 0
if index_name: if index_name:
err = InvalidIndexNameError( self.get_index(index_name, error_if_not=True)
"The table does not have the specified index: %s" % index_name
)
self.get_index(index_name, err)
items = self.has_idx_items(index_name) items = self.has_idx_items(index_name)
else: else:
items = self.all_items() items = self.all_items()
@ -1182,12 +1191,14 @@ class DynamoDBBackend(BaseBackend):
def create_table(self, name, **params): def create_table(self, name, **params):
if name in self.tables: if name in self.tables:
return None raise ResourceInUseException
table = Table(name, region=self.region_name, **params) table = Table(name, region=self.region_name, **params)
self.tables[name] = table self.tables[name] = table
return table return table
def delete_table(self, name): def delete_table(self, name):
if name not in self.tables:
raise ResourceNotFoundException
return self.tables.pop(name, None) return self.tables.pop(name, None)
def describe_endpoints(self): def describe_endpoints(self):
@ -1211,11 +1222,10 @@ class DynamoDBBackend(BaseBackend):
] ]
def list_tags_of_resource(self, table_arn): def list_tags_of_resource(self, table_arn):
required_table = None
for table in self.tables: for table in self.tables:
if self.tables[table].table_arn == table_arn: if self.tables[table].table_arn == table_arn:
required_table = self.tables[table] return self.tables[table].tags
return required_table.tags raise ResourceNotFoundException
def list_tables(self, limit, exclusive_start_table_name): def list_tables(self, limit, exclusive_start_table_name):
all_tables = list(self.tables.keys()) all_tables = list(self.tables.keys())
@ -1240,7 +1250,7 @@ class DynamoDBBackend(BaseBackend):
return tables, None return tables, None
def describe_table(self, name): def describe_table(self, name):
table = self.tables[name] table = self.get_table(name)
return table.describe(base_key="Table") return table.describe(base_key="Table")
def update_table( def update_table(
@ -1281,7 +1291,7 @@ class DynamoDBBackend(BaseBackend):
stream_specification.get("StreamEnabled") stream_specification.get("StreamEnabled")
or stream_specification.get("StreamViewType") or stream_specification.get("StreamViewType")
) and table.latest_stream_label: ) and table.latest_stream_label:
raise ValueError("Table already has stream enabled") raise StreamAlreadyEnabledException
table.set_stream_specification(stream_specification) table.set_stream_specification(stream_specification)
return table return table
@ -1338,9 +1348,7 @@ class DynamoDBBackend(BaseBackend):
expression_attribute_values=None, expression_attribute_values=None,
overwrite=False, overwrite=False,
): ):
table = self.tables.get(table_name) table = self.get_table(table_name)
if not table:
return None
return table.put_item( return table.put_item(
item_attrs, item_attrs,
expected, expected,
@ -1377,22 +1385,37 @@ class DynamoDBBackend(BaseBackend):
if table.hash_key_attr not in keys or ( if table.hash_key_attr not in keys or (
table.has_range_key and table.range_key_attr not in keys 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]) hash_key = DynamoType(keys[table.hash_key_attr])
range_key = ( range_key = (
DynamoType(keys[table.range_key_attr]) if table.has_range_key else None DynamoType(keys[table.range_key_attr]) if table.has_range_key else None
) )
return hash_key, range_key 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): def get_table(self, table_name):
if table_name not in self.tables:
raise ResourceNotFoundException()
return self.tables.get(table_name) return self.tables.get(table_name)
def get_item(self, table_name, keys, projection_expression=None): def get_item(self, table_name, keys, projection_expression=None):
table = self.get_table(table_name) table = self.get_table(table_name)
if not table:
raise ValueError("No table found")
hash_key, range_key = self.get_keys_value(table, keys) hash_key, range_key = self.get_keys_value(table, keys)
return table.get_item(hash_key, range_key, projection_expression) return table.get_item(hash_key, range_key, projection_expression)
@ -1412,9 +1435,7 @@ class DynamoDBBackend(BaseBackend):
filter_expression=None, filter_expression=None,
**filter_kwargs, **filter_kwargs,
): ):
table = self.tables.get(table_name) table = self.get_table(table_name)
if not table:
return None, None
hash_key = DynamoType(hash_key_dict) hash_key = DynamoType(hash_key_dict)
range_values = [DynamoType(range_value) for range_value in range_value_dicts] range_values = [DynamoType(range_value) for range_value in range_value_dicts]
@ -1448,9 +1469,7 @@ class DynamoDBBackend(BaseBackend):
index_name, index_name,
projection_expression, projection_expression,
): ):
table = self.tables.get(table_name) table = self.get_table(table_name)
if not table:
return None, None, None
scan_filters = {} scan_filters = {}
for key, (comparison_operator, comparison_values) in filters.items(): for key, (comparison_operator, comparison_values) in filters.items():
@ -1582,8 +1601,6 @@ class DynamoDBBackend(BaseBackend):
condition_expression=None, condition_expression=None,
): ):
table = self.get_table(table_name) table = self.get_table(table_name)
if not table:
return None
hash_value, range_value = self.get_keys_value(table, key) hash_value, range_value = self.get_keys_value(table, key)
item = table.get_item(hash_value, range_value) item = table.get_item(hash_value, range_value)
@ -1719,25 +1736,31 @@ class DynamoDBBackend(BaseBackend):
) )
else: else:
raise ValueError raise ValueError
errors.append(None) errors.append((None, None))
except MultipleTransactionsException: except MultipleTransactionsException:
# Rollback to the original state, and reraise the error # Rollback to the original state, and reraise the error
self.tables = original_table_state self.tables = original_table_state
raise MultipleTransactionsException() raise MultipleTransactionsException()
except Exception as e: # noqa: E722 Do not use bare except except Exception as e: # noqa: E722 Do not use bare except
errors.append(type(e).__name__) errors.append((type(e).__name__, e.message))
if any(errors): if set(errors) != set([(None, None)]):
# Rollback to the original state, and reraise the errors # Rollback to the original state, and reraise the errors
self.tables = original_table_state self.tables = original_table_state
raise TransactionCanceledException(errors) raise TransactionCanceledException(errors)
def describe_continuous_backups(self, table_name): def describe_continuous_backups(self, table_name):
try:
table = self.get_table(table_name) table = self.get_table(table_name)
except ResourceNotFoundException:
raise TableNotFoundException(table_name)
return table.continuous_backups return table.continuous_backups
def update_continuous_backups(self, table_name, point_in_time_spec): def update_continuous_backups(self, table_name, point_in_time_spec):
try:
table = self.get_table(table_name) table = self.get_table(table_name)
except ResourceNotFoundException:
raise TableNotFoundException(table_name)
if ( if (
point_in_time_spec["PointInTimeRecoveryEnabled"] point_in_time_spec["PointInTimeRecoveryEnabled"]
@ -1759,6 +1782,8 @@ class DynamoDBBackend(BaseBackend):
return table.continuous_backups return table.continuous_backups
def get_backup(self, backup_arn): def get_backup(self, backup_arn):
if backup_arn not in self.backups:
raise BackupNotFoundException(backup_arn)
return self.backups.get(backup_arn) return self.backups.get(backup_arn)
def list_backups(self, table_name): def list_backups(self, table_name):
@ -1768,9 +1793,10 @@ class DynamoDBBackend(BaseBackend):
return backups return backups
def create_backup(self, table_name, backup_name): def create_backup(self, table_name, backup_name):
try:
table = self.get_table(table_name) table = self.get_table(table_name)
if table is None: except ResourceNotFoundException:
raise KeyError() raise TableNotFoundException(table_name)
backup = Backup(self, backup_name, table) backup = Backup(self, backup_name, table)
self.backups[backup.arn] = backup self.backups[backup.arn] = backup
return backup return backup
@ -1791,11 +1817,8 @@ class DynamoDBBackend(BaseBackend):
def restore_table_from_backup(self, target_table_name, backup_arn): def restore_table_from_backup(self, target_table_name, backup_arn):
backup = self.get_backup(backup_arn) backup = self.get_backup(backup_arn)
if backup is None: if target_table_name in self.tables:
raise KeyError() raise TableAlreadyExistsException(target_table_name)
existing_table = self.get_table(target_table_name)
if existing_table is not None:
raise ValueError()
new_table = RestoredTable( new_table = RestoredTable(
target_table_name, region=self.region_name, backup=backup 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. copy all items from the source without respect to other arguments.
""" """
try:
source = self.get_table(source_table_name) source = self.get_table(source_table_name)
if source is None: except ResourceNotFoundException:
raise KeyError() raise SourceTableNotFoundException(source_table_name)
existing_table = self.get_table(target_table_name) if target_table_name in self.tables:
if existing_table is not None: raise TableAlreadyExistsException(target_table_name)
raise ValueError()
new_table = RestoredPITTable( new_table = RestoredPITTable(
target_table_name, region=self.region_name, source=source target_table_name, region=self.region_name, source=source
) )

View File

@ -8,9 +8,9 @@ from functools import wraps
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
from moto.core.utils import camelcase_to_underscores, amz_crc32, amzn_request_id from moto.core.utils import camelcase_to_underscores, amz_crc32, amzn_request_id
from .exceptions import ( from .exceptions import (
InvalidIndexNameError,
MockValidationException, MockValidationException,
TransactionCanceledException, ResourceNotFoundException,
ConditionalCheckFailed,
) )
from moto.dynamodb.models import dynamodb_backends, dynamo_json_dump from moto.dynamodb.models import dynamodb_backends, dynamo_json_dump
@ -111,13 +111,6 @@ class DynamoHandler(BaseResponse):
if match: if match:
return match.split(".")[1] return match.split(".")[1]
def error(self, type_, message, status=400):
return (
status,
self.response_headers,
dynamo_json_dump({"__type": type_, "message": message}),
)
@property @property
def dynamodb_backend(self): def dynamodb_backend(self):
""" """
@ -165,19 +158,16 @@ class DynamoHandler(BaseResponse):
# check billing mode and get the throughput # check billing mode and get the throughput
if "BillingMode" in body.keys() and body["BillingMode"] == "PAY_PER_REQUEST": if "BillingMode" in body.keys() and body["BillingMode"] == "PAY_PER_REQUEST":
if "ProvisionedThroughput" in body.keys(): if "ProvisionedThroughput" in body.keys():
er = "com.amazonaws.dynamodb.v20111205#ValidationException" raise MockValidationException(
return self.error( "ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST"
er,
"ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST",
) )
throughput = None throughput = None
billing_mode = "PAY_PER_REQUEST" billing_mode = "PAY_PER_REQUEST"
else: # Provisioned (default billing mode) else: # Provisioned (default billing mode)
throughput = body.get("ProvisionedThroughput") throughput = body.get("ProvisionedThroughput")
if throughput is None: if throughput is None:
return self.error( raise MockValidationException(
"ValidationException", "One or more parameter values were invalid: ReadCapacityUnits and WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
"One or more parameter values were invalid: ReadCapacityUnits and WriteCapacityUnits must both be specified when BillingMode is PROVISIONED",
) )
billing_mode = "PROVISIONED" billing_mode = "PROVISIONED"
# getting ServerSideEncryption details # getting ServerSideEncryption details
@ -189,16 +179,14 @@ class DynamoHandler(BaseResponse):
# getting the indexes # getting the indexes
global_indexes = body.get("GlobalSecondaryIndexes") global_indexes = body.get("GlobalSecondaryIndexes")
if global_indexes == []: if global_indexes == []:
return self.error( raise MockValidationException(
"ValidationException", "One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty"
"One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty",
) )
global_indexes = global_indexes or [] global_indexes = global_indexes or []
local_secondary_indexes = body.get("LocalSecondaryIndexes") local_secondary_indexes = body.get("LocalSecondaryIndexes")
if local_secondary_indexes == []: if local_secondary_indexes == []:
return self.error( raise MockValidationException(
"ValidationException", "One or more parameter values were invalid: List of LocalSecondaryIndexes is empty"
"One or more parameter values were invalid: List of LocalSecondaryIndexes is empty",
) )
local_secondary_indexes = local_secondary_indexes or [] local_secondary_indexes = local_secondary_indexes or []
# Verify AttributeDefinitions list all # Verify AttributeDefinitions list all
@ -241,76 +229,62 @@ class DynamoHandler(BaseResponse):
sse_specification=sse_spec, sse_specification=sse_spec,
tags=tags, tags=tags,
) )
if table is not None:
return dynamo_json_dump(table.describe()) return dynamo_json_dump(table.describe())
else:
er = "com.amazonaws.dynamodb.v20111205#ResourceInUseException"
return self.error(er, "Resource in use")
def _throw_attr_error(self, actual_attrs, expected_attrs, indexes): def _throw_attr_error(self, actual_attrs, expected_attrs, indexes):
def dump_list(list_): def dump_list(list_):
return str(list_).replace("'", "") return str(list_).replace("'", "")
er = "com.amazonaws.dynamodb.v20111205#ValidationException"
err_head = "One or more parameter values were invalid: " err_head = "One or more parameter values were invalid: "
if len(actual_attrs) > len(expected_attrs): if len(actual_attrs) > len(expected_attrs):
if indexes: if indexes:
return self.error( raise MockValidationException(
er,
err_head err_head
+ "Some AttributeDefinitions are not used. AttributeDefinitions: " + "Some AttributeDefinitions are not used. AttributeDefinitions: "
+ dump_list(actual_attrs) + dump_list(actual_attrs)
+ ", keys used: " + ", keys used: "
+ dump_list(expected_attrs), + dump_list(expected_attrs)
) )
else: else:
return self.error( raise MockValidationException(
er,
err_head 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): elif len(actual_attrs) < len(expected_attrs):
if indexes: if indexes:
return self.error( raise MockValidationException(
er,
err_head err_head
+ "Some index key attributes are not defined in AttributeDefinitions. Keys: " + "Some index key attributes are not defined in AttributeDefinitions. Keys: "
+ dump_list(list(set(expected_attrs) - set(actual_attrs))) + dump_list(list(set(expected_attrs) - set(actual_attrs)))
+ ", AttributeDefinitions: " + ", AttributeDefinitions: "
+ dump_list(actual_attrs), + dump_list(actual_attrs)
) )
else: else:
return self.error( raise MockValidationException(
er, "Invalid KeySchema: Some index key attribute have no definition" "Invalid KeySchema: Some index key attribute have no definition"
) )
else: else:
if indexes: if indexes:
return self.error( raise MockValidationException(
er,
err_head err_head
+ "Some index key attributes are not defined in AttributeDefinitions. Keys: " + "Some index key attributes are not defined in AttributeDefinitions. Keys: "
+ dump_list(list(set(expected_attrs) - set(actual_attrs))) + dump_list(list(set(expected_attrs) - set(actual_attrs)))
+ ", AttributeDefinitions: " + ", AttributeDefinitions: "
+ dump_list(actual_attrs), + dump_list(actual_attrs)
) )
else: else:
return self.error( raise MockValidationException(
er,
err_head err_head
+ "Some index key attributes are not defined in AttributeDefinitions. Keys: " + "Some index key attributes are not defined in AttributeDefinitions. Keys: "
+ dump_list(expected_attrs) + dump_list(expected_attrs)
+ ", AttributeDefinitions: " + ", AttributeDefinitions: "
+ dump_list(actual_attrs), + dump_list(actual_attrs)
) )
def delete_table(self): def delete_table(self):
name = self.body["TableName"] name = self.body["TableName"]
table = self.dynamodb_backend.delete_table(name) table = self.dynamodb_backend.delete_table(name)
if table is not None:
return dynamo_json_dump(table.describe()) return dynamo_json_dump(table.describe())
else:
er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"
return self.error(er, "Requested resource not found")
def describe_endpoints(self): def describe_endpoints(self):
response = {"Endpoints": self.dynamodb_backend.describe_endpoints()} response = {"Endpoints": self.dynamodb_backend.describe_endpoints()}
@ -329,7 +303,6 @@ class DynamoHandler(BaseResponse):
return "" return ""
def list_tags_of_resource(self): def list_tags_of_resource(self):
try:
table_arn = self.body["ResourceArn"] table_arn = self.body["ResourceArn"]
all_tags = self.dynamodb_backend.list_tags_of_resource(table_arn) all_tags = self.dynamodb_backend.list_tags_of_resource(table_arn)
all_tag_keys = [tag["Key"] for tag in all_tags] all_tag_keys = [tag["Key"] for tag in all_tags]
@ -346,9 +319,6 @@ class DynamoHandler(BaseResponse):
if next_marker: if next_marker:
return json.dumps({"Tags": tags_resp, "NextToken": next_marker}) return json.dumps({"Tags": tags_resp, "NextToken": next_marker})
return json.dumps({"Tags": tags_resp}) return json.dumps({"Tags": tags_resp})
except AttributeError:
er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"
return self.error(er, "Requested resource not found")
def update_table(self): def update_table(self):
name = self.body["TableName"] name = self.body["TableName"]
@ -357,7 +327,6 @@ class DynamoHandler(BaseResponse):
throughput = self.body.get("ProvisionedThroughput", None) throughput = self.body.get("ProvisionedThroughput", None)
billing_mode = self.body.get("BillingMode", None) billing_mode = self.body.get("BillingMode", None)
stream_spec = self.body.get("StreamSpecification", None) stream_spec = self.body.get("StreamSpecification", None)
try:
table = self.dynamodb_backend.update_table( table = self.dynamodb_backend.update_table(
name=name, name=name,
attr_definitions=attr_definitions, attr_definitions=attr_definitions,
@ -367,18 +336,11 @@ class DynamoHandler(BaseResponse):
stream_spec=stream_spec, stream_spec=stream_spec,
) )
return dynamo_json_dump(table.describe()) return dynamo_json_dump(table.describe())
except ValueError:
er = "com.amazonaws.dynamodb.v20111205#ResourceInUseException"
return self.error(er, "Cannot enable stream")
def describe_table(self): def describe_table(self):
name = self.body["TableName"] name = self.body["TableName"]
try:
table = self.dynamodb_backend.describe_table(name) table = self.dynamodb_backend.describe_table(name)
return dynamo_json_dump(table) return dynamo_json_dump(table)
except KeyError:
er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"
return self.error(er, "Requested resource not found")
@include_consumed_capacity() @include_consumed_capacity()
def put_item(self): def put_item(self):
@ -387,8 +349,7 @@ class DynamoHandler(BaseResponse):
return_values = self.body.get("ReturnValues", "NONE") return_values = self.body.get("ReturnValues", "NONE")
if return_values not in ("ALL_OLD", "NONE"): if return_values not in ("ALL_OLD", "NONE"):
er = "com.amazonaws.dynamodb.v20111205#ValidationException" raise MockValidationException("Return values set to invalid value")
return self.error(er, "Return values set to invalid value")
if put_has_empty_keys(item, self.dynamodb_backend.get_table(name)): if put_has_empty_keys(item, self.dynamodb_backend.get_table(name)):
return get_empty_str_error() return get_empty_str_error()
@ -415,7 +376,6 @@ class DynamoHandler(BaseResponse):
if condition_expression: if condition_expression:
overwrite = False overwrite = False
try:
result = self.dynamodb_backend.put_item( result = self.dynamodb_backend.put_item(
name, name,
item, item,
@ -425,26 +385,13 @@ class DynamoHandler(BaseResponse):
expression_attribute_values, expression_attribute_values,
overwrite, 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))
if result:
item_dict = result.to_json() item_dict = result.to_json()
if return_values == "ALL_OLD": if return_values == "ALL_OLD":
item_dict["Attributes"] = existing_attributes item_dict["Attributes"] = existing_attributes
else: else:
item_dict.pop("Attributes", None) item_dict.pop("Attributes", None)
return dynamo_json_dump(item_dict) return dynamo_json_dump(item_dict)
else:
er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"
return self.error(er, "Requested resource not found")
def batch_write_item(self): def batch_write_item(self):
table_batches = self.body["RequestItems"] table_batches = self.body["RequestItems"]
@ -455,15 +402,10 @@ class DynamoHandler(BaseResponse):
request = list(table_request.values())[0] request = list(table_request.values())[0]
if request_type == "PutRequest": if request_type == "PutRequest":
item = request["Item"] item = request["Item"]
res = self.dynamodb_backend.put_item(table_name, item) self.dynamodb_backend.put_item(table_name, item)
if not res:
return self.error(
"com.amazonaws.dynamodb.v20111205#ResourceNotFoundException",
"Requested resource not found",
)
elif request_type == "DeleteRequest": elif request_type == "DeleteRequest":
keys = request["Key"] keys = request["Key"]
item = self.dynamodb_backend.delete_item(table_name, keys) self.dynamodb_backend.delete_item(table_name, keys)
response = { response = {
"ConsumedCapacity": [ "ConsumedCapacity": [
@ -483,36 +425,26 @@ class DynamoHandler(BaseResponse):
@include_consumed_capacity(0.5) @include_consumed_capacity(0.5)
def get_item(self): def get_item(self):
name = self.body["TableName"] name = self.body["TableName"]
table = self.dynamodb_backend.get_table(name) self.dynamodb_backend.get_table(name)
if table is None:
return self.error(
"com.amazonaws.dynamodb.v20120810#ResourceNotFoundException",
"Requested resource not found",
)
key = self.body["Key"] key = self.body["Key"]
projection_expression = self.body.get("ProjectionExpression") projection_expression = self.body.get("ProjectionExpression")
expression_attribute_names = self.body.get("ExpressionAttributeNames") expression_attribute_names = self.body.get("ExpressionAttributeNames")
if expression_attribute_names == {}: if expression_attribute_names == {}:
if projection_expression is None: if projection_expression is None:
er = "ValidationException" raise MockValidationException(
return self.error( "ExpressionAttributeNames can only be specified when using expressions"
er,
"ExpressionAttributeNames can only be specified when using expressions",
) )
else: else:
er = "ValidationException" raise MockValidationException(
return self.error(er, "ExpressionAttributeNames must not be empty") "ExpressionAttributeNames must not be empty"
)
expression_attribute_names = expression_attribute_names or {} expression_attribute_names = expression_attribute_names or {}
projection_expression = self._adjust_projection_expression( projection_expression = self._adjust_projection_expression(
projection_expression, expression_attribute_names projection_expression, expression_attribute_names
) )
try:
item = self.dynamodb_backend.get_item(name, key, projection_expression) item = self.dynamodb_backend.get_item(name, key, projection_expression)
except ValueError:
er = "com.amazon.coral.validate#ValidationException"
return self.error(er, "Validation Exception")
if item: if item:
item_dict = item.describe_attrs(attributes=None) item_dict = item.describe_attrs(attributes=None)
return dynamo_json_dump(item_dict) 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 # Scenario 1: We're requesting more than a 100 keys from a single table
for table_name, table_request in table_batches.items(): for table_name, table_request in table_batches.items():
if len(table_request["Keys"]) > 100: if len(table_request["Keys"]) > 100:
return self.error( raise MockValidationException(
"com.amazonaws.dynamodb.v20111205#ValidationException",
"1 validation error detected: Value at 'requestItems." "1 validation error detected: Value at 'requestItems."
+ table_name + 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 # Scenario 2: We're requesting more than a 100 keys across all tables
nr_of_keys_across_all_tables = sum( nr_of_keys_across_all_tables = sum(
[len(req["Keys"]) for _, req in table_batches.items()] [len(req["Keys"]) for _, req in table_batches.items()]
) )
if nr_of_keys_across_all_tables > 100: if nr_of_keys_across_all_tables > 100:
return self.error( raise MockValidationException(
"com.amazonaws.dynamodb.v20111205#ValidationException", "Too many items requested for the BatchGetItem call"
"Too many items requested for the BatchGetItem call",
) )
for table_name, table_request in table_batches.items(): for table_name, table_request in table_batches.items():
keys = table_request["Keys"] keys = table_request["Keys"]
if self._contains_duplicates(keys): if self._contains_duplicates(keys):
er = "com.amazon.coral.validate#ValidationException" raise MockValidationException(
return self.error(er, "Provided list of item keys contains duplicates") "Provided list of item keys contains duplicates"
)
attributes_to_get = table_request.get("AttributesToGet") attributes_to_get = table_request.get("AttributesToGet")
projection_expression = table_request.get("ProjectionExpression") projection_expression = table_request.get("ProjectionExpression")
expression_attribute_names = table_request.get( expression_attribute_names = table_request.get(
@ -562,15 +493,9 @@ class DynamoHandler(BaseResponse):
results["Responses"][table_name] = [] results["Responses"][table_name] = []
for key in keys: for key in keys:
try:
item = self.dynamodb_backend.get_item( item = self.dynamodb_backend.get_item(
table_name, key, projection_expression table_name, key, projection_expression
) )
except ValueError:
return self.error(
"com.amazonaws.dynamodb.v20111205#ResourceNotFoundException",
"Requested resource not found",
)
if item: if item:
item_describe = item.describe_attrs(attributes_to_get) item_describe = item.describe_attrs(attributes_to_get)
results["Responses"][table_name].append(item_describe["Item"]) results["Responses"][table_name].append(item_describe["Item"])
@ -607,32 +532,11 @@ class DynamoHandler(BaseResponse):
if key_condition_expression: if key_condition_expression:
value_alias_map = self.body.get("ExpressionAttributeValues", {}) 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") index_name = self.body.get("IndexName")
if index_name: schema = self.dynamodb_backend.get_schema(
all_indexes = (table.global_indexes or []) + (table.indexes or []) table_name=name, index_name=index_name
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
reverse_attribute_lookup = dict( reverse_attribute_lookup = dict(
(v, k) for k, v in self.body.get("ExpressionAttributeNames", {}).items() (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 " 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( hash_key_var = reverse_attribute_lookup.get(
index_hash_key["AttributeName"], index_hash_key["AttributeName"] index_hash_key["AttributeName"], index_hash_key["AttributeName"]
) )
@ -656,11 +560,10 @@ class DynamoHandler(BaseResponse):
(None, None), (None, None),
) )
if hash_key_expression is None: if hash_key_expression is None:
return self.error( raise MockValidationException(
"ValidationException",
"Query condition missed key schema element: {}".format( "Query condition missed key schema element: {}".format(
hash_key_var hash_key_var
), )
) )
hash_key_expression = hash_key_expression.strip("()") hash_key_expression = hash_key_expression.strip("()")
expressions.pop(i) expressions.pop(i)
@ -698,11 +601,10 @@ class DynamoHandler(BaseResponse):
"begins_with" "begins_with"
) )
] ]
return self.error( raise MockValidationException(
"com.amazonaws.dynamodb.v20111205#ValidationException",
"Invalid KeyConditionExpression: Invalid function name; function: {}".format( "Invalid KeyConditionExpression: Invalid function name; function: {}".format(
function_used function_used
), )
) )
else: else:
# [range_key, =, x] # [range_key, =, x]
@ -713,14 +615,13 @@ class DynamoHandler(BaseResponse):
supplied_range_key, supplied_range_key supplied_range_key, supplied_range_key
) )
range_keys = [ 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: if supplied_range_key not in range_keys:
return self.error( raise MockValidationException(
"ValidationException",
"Query condition missed key schema element: {}".format( "Query condition missed key schema element: {}".format(
range_keys[0] range_keys[0]
), )
) )
else: else:
hash_key_expression = key_condition_expression.strip("()") hash_key_expression = key_condition_expression.strip("()")
@ -728,10 +629,7 @@ class DynamoHandler(BaseResponse):
range_values = [] range_values = []
if not re.search("[^<>]=", hash_key_expression): if not re.search("[^<>]=", hash_key_expression):
return self.error( raise MockValidationException("Query key condition not supported")
"com.amazonaws.dynamodb.v20111205#ValidationException",
"Query key condition not supported",
)
hash_key_value_alias = hash_key_expression.split("=")[1].strip() hash_key_value_alias = hash_key_expression.split("=")[1].strip()
# Temporary fix until we get proper KeyConditionExpression function # Temporary fix until we get proper KeyConditionExpression function
hash_key = value_alias_map.get( hash_key = value_alias_map.get(
@ -743,9 +641,8 @@ class DynamoHandler(BaseResponse):
query_filters = self.body.get("QueryFilter") query_filters = self.body.get("QueryFilter")
if not (key_conditions or query_filters): if not (key_conditions or query_filters):
return self.error( raise MockValidationException(
"com.amazonaws.dynamodb.v20111205#ValidationException", "Either KeyConditions or QueryFilter should be present"
"Either KeyConditions or QueryFilter should be present",
) )
if key_conditions: if key_conditions:
@ -759,16 +656,14 @@ class DynamoHandler(BaseResponse):
if key not in (hash_key_name, range_key_name): if key not in (hash_key_name, range_key_name):
filter_kwargs[key] = value filter_kwargs[key] = value
if hash_key_name is None: if hash_key_name is None:
er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" raise ResourceNotFoundException
return self.error(er, "Requested resource not found")
hash_key = key_conditions[hash_key_name]["AttributeValueList"][0] hash_key = key_conditions[hash_key_name]["AttributeValueList"][0]
if len(key_conditions) == 1: if len(key_conditions) == 1:
range_comparison = None range_comparison = None
range_values = [] range_values = []
else: else:
if range_key_name is None and not filter_kwargs: if range_key_name is None and not filter_kwargs:
er = "com.amazon.coral.validate#ValidationException" raise MockValidationException("Validation Exception")
return self.error(er, "Validation Exception")
else: else:
range_condition = key_conditions.get(range_key_name) range_condition = key_conditions.get(range_key_name)
if range_condition: if range_condition:
@ -798,9 +693,6 @@ class DynamoHandler(BaseResponse):
filter_expression=filter_expression, filter_expression=filter_expression,
**filter_kwargs **filter_kwargs
) )
if items is None:
er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"
return self.error(er, "Requested resource not found")
result = { result = {
"Count": len(items), "Count": len(items),
@ -867,21 +759,8 @@ class DynamoHandler(BaseResponse):
index_name, index_name,
projection_expression, projection_expression,
) )
except InvalidIndexNameError as err:
er = "com.amazonaws.dynamodb.v20111205#ValidationException"
return self.error(er, str(err))
except ValueError as err: except ValueError as err:
er = "com.amazonaws.dynamodb.v20111205#ValidationError" raise MockValidationException("Bad Filter Expression: {0}".format(err))
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")
result = { result = {
"Count": len(items), "Count": len(items),
@ -897,14 +776,13 @@ class DynamoHandler(BaseResponse):
key = self.body["Key"] key = self.body["Key"]
return_values = self.body.get("ReturnValues", "NONE") return_values = self.body.get("ReturnValues", "NONE")
if return_values not in ("ALL_OLD", "NONE"): if return_values not in ("ALL_OLD", "NONE"):
er = "com.amazonaws.dynamodb.v20111205#ValidationException" raise MockValidationException("Return values set to invalid value")
return self.error(er, "Return values set to invalid value")
table = self.dynamodb_backend.get_table(name) try:
if not table: self.dynamodb_backend.get_table(name)
er = "com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException" except ResourceNotFoundException:
return self.error( raise ConditionalCheckFailed(
er, "A condition specified in the operation could not be evaluated." "A condition specified in the operation could not be evaluated."
) )
# Attempt to parse simple ConditionExpressions into an Expected # Attempt to parse simple ConditionExpressions into an Expected
@ -913,7 +791,6 @@ class DynamoHandler(BaseResponse):
expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) expression_attribute_names = self.body.get("ExpressionAttributeNames", {})
expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) expression_attribute_values = self.body.get("ExpressionAttributeValues", {})
try:
item = self.dynamodb_backend.delete_item( item = self.dynamodb_backend.delete_item(
name, name,
key, key,
@ -921,11 +798,6 @@ class DynamoHandler(BaseResponse):
expression_attribute_values, expression_attribute_values,
condition_expression, condition_expression,
) )
except ValueError:
er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException"
return self.error(
er, "A condition specified in the operation could not be evaluated."
)
if item and return_values == "ALL_OLD": if item and return_values == "ALL_OLD":
item_dict = item.to_json() item_dict = item.to_json()
@ -941,19 +813,11 @@ class DynamoHandler(BaseResponse):
update_expression = self.body.get("UpdateExpression", "").strip() update_expression = self.body.get("UpdateExpression", "").strip()
attribute_updates = self.body.get("AttributeUpdates") attribute_updates = self.body.get("AttributeUpdates")
if update_expression and attribute_updates: if update_expression and attribute_updates:
er = "com.amazonaws.dynamodb.v20111205#ValidationException" raise MockValidationException(
return self.error( "Can not use both expression and non-expression parameters in the same request: Non-expression parameters: {AttributeUpdates} Expression parameters: {UpdateExpression}"
er,
"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 # 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)) 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",
)
if existing_item: if existing_item:
existing_attributes = existing_item.to_json()["Attributes"] existing_attributes = existing_item.to_json()["Attributes"]
else: else:
@ -966,8 +830,7 @@ class DynamoHandler(BaseResponse):
"UPDATED_OLD", "UPDATED_OLD",
"UPDATED_NEW", "UPDATED_NEW",
): ):
er = "com.amazonaws.dynamodb.v20111205#ValidationException" raise MockValidationException("Return values set to invalid value")
return self.error(er, "Return values set to invalid value")
if "Expected" in self.body: if "Expected" in self.body:
expected = self.body["Expected"] expected = self.body["Expected"]
@ -980,7 +843,6 @@ class DynamoHandler(BaseResponse):
expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) expression_attribute_names = self.body.get("ExpressionAttributeNames", {})
expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) expression_attribute_values = self.body.get("ExpressionAttributeValues", {})
try:
item = self.dynamodb_backend.update_item( item = self.dynamodb_backend.update_item(
name, name,
key, key,
@ -991,15 +853,6 @@ class DynamoHandler(BaseResponse):
expected=expected, expected=expected,
condition_expression=condition_expression, 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_dict = item.to_json() item_dict = item.to_json()
item_dict["ConsumedCapacity"] = {"TableName": name, "CapacityUnits": 0.5} item_dict["ConsumedCapacity"] = {"TableName": name, "CapacityUnits": 0.5}
@ -1100,7 +953,7 @@ class DynamoHandler(BaseResponse):
% TRANSACTION_MAX_ITEMS % TRANSACTION_MAX_ITEMS
) )
return self.error("ValidationException", msg) raise MockValidationException(msg)
ret_consumed_capacity = self.body.get("ReturnConsumedCapacity", "NONE") ret_consumed_capacity = self.body.get("ReturnConsumedCapacity", "NONE")
consumed_capacity = dict() consumed_capacity = dict()
@ -1109,11 +962,7 @@ class DynamoHandler(BaseResponse):
table_name = transact_item["Get"]["TableName"] table_name = transact_item["Get"]["TableName"]
key = transact_item["Get"]["Key"] key = transact_item["Get"]["Key"]
try:
item = self.dynamodb_backend.get_item(table_name, key) 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")
if not item: if not item:
responses.append({}) responses.append({})
@ -1145,26 +994,13 @@ class DynamoHandler(BaseResponse):
def transact_write_items(self): def transact_write_items(self):
transact_items = self.body["TransactItems"] transact_items = self.body["TransactItems"]
try:
self.dynamodb_backend.transact_write_items(transact_items) 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)
response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}} response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}}
return dynamo_json_dump(response) return dynamo_json_dump(response)
def describe_continuous_backups(self): def describe_continuous_backups(self):
name = self.body["TableName"] 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) response = self.dynamodb_backend.describe_continuous_backups(name)
return json.dumps({"ContinuousBackupsDescription": response}) return json.dumps({"ContinuousBackupsDescription": response})
@ -1173,12 +1009,6 @@ class DynamoHandler(BaseResponse):
name = self.body["TableName"] name = self.body["TableName"]
point_in_time_spec = self.body["PointInTimeRecoverySpecification"] 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( response = self.dynamodb_backend.update_continuous_backups(
name, point_in_time_spec name, point_in_time_spec
) )
@ -1196,64 +1026,38 @@ class DynamoHandler(BaseResponse):
body = self.body body = self.body
table_name = body.get("TableName") table_name = body.get("TableName")
backup_name = body.get("BackupName") backup_name = body.get("BackupName")
try:
backup = self.dynamodb_backend.create_backup(table_name, backup_name) backup = self.dynamodb_backend.create_backup(table_name, backup_name)
response = {"BackupDetails": backup.details} response = {"BackupDetails": backup.details}
return dynamo_json_dump(response) return dynamo_json_dump(response)
except KeyError:
er = "com.amazonaws.dynamodb.v20111205#TableNotFoundException"
return self.error(er, "Table not found: %s" % table_name)
def delete_backup(self): def delete_backup(self):
body = self.body body = self.body
backup_arn = body.get("BackupArn") backup_arn = body.get("BackupArn")
try:
backup = self.dynamodb_backend.delete_backup(backup_arn) backup = self.dynamodb_backend.delete_backup(backup_arn)
response = {"BackupDescription": backup.description} response = {"BackupDescription": backup.description}
return dynamo_json_dump(response) return dynamo_json_dump(response)
except KeyError:
er = "com.amazonaws.dynamodb.v20111205#BackupNotFoundException"
return self.error(er, "Backup not found: %s" % backup_arn)
def describe_backup(self): def describe_backup(self):
body = self.body body = self.body
backup_arn = body.get("BackupArn") backup_arn = body.get("BackupArn")
try:
backup = self.dynamodb_backend.describe_backup(backup_arn) backup = self.dynamodb_backend.describe_backup(backup_arn)
response = {"BackupDescription": backup.description} response = {"BackupDescription": backup.description}
return dynamo_json_dump(response) return dynamo_json_dump(response)
except KeyError:
er = "com.amazonaws.dynamodb.v20111205#BackupNotFoundException"
return self.error(er, "Backup not found: %s" % backup_arn)
def restore_table_from_backup(self): def restore_table_from_backup(self):
body = self.body body = self.body
target_table_name = body.get("TargetTableName") target_table_name = body.get("TargetTableName")
backup_arn = body.get("BackupArn") backup_arn = body.get("BackupArn")
try:
restored_table = self.dynamodb_backend.restore_table_from_backup( restored_table = self.dynamodb_backend.restore_table_from_backup(
target_table_name, backup_arn target_table_name, backup_arn
) )
return dynamo_json_dump(restored_table.describe()) 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)
def restore_table_to_point_in_time(self): def restore_table_to_point_in_time(self):
body = self.body body = self.body
target_table_name = body.get("TargetTableName") target_table_name = body.get("TargetTableName")
source_table_name = body.get("SourceTableName") source_table_name = body.get("SourceTableName")
try:
restored_table = self.dynamodb_backend.restore_table_to_point_in_time( restored_table = self.dynamodb_backend.restore_table_to_point_in_time(
target_table_name, source_table_name target_table_name, source_table_name
) )
return dynamo_json_dump(restored_table.describe()) 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)

View File

@ -6,7 +6,7 @@ import boto3
from boto3.dynamodb.conditions import Attr, Key from boto3.dynamodb.conditions import Attr, Key
import re import re
import sure # noqa # pylint: disable=unused-import 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 moto.dynamodb import dynamodb_backends
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
@ -399,10 +399,9 @@ def test_put_item_with_streams():
"Data": {"M": {"Key1": {"S": "Value1"}, "Key2": {"S": "Value2"}}}, "Data": {"M": {"Key1": {"S": "Value1"}, "Key2": {"S": "Value2"}}},
} }
) )
if not settings.TEST_SERVER_MODE:
table = dynamodb_backends["us-west-2"].get_table(name) 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) len(table.stream_shard.items).should.be.equal(1)
stream_record = table.stream_shard.items[0].record stream_record = table.stream_shard.items[0].record
stream_record["eventName"].should.be.equal("INSERT") stream_record["eventName"].should.be.equal("INSERT")
@ -1597,12 +1596,9 @@ def test_bad_scan_filter():
table = dynamodb.Table("test1") table = dynamodb.Table("test1")
# Bad expression # Bad expression
try: with pytest.raises(ClientError) as exc:
table.scan(FilterExpression="client test") table.scan(FilterExpression="client test")
except ClientError as err: exc.value.response["Error"]["Code"].should.equal("ValidationException")
err.response["Error"]["Code"].should.equal("ValidationError")
else:
raise RuntimeError("Should have raised ResourceInUseException")
@mock_dynamodb @mock_dynamodb
@ -3870,6 +3866,12 @@ def test_transact_write_items_put_conditional_expressions():
) )
# Assert the exception is correct # Assert the exception is correct
ex.value.response["Error"]["Code"].should.equal("TransactionCanceledException") 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) ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
# Assert all are present # Assert all are present
items = dynamodb.scan(TableName="test-table")["Items"] items = dynamodb.scan(TableName="test-table")["Items"]