Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion docs/conditional.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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')``.
4 changes: 3 additions & 1 deletion docs/transaction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ The ``Delete`` operation functions similarly to ``Model.delete``.

* ``model`` (required)
* ``condition`` (optional) - of type :py:class:`Condition <pynamodb.expressions.condition.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

Expand All @@ -162,7 +163,7 @@ The ``Put`` operation functions similarly to ``Model.save``.

* ``model`` (required)
* ``condition`` (optional) - of type :py:class:`Condition <pynamodb.expressions.condition.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

Expand Down Expand Up @@ -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
17 changes: 13 additions & 4 deletions pynamodb/connection/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
)
Expand All @@ -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
Expand All @@ -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,
)
Expand All @@ -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:
Expand All @@ -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
)
Expand Down
6 changes: 6 additions & 0 deletions pynamodb/connection/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
)
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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,
)
Expand Down
56 changes: 49 additions & 7 deletions pynamodb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -432,19 +450,36 @@ 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):
raise ValueError("Cannot update this item from the returned class: {}".format(stored_cls.__name__))
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
Expand Down Expand Up @@ -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

Expand All @@ -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]]:
Expand Down
10 changes: 9 additions & 1 deletion pynamodb/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading