From ba68944ea281cb55bfce1b3fab0190cecdf6b7cd Mon Sep 17 00:00:00 2001 From: Derek Knapp Date: Fri, 9 Feb 2024 20:13:51 -0800 Subject: [PATCH 1/6] return item --- pynamodb/connection/base.py | 1 + pynamodb/exceptions.py | 1 + .../test_transaction_integration.py | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/pynamodb/connection/base.py b/pynamodb/connection/base.py index 3e20255d..00d4d5c6 100644 --- a/pynamodb/connection/base.py +++ b/pynamodb/connection/base.py @@ -357,6 +357,7 @@ def _make_api_call(self, operation_name: str, operation_kwargs: Dict) -> Dict: CancellationReason( code=d['Code'], message=d.get('Message'), + item=d.get('Item'), ) if d['Code'] != 'None' else None ) for d in cancellation_reasons diff --git a/pynamodb/exceptions.py b/pynamodb/exceptions.py index 1c78c69f..0f85b1c7 100644 --- a/pynamodb/exceptions.py +++ b/pynamodb/exceptions.py @@ -134,6 +134,7 @@ class CancellationReason: """ code: str message: Optional[str] = None + item: Optional[Dict[str, Any]] = None class TransactWriteError(PynamoDBException): diff --git a/tests/integration/test_transaction_integration.py b/tests/integration/test_transaction_integration.py index 48ef041c..48e44641 100644 --- a/tests/integration/test_transaction_integration.py +++ b/tests/integration/test_transaction_integration.py @@ -5,6 +5,7 @@ import pytest from pynamodb.connection import Connection +from pynamodb.constants import ALL_OLD from pynamodb.exceptions import CancellationReason from pynamodb.exceptions import DoesNotExist, TransactWriteError, InvalidStateError @@ -168,6 +169,24 @@ def test_transact_write__error__transaction_cancelled__condition_check_failure(c assert BankStatement.Meta.table_name in exc_info.value.cause.MSG_TEMPLATE +@pytest.mark.ddblocal +def test_transact_write__error__transaction_cancelled__condition_check_failure__return_all_old(connection): + # create a users and a bank statements for them + User(1).save() + + # attempt to do this as a transaction with the condition that they don't already exist + with pytest.raises(TransactWriteError) as exc_info: + with TransactWrite(connection=connection) as transaction: + transaction.save(User(1), condition=(User.user_id.does_not_exist()), return_values=ALL_OLD) + assert exc_info.value.cause_response_code == TRANSACTION_CANCELLED + assert 'ConditionalCheckFailed' in exc_info.value.cause_response_message + assert exc_info.value.cancellation_reasons == [ + CancellationReason(code='ConditionalCheckFailed', message='The conditional request failed', item=User(1).to_dynamodb_dict()), + ] + assert isinstance(exc_info.value.cause, botocore.exceptions.ClientError) + assert User.Meta.table_name in exc_info.value.cause.MSG_TEMPLATE + + @pytest.mark.ddblocal def test_transact_write__error__transaction_cancelled__partial_failure(connection): User(2).delete() From 57280ac3d23c33c5a1e81e05d6be83ad6b8f9c8b Mon Sep 17 00:00:00 2001 From: Derek Knapp Date: Fri, 9 Feb 2024 20:19:51 -0800 Subject: [PATCH 2/6] change type --- pynamodb/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynamodb/exceptions.py b/pynamodb/exceptions.py index 0f85b1c7..42f69fcb 100644 --- a/pynamodb/exceptions.py +++ b/pynamodb/exceptions.py @@ -134,7 +134,7 @@ class CancellationReason: """ code: str message: Optional[str] = None - item: Optional[Dict[str, Any]] = None + item: Optional[Dict[str, Dict[str, Any]]] = None class TransactWriteError(PynamoDBException): From 33c66a29df68dada7a7f1d4fe0e9b60bf3b9bc8a Mon Sep 17 00:00:00 2001 From: Derek Knapp Date: Fri, 9 Feb 2024 21:59:15 -0800 Subject: [PATCH 3/6] cast --- pynamodb/connection/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynamodb/connection/base.py b/pynamodb/connection/base.py index 00d4d5c6..939d5c08 100644 --- a/pynamodb/connection/base.py +++ b/pynamodb/connection/base.py @@ -357,7 +357,7 @@ def _make_api_call(self, operation_name: str, operation_kwargs: Dict) -> Dict: CancellationReason( code=d['Code'], message=d.get('Message'), - item=d.get('Item'), + item=cast(Optional[Dict[str, Dict[str, Any]]], d.get('Item')), ) if d['Code'] != 'None' else None ) for d in cancellation_reasons From 7f757fb44ad06fa8436facf157c4ed96ddfe0b53 Mon Sep 17 00:00:00 2001 From: Derek Knapp Date: Fri, 9 Feb 2024 22:09:12 -0800 Subject: [PATCH 4/6] docs --- docs/transaction.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/transaction.rst b/docs/transaction.rst index d5f6788d..8fe31a8e 100644 --- a/docs/transaction.rst +++ b/docs/transaction.rst @@ -94,7 +94,8 @@ Now, say you make another attempt to debit one of the accounts when they don't h condition=( (BankStatement.account_balance >= transfer_amount) & (BankStatement.is_active == True) - ) + ), + return_values=ALL_OLD ) transaction.update( BankStatement(user_id='user2'), @@ -107,6 +108,8 @@ Now, say you make another attempt to debit one of the accounts when they don't h assert e.cause_response_code == 'TransactionCanceledException' # the first 'update' was a reason for the cancellation assert e.cancellation_reasons[0].code == 'ConditionalCheckFailed' + # when return_values=ALL_OLD, the old values can be accessed from the item property + assert BankStatement.from_dynamodb_dict(e.cancellation_reasons[0].item) == user1_statement # the second 'update' wasn't a reason, but was cancelled too assert e.cancellation_reasons[1] is None From 0a423dcb379fee526853ae6e00f0ecabbb02d4bf Mon Sep 17 00:00:00 2001 From: Derek Knapp Date: Sat, 10 Feb 2024 14:58:39 -0800 Subject: [PATCH 5/6] rename to raw_item --- docs/transaction.rst | 4 ++-- pynamodb/connection/base.py | 2 +- pynamodb/exceptions.py | 2 +- tests/integration/test_transaction_integration.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/transaction.rst b/docs/transaction.rst index 8fe31a8e..30c0b2dd 100644 --- a/docs/transaction.rst +++ b/docs/transaction.rst @@ -108,8 +108,8 @@ Now, say you make another attempt to debit one of the accounts when they don't h assert e.cause_response_code == 'TransactionCanceledException' # the first 'update' was a reason for the cancellation assert e.cancellation_reasons[0].code == 'ConditionalCheckFailed' - # when return_values=ALL_OLD, the old values can be accessed from the item property - assert BankStatement.from_dynamodb_dict(e.cancellation_reasons[0].item) == user1_statement + # when return_values=ALL_OLD, the old values can be accessed from the raw_item property + assert BankStatement.from_dynamodb_dict(e.cancellation_reasons[0].raw_item) == user1_statement # the second 'update' wasn't a reason, but was cancelled too assert e.cancellation_reasons[1] is None diff --git a/pynamodb/connection/base.py b/pynamodb/connection/base.py index 939d5c08..d0181a81 100644 --- a/pynamodb/connection/base.py +++ b/pynamodb/connection/base.py @@ -357,7 +357,7 @@ def _make_api_call(self, operation_name: str, operation_kwargs: Dict) -> Dict: CancellationReason( code=d['Code'], message=d.get('Message'), - item=cast(Optional[Dict[str, Dict[str, Any]]], d.get('Item')), + raw_item=cast(Optional[Dict[str, Dict[str, Any]]], d.get('Item')), ) if d['Code'] != 'None' else None ) for d in cancellation_reasons diff --git a/pynamodb/exceptions.py b/pynamodb/exceptions.py index 42f69fcb..822230e3 100644 --- a/pynamodb/exceptions.py +++ b/pynamodb/exceptions.py @@ -134,7 +134,7 @@ class CancellationReason: """ code: str message: Optional[str] = None - item: Optional[Dict[str, Dict[str, Any]]] = None + raw_item: Optional[Dict[str, Dict[str, Any]]] = None class TransactWriteError(PynamoDBException): diff --git a/tests/integration/test_transaction_integration.py b/tests/integration/test_transaction_integration.py index 48e44641..15651dfc 100644 --- a/tests/integration/test_transaction_integration.py +++ b/tests/integration/test_transaction_integration.py @@ -181,7 +181,7 @@ def test_transact_write__error__transaction_cancelled__condition_check_failure__ assert exc_info.value.cause_response_code == TRANSACTION_CANCELLED assert 'ConditionalCheckFailed' in exc_info.value.cause_response_message assert exc_info.value.cancellation_reasons == [ - CancellationReason(code='ConditionalCheckFailed', message='The conditional request failed', item=User(1).to_dynamodb_dict()), + CancellationReason(code='ConditionalCheckFailed', message='The conditional request failed', raw_item=User(1).to_dynamodb_dict()), ] assert isinstance(exc_info.value.cause, botocore.exceptions.ClientError) assert User.Meta.table_name in exc_info.value.cause.MSG_TEMPLATE From dee4051e82b50c9b4e1a7bef0de4e6bfd77c5453 Mon Sep 17 00:00:00 2001 From: Derek Knapp Date: Sat, 10 Feb 2024 18:57:20 -0800 Subject: [PATCH 6/6] Update test_transaction_integration.py Co-authored-by: Ilya Priven --- tests/integration/test_transaction_integration.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/test_transaction_integration.py b/tests/integration/test_transaction_integration.py index 15651dfc..e247fd85 100644 --- a/tests/integration/test_transaction_integration.py +++ b/tests/integration/test_transaction_integration.py @@ -183,8 +183,6 @@ def test_transact_write__error__transaction_cancelled__condition_check_failure__ assert exc_info.value.cancellation_reasons == [ CancellationReason(code='ConditionalCheckFailed', message='The conditional request failed', raw_item=User(1).to_dynamodb_dict()), ] - assert isinstance(exc_info.value.cause, botocore.exceptions.ClientError) - assert User.Meta.table_name in exc_info.value.cause.MSG_TEMPLATE @pytest.mark.ddblocal