diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index d211f9e20..63c015458 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -36,7 +36,7 @@ class Item(object): class Table(object): - def __init__(self, name, hash_key_attr=None, hash_key_type=None, + def __init__(self, name, hash_key_attr, hash_key_type, range_key_attr=None, range_key_type=None, read_capacity=None, write_capacity=None): self.name = name @@ -51,7 +51,7 @@ class Table(object): @property def describe(self): - return { + results = { "Table": { "CreationDateTime": unix_time(self.created_at), "KeySchema": { @@ -59,10 +59,6 @@ class Table(object): "AttributeName": self.hash_key_attr, "AttributeType": self.hash_key_type }, - "RangeKeyElement": { - "AttributeName": self.range_key_attr, - "AttributeType": self.range_key_type - } }, "ProvisionedThroughput": { "ReadCapacityUnits": self.read_capacity, @@ -74,11 +70,20 @@ class Table(object): "TableSizeBytes": 0, } } + if self.range_key_attr: + results["Table"]["KeySchema"]["RangeKeyElement"] = { + "AttributeName": self.range_key_attr, + "AttributeType": self.range_key_type + } + return results def __len__(self): count = 0 for key, value in self.items.iteritems(): - count += len(value) + if self.range_key_attr: + count += len(value) + else: + count += 1 return count def __nonzero__(self): @@ -86,14 +91,24 @@ class Table(object): def put_item(self, item_attrs): hash_value = item_attrs.get(self.hash_key_attr).values()[0] - range_value = item_attrs.get(self.range_key_attr).values()[0] + if self.range_key_attr: + range_value = item_attrs.get(self.range_key_attr).values()[0] + else: + range_value = None item = Item(hash_value, self.hash_key_type, range_value, self.range_key_type, item_attrs) - self.items[hash_value][range_value] = item + + if range_value: + self.items[hash_value][range_value] = item + else: + self.items[hash_value] = item return item def get_item(self, hash_key, range_key): try: - return self.items[hash_key][range_key] + if range_key: + return self.items[hash_key][range_key] + else: + return self.items[hash_key] except KeyError: return None @@ -101,17 +116,24 @@ class Table(object): results = [] last_page = True # Once pagination is implemented, change this - possible_results = self.items.get(hash_key, []) - comparison_func = get_comparison_func(range_comparison) - for result in possible_results.values(): - if comparison_func(result.range_key, *range_values): - results.append(result) + possible_results = list(self.all_items()) + if range_comparison: + comparison_func = get_comparison_func(range_comparison) + for result in possible_results: + if comparison_func(result.range_key, *range_values): + results.append(result) + else: + # If we're not filtering on range key, return all values + results = possible_results return results, last_page def all_items(self): for hash_set in self.items.values(): - for item in hash_set.values(): - yield item + if self.range_key_attr: + for item in hash_set.values(): + yield item + else: + yield hash_set def scan(self, filters): results = [] @@ -146,7 +168,10 @@ class Table(object): def delete_item(self, hash_key, range_key): try: - return self.items[hash_key].pop(range_key) + if range_key: + return self.items[hash_key].pop(range_key) + else: + return self.items.pop(hash_key) except KeyError: return None @@ -187,14 +212,14 @@ class DynamoDBBackend(BaseBackend): def query(self, table_name, hash_key, range_comparison, range_values): table = self.tables.get(table_name) if not table: - return None + return None, None return table.query(hash_key, range_comparison, range_values) def scan(self, table_name, filters): table = self.tables.get(table_name) if not table: - return None + return None, None, None return table.scan(filters) diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 1c046a3d6..d51290825 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -57,9 +57,9 @@ class DynamoHandler(object): hash_key_attr = hash_hey['AttributeName'] hash_key_type = hash_hey['AttributeType'] - range_hey = key_schema['RangeKeyElement'] - range_key_attr = range_hey['AttributeName'] - range_key_type = range_hey['AttributeType'] + range_hey = key_schema.get('RangeKeyElement', {}) + range_key_attr = range_hey.get('AttributeName') + range_key_type = range_hey.get('AttributeType') throughput = body["ProvisionedThroughput"] read_units = throughput["ReadCapacityUnits"] @@ -106,9 +106,13 @@ class DynamoHandler(object): name = body['TableName'] item = body['Item'] result = dynamodb_backend.put_item(name, item) - item_dict = result.describe - item_dict['ConsumedCapacityUnits'] = 1 - return json.dumps(item_dict) + if result: + item_dict = result.describe + item_dict['ConsumedCapacityUnits'] = 1 + return json.dumps(item_dict) + else: + er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' + return self.error(er) def BatchWriteItem(self, uri, body, headers): table_batches = body['RequestItems'] @@ -143,8 +147,9 @@ class DynamoHandler(object): def GetItem(self, uri, body, headers): name = body['TableName'] - hash_key = body['Key']['HashKeyElement'].values()[0] - range_key = body['Key']['RangeKeyElement'].values()[0] + key = body['Key'] + hash_key = key['HashKeyElement'].values()[0] + range_key = value_from_dynamo_type(key.get('RangeKeyElement')) attrs_to_get = body.get('AttributesToGet') item = dynamodb_backend.get_item(name, hash_key, range_key) if item: @@ -181,23 +186,31 @@ class DynamoHandler(object): def Query(self, uri, body, headers): name = body['TableName'] hash_key = body['HashKeyValue'].values()[0] - range_condition = body['RangeKeyCondition'] - range_comparison = range_condition['ComparisonOperator'] - range_values = values_from_dynamo_types(range_condition['AttributeValueList']) + range_condition = body.get('RangeKeyCondition') + if range_condition: + range_comparison = range_condition['ComparisonOperator'] + range_values = values_from_dynamo_types(range_condition['AttributeValueList']) + else: + range_comparison = range_values = None items, last_page = dynamodb_backend.query(name, hash_key, range_comparison, range_values) + if items is None: + er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' + return self.error(er) + result = { "Count": len(items), "Items": [item.attrs for item in items], "ConsumedCapacityUnits": 1, } - if not last_page: - result["LastEvaluatedKey"] = { - "HashKeyElement": items[-1].hash_key, - "RangeKeyElement": items[-1].range_key, - } + # Implement this when we do pagination + # if not last_page: + # result["LastEvaluatedKey"] = { + # "HashKeyElement": items[-1].hash_key, + # "RangeKeyElement": items[-1].range_key, + # } return json.dumps(result) def Scan(self, uri, body, headers): @@ -216,6 +229,10 @@ class DynamoHandler(object): items, scanned_count, last_page = dynamodb_backend.scan(name, filters) + if items is None: + er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' + return self.error(er) + result = { "Count": len(items), "Items": [item.attrs for item in items], @@ -223,17 +240,19 @@ class DynamoHandler(object): "ScannedCount": scanned_count } - if not last_page: - result["LastEvaluatedKey"] = { - "HashKeyElement": items[-1].hash_key, - "RangeKeyElement": items[-1].range_key, - } + # Implement this when we do pagination + # if not last_page: + # result["LastEvaluatedKey"] = { + # "HashKeyElement": items[-1].hash_key, + # "RangeKeyElement": items[-1].range_key, + # } return json.dumps(result) def DeleteItem(self, uri, body, headers): name = body['TableName'] - hash_key = body['Key']['HashKeyElement'].values()[0] - range_key = body['Key']['RangeKeyElement'].values()[0] + key = body['Key'] + hash_key = value_from_dynamo_type(key['HashKeyElement']) + range_key = value_from_dynamo_type(key.get('RangeKeyElement')) return_values = body.get('ReturnValues', '') item = dynamodb_backend.delete_item(name, hash_key, range_key) if item: diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index fec27bdd3..4963127c5 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -12,15 +12,15 @@ from boto.exception import DynamoDBResponseError @mock_dynamodb def test_list_tables(): name = 'TestTable' - dynamodb_backend.create_table(name) + dynamodb_backend.create_table(name, hash_key_attr="name", hash_key_type="S") conn = boto.connect_dynamodb('the_key', 'the_secret') assert conn.list_tables() == ['TestTable'] @mock_dynamodb def test_list_tables_layer_1(): - dynamodb_backend.create_table("test_1") - dynamodb_backend.create_table("test_2") + dynamodb_backend.create_table("test_1", hash_key_attr="name", hash_key_type="S") + dynamodb_backend.create_table("test_2", hash_key_attr="name", hash_key_type="S") conn = boto.connect_dynamodb('the_key', 'the_secret') res = conn.layer1.list_tables(limit=1) expected = {"TableNames": ["test_1"], "LastEvaluatedTableName": "test_1"} @@ -35,347 +35,3 @@ def test_list_tables_layer_1(): def test_describe_missing_table(): conn = boto.connect_dynamodb('the_key', 'the_secret') conn.describe_table.when.called_with('messages').should.throw(DynamoDBResponseError) - - -def create_table(conn): - message_table_schema = conn.create_schema( - hash_key_name='forum_name', - hash_key_proto_value=str, - range_key_name='subject', - range_key_proto_value=str - ) - - table = conn.create_table( - name='messages', - schema=message_table_schema, - read_units=10, - write_units=10 - ) - return table - - -@freeze_time("2012-01-14") -@mock_dynamodb -def test_create_table(): - conn = boto.connect_dynamodb() - create_table(conn) - - expected = { - 'Table': { - 'CreationDateTime': 1326499200.0, - 'ItemCount': 0, - 'KeySchema': { - 'HashKeyElement': { - 'AttributeName': 'forum_name', - 'AttributeType': 'S' - }, - 'RangeKeyElement': { - 'AttributeName': 'subject', - 'AttributeType': 'S' - } - }, - 'ProvisionedThroughput': { - 'ReadCapacityUnits': 10, - 'WriteCapacityUnits': 10 - }, - 'TableName': 'messages', - 'TableSizeBytes': 0, - 'TableStatus': 'ACTIVE' - } - } - conn.describe_table('messages').should.equal(expected) - - -@mock_dynamodb -def test_delete_table(): - conn = boto.connect_dynamodb() - create_table(conn) - conn.list_tables().should.have.length_of(1) - - conn.layer1.delete_table('messages') - conn.list_tables().should.have.length_of(0) - - conn.layer1.delete_table.when.called_with('messages').should.throw(DynamoDBResponseError) - - -@mock_dynamodb -def test_update_table_throughput(): - conn = boto.connect_dynamodb() - table = create_table(conn) - table.read_units.should.equal(10) - table.write_units.should.equal(10) - - table.update_throughput(5, 6) - table.refresh() - - table.read_units.should.equal(5) - table.write_units.should.equal(6) - - -@mock_dynamodb -def test_item_add_and_describe_and_update(): - conn = boto.connect_dynamodb() - table = create_table(conn) - - item_data = { - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User A', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - } - item = table.new_item( - hash_key='LOLCat Forum', - range_key='Check this out!', - attrs=item_data, - ) - item.put() - - returned_item = table.get_item( - hash_key='LOLCat Forum', - range_key='Check this out!', - attributes_to_get=['Body', 'SentBy'] - ) - dict(returned_item).should.equal({ - 'forum_name': 'LOLCat Forum', - 'subject': 'Check this out!', - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User A', - }) - - item['SentBy'] = 'User B' - item.put() - - returned_item = table.get_item( - hash_key='LOLCat Forum', - range_key='Check this out!', - attributes_to_get=['Body', 'SentBy'] - ) - dict(returned_item).should.equal({ - 'forum_name': 'LOLCat Forum', - 'subject': 'Check this out!', - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User B', - }) - - -@mock_dynamodb -def test_delete_item(): - conn = boto.connect_dynamodb() - table = create_table(conn) - - item_data = { - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User A', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - } - item = table.new_item( - hash_key='LOLCat Forum', - range_key='Check this out!', - attrs=item_data, - ) - item.put() - - table.refresh() - table.item_count.should.equal(1) - - item.delete() - table.refresh() - table.item_count.should.equal(0) - - item.delete.when.called_with().should.throw(DynamoDBResponseError) - - -@mock_dynamodb -def test_query(): - conn = boto.connect_dynamodb() - table = create_table(conn) - - item_data = { - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User A', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - } - item = table.new_item( - hash_key='the-key', - range_key='456', - attrs=item_data, - ) - item.put() - - item = table.new_item( - hash_key='the-key', - range_key='123', - attrs=item_data, - ) - item.put() - - item = table.new_item( - hash_key='the-key', - range_key='789', - attrs=item_data, - ) - item.put() - - results = table.query(hash_key='the-key', range_key_condition=condition.GT('1')) - results.response['Items'].should.have.length_of(3) - - results = table.query(hash_key='the-key', range_key_condition=condition.GT('234')) - results.response['Items'].should.have.length_of(2) - - results = table.query(hash_key='the-key', range_key_condition=condition.GT('9999')) - results.response['Items'].should.have.length_of(0) - - results = table.query(hash_key='the-key', range_key_condition=condition.CONTAINS('12')) - results.response['Items'].should.have.length_of(1) - - results = table.query(hash_key='the-key', range_key_condition=condition.BEGINS_WITH('7')) - results.response['Items'].should.have.length_of(1) - - results = table.query(hash_key='the-key', range_key_condition=condition.BETWEEN('567', '890')) - results.response['Items'].should.have.length_of(1) - - -@mock_dynamodb -def test_scan(): - conn = boto.connect_dynamodb() - table = create_table(conn) - - item_data = { - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User A', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - } - item = table.new_item( - hash_key='the-key', - range_key='456', - attrs=item_data, - ) - item.put() - - item = table.new_item( - hash_key='the-key', - range_key='123', - attrs=item_data, - ) - item.put() - - item_data = { - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User B', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, - 'PK': 7, - } - item = table.new_item( - hash_key='the-key', - range_key='789', - attrs=item_data, - ) - item.put() - - results = table.scan(scan_filter={'SentBy': condition.EQ('User B')}) - results.response['Items'].should.have.length_of(1) - - results = table.scan(scan_filter={'Body': condition.BEGINS_WITH('http')}) - results.response['Items'].should.have.length_of(3) - - results = table.scan(scan_filter={'Ids': condition.CONTAINS(2)}) - results.response['Items'].should.have.length_of(1) - - results = table.scan(scan_filter={'Ids': condition.NOT_NULL()}) - results.response['Items'].should.have.length_of(1) - - results = table.scan(scan_filter={'Ids': condition.NULL()}) - results.response['Items'].should.have.length_of(2) - - results = table.scan(scan_filter={'PK': condition.BETWEEN(8, 9)}) - results.response['Items'].should.have.length_of(0) - - results = table.scan(scan_filter={'PK': condition.BETWEEN(5, 8)}) - results.response['Items'].should.have.length_of(1) - - -@mock_dynamodb -def test_write_batch(): - conn = boto.connect_dynamodb() - table = create_table(conn) - - batch_list = conn.new_batch_write_list() - - items = [] - items.append(table.new_item( - hash_key='the-key', - range_key='123', - attrs={ - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User A', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - }, - )) - - items.append(table.new_item( - hash_key='the-key', - range_key='789', - attrs={ - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User B', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, - 'PK': 7, - }, - )) - - batch_list.add_batch(table, puts=items) - conn.batch_write_item(batch_list) - - table.refresh() - table.item_count.should.equal(2) - - batch_list = conn.new_batch_write_list() - batch_list.add_batch(table, deletes=[('the-key', '789')]) - conn.batch_write_item(batch_list) - - table.refresh() - table.item_count.should.equal(1) - - -@mock_dynamodb -def test_batch_read(): - conn = boto.connect_dynamodb() - table = create_table(conn) - - item_data = { - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User A', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - } - item = table.new_item( - hash_key='the-key', - range_key='456', - attrs=item_data, - ) - item.put() - - item = table.new_item( - hash_key='the-key', - range_key='123', - attrs=item_data, - ) - item.put() - - item_data = { - 'Body': 'http://url_to_lolcat.gif', - 'SentBy': 'User B', - 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, - 'PK': 7, - } - item = table.new_item( - hash_key='another-key', - range_key='789', - attrs=item_data, - ) - item.put() - - items = table.batch_get_item([('the-key', '123'), ('another-key', '789')]) - count = len([item for item in items]) - count.should.equal(2) diff --git a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py new file mode 100644 index 000000000..b4516e7a8 --- /dev/null +++ b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py @@ -0,0 +1,474 @@ +import boto +import sure # flake8: noqa +from freezegun import freeze_time + +from moto import mock_dynamodb +from moto.dynamodb import dynamodb_backend + +from boto.dynamodb import condition +from boto.exception import DynamoDBResponseError + + +def create_table(conn): + message_table_schema = conn.create_schema( + hash_key_name='forum_name', + hash_key_proto_value=str, + range_key_name='subject', + range_key_proto_value=str + ) + + table = conn.create_table( + name='messages', + schema=message_table_schema, + read_units=10, + write_units=10 + ) + return table + + +@freeze_time("2012-01-14") +@mock_dynamodb +def test_create_table(): + conn = boto.connect_dynamodb() + create_table(conn) + + expected = { + 'Table': { + 'CreationDateTime': 1326499200.0, + 'ItemCount': 0, + 'KeySchema': { + 'HashKeyElement': { + 'AttributeName': 'forum_name', + 'AttributeType': 'S' + }, + 'RangeKeyElement': { + 'AttributeName': 'subject', + 'AttributeType': 'S' + } + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 10, + 'WriteCapacityUnits': 10 + }, + 'TableName': 'messages', + 'TableSizeBytes': 0, + 'TableStatus': 'ACTIVE' + } + } + conn.describe_table('messages').should.equal(expected) + + +@mock_dynamodb +def test_delete_table(): + conn = boto.connect_dynamodb() + create_table(conn) + conn.list_tables().should.have.length_of(1) + + conn.layer1.delete_table('messages') + conn.list_tables().should.have.length_of(0) + + conn.layer1.delete_table.when.called_with('messages').should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_update_table_throughput(): + conn = boto.connect_dynamodb() + table = create_table(conn) + table.read_units.should.equal(10) + table.write_units.should.equal(10) + + table.update_throughput(5, 6) + table.refresh() + + table.read_units.should.equal(5) + table.write_units.should.equal(6) + + +@mock_dynamodb +def test_item_add_and_describe_and_update(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='LOLCat Forum', + range_key='Check this out!', + attrs=item_data, + ) + item.put() + + returned_item = table.get_item( + hash_key='LOLCat Forum', + range_key='Check this out!', + attributes_to_get=['Body', 'SentBy'] + ) + dict(returned_item).should.equal({ + 'forum_name': 'LOLCat Forum', + 'subject': 'Check this out!', + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + }) + + item['SentBy'] = 'User B' + item.put() + + returned_item = table.get_item( + hash_key='LOLCat Forum', + range_key='Check this out!', + attributes_to_get=['Body', 'SentBy'] + ) + dict(returned_item).should.equal({ + 'forum_name': 'LOLCat Forum', + 'subject': 'Check this out!', + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + }) + + +@mock_dynamodb +def test_item_put_without_table(): + conn = boto.connect_dynamodb() + + conn.layer1.put_item.when.called_with( + table_name='undeclared-table', + item =dict( + hash_key='LOLCat Forum', + range_key='Check this out!', + ), + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_get_missing_item(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + table.get_item.when.called_with( + hash_key='tester', + range_key='other', + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_get_item_with_undeclared_table(): + conn = boto.connect_dynamodb() + + conn.layer1.get_item.when.called_with( + table_name='undeclared-table', + key={ + 'HashKeyElement': {'S': 'tester'}, + 'RangeKeyElement': {'S': 'test-range'}, + }, + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_delete_item(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='LOLCat Forum', + range_key='Check this out!', + attrs=item_data, + ) + item.put() + + table.refresh() + table.item_count.should.equal(1) + + response = item.delete() + response.should.equal({u'Attributes': [], u'ConsumedCapacityUnits': 0.5}) + table.refresh() + table.item_count.should.equal(0) + + item.delete.when.called_with().should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_delete_item_with_attribute_response(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='LOLCat Forum', + range_key='Check this out!', + attrs=item_data, + ) + item.put() + + table.refresh() + table.item_count.should.equal(1) + + response = item.delete(return_values='ALL_OLD') + response.should.equal({ + u'Attributes': { + u'Body': u'http://url_to_lolcat.gif', + u'forum_name': u'LOLCat Forum', + u'ReceivedTime': u'12/9/2011 11:36:03 PM', + u'SentBy': u'User A', + u'subject': u'Check this out!' + }, + u'ConsumedCapacityUnits': 0.5 + }) + table.refresh() + table.item_count.should.equal(0) + + item.delete.when.called_with().should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_delete_item_with_undeclared_table(): + conn = boto.connect_dynamodb() + + conn.layer1.delete_item.when.called_with( + table_name='undeclared-table', + key={ + 'HashKeyElement': {'S': 'tester'}, + 'RangeKeyElement': {'S': 'test-range'}, + }, + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_query(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='the-key', + range_key='456', + attrs=item_data, + ) + item.put() + + item = table.new_item( + hash_key='the-key', + range_key='123', + attrs=item_data, + ) + item.put() + + item = table.new_item( + hash_key='the-key', + range_key='789', + attrs=item_data, + ) + item.put() + + results = table.query(hash_key='the-key', range_key_condition=condition.GT('1')) + results.response['Items'].should.have.length_of(3) + + results = table.query(hash_key='the-key', range_key_condition=condition.GT('234')) + results.response['Items'].should.have.length_of(2) + + results = table.query(hash_key='the-key', range_key_condition=condition.GT('9999')) + results.response['Items'].should.have.length_of(0) + + results = table.query(hash_key='the-key', range_key_condition=condition.CONTAINS('12')) + results.response['Items'].should.have.length_of(1) + + results = table.query(hash_key='the-key', range_key_condition=condition.BEGINS_WITH('7')) + results.response['Items'].should.have.length_of(1) + + results = table.query(hash_key='the-key', range_key_condition=condition.BETWEEN('567', '890')) + results.response['Items'].should.have.length_of(1) + + +@mock_dynamodb +def test_query_with_undeclared_table(): + conn = boto.connect_dynamodb() + + conn.layer1.query.when.called_with( + table_name='undeclared-table', + hash_key_value={'S': 'the-key'}, + range_key_conditions={ + "AttributeValueList":[{ + "S":"User B" + }], + "ComparisonOperator":"EQ", + }, + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_scan(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='the-key', + range_key='456', + attrs=item_data, + ) + item.put() + + item = table.new_item( + hash_key='the-key', + range_key='123', + attrs=item_data, + ) + item.put() + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + 'Ids': {1, 2, 3}, + 'PK': 7, + } + item = table.new_item( + hash_key='the-key', + range_key='789', + attrs=item_data, + ) + item.put() + + results = table.scan(scan_filter={'SentBy': condition.EQ('User B')}) + results.response['Items'].should.have.length_of(1) + + results = table.scan(scan_filter={'Body': condition.BEGINS_WITH('http')}) + results.response['Items'].should.have.length_of(3) + + results = table.scan(scan_filter={'Ids': condition.CONTAINS(2)}) + results.response['Items'].should.have.length_of(1) + + results = table.scan(scan_filter={'Ids': condition.NOT_NULL()}) + results.response['Items'].should.have.length_of(1) + + results = table.scan(scan_filter={'Ids': condition.NULL()}) + results.response['Items'].should.have.length_of(2) + + results = table.scan(scan_filter={'PK': condition.BETWEEN(8, 9)}) + results.response['Items'].should.have.length_of(0) + + results = table.scan(scan_filter={'PK': condition.BETWEEN(5, 8)}) + results.response['Items'].should.have.length_of(1) + + +@mock_dynamodb +def test_scan_with_undeclared_table(): + conn = boto.connect_dynamodb() + + conn.layer1.scan.when.called_with( + table_name='undeclared-table', + scan_filter={ + "SentBy": { + "AttributeValueList":[{ + "S":"User B"} + ], + "ComparisonOperator":"EQ" + } + }, + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_write_batch(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + batch_list = conn.new_batch_write_list() + + items = [] + items.append(table.new_item( + hash_key='the-key', + range_key='123', + attrs={ + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + }, + )) + + items.append(table.new_item( + hash_key='the-key', + range_key='789', + attrs={ + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + 'Ids': {1, 2, 3}, + 'PK': 7, + }, + )) + + batch_list.add_batch(table, puts=items) + conn.batch_write_item(batch_list) + + table.refresh() + table.item_count.should.equal(2) + + batch_list = conn.new_batch_write_list() + batch_list.add_batch(table, deletes=[('the-key', '789')]) + conn.batch_write_item(batch_list) + + table.refresh() + table.item_count.should.equal(1) + + +@mock_dynamodb +def test_batch_read(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='the-key', + range_key='456', + attrs=item_data, + ) + item.put() + + item = table.new_item( + hash_key='the-key', + range_key='123', + attrs=item_data, + ) + item.put() + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + 'Ids': {1, 2, 3}, + 'PK': 7, + } + item = table.new_item( + hash_key='another-key', + range_key='789', + attrs=item_data, + ) + item.put() + + items = table.batch_get_item([('the-key', '123'), ('another-key', '789')]) + count = len([item for item in items]) + count.should.equal(2) diff --git a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py new file mode 100644 index 000000000..4222e3c18 --- /dev/null +++ b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py @@ -0,0 +1,412 @@ +import boto +import sure # flake8: noqa +from freezegun import freeze_time + +from moto import mock_dynamodb +from moto.dynamodb import dynamodb_backend + +from boto.dynamodb import condition +from boto.exception import DynamoDBResponseError + + +def create_table(conn): + message_table_schema = conn.create_schema( + hash_key_name='forum_name', + hash_key_proto_value=str, + ) + + table = conn.create_table( + name='messages', + schema=message_table_schema, + read_units=10, + write_units=10 + ) + return table + + +@freeze_time("2012-01-14") +@mock_dynamodb +def test_create_table(): + conn = boto.connect_dynamodb() + create_table(conn) + + expected = { + 'Table': { + 'CreationDateTime': 1326499200.0, + 'ItemCount': 0, + 'KeySchema': { + 'HashKeyElement': { + 'AttributeName': 'forum_name', + 'AttributeType': 'S' + }, + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 10, + 'WriteCapacityUnits': 10 + }, + 'TableName': 'messages', + 'TableSizeBytes': 0, + 'TableStatus': 'ACTIVE' + } + } + conn.describe_table('messages').should.equal(expected) + + +@mock_dynamodb +def test_delete_table(): + conn = boto.connect_dynamodb() + create_table(conn) + conn.list_tables().should.have.length_of(1) + + conn.layer1.delete_table('messages') + conn.list_tables().should.have.length_of(0) + + conn.layer1.delete_table.when.called_with('messages').should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_update_table_throughput(): + conn = boto.connect_dynamodb() + table = create_table(conn) + table.read_units.should.equal(10) + table.write_units.should.equal(10) + + table.update_throughput(5, 6) + table.refresh() + + table.read_units.should.equal(5) + table.write_units.should.equal(6) + + +@mock_dynamodb +def test_item_add_and_describe_and_update(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='LOLCat Forum', + attrs=item_data, + ) + item.put() + + returned_item = table.get_item( + hash_key='LOLCat Forum', + attributes_to_get=['Body', 'SentBy'] + ) + dict(returned_item).should.equal({ + 'forum_name': 'LOLCat Forum', + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + }) + + item['SentBy'] = 'User B' + item.put() + + returned_item = table.get_item( + hash_key='LOLCat Forum', + attributes_to_get=['Body', 'SentBy'] + ) + dict(returned_item).should.equal({ + 'forum_name': 'LOLCat Forum', + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + }) + + +@mock_dynamodb +def test_item_put_without_table(): + conn = boto.connect_dynamodb() + + conn.layer1.put_item.when.called_with( + table_name='undeclared-table', + item=dict( + hash_key='LOLCat Forum', + ), + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_get_missing_item(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + table.get_item.when.called_with( + hash_key='tester', + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_get_item_with_undeclared_table(): + conn = boto.connect_dynamodb() + + conn.layer1.get_item.when.called_with( + table_name='undeclared-table', + key={ + 'HashKeyElement': {'S': 'tester'}, + }, + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_delete_item(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='LOLCat Forum', + attrs=item_data, + ) + item.put() + + table.refresh() + table.item_count.should.equal(1) + + response = item.delete() + response.should.equal({u'Attributes': [], u'ConsumedCapacityUnits': 0.5}) + table.refresh() + table.item_count.should.equal(0) + + item.delete.when.called_with().should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_delete_item_with_attribute_response(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='LOLCat Forum', + attrs=item_data, + ) + item.put() + + table.refresh() + table.item_count.should.equal(1) + + response = item.delete(return_values='ALL_OLD') + response.should.equal({ + u'Attributes': { + u'Body': u'http://url_to_lolcat.gif', + u'forum_name': u'LOLCat Forum', + u'ReceivedTime': u'12/9/2011 11:36:03 PM', + u'SentBy': u'User A', + }, + u'ConsumedCapacityUnits': 0.5 + }) + table.refresh() + table.item_count.should.equal(0) + + item.delete.when.called_with().should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_delete_item_with_undeclared_table(): + conn = boto.connect_dynamodb() + + conn.layer1.delete_item.when.called_with( + table_name='undeclared-table', + key={ + 'HashKeyElement': {'S': 'tester'}, + }, + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_query(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='the-key', + attrs=item_data, + ) + item.put() + + results = table.query(hash_key='the-key') + results.response['Items'].should.have.length_of(1) + + +@mock_dynamodb +def test_query_with_undeclared_table(): + conn = boto.connect_dynamodb() + + conn.layer1.query.when.called_with( + table_name='undeclared-table', + hash_key_value={'S': 'the-key'}, + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_scan(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='the-key', + attrs=item_data, + ) + item.put() + + item = table.new_item( + hash_key='the-key2', + attrs=item_data, + ) + item.put() + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + 'Ids': {1, 2, 3}, + 'PK': 7, + } + item = table.new_item( + hash_key='the-key3', + attrs=item_data, + ) + item.put() + + results = table.scan(scan_filter={'SentBy': condition.EQ('User B')}) + results.response['Items'].should.have.length_of(1) + + results = table.scan(scan_filter={'Body': condition.BEGINS_WITH('http')}) + results.response['Items'].should.have.length_of(3) + + results = table.scan(scan_filter={'Ids': condition.CONTAINS(2)}) + results.response['Items'].should.have.length_of(1) + + results = table.scan(scan_filter={'Ids': condition.NOT_NULL()}) + results.response['Items'].should.have.length_of(1) + + results = table.scan(scan_filter={'Ids': condition.NULL()}) + results.response['Items'].should.have.length_of(2) + + results = table.scan(scan_filter={'PK': condition.BETWEEN(8, 9)}) + results.response['Items'].should.have.length_of(0) + + results = table.scan(scan_filter={'PK': condition.BETWEEN(5, 8)}) + results.response['Items'].should.have.length_of(1) + + +@mock_dynamodb +def test_scan_with_undeclared_table(): + conn = boto.connect_dynamodb() + + conn.layer1.scan.when.called_with( + table_name='undeclared-table', + scan_filter={ + "SentBy": { + "AttributeValueList": [{ + "S": "User B"} + ], + "ComparisonOperator": "EQ" + } + }, + ).should.throw(DynamoDBResponseError) + + +@mock_dynamodb +def test_write_batch(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + batch_list = conn.new_batch_write_list() + + items = [] + items.append(table.new_item( + hash_key='the-key', + attrs={ + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + }, + )) + + items.append(table.new_item( + hash_key='the-key2', + attrs={ + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + 'Ids': {1, 2, 3}, + 'PK': 7, + }, + )) + + batch_list.add_batch(table, puts=items) + conn.batch_write_item(batch_list) + + table.refresh() + table.item_count.should.equal(2) + + batch_list = conn.new_batch_write_list() + batch_list.add_batch(table, deletes=[('the-key')]) + conn.batch_write_item(batch_list) + + table.refresh() + table.item_count.should.equal(1) + + +@mock_dynamodb +def test_batch_read(): + conn = boto.connect_dynamodb() + table = create_table(conn) + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + } + item = table.new_item( + hash_key='the-key1', + attrs=item_data, + ) + item.put() + + item = table.new_item( + hash_key='the-key2', + attrs=item_data, + ) + item.put() + + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + 'Ids': {1, 2, 3}, + 'PK': 7, + } + item = table.new_item( + hash_key='another-key', + attrs=item_data, + ) + item.put() + + items = table.batch_get_item([('the-key1'), ('another-key')]) + count = len([item for item in items]) + count.should.equal(2)