diff --git a/dapr/clients/grpc/_request.py b/dapr/clients/grpc/_request.py index 0f149e1f..9ad291cf 100644 --- a/dapr/clients/grpc/_request.py +++ b/dapr/clients/grpc/_request.py @@ -281,7 +281,7 @@ class TransactionalStateOperation: def __init__( self, key: str, - data: Union[bytes, str], + data: Optional[Union[bytes, str]] = None, etag: Optional[str] = None, operation_type: TransactionOperationType = TransactionOperationType.upsert, ): @@ -297,7 +297,7 @@ def __init__( Raises: ValueError: data is not bytes or str. """ - if not isinstance(data, (bytes, str)): + if operation_type != TransactionOperationType.delete and not isinstance(data, (bytes, str)): raise ValueError(f'invalid type for data {type(data)}') self._key = key @@ -311,7 +311,7 @@ def key(self) -> str: return self._key @property - def data(self) -> Union[bytes, str]: + def data(self) -> Union[bytes, str, None]: """Gets raw data.""" return self._data diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 19f4a3df..4aa75b55 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -933,7 +933,7 @@ def execute_state_transaction( operationType=o.operation_type.value, request=common_v1.StateItem( key=o.key, - value=to_bytes(o.data), + value=to_bytes(o.data) if o.data is not None else to_bytes(''), etag=common_v1.Etag(value=o.etag) if o.etag is not None else None, ), ) diff --git a/examples/state_store/README.md b/examples/state_store/README.md index 7c323873..67ea5791 100644 --- a/examples/state_store/README.md +++ b/examples/state_store/README.md @@ -41,6 +41,7 @@ expected_stdout_lines: - "== APP == Cannot save bulk due to bad etags. ErrorCode=StatusCode.ABORTED" - "== APP == Got value=b'value_1' eTag=1" - "== APP == Got items with etags: [(b'value_1_updated', '2'), (b'value_2', '2')]" + - "== APP == Got values after transaction delete: [b'', b'']" - "== APP == Got value after delete: b''" timeout_seconds: 5 --> @@ -67,6 +68,8 @@ The output should be as follows: == APP == Got items with etags: [(b'value_1_updated', '2'), (b'value_2', '2')] +== APP == Got values after transaction delete: [b'', b''] + == APP == Got value after delete: b'' ``` diff --git a/examples/state_store/state_store.py b/examples/state_store/state_store.py index f87167f5..a7b449d6 100644 --- a/examples/state_store/state_store.py +++ b/examples/state_store/state_store.py @@ -78,6 +78,7 @@ etag=state.etag, ), TransactionalStateOperation(key=another_key, data=another_value), + TransactionalStateOperation(key=yet_another_key, data=yet_another_value), ], ) @@ -87,7 +88,26 @@ ).items print(f'Got items with etags: {[(i.data, i.etag) for i in items]}') + # Transaction delete + d.execute_state_transaction( + store_name=storeName, + operations=[ + TransactionalStateOperation(operation_type=TransactionOperationType.delete, key=key), + TransactionalStateOperation( + operation_type=TransactionOperationType.delete, key=another_key + ), + ], + ) + + # Batch get + items = d.get_bulk_state( + store_name=storeName, keys=[key, another_key], states_metadata={'metakey': 'metavalue'} + ).items + print(f'Got values after transaction delete: {[data.data for data in items]}') + # Delete one state by key. - d.delete_state(store_name=storeName, key=key, state_metadata={'metakey': 'metavalue'}) - data = d.get_state(store_name=storeName, key=key).data + d.delete_state( + store_name=storeName, key=yet_another_key, state_metadata={'metakey': 'metavalue'} + ) + data = d.get_state(store_name=storeName, key=yet_another_key).data print(f'Got value after delete: {data}') diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index d3eab236..ad90d8cc 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -33,7 +33,7 @@ from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings from dapr.clients.grpc._helpers import to_bytes -from dapr.clients.grpc._request import TransactionalStateOperation +from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc._response import ( @@ -508,6 +508,23 @@ def test_transaction_then_get_states(self): self.assertEqual(resp.items[1].key, another_key) self.assertEqual(resp.items[1].data, to_bytes(another_value.upper())) + dapr.execute_state_transaction( + store_name='statestore', + operations=[ + TransactionalStateOperation( + key=key, operation_type=TransactionOperationType.delete + ), + TransactionalStateOperation( + key=another_key, operation_type=TransactionOperationType.delete + ), + ], + ) + resp = dapr.get_state(store_name='statestore', key=key) + self.assertEqual(resp.data, b'') + + resp = dapr.get_state(store_name='statestore', key=another_key) + self.assertEqual(resp.data, b'') + self._fake_dapr_server.raise_exception_on_next_call( status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='my invalid argument message') )