Repository: getmoto/moto

Issue:
Feature: Implement 'update_item' for mock_dynamodb (v1)
I am running as standalone/server mode,
`# moto_server dynamodb -H 0.0.0.0`

I'm getting following traceback when I try to update item,
`Error on request:
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/werkzeug/serving.py", line 323, in run_wsgi
    execute(self.server.app)
  File "/usr/local/lib/python2.7/dist-packages/werkzeug/serving.py", line 312, in execute
    application_iter = app(environ, start_response)
  File "/usr/local/lib/python2.7/dist-packages/moto/server.py", line 135, in __call__
    return backend_app(environ, start_response)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2463, in __call__
    return self.wsgi_app(environ, start_response)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2449, in wsgi_app
    response = self.handle_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1866, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2446, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1951, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1820, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1949, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1935, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/local/lib/python2.7/dist-packages/moto/core/utils.py", line 144, in __call__
    result = self.callback(request, request.url, {})
  File "/usr/local/lib/python2.7/dist-packages/moto/core/responses.py", line 197, in dispatch
    return cls()._dispatch(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/moto/core/responses.py", line 295, in _dispatch
    return self.call_action()
  File "/usr/local/lib/python2.7/dist-packages/moto/dynamodb/responses.py", line 30, in call_action
    response = getattr(self, endpoint)()
AttributeError: 'DynamoHandler' object has no attribute 'update_item'
`

I have following questions as well,
1. Does moto only support Dynamodb 2011 API version, does this support 2012 API version?
2. moto::dynamodb update_item does not support update_item.




Discussion:
Hi @darshan-ghumare, moto.dynamodb supports the 2011 API - moto.dynamodb2 supports the 2012 API version.

You're right that moto.dynamodb does not support update_item, but moto.dynamodb2 does support it. Is it possible to upgrade for you?

@bblommers It would be difficult to upgrade the App to 2012 version.
Is there any branch/implementation that added update_item in moto for API 2011 version?
 
Not yet, but I'll mark this as an enhancement if people want to pick this up

Resolving patch:
diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py
--- a/moto/dynamodb/models.py
+++ b/moto/dynamodb/models.py
@@ -38,6 +38,12 @@ def __eq__(self, other):
     def __repr__(self):
         return "DynamoType: {0}".format(self.to_json())
 
+    def add(self, dyn_type):
+        if self.type == "SS":
+            self.value.append(dyn_type.value)
+        if self.type == "N":
+            self.value = str(int(self.value) + int(dyn_type.value))
+
     def to_json(self):
         return {self.type: self.value}
 
@@ -281,6 +287,20 @@ def delete_item(self, hash_key, range_key):
         except KeyError:
             return None
 
+    def update_item(self, hash_key, range_key, attr_updates):
+        item = self.get_item(hash_key, range_key)
+        if not item:
+            return None
+
+        for attr, update in attr_updates.items():
+            if update["Action"] == "PUT":
+                item.attrs[attr] = DynamoType(update["Value"])
+            if update["Action"] == "DELETE":
+                item.attrs.pop(attr)
+            if update["Action"] == "ADD":
+                item.attrs[attr].add(DynamoType(update["Value"]))
+        return item
+
     def get_cfn_attribute(self, attribute_name):
         from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
 
@@ -360,5 +380,15 @@ def delete_item(self, table_name, hash_key_dict, range_key_dict):
 
         return table.delete_item(hash_key, range_key)
 
+    def update_item(self, table_name, hash_key_dict, range_key_dict, attr_updates):
+        table = self.tables.get(table_name)
+        if not table:
+            return None
+
+        hash_key = DynamoType(hash_key_dict)
+        range_key = DynamoType(range_key_dict) if range_key_dict else None
+
+        return table.update_item(hash_key, range_key, attr_updates)
+
 
 dynamodb_backend = DynamoDBBackend()
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -276,3 +276,22 @@ def delete_item(self):
         else:
             er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"
             return self.error(er)
+
+    def update_item(self):
+        name = self.body["TableName"]
+        key = self.body["Key"]
+        hash_key = key["HashKeyElement"]
+        range_key = key.get("RangeKeyElement")
+        updates = self.body["AttributeUpdates"]
+        return_values = self.body.get("ReturnValues", "")  # noqa
+
+        item = dynamodb_backend.update_item(name, hash_key, range_key, updates)
+
+        if item:
+            item_dict = item.to_json()
+            item_dict["ConsumedCapacityUnits"] = 0.5
+
+            return dynamo_json_dump(item_dict)
+        else:
+            er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"
+            return self.error(er)


Test patch:
diff --git a/tests/test_dynamodb/test_server.py b/tests/test_dynamodb/test_server.py
--- a/tests/test_dynamodb/test_server.py
+++ b/tests/test_dynamodb/test_server.py
@@ -1,4 +1,5 @@
 from __future__ import unicode_literals
+import json
 import sure  # noqa
 
 import moto.server as server
@@ -18,3 +19,169 @@ def test_table_list():
     headers = {"X-Amz-Target": "TestTable.ListTables"}
     res = test_client.get("/", headers=headers)
     res.data.should.contain(b"TableNames")
+
+
+def test_update_item():
+    backend = server.create_backend_app("dynamodb")
+    test_client = backend.test_client()
+
+    create_table(test_client)
+
+    headers, res = put_item(test_client)
+
+    # UpdateItem
+    headers["X-Amz-Target"] = "DynamoDB_20111205.UpdateItem"
+    request_body = {
+        "TableName": "Table1",
+        "Key": {
+            "HashKeyElement": {"S": "customer"},
+            "RangeKeyElement": {"N": "12341234"},
+        },
+        "AttributeUpdates": {"new_att": {"Value": {"SS": ["val"]}, "Action": "PUT"}},
+    }
+    res = test_client.post("/", headers=headers, json=request_body)
+
+    # UpdateItem
+    headers["X-Amz-Target"] = "DynamoDB_20111205.UpdateItem"
+    request_body = {
+        "TableName": "Table1",
+        "Key": {
+            "HashKeyElement": {"S": "customer"},
+            "RangeKeyElement": {"N": "12341234"},
+        },
+        "AttributeUpdates": {"new_n": {"Value": {"N": "42"}, "Action": "PUT"}},
+    }
+    res = test_client.post("/", headers=headers, json=request_body)
+    res = json.loads(res.data)
+
+    res["ConsumedCapacityUnits"].should.equal(0.5)
+    res["Attributes"].should.equal(
+        {
+            "hkey": "customer",
+            "name": "myname",
+            "rkey": "12341234",
+            "new_att": ["val"],
+            "new_n": "42",
+        }
+    )
+
+    # UpdateItem - multiples
+    headers["X-Amz-Target"] = "DynamoDB_20111205.UpdateItem"
+    request_body = {
+        "TableName": "Table1",
+        "Key": {
+            "HashKeyElement": {"S": "customer"},
+            "RangeKeyElement": {"N": "12341234"},
+        },
+        "AttributeUpdates": {
+            "new_n": {"Value": {"N": 7}, "Action": "ADD"},
+            "new_att": {"Value": {"S": "val2"}, "Action": "ADD"},
+            "name": {"Action": "DELETE"},
+        },
+    }
+    res = test_client.post("/", headers=headers, json=request_body)
+    res = json.loads(res.data)
+
+    res["ConsumedCapacityUnits"].should.equal(0.5)
+    res["Attributes"].should.equal(
+        {
+            "hkey": "customer",
+            "rkey": "12341234",
+            "new_att": ["val", "val2"],
+            "new_n": "49",
+        }
+    )
+
+    # GetItem
+    headers["X-Amz-Target"] = "DynamoDB_20111205.GetItem"
+    request_body = {
+        "TableName": "Table1",
+        "Key": {
+            "HashKeyElement": {"S": "customer"},
+            "RangeKeyElement": {"N": "12341234"},
+        },
+    }
+    res = test_client.post("/", headers=headers, json=request_body)
+    res = json.loads(res.data)
+    res["Item"].should.have.key("new_att").equal({"SS": ["val", "val2"]})
+    res["Item"].should.have.key("new_n").equal({"N": "49"})
+    res["Item"].shouldnt.have.key("name")
+
+
+def test_update_item_that_doesnt_exist():
+    backend = server.create_backend_app("dynamodb")
+    test_client = backend.test_client()
+
+    create_table(test_client)
+
+    # UpdateItem
+    headers = {"X-Amz-Target": "DynamoDB_20111205.UpdateItem"}
+    request_body = {
+        "TableName": "Table1",
+        "Key": {
+            "HashKeyElement": {"S": "customer"},
+            "RangeKeyElement": {"N": "12341234"},
+        },
+        "AttributeUpdates": {"new_att": {"Value": {"SS": ["val"]}, "Action": "PUT"}},
+    }
+    res = test_client.post("/", headers=headers, json=request_body)
+    res.status_code.should.equal(400)
+    json.loads(res.data).should.equal(
+        {"__type": "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"}
+    )
+
+
+def test_update_item_in_nonexisting_table():
+    backend = server.create_backend_app("dynamodb")
+    test_client = backend.test_client()
+
+    # UpdateItem
+    headers = {"X-Amz-Target": "DynamoDB_20111205.UpdateItem"}
+    request_body = {
+        "TableName": "nonexistent",
+        "Key": {
+            "HashKeyElement": {"S": "customer"},
+            "RangeKeyElement": {"N": "12341234"},
+        },
+        "AttributeUpdates": {"new_att": {"Value": {"SS": ["val"]}, "Action": "PUT"}},
+    }
+    res = test_client.post("/", headers=headers, json=request_body)
+    res.status_code.should.equal(400)
+    json.loads(res.data).should.equal(
+        {"__type": "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"}
+    )
+
+
+def put_item(test_client, rkey="12341234"):
+    headers = {
+        "X-Amz-Target": "DynamoDB_20111205.PutItem",
+        "Content-Type": "application/x-amz-json-1.0",
+    }
+    request_body = {
+        "TableName": "Table1",
+        "Item": {
+            "hkey": {"S": "customer"},
+            "rkey": {"N": rkey},
+            "name": {"S": "myname"},
+        },
+        "ReturnValues": "ALL_OLD",
+    }
+    res = test_client.post("/", headers=headers, json=request_body)
+    res = json.loads(res.data)
+    return headers, res
+
+
+def create_table(test_client):
+    headers = {
+        "X-Amz-Target": "DynamoDB_20111205.CreateTable",
+        "Content-Type": "application/x-amz-json-1.0",
+    }
+    request_body = {
+        "TableName": "Table1",
+        "KeySchema": {
+            "HashKeyElement": {"AttributeName": "hkey", "AttributeType": "S"},
+            "RangeKeyElement": {"AttributeName": "rkey", "AttributeType": "N"},
+        },
+        "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 10},
+    }
+    return test_client.post("/", headers=headers, json=request_body)