Repository: boto/boto

Issue:
Add custom exception for Layer1.put_item() with expected_value
Currently, boto.dynamodb.layer1.Layer1.put_item raises a generic `DynamoDBResponseError` when a value that isn't expected is found. We'll definitely want a custom exception for this, since DynamoDBResponseError can mean all sorts of things right now. Perhaps `DynamoDBUnexpectedValueFoundError` or something?

```
>>> item.put(expected_value={'yay': False})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "boto/dynamodb/item.py", line 173, in put
    return self.table.layer2.put_item(self, expected_value, return_values)
  File "boto/dynamodb/layer2.py", line 517, in put_item
    object_hook=item_object_hook)
  File "boto/dynamodb/layer1.py", line 334, in put_item
    object_hook=object_hook)
  File "boto/dynamodb/layer1.py", line 121, in make_request
    retry_handler=self._retry_handler)
  File "boto/connection.py", line 746, in _mexe
    status = retry_handler(response, i, next_sleep)
  File "boto/dynamodb/layer1.py", line 148, in _retry_handler
    json_response)
boto.exception.DynamoDBResponseError: DynamoDBResponseError: 400 Bad Request
{'message': 'The conditional request failed', '__type': 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException'}
```

Add custom exception for Layer1.put_item() with expected_value
Currently, boto.dynamodb.layer1.Layer1.put_item raises a generic `DynamoDBResponseError` when a value that isn't expected is found. We'll definitely want a custom exception for this, since DynamoDBResponseError can mean all sorts of things right now. Perhaps `DynamoDBUnexpectedValueFoundError` or something?

```
>>> item.put(expected_value={'yay': False})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "boto/dynamodb/item.py", line 173, in put
    return self.table.layer2.put_item(self, expected_value, return_values)
  File "boto/dynamodb/layer2.py", line 517, in put_item
    object_hook=item_object_hook)
  File "boto/dynamodb/layer1.py", line 334, in put_item
    object_hook=object_hook)
  File "boto/dynamodb/layer1.py", line 121, in make_request
    retry_handler=self._retry_handler)
  File "boto/connection.py", line 746, in _mexe
    status = retry_handler(response, i, next_sleep)
  File "boto/dynamodb/layer1.py", line 148, in _retry_handler
    json_response)
boto.exception.DynamoDBResponseError: DynamoDBResponseError: 400 Bad Request
{'message': 'The conditional request failed', '__type': 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException'}
```



Discussion:

Resolving patch:
diff --git a/boto/dynamodb/exceptions.py b/boto/dynamodb/exceptions.py
--- a/boto/dynamodb/exceptions.py
+++ b/boto/dynamodb/exceptions.py
@@ -2,7 +2,7 @@
 Exceptions that are specific to the dynamodb module.
 """
 from boto.exception import BotoServerError, BotoClientError
-
+from boto.exception import DynamoDBResponseError
 
 class DynamoDBExpiredTokenError(BotoServerError):
     """
@@ -26,3 +26,20 @@ class DynamoDBItemError(BotoClientError):
     new Item in DynamoDB.
     """
     pass
+
+
+class DynamoDBConditionalCheckFailedError(DynamoDBResponseError):
+    """
+    Raised when a ConditionalCheckFailedException response is received.
+    This happens when a conditional check, expressed via the expected_value
+    paramenter, fails.
+    """
+    pass
+
+class DynamoDBValidationError(DynamoDBResponseError):
+    """
+    Raised when a ValidationException response is received. This happens
+    when one or more required parameter values are missing, or if the item
+    has exceeded the 64Kb size limit.
+    """
+    pass
diff --git a/boto/dynamodb/layer1.py b/boto/dynamodb/layer1.py
--- a/boto/dynamodb/layer1.py
+++ b/boto/dynamodb/layer1.py
@@ -70,6 +70,12 @@ class Layer1(AWSAuthConnection):
     SessionExpiredError = 'com.amazon.coral.service#ExpiredTokenException'
     """The error response returned when session token has expired"""
 
+    ConditionalCheckFailedError = 'ConditionalCheckFailedException'
+    """The error response returned when a conditional check fails"""
+
+    ValidationError = 'ValidationException'
+    """The error response returned when an item is invalid in some way"""
+
     ResponseError = DynamoDBResponseError
 
     def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
@@ -160,6 +166,12 @@ def _retry_handler(self, response, i, next_sleep):
                 self.creds = self._get_session_token()
                 self._update_provider()
                 status = (msg, i + self.num_retries - 1, 0)
+            elif self.ConditionalCheckFailedError in data.get('__type'):
+                raise dynamodb_exceptions.DynamoDBConditionalCheckFailedError(
+                    response.status, response.reason, data)
+            elif self.ValidationError in data.get('__type'):
+                raise dynamodb_exceptions.DynamoDBValidationError(
+                    response.status, response.reason, data)
             else:
                 raise self.ResponseError(response.status, response.reason,
                                          data)


Test patch:
diff --git a/tests/dynamodb/test_layer1.py b/tests/dynamodb/test_layer1.py
--- a/tests/dynamodb/test_layer1.py
+++ b/tests/dynamodb/test_layer1.py
@@ -27,6 +27,8 @@
 import unittest
 import time
 from boto.dynamodb.exceptions import DynamoDBKeyNotFoundError
+from boto.dynamodb.exceptions import DynamoDBConditionalCheckFailedError
+from boto.dynamodb.exceptions import DynamoDBValidationError
 from boto.dynamodb.layer1 import Layer1
 from boto.sts.credentials import Credentials
 
@@ -134,10 +136,9 @@ def test_layer1_basic(self):
 
         # Try to delete the item with the wrong Expected value
         expected = {'Views': {'Value': {'N': '1'}}}
-        try:
-            result = c.delete_item(table_name, key=key1, expected=expected)
-        except c.ResponseError, e:
-            assert e.error_code == 'ConditionalCheckFailedException'
+        self.assertRaises(DynamoDBConditionalCheckFailedError,
+                          c.delete_item, table_name, key=key1,
+                          expected=expected)
 
         # Now update the existing object
         attribute_updates = {'Views': {'Value': {'N': '5'},
@@ -147,6 +148,17 @@ def test_layer1_basic(self):
         result = c.update_item(table_name, key=key1,
                                attribute_updates=attribute_updates)
 
+        # Try and update an item, in a fashion which makes it too large.
+        # The new message text is the item size limit minus 32 bytes and
+        # the current object is larger than 32 bytes.
+        item_size_overflow_text = 'Text to be padded'.zfill(64*1024-32)
+        attribute_updates = {'Message': {'Value': {'S': item_size_overflow_text},
+                                       'Action': 'PUT'}}
+        self.assertRaises(DynamoDBValidationError,
+                          c.update_item, table_name, key=key1,
+                           attribute_updates=attribute_updates)
+
+
         # Put a few more items into the table
         item2_key = 'Amazon DynamoDB'
         item2_range = 'DynamoDB Thread 2'
diff --git a/tests/dynamodb/test_layer2.py b/tests/dynamodb/test_layer2.py
--- a/tests/dynamodb/test_layer2.py
+++ b/tests/dynamodb/test_layer2.py
@@ -28,6 +28,7 @@
 import time
 import uuid
 from boto.dynamodb.exceptions import DynamoDBKeyNotFoundError, DynamoDBItemError
+from boto.dynamodb.exceptions import DynamoDBConditionalCheckFailedError
 from boto.dynamodb.layer2 import Layer2
 from boto.dynamodb.types import get_dynamodb_type
 from boto.dynamodb.condition import *
@@ -166,12 +167,8 @@ def test_layer2_basic(self):
 
         # Try to delete the item with the wrong Expected value
         expected = {'Views': 1}
-        try:
-            item1.delete(expected_value=expected)
-        except c.layer1.ResponseError, e:
-            assert e.error_code == 'ConditionalCheckFailedException'
-        else:
-            raise Exception("Expected Value condition failed")
+        self.assertRaises(DynamoDBConditionalCheckFailedError,
+                          item1.delete, expected_value=expected)
 
         # Try to delete a value while expecting a non-existant attribute
         expected = {'FooBar': True}