diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index 3eab73db0..bc1362e4d 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -1,11 +1,39 @@ import datetime -from collections import OrderedDict +from collections import defaultdict, OrderedDict from moto.core import BaseBackend +from .comparisons import get_comparison_func from .utils import unix_time +class Item(object): + def __init__(self, hash_key, hash_key_type, range_key, range_key_type, attrs): + self.hash_key = hash_key + self.hash_key_type = hash_key_type + self.range_key = range_key + self.range_key_type = range_key_type + self.attrs = attrs + + @property + def describe(self): + return { + "Attributes": self.attrs + } + + def describe_attrs(self, attributes): + if attributes: + included = {} + for key, value in self.attrs.iteritems(): + if key in attributes: + included[key] = value + else: + included = self.attrs + return { + "Item": included + } + + class Table(object): def __init__(self, name, hash_key_attr=None, hash_key_type=None, @@ -19,6 +47,7 @@ class Table(object): self.read_capacity = read_capacity self.write_capacity = write_capacity self.created_at = datetime.datetime.now() + self.items = defaultdict(dict) @property def describe(self): @@ -41,11 +70,50 @@ class Table(object): }, "TableName": self.name, "TableStatus": "ACTIVE", - "ItemCount": 0, + "ItemCount": len(self), "TableSizeBytes": 0, } } + def __len__(self): + count = 0 + for key, value in self.items.iteritems(): + count += len(value) + return count + + def __nonzero__(self): + return True + + 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] + item = Item(hash_value, self.hash_key_type, range_value, self.range_key_type, item_attrs) + self.items[hash_value][range_value] = item + return item + + def get_item(self, hash_key, range_key): + try: + return self.items[hash_key][range_key] + except KeyError: + return None + + def query(self, hash_key, range_comparison, range_value): + 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_value): + results.append(result) + return results, last_page + + def delete_item(self, hash_key, range_key): + try: + return self.items[hash_key].pop(range_key) + except KeyError: + return None + class DynamoDBBackend(BaseBackend): @@ -53,6 +121,46 @@ class DynamoDBBackend(BaseBackend): self.tables = OrderedDict() def create_table(self, name, **params): - self.tables[name] = Table(name, **params) + table = Table(name, **params) + self.tables[name] = table + return table + + def delete_table(self, name): + return self.tables.pop(name, None) + + def update_table_throughput(self, name, new_read_units, new_write_units): + table = self.tables[name] + table.read_capacity = new_read_units + table.write_capacity = new_write_units + return table + + def put_item(self, table_name, item_attrs): + table = self.tables.get(table_name) + if not table: + return None + + return table.put_item(item_attrs) + + def get_item(self, table_name, hash_key, range_key): + table = self.tables.get(table_name) + if not table: + return None + + return table.get_item(hash_key, range_key) + + def query(self, table_name, hash_key, range_comparison, range_value): + table = self.tables.get(table_name) + if not table: + return None + + return table.query(hash_key, range_comparison, range_value) + + def delete_item(self, table_name, hash_key, range_key): + table = self.tables.get(table_name) + if not table: + return None + + return table.delete_item(hash_key, range_key) + dynamodb_backend = DynamoDBBackend() diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index bac647295..0442c0764 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -48,6 +48,50 @@ class DynamoHandler(object): response["LastEvaluatedTableName"] = tables[-1] return json.dumps(response) + def CreateTable(self, uri, body, headers): + name = body['TableName'] + + key_schema = body['KeySchema'] + hash_hey = key_schema['HashKeyElement'] + 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'] + + throughput = body["ProvisionedThroughput"] + read_units = throughput["ReadCapacityUnits"] + write_units = throughput["WriteCapacityUnits"] + + table = dynamodb_backend.create_table( + name, + hash_key_attr=hash_key_attr, + hash_key_type=hash_key_type, + range_key_attr=range_key_attr, + range_key_type=range_key_type, + read_capacity=int(read_units), + write_capacity=int(write_units), + ) + return json.dumps(table.describe) + + def DeleteTable(self, uri, body, headers): + name = body['TableName'] + table = dynamodb_backend.delete_table(name) + if table: + return json.dumps(table.describe) + else: + er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' + return self.error(er) + + def UpdateTable(self, uri, body, headers): + name = body['TableName'] + throughput = body["ProvisionedThroughput"] + new_read_units = throughput["ReadCapacityUnits"] + new_write_units = throughput["WriteCapacityUnits"] + table = dynamodb_backend.update_table_throughput(name, new_read_units, new_write_units) + return json.dumps(table.describe) + def DescribeTable(self, uri, body, headers): name = body['TableName'] try: @@ -57,6 +101,66 @@ class DynamoHandler(object): return self.error(er) return json.dumps(table.describe) + def PutItem(self, uri, body, headers): + 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) + + def GetItem(self, uri, body, headers): + name = body['TableName'] + hash_key = body['Key']['HashKeyElement'].values()[0] + range_key = body['Key']['RangeKeyElement'].values()[0] + attrs_to_get = body.get('AttributesToGet') + item = dynamodb_backend.get_item(name, hash_key, range_key) + if item: + item_dict = item.describe_attrs(attrs_to_get) + item_dict['ConsumedCapacityUnits'] = 0.5 + return json.dumps(item_dict) + else: + er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' + return self.error(er) + + 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_value = range_condition['AttributeValueList'][0].values()[0] + items, last_page = dynamodb_backend.query(name, hash_key, range_comparison, range_value) + + 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, + } + 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] + return_values = body.get('ReturnValues', '') + item = dynamodb_backend.delete_item(name, hash_key, range_key) + if item: + if return_values == 'ALL_OLD': + item_dict = item.describe + else: + item_dict = {'Attributes': []} + item_dict['ConsumedCapacityUnits'] = 0.5 + return json.dumps(item_dict) + else: + er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' + return self.error(er) + def handler(uri, body, headers): body = json.loads(body or '{}') diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index 366994fc1..4a9ea5106 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -1,10 +1,11 @@ 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.condition import GT from boto.exception import DynamoDBResponseError @@ -36,19 +37,29 @@ def test_describe_missing_table(): 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_describe_table(): - dynamodb_backend.create_table( - 'messages', - hash_key_attr='forum_name', - hash_key_type='S', - range_key_attr='subject', - range_key_type='S', - read_capacity=10, - write_capacity=10, - ) - conn = boto.connect_dynamodb('the_key', 'the_secret') +def test_create_table(): + conn = boto.connect_dynamodb() + create_table(conn) + expected = { 'Table': { 'CreationDateTime': 1326499200.0, @@ -72,4 +83,147 @@ def test_describe_table(): 'TableStatus': 'ACTIVE' } } - assert conn.describe_table('messages') == expected + 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=GT('1')) + results.response['Items'].should.have.length_of(3) + + results = table.query(hash_key='the-key', range_key_condition=GT('234')) + results.response['Items'].should.have.length_of(2) + + results = table.query(hash_key='the-key', range_key_condition=GT('9999')) + results.response['Items'].should.have.length_of(0) + +# Batch read +# Batch write +# scan