From eaf2a09020851ad3b92d45c79c0a2742b0a367a7 Mon Sep 17 00:00:00 2001 From: Tomas Neme Date: Thu, 25 Jun 2026 20:48:20 +0200 Subject: [PATCH 1/4] Add condition failure return value tests --- tests/test_base_connection.py | 122 +++++++++++++++++++++++++++++++++ tests/test_model.py | 89 ++++++++++++++++++++++++ tests/test_table_connection.py | 69 +++++++++++++++++++ tests/test_transaction.py | 3 +- 4 files changed, 282 insertions(+), 1 deletion(-) diff --git a/tests/test_base_connection.py b/tests/test_base_connection.py index c90acd27d..a9f935d89 100644 --- a/tests/test_base_connection.py +++ b/tests/test_base_connection.py @@ -60,6 +60,21 @@ def test_meta_table_has_index_name(meta_table): assert not meta_table.has_index_name("NonExistentIndexName") +@pytest.mark.parametrize('return_values', ['NONE', 'ALL_OLD']) +def test_get_return_values_on_condition_failure_map__valid(return_values): + conn = Connection(REGION) + assert conn.get_return_values_on_condition_failure_map(return_values) == { + 'ReturnValuesOnConditionCheckFailure': return_values, + } + + +@pytest.mark.parametrize('return_values', ['ALL_NEW', 'UPDATED_OLD', 'UPDATED_NEW', 'badvalue']) +def test_get_return_values_on_condition_failure_map__invalid(return_values): + conn = Connection(REGION) + with pytest.raises(ValueError): + conn.get_return_values_on_condition_failure_map(return_values) + + def test_connection__create(): _ = Connection() conn = Connection(host='http://foohost') @@ -487,6 +502,29 @@ def test_connection_delete_item(): with pytest.raises(ValueError): conn.delete_item(TEST_TABLE_NAME, "foo", "bar", return_item_collection_metrics='badvalue') + with patch(PATCH_METHOD) as req: + req.return_value = {} + conn.delete_item( + TEST_TABLE_NAME, + "Amazon DynamoDB", + "How do I update multiple items?", + return_values_on_condition_failure='ALL_OLD' + ) + params = { + 'ReturnConsumedCapacity': 'TOTAL', + 'Key': { + 'ForumName': { + 'S': 'Amazon DynamoDB' + }, + 'Subject': { + 'S': 'How do I update multiple items?' + } + }, + 'TableName': TEST_TABLE_NAME, + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD' + } + assert req.call_args[0][1] == params + with patch(PATCH_METHOD) as req: req.return_value = {} conn.delete_item( @@ -532,6 +570,39 @@ def test_connection_delete_item(): } assert req.call_args[0][1] == params + with patch(PATCH_METHOD) as req: + req.return_value = {} + conn.update_item( + TEST_TABLE_NAME, + 'foo-key', + actions=[Path('Subject').set('foo-subject')], + range_key='foo-range-key', + return_values_on_condition_failure='ALL_OLD', + ) + params = { + 'ReturnConsumedCapacity': 'TOTAL', + 'Key': { + 'ForumName': { + 'S': 'foo-key' + }, + 'Subject': { + 'S': 'foo-range-key' + } + }, + 'UpdateExpression': 'SET #0 = :0', + 'ExpressionAttributeNames': { + '#0': 'Subject' + }, + 'ExpressionAttributeValues': { + ':0': { + 'S': 'foo-subject' + } + }, + 'TableName': TEST_TABLE_NAME, + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD' + } + assert req.call_args[0][1] == params + with patch(PATCH_METHOD) as req: req.return_value = {} conn.delete_item( @@ -792,6 +863,30 @@ def test_connection_put_item(): } assert req.call_args[0][1] == params + with patch(PATCH_METHOD) as req: + req.return_value = {} + conn.put_item( + TEST_TABLE_NAME, + 'foo-key', + range_key='foo-range-key', + attributes={'ForumName': 'foo-value'}, + return_values_on_condition_failure='ALL_OLD' + ) + params = { + 'ReturnConsumedCapacity': 'TOTAL', + 'Item': { + 'ForumName': { + 'S': 'foo-value' + }, + 'Subject': { + 'S': 'foo-range-key' + } + }, + 'TableName': TEST_TABLE_NAME, + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD' + } + assert req.call_args[0][1] == params + with patch(PATCH_METHOD) as req: req.return_value = {} conn.put_item( @@ -1371,6 +1466,33 @@ def test_connection__make_api_call__wraps_verbose_client_error_create(send_mock) in str(excinfo.value) ) + +def test_connection__make_api_call__preserves_error_response_item(): + c = Connection() + client = mock.Mock() + client._request_signer = None + client._make_api_call.side_effect = ClientError( + { + 'Error': { + 'Code': 'ConditionalCheckFailedException', + 'Message': 'The conditional request failed', + }, + 'Item': { + 'ForumName': {'S': 'Amazon DynamoDB'}, + }, + }, + 'PutItem', + ) + c._client = client + + with pytest.raises(VerboseClientError) as excinfo: + c._make_api_call('PutItem', {'TableName': TEST_TABLE_NAME}) + + assert excinfo.value.response['Item'] == { + 'ForumName': {'S': 'Amazon DynamoDB'}, + } + + @mock.patch('botocore.httpsession.URLLib3Session.send') def test_connection__make_api_call__wraps_verbose_client_error_batch(send_mock): response = AWSResponse( diff --git a/tests/test_model.py b/tests/test_model.py index da54303bb..d3a67e76c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -825,6 +825,48 @@ def test_update(self, mock_time): assert item.views is None self.assertEqual({'bob'}, item.custom_aliases) + def test_update_return_values_on_condition_failure(self): + item = SimpleUserModel(user_name='foo', email='foo@example.com') + + with patch(PATCH_METHOD) as req: + req.return_value = { + ATTRIBUTES: { + 'user_name': { + 'S': 'foo' + }, + 'email': { + 'S': 'bar@example.com' + } + } + } + item.update( + actions=[SimpleUserModel.email.set('bar@example.com')], + return_values_on_condition_failure='ALL_OLD', + ) + + args = req.call_args[0][1] + params = { + 'TableName': 'SimpleModel', + 'ReturnValues': 'ALL_NEW', + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD', + 'Key': { + 'user_name': { + 'S': 'foo' + } + }, + 'UpdateExpression': 'SET #0 = :0', + 'ExpressionAttributeNames': { + '#0': 'email' + }, + 'ExpressionAttributeValues': { + ':0': { + 'S': 'bar@example.com' + } + }, + 'ReturnConsumedCapacity': 'TOTAL' + } + deep_eq(args, params, _assert=True) + def test_update_doesnt_do_validation_on_null_attributes(self): item = CarModel(12345) item.car_info = CarInfoMap(make='Foo', model='Bar') @@ -899,6 +941,31 @@ def test_save(self): } deep_eq(args, params, _assert=True) + with patch(PATCH_METHOD) as req: + req.return_value = {} + item.save(return_values_on_condition_failure='ALL_OLD') + args = req.call_args[0][1] + params = { + 'Item': { + 'callable_field': { + 'N': '42' + }, + 'email': { + 'S': u'needs_email' + }, + 'user_id': { + 'S': u'bar' + }, + 'user_name': { + 'S': u'foo' + }, + }, + 'ReturnConsumedCapacity': 'TOTAL', + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD', + 'TableName': 'UserModel' + } + deep_eq(args, params, _assert=True) + with patch(PATCH_METHOD) as req: req.return_value = {} item.save(UserModel.email.does_not_exist() & UserModel.zip_code.exists()) @@ -3385,6 +3452,28 @@ def test_delete(add_version_condition: bool) -> None: args = req.call_args[0][1] assert args == expected + with patch(PATCH_METHOD) as req: + req.return_value = None + item.delete( + add_version_condition=add_version_condition, + return_values_on_condition_failure='ALL_OLD', + ) + expected = { + 'Key': { + 'user_id': { + 'S': 'bar' + }, + 'user_name': { + 'S': 'foo' + } + }, + 'ReturnConsumedCapacity': 'TOTAL', + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD', + 'TableName': 'UserModel' + } + args = req.call_args[0][1] + assert args == expected + with patch(PATCH_METHOD) as req: req.return_value = None item.delete(UserModel.user_id =='bar', add_version_condition=add_version_condition) diff --git a/tests/test_table_connection.py b/tests/test_table_connection.py index 2ccf27ff6..e09579076 100644 --- a/tests/test_table_connection.py +++ b/tests/test_table_connection.py @@ -331,6 +331,27 @@ def test_delete_item(self): } self.assertEqual(req.call_args[0][1], params) + with patch(PATCH_METHOD) as req: + req.return_value = {} + conn.delete_item( + "Amazon DynamoDB", + "How do I update multiple items?", + return_values_on_condition_failure='ALL_OLD') + params = { + 'ReturnConsumedCapacity': 'TOTAL', + 'Key': { + 'ForumName': { + 'S': 'Amazon DynamoDB' + }, + 'Subject': { + 'S': 'How do I update multiple items?' + } + }, + 'TableName': self.test_table_name, + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD' + } + self.assertEqual(req.call_args[0][1], params) + def test_update_item(self): """ TableConnection.update_item @@ -367,6 +388,38 @@ def test_update_item(self): } self.assertEqual(req.call_args[0][1], params) + with patch(PATCH_METHOD) as req: + req.return_value = HttpOK(), {} + conn.update_item( + 'foo-key', + actions=[Path('Subject').set('foo-subject')], + range_key='foo-range-key', + return_values_on_condition_failure='ALL_OLD', + ) + params = { + 'Key': { + 'ForumName': { + 'S': 'foo-key' + }, + 'Subject': { + 'S': 'foo-range-key' + } + }, + 'UpdateExpression': 'SET #0 = :0', + 'ExpressionAttributeNames': { + '#0': 'Subject' + }, + 'ExpressionAttributeValues': { + ':0': { + 'S': 'foo-subject' + } + }, + 'ReturnConsumedCapacity': 'TOTAL', + 'TableName': 'Thread', + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD' + } + self.assertEqual(req.call_args[0][1], params) + def test_get_item(self): """ TableConnection.get_item @@ -398,6 +451,22 @@ def test_put_item(self): } self.assertEqual(req.call_args[0][1], params) + with patch(PATCH_METHOD) as req: + req.return_value = {} + conn.put_item( + 'foo-key', + range_key='foo-range-key', + attributes={'ForumName': 'foo-value'}, + return_values_on_condition_failure='ALL_OLD' + ) + params = { + 'ReturnConsumedCapacity': 'TOTAL', + 'TableName': self.test_table_name, + 'Item': {'ForumName': {'S': 'foo-value'}, 'Subject': {'S': 'foo-range-key'}}, + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD' + } + self.assertEqual(req.call_args[0][1], params) + with patch(PATCH_METHOD) as req: req.return_value = {} conn.put_item( diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 94cb3c7fb..a35864c18 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -85,7 +85,7 @@ def test_commit(self, mocker): mock_connection_transact_write = mocker.patch.object(connection, 'transact_write_items') with TransactWrite(connection=connection) as t: t.condition_check(MockModel, 1, 3, condition=(MockModel.mock_hash.does_not_exist())) - t.delete(MockModel(2, 4)) + t.delete(MockModel(2, 4), return_values='ALL_OLD') t.save(MockModel(3, 5)) t.update(MockModel(4, 6), actions=[MockModel.mock_toot.set('hello')], return_values='ALL_OLD') @@ -99,6 +99,7 @@ def test_commit(self, mocker): 'ConditionExpression': 'attribute_not_exists (#0)', 'ExpressionAttributeNames': {'#0': 'mock_version'}, 'Key': {'mock_hash': {'N': '2'}, 'mock_range': {'N': '4'}}, + 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD', 'TableName': 'mock' }] expected_puts = [{ From d4d0040b79878885e21950415c590bf5c3c73e64 Mon Sep 17 00:00:00 2001 From: Tomas Neme Date: Thu, 25 Jun 2026 20:48:33 +0200 Subject: [PATCH 2/4] Support condition failure return values on writes --- pynamodb/connection/base.py | 17 ++++++++--- pynamodb/connection/table.py | 6 ++++ pynamodb/models.py | 56 +++++++++++++++++++++++++++++++----- pynamodb/transactions.py | 10 ++++++- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/pynamodb/connection/base.py b/pynamodb/connection/base.py index 301ee1715..0becbfe82 100644 --- a/pynamodb/connection/base.py +++ b/pynamodb/connection/base.py @@ -364,10 +364,13 @@ def _make_api_call(self, operation_name: str, operation_kwargs: Dict) -> Dict: try: return self.client._make_api_call(operation_name, operation_kwargs) except ClientError as e: - resp_metadata = e.response.get('ResponseMetadata', {}).get('HTTPHeaders', {}) - cancellation_reasons = e.response.get('CancellationReasons', []) + error_response = cast(Dict[str, Any], e.response) + resp_metadata = error_response.get('ResponseMetadata', {}).get('HTTPHeaders', {}) + cancellation_reasons = error_response.get('CancellationReasons', []) - botocore_props = {'Error': e.response.get('Error', {})} + botocore_props = {'Error': error_response.get('Error', {})} + if ITEM in error_response: + botocore_props[ITEM] = error_response[ITEM] verbose_props = { 'request_id': resp_metadata.get('x-amzn-requestid', ''), 'table_name': self._get_table_name_for_error_context(operation_kwargs), @@ -769,7 +772,7 @@ def get_return_values_on_condition_failure_map( """ Builds the return values map that is common to several operations """ - if return_values_on_condition_failure.upper() not in RETURN_VALUES_VALUES: + if return_values_on_condition_failure.upper() not in RETURN_VALUES_ON_CONDITION_FAILURE_VALUES: raise ValueError("{} must be one of {}".format( RETURN_VALUES_ON_CONDITION_FAILURE, RETURN_VALUES_ON_CONDITION_FAILURE_VALUES @@ -859,6 +862,7 @@ def delete_item( range_key: Optional[str] = None, condition: Optional[Condition] = None, return_values: Optional[str] = None, + return_values_on_condition_failure: Optional[str] = None, return_consumed_capacity: Optional[str] = None, return_item_collection_metrics: Optional[str] = None, ) -> Dict: @@ -871,6 +875,7 @@ def delete_item( range_key=range_key, condition=condition, return_values=return_values, + return_values_on_condition_failure=return_values_on_condition_failure, return_consumed_capacity=return_consumed_capacity, return_item_collection_metrics=return_item_collection_metrics ) @@ -889,6 +894,7 @@ def update_item( return_consumed_capacity: Optional[str] = None, return_item_collection_metrics: Optional[str] = None, return_values: Optional[str] = None, + return_values_on_condition_failure: Optional[str] = None, ) -> Dict: """ Performs the UpdateItem operation @@ -903,6 +909,7 @@ def update_item( actions=actions, condition=condition, return_values=return_values, + return_values_on_condition_failure=return_values_on_condition_failure, return_consumed_capacity=return_consumed_capacity, return_item_collection_metrics=return_item_collection_metrics, ) @@ -919,6 +926,7 @@ def put_item( attributes: Optional[Any] = None, condition: Optional[Condition] = None, return_values: Optional[str] = None, + return_values_on_condition_failure: Optional[str] = None, return_consumed_capacity: Optional[str] = None, return_item_collection_metrics: Optional[str] = None, ) -> Dict: @@ -933,6 +941,7 @@ def put_item( attributes=attributes, condition=condition, return_values=return_values, + return_values_on_condition_failure=return_values_on_condition_failure, return_consumed_capacity=return_consumed_capacity, return_item_collection_metrics=return_item_collection_metrics ) diff --git a/pynamodb/connection/table.py b/pynamodb/connection/table.py index 5e70ba5cc..bb22be7f5 100644 --- a/pynamodb/connection/table.py +++ b/pynamodb/connection/table.py @@ -90,6 +90,7 @@ def delete_item( range_key: Optional[str] = None, condition: Optional[Condition] = None, return_values: Optional[str] = None, + return_values_on_condition_failure: Optional[str] = None, return_consumed_capacity: Optional[str] = None, return_item_collection_metrics: Optional[str] = None, ) -> Dict: @@ -102,6 +103,7 @@ def delete_item( range_key=range_key, condition=condition, return_values=return_values, + return_values_on_condition_failure=return_values_on_condition_failure, return_consumed_capacity=return_consumed_capacity, return_item_collection_metrics=return_item_collection_metrics, ) @@ -115,6 +117,7 @@ def update_item( return_consumed_capacity: Optional[str] = None, return_item_collection_metrics: Optional[str] = None, return_values: Optional[str] = None, + return_values_on_condition_failure: Optional[str] = None, ) -> Dict: """ Performs the UpdateItem operation @@ -128,6 +131,7 @@ def update_item( return_consumed_capacity=return_consumed_capacity, return_item_collection_metrics=return_item_collection_metrics, return_values=return_values, + return_values_on_condition_failure=return_values_on_condition_failure, ) def put_item( @@ -137,6 +141,7 @@ def put_item( attributes: Optional[Any] = None, condition: Optional[Condition] = None, return_values: Optional[str] = None, + return_values_on_condition_failure: Optional[str] = None, return_consumed_capacity: Optional[str] = None, return_item_collection_metrics: Optional[str] = None, ) -> Dict: @@ -150,6 +155,7 @@ def put_item( attributes=attributes, condition=condition, return_values=return_values, + return_values_on_condition_failure=return_values_on_condition_failure, return_consumed_capacity=return_consumed_capacity, return_item_collection_metrics=return_item_collection_metrics, ) diff --git a/pynamodb/models.py b/pynamodb/models.py index 8e14918ed..35e15ce59 100644 --- a/pynamodb/models.py +++ b/pynamodb/models.py @@ -394,7 +394,13 @@ def batch_write(cls: Type[_T], auto_commit: bool = True) -> BatchWrite[_T]: """ return BatchWrite(cls, auto_commit=auto_commit) - def delete(self, condition: Optional[Condition] = None, *, add_version_condition: bool = True) -> Any: + def delete( + self, + condition: Optional[Condition] = None, + *, + add_version_condition: bool = True, + return_values_on_condition_failure: Optional[str] = None, + ) -> Any: """ Deletes this object from DynamoDB. @@ -409,9 +415,21 @@ def delete(self, condition: Optional[Condition] = None, *, add_version_condition if add_version_condition and version_condition is not None: condition &= version_condition - return self._get_connection().delete_item(hk_value, range_key=rk_value, condition=condition) + return self._get_connection().delete_item( + hk_value, + range_key=rk_value, + condition=condition, + return_values_on_condition_failure=return_values_on_condition_failure, + ) - def update(self, actions: List[Action], condition: Optional[Condition] = None, *, add_version_condition: bool = True) -> Any: + def update( + self, + actions: List[Action], + condition: Optional[Condition] = None, + *, + add_version_condition: bool = True, + return_values_on_condition_failure: Optional[str] = None, + ) -> Any: """ Updates an item using the UpdateItem operation. @@ -432,7 +450,14 @@ def update(self, actions: List[Action], condition: Optional[Condition] = None, * if add_version_condition and version_condition is not None: condition &= version_condition - data = self._get_connection().update_item(hk_value, range_key=rk_value, return_values=ALL_NEW, condition=condition, actions=actions) + data = self._get_connection().update_item( + hk_value, + range_key=rk_value, + return_values=ALL_NEW, + condition=condition, + actions=actions, + return_values_on_condition_failure=return_values_on_condition_failure, + ) item_data = data[ATTRIBUTES] stored_cls = self._get_discriminator_class(item_data) if stored_cls and stored_cls != type(self): @@ -440,11 +465,21 @@ def update(self, actions: List[Action], condition: Optional[Condition] = None, * self.deserialize(item_data) return data - def save(self, condition: Optional[Condition] = None, *, add_version_condition: bool = True) -> Dict[str, Any]: + def save( + self, + condition: Optional[Condition] = None, + *, + add_version_condition: bool = True, + return_values_on_condition_failure: Optional[str] = None, + ) -> Dict[str, Any]: """ Save this object to dynamodb """ - args, kwargs = self._get_save_args(condition=condition, add_version_condition=add_version_condition) + args, kwargs = self._get_save_args( + condition=condition, + add_version_condition=add_version_condition, + return_values_on_condition_failure=return_values_on_condition_failure, + ) data = self._get_connection().put_item(*args, **kwargs) self.update_local_version_attribute() return data @@ -894,7 +929,13 @@ def _get_schema(cls) -> ModelSchema: return schema - def _get_save_args(self, condition: Optional[Condition] = None, *, add_version_condition: bool = True) -> Tuple[Iterable[Any], Dict[str, Any]]: + def _get_save_args( + self, + condition: Optional[Condition] = None, + *, + add_version_condition: bool = True, + return_values_on_condition_failure: Optional[str] = None, + ) -> Tuple[Iterable[Any], Dict[str, Any]]: """ Gets the proper *args, **kwargs for saving and retrieving this object @@ -921,6 +962,7 @@ def _get_save_args(self, condition: Optional[Condition] = None, *, add_version_c condition &= version_condition kwargs['attributes'] = attribute_values kwargs['condition'] = condition + kwargs['return_values_on_condition_failure'] = return_values_on_condition_failure return args, kwargs def _get_hash_range_key_serialized_values(self) -> Tuple[Any, Optional[Any]]: diff --git a/pynamodb/transactions.py b/pynamodb/transactions.py index 5b141097a..d07f2d3e5 100644 --- a/pynamodb/transactions.py +++ b/pynamodb/transactions.py @@ -100,9 +100,17 @@ def condition_check(self, model_cls: Type[_M], hash_key: _KeyType, range_key: Op ) self._condition_check_items.append(operation_kwargs) - def delete(self, model: _M, condition: Optional[Condition] = None, *, add_version_condition: bool = True) -> None: + def delete( + self, + model: _M, + condition: Optional[Condition] = None, + return_values: Optional[str] = None, + *, + add_version_condition: bool = True, + ) -> None: operation_kwargs = model.get_delete_kwargs_from_instance( condition=condition, + return_values_on_condition_failure=return_values, add_version_condition=add_version_condition, ) self._delete_items.append(operation_kwargs) From ac680a8f13ee2ce45e6de629d0b047ef67368ecf Mon Sep 17 00:00:00 2001 From: Tomas Neme Date: Thu, 25 Jun 2026 20:48:39 +0200 Subject: [PATCH 3/4] Document condition failure return values --- docs/conditional.rst | 27 ++++++++++++++++++++++++++- docs/transaction.rst | 4 +++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/conditional.rst b/docs/conditional.rst index 74d3fb74d..db7046041 100644 --- a/docs/conditional.rst +++ b/docs/conditional.rst @@ -158,5 +158,30 @@ You can check for conditional operation failures by inspecting the cause of the try: thread_item.save(Thread.forum_name.exists()) except PutError as e: - if e.cause_response_code = "ConditionalCheckFailedException": + if e.cause_response_code == "ConditionalCheckFailedException": raise ThreadDidNotExistError() + +DynamoDB can also return the old item when a conditional write fails. ``Model.save()``, +``Model.update()``, and ``Model.delete()`` accept ``return_values_on_condition_failure`` +for this DynamoDB option: + +.. code-block:: python + + from pynamodb.constants import ALL_OLD + from pynamodb.exceptions import PutError + + try: + thread_item.save( + condition=Thread.forum_name.exists(), + return_values_on_condition_failure=ALL_OLD, + ) + except PutError as e: + if e.cause_response_code == "ConditionalCheckFailedException": + old_item = e.cause.response.get('Item') + +``return_values_on_condition_failure=ALL_OLD`` can also be passed to ``update()`` and +``delete()``. PynamoDB still raises the existing write error wrapper +(:class:`~pynamodb.exceptions.PutError`, :class:`~pynamodb.exceptions.UpdateError`, or +:class:`~pynamodb.exceptions.DeleteError`) when the condition fails. If DynamoDB includes +the old item, it is available as a raw DynamoDB AttributeValue map through the wrapped +botocore error response, for example ``e.cause.response.get('Item')``. diff --git a/docs/transaction.rst b/docs/transaction.rst index 30c0b2ddd..db6c1ce09 100644 --- a/docs/transaction.rst +++ b/docs/transaction.rst @@ -145,6 +145,7 @@ The ``Delete`` operation functions similarly to ``Model.delete``. * ``model`` (required) * ``condition`` (optional) - of type :py:class:`Condition ` (see :ref:`conditional_operations`) +* ``return_values`` (optional) - the values that should be returned if the condition fails (see `Delete ReturnValuesOnConditionCheckFailure`_ in the DynamoDB API reference) .. code-block:: python @@ -162,7 +163,7 @@ The ``Put`` operation functions similarly to ``Model.save``. * ``model`` (required) * ``condition`` (optional) - of type :py:class:`Condition ` (see :ref:`conditional_operations`) -* ``return_values`` (optional) - the values that should be returned if the condition fails ((see `Put ReturnValuesOnConditionCheckFailure`_ in the DynamoDB API reference) +* ``return_values`` (optional) - the values that should be returned if the condition fails (see `Put ReturnValuesOnConditionCheckFailure`_ in the DynamoDB API reference) .. code-block:: python @@ -228,5 +229,6 @@ You can expect some new error types with transactions, such as: .. _Update ReturnValuesOnConditionCheckFailure: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Update.html#DDB-Type-Update-ReturnValuesOnConditionCheckFailure> .. _Put ReturnValuesOnConditionCheckFailure: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Put.html#DDB-Type-Put-ReturnValuesOnConditionCheckFailure +.. _Delete ReturnValuesOnConditionCheckFailure: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Delete.html#DDB-Type-Delete-ReturnValuesOnConditionCheckFailure .. _TransactWriteItems Errors: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html#API_TransactWriteItems_Errors .. _TransactGetItems Errors: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html#API_TransactGetItems_Errors From 276a509f3452708dafc3a3b4de040cda45508d2d Mon Sep 17 00:00:00 2001 From: Tomas Neme Date: Thu, 25 Jun 2026 21:51:21 +0200 Subject: [PATCH 4/4] use constants in test calls --- tests/test_base_connection.py | 22 +++++++++++----------- tests/test_model.py | 8 ++++---- tests/test_table_connection.py | 8 ++++---- tests/test_transaction.py | 6 +++--- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/test_base_connection.py b/tests/test_base_connection.py index a9f935d89..e17009b5d 100644 --- a/tests/test_base_connection.py +++ b/tests/test_base_connection.py @@ -24,7 +24,7 @@ TableError, DeleteError, PutError, ScanError, GetError, UpdateError, TableDoesNotExist, VerboseClientError) from pynamodb.constants import ( UNPROCESSED_ITEMS, STRING, BINARY, DEFAULT_ENCODING, TABLE_KEY, - PAY_PER_REQUEST_BILLING_MODE) + PAY_PER_REQUEST_BILLING_MODE, ALL_NEW, ALL_OLD, NONE, UPDATED_OLD, UPDATED_NEW) from pynamodb.expressions.operand import Path, Value from pynamodb.expressions.update import SetAction from .data import DESCRIBE_TABLE_DATA, GET_ITEM_DATA, LIST_TABLE_DATA @@ -60,7 +60,7 @@ def test_meta_table_has_index_name(meta_table): assert not meta_table.has_index_name("NonExistentIndexName") -@pytest.mark.parametrize('return_values', ['NONE', 'ALL_OLD']) +@pytest.mark.parametrize('return_values', [NONE, ALL_OLD]) def test_get_return_values_on_condition_failure_map__valid(return_values): conn = Connection(REGION) assert conn.get_return_values_on_condition_failure_map(return_values) == { @@ -68,7 +68,7 @@ def test_get_return_values_on_condition_failure_map__valid(return_values): } -@pytest.mark.parametrize('return_values', ['ALL_NEW', 'UPDATED_OLD', 'UPDATED_NEW', 'badvalue']) +@pytest.mark.parametrize('return_values', [ALL_NEW, UPDATED_OLD, UPDATED_NEW, 'badvalue']) def test_get_return_values_on_condition_failure_map__invalid(return_values): conn = Connection(REGION) with pytest.raises(ValueError): @@ -476,7 +476,7 @@ def test_connection_delete_item(): TEST_TABLE_NAME, "Amazon DynamoDB", "How do I update multiple items?", - return_values='ALL_NEW' + return_values=ALL_NEW ) params = { 'ReturnConsumedCapacity': 'TOTAL', @@ -508,7 +508,7 @@ def test_connection_delete_item(): TEST_TABLE_NAME, "Amazon DynamoDB", "How do I update multiple items?", - return_values_on_condition_failure='ALL_OLD' + return_values_on_condition_failure=ALL_OLD ) params = { 'ReturnConsumedCapacity': 'TOTAL', @@ -577,7 +577,7 @@ def test_connection_delete_item(): 'foo-key', actions=[Path('Subject').set('foo-subject')], range_key='foo-range-key', - return_values_on_condition_failure='ALL_OLD', + return_values_on_condition_failure=ALL_OLD, ) params = { 'ReturnConsumedCapacity': 'TOTAL', @@ -698,8 +698,8 @@ def test_connection_update_item(): TEST_TABLE_NAME, 'foo-key', return_consumed_capacity='TOTAL', - return_item_collection_metrics='NONE', - return_values='ALL_NEW', + return_item_collection_metrics=NONE, + return_values=ALL_NEW, actions=[Path('Subject').set('foo-subject')], condition=Path('Forum').does_not_exist(), range_key='foo-range-key', @@ -794,7 +794,7 @@ def test_connection_put_item(): with patch(PATCH_METHOD) as req: req.side_effect = BotoCoreError with pytest.raises(TableError): - conn.put_item('foo-key', TEST_TABLE_NAME, return_values='ALL_NEW', attributes={'ForumName': 'foo-value'}) + conn.put_item('foo-key', TEST_TABLE_NAME, return_values=ALL_NEW, attributes={'ForumName': 'foo-value'}) with patch(PATCH_METHOD) as req: req.return_value = {} @@ -804,7 +804,7 @@ def test_connection_put_item(): range_key='foo-range-key', return_consumed_capacity='TOTAL', return_item_collection_metrics='SIZE', - return_values='ALL_NEW', + return_values=ALL_NEW, attributes={'ForumName': 'foo-value'} ) params = { @@ -870,7 +870,7 @@ def test_connection_put_item(): 'foo-key', range_key='foo-range-key', attributes={'ForumName': 'foo-value'}, - return_values_on_condition_failure='ALL_OLD' + return_values_on_condition_failure=ALL_OLD ) params = { 'ReturnConsumedCapacity': 'TOTAL', diff --git a/tests/test_model.py b/tests/test_model.py index d3a67e76c..bccb3fff1 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -18,7 +18,7 @@ from pynamodb.constants import ( ITEM, STRING, ALL, KEYS_ONLY, INCLUDE, REQUEST_ITEMS, UNPROCESSED_KEYS, CAMEL_COUNT, RESPONSES, KEYS, ITEMS, LAST_EVALUATED_KEY, EXCLUSIVE_START_KEY, ATTRIBUTES, BINARY, - UNPROCESSED_ITEMS, DEFAULT_ENCODING, MAP, LIST, NUMBER, SCANNED_COUNT, + UNPROCESSED_ITEMS, DEFAULT_ENCODING, MAP, LIST, NUMBER, SCANNED_COUNT, ALL_NEW, ALL_OLD, ) from pynamodb.models import Model from pynamodb.indexes import ( @@ -841,7 +841,7 @@ def test_update_return_values_on_condition_failure(self): } item.update( actions=[SimpleUserModel.email.set('bar@example.com')], - return_values_on_condition_failure='ALL_OLD', + return_values_on_condition_failure=ALL_OLD, ) args = req.call_args[0][1] @@ -943,7 +943,7 @@ def test_save(self): with patch(PATCH_METHOD) as req: req.return_value = {} - item.save(return_values_on_condition_failure='ALL_OLD') + item.save(return_values_on_condition_failure=ALL_OLD) args = req.call_args[0][1] params = { 'Item': { @@ -3456,7 +3456,7 @@ def test_delete(add_version_condition: bool) -> None: req.return_value = None item.delete( add_version_condition=add_version_condition, - return_values_on_condition_failure='ALL_OLD', + return_values_on_condition_failure=ALL_OLD, ) expected = { 'Key': { diff --git a/tests/test_table_connection.py b/tests/test_table_connection.py index e09579076..c61bd5bb0 100644 --- a/tests/test_table_connection.py +++ b/tests/test_table_connection.py @@ -6,7 +6,7 @@ from pynamodb.connection import TableConnection from pynamodb.connection.base import MetaTable -from pynamodb.constants import TABLE_KEY +from pynamodb.constants import ALL_OLD, TABLE_KEY from pynamodb.expressions.operand import Path from .data import DESCRIBE_TABLE_DATA, GET_ITEM_DATA from .response import HttpOK @@ -336,7 +336,7 @@ def test_delete_item(self): conn.delete_item( "Amazon DynamoDB", "How do I update multiple items?", - return_values_on_condition_failure='ALL_OLD') + return_values_on_condition_failure=ALL_OLD) params = { 'ReturnConsumedCapacity': 'TOTAL', 'Key': { @@ -394,7 +394,7 @@ def test_update_item(self): 'foo-key', actions=[Path('Subject').set('foo-subject')], range_key='foo-range-key', - return_values_on_condition_failure='ALL_OLD', + return_values_on_condition_failure=ALL_OLD, ) params = { 'Key': { @@ -457,7 +457,7 @@ def test_put_item(self): 'foo-key', range_key='foo-range-key', attributes={'ForumName': 'foo-value'}, - return_values_on_condition_failure='ALL_OLD' + return_values_on_condition_failure=ALL_OLD ) params = { 'ReturnConsumedCapacity': 'TOTAL', diff --git a/tests/test_transaction.py b/tests/test_transaction.py index a35864c18..fd243e38c 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -3,7 +3,7 @@ from pynamodb.connection import Connection from pynamodb.connection.base import MetaTable -from pynamodb.constants import TABLE_KEY +from pynamodb.constants import ALL_OLD, TABLE_KEY from pynamodb.transactions import Transaction, TransactGet, TransactWrite from pynamodb.models import Model from tests.test_base_connection import PATCH_METHOD @@ -85,9 +85,9 @@ def test_commit(self, mocker): mock_connection_transact_write = mocker.patch.object(connection, 'transact_write_items') with TransactWrite(connection=connection) as t: t.condition_check(MockModel, 1, 3, condition=(MockModel.mock_hash.does_not_exist())) - t.delete(MockModel(2, 4), return_values='ALL_OLD') + t.delete(MockModel(2, 4), return_values=ALL_OLD) t.save(MockModel(3, 5)) - t.update(MockModel(4, 6), actions=[MockModel.mock_toot.set('hello')], return_values='ALL_OLD') + t.update(MockModel(4, 6), actions=[MockModel.mock_toot.set('hello')], return_values=ALL_OLD) expected_condition_checks = [{ 'ConditionExpression': 'attribute_not_exists (#0)',