Skip to content

Commit a980548

Browse files
author
Lucas McDonald
committed
m
1 parent 436f010 commit a980548

File tree

7 files changed

+336
-48
lines changed

7 files changed

+336
-48
lines changed

DynamoDbEncryption/runtimes/python/src/aws_dbesdk_dynamodb/encrypted/client.py

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,18 @@
1717
DynamoDbEncryptionTransforms,
1818
)
1919
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import (
20+
BatchExecuteStatementInputTransformInput,
21+
BatchExecuteStatementOutputTransformInput,
2022
BatchGetItemInputTransformInput,
2123
BatchGetItemOutputTransformInput,
2224
BatchWriteItemInputTransformInput,
2325
BatchWriteItemOutputTransformInput,
26+
DeleteItemInputTransformInput,
27+
DeleteItemOutputTransformInput,
28+
ExecuteStatementInputTransformInput,
29+
ExecuteStatementOutputTransformInput,
30+
ExecuteTransactionInputTransformInput,
31+
ExecuteTransactionOutputTransformInput,
2432
GetItemInputTransformInput,
2533
GetItemOutputTransformInput,
2634
PutItemInputTransformInput,
@@ -33,6 +41,8 @@
3341
TransactGetItemsOutputTransformInput,
3442
TransactWriteItemsInputTransformInput,
3543
TransactWriteItemsOutputTransformInput,
44+
UpdateItemInputTransformInput,
45+
UpdateItemOutputTransformInput,
3646
)
3747

3848

@@ -57,8 +67,14 @@ class EncryptedClient(EncryptedBotoInterface):
5767
* ``batch_get_item``
5868
* ``transact_get_items``
5969
* ``transact_write_items``
70+
* ``delete_item``
6071
61-
The ``update_item`` operation is not currently supported. Calling this operation will raise ``NotImplementedError``.
72+
The following operations are not supported and will raise DynamoDbEncryptionTransformsException:
73+
74+
* ``execute_statement``
75+
* ``execute_transaction``
76+
* ``batch_execute_statement``
77+
* ``update_item``
6278
6379
Any other operations on this class will defer to the underlying boto3 DynamoDB client's implementation.
6480
@@ -337,18 +353,132 @@ def transact_write_items(self, **kwargs) -> dict[str, Any]:
337353
output_item_to_ddb_transform_method=self._resource_to_client_shape_converter.transact_write_items_response,
338354
)
339355

356+
def delete_item(self, **kwargs):
357+
"""
358+
Delete an item from a table by the specified key.
359+
360+
The input and output syntaxes match those for the boto3 DynamoDB client ``delete_item`` API:
361+
362+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/delete_item.html
363+
364+
Args:
365+
**kwargs: Keyword arguments to pass to the operation. This matches the boto3 client ``delete_item``
366+
request syntax.
367+
368+
Returns:
369+
dict: The response from DynamoDB. This matches the boto3 client ``delete_item`` response syntax.
370+
Any values in the ``"Attributes"`` field will be decrypted locally after being read from DynamoDB.
371+
372+
"""
373+
return self._client_operation_logic(
374+
operation_input=kwargs,
375+
input_item_to_ddb_transform_method=self._resource_to_client_shape_converter.delete_item_request,
376+
input_item_to_dict_transform_method=self._client_to_resource_shape_converter.delete_item_request,
377+
input_transform_method=self._transformer.delete_item_input_transform,
378+
input_transform_shape=DeleteItemInputTransformInput,
379+
output_transform_method=self._transformer.delete_item_output_transform,
380+
output_transform_shape=DeleteItemOutputTransformInput,
381+
client_method=self._client.delete_item,
382+
output_item_to_dict_transform_method=self._client_to_resource_shape_converter.delete_item_response,
383+
output_item_to_ddb_transform_method=self._resource_to_client_shape_converter.delete_item_response,
384+
)
385+
386+
def execute_statement(self, **kwargs):
387+
"""
388+
Not implemented. Raises DynamoDbEncryptionTransformsException.
389+
390+
Args:
391+
**kwargs: Any arguments passed to this method
392+
393+
Raises:
394+
DynamoDbEncryptionTransformsException: This operation is not supported on encrypted tables.
395+
396+
"""
397+
return self._client_operation_logic(
398+
operation_input=kwargs,
399+
input_item_to_ddb_transform_method=self._resource_to_client_shape_converter.execute_statement_request,
400+
input_item_to_dict_transform_method=self._client_to_resource_shape_converter.execute_statement_request,
401+
input_transform_method=self._transformer.execute_statement_input_transform,
402+
input_transform_shape=ExecuteStatementInputTransformInput,
403+
output_transform_method=self._transformer.execute_statement_output_transform,
404+
output_transform_shape=ExecuteStatementOutputTransformInput,
405+
client_method=self._client.execute_statement,
406+
output_item_to_dict_transform_method=self._client_to_resource_shape_converter.execute_statement_response,
407+
output_item_to_ddb_transform_method=self._resource_to_client_shape_converter.execute_statement_response,
408+
)
409+
410+
def execute_transaction(self, **kwargs):
411+
"""
412+
Not implemented. Raises DynamoDbEncryptionTransformsException.
413+
414+
Args:
415+
**kwargs: Any arguments passed to this method
416+
417+
Raises:
418+
DynamoDbEncryptionTransformsException: This operation is not supported on encrypted tables.
419+
420+
"""
421+
return self._client_operation_logic(
422+
operation_input=kwargs,
423+
input_item_to_ddb_transform_method=self._resource_to_client_shape_converter.execute_transaction_request,
424+
input_item_to_dict_transform_method=self._client_to_resource_shape_converter.execute_transaction_request,
425+
input_transform_method=self._transformer.execute_transaction_input_transform,
426+
input_transform_shape=ExecuteTransactionInputTransformInput,
427+
output_transform_method=self._transformer.execute_transaction_output_transform,
428+
output_transform_shape=ExecuteTransactionOutputTransformInput,
429+
client_method=self._client.execute_transaction,
430+
output_item_to_dict_transform_method=self._client_to_resource_shape_converter.execute_transaction_response,
431+
output_item_to_ddb_transform_method=self._resource_to_client_shape_converter.execute_transaction_response,
432+
)
433+
340434
def update_item(self, **kwargs):
341435
"""
342-
Not implemented. Raises NotImplementedError.
436+
Not implemented. Raises DynamoDbEncryptionTransformsException.
437+
438+
Args:
439+
**kwargs: Any arguments passed to this method
440+
441+
Raises:
442+
DynamoDbEncryptionTransformsException: This operation is not supported on encrypted tables.
443+
444+
"""
445+
return self._client_operation_logic(
446+
operation_input=kwargs,
447+
input_item_to_ddb_transform_method=self._resource_to_client_shape_converter.update_item_request,
448+
input_item_to_dict_transform_method=self._client_to_resource_shape_converter.update_item_request,
449+
input_transform_method=self._transformer.update_item_input_transform,
450+
input_transform_shape=UpdateItemInputTransformInput,
451+
output_transform_method=self._transformer.update_item_output_transform,
452+
output_transform_shape=UpdateItemOutputTransformInput,
453+
client_method=self._client.update_item,
454+
output_item_to_dict_transform_method=self._client_to_resource_shape_converter.update_item_response,
455+
output_item_to_ddb_transform_method=self._resource_to_client_shape_converter.update_item_response,
456+
)
457+
458+
def batch_execute_statement(self, **kwargs):
459+
"""
460+
Not implemented. Raises DynamoDbEncryptionTransformsException.
343461
344462
Args:
345463
**kwargs: Any arguments passed to this method
346464
347465
Raises:
348-
NotImplementedError: This operation is not yet implemented
466+
DynamoDbEncryptionTransformsException: This operation is not supported on encrypted tables.
349467
350468
"""
351-
raise NotImplementedError('"update_item" is not yet implemented')
469+
return self._client_operation_logic(
470+
operation_input=kwargs,
471+
input_item_to_ddb_transform_method=self._resource_to_client_shape_converter.batch_execute_statement_request,
472+
input_item_to_dict_transform_method=self._client_to_resource_shape_converter.batch_execute_statement_request,
473+
input_transform_method=self._transformer.batch_execute_statement_input_transform,
474+
input_transform_shape=BatchExecuteStatementInputTransformInput,
475+
output_transform_method=self._transformer.batch_execute_statement_output_transform,
476+
output_transform_shape=BatchExecuteStatementOutputTransformInput,
477+
client_method=self._client.batch_execute_statement,
478+
output_item_to_dict_transform_method=self._client_to_resource_shape_converter.batch_execute_statement_response,
479+
output_item_to_ddb_transform_method=self._resource_to_client_shape_converter.batch_execute_statement_response,
480+
)
481+
352482

353483
def get_paginator(self, operation_name: str) -> EncryptedPaginator | botocore.client.Paginator:
354484
"""

DynamoDbEncryption/runtimes/python/src/aws_dbesdk_dynamodb/internal/client_to_resource.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,33 @@ def batch_write_item_request(self, batch_write_item_request):
9898

9999
def batch_write_item_response(self, batch_write_item_response):
100100
return self.boto3_converter.BatchWriteItemOutput(batch_write_item_response)
101+
102+
def update_item_request(self, update_item_request):
103+
return self.boto3_converter.UpdateItemInput(update_item_request)
104+
105+
def update_item_response(self, update_item_response):
106+
return self.boto3_converter.UpdateItemOutput(update_item_response)
107+
108+
def batch_execute_statement_request(self, batch_execute_statement_request):
109+
return self.boto3_converter.BatchExecuteStatementInput(batch_execute_statement_request)
110+
111+
def batch_execute_statement_response(self, batch_execute_statement_response):
112+
return self.boto3_converter.BatchExecuteStatementOutput(batch_execute_statement_response)
113+
114+
def delete_item_request(self, delete_item_request):
115+
return self.boto3_converter.DeleteItemInput(delete_item_request)
116+
117+
def delete_item_response(self, delete_item_response):
118+
return self.boto3_converter.DeleteItemOutput(delete_item_response)
119+
120+
def execute_statement_request(self, execute_statement_request):
121+
return self.boto3_converter.ExecuteStatementInput(execute_statement_request)
122+
123+
def execute_statement_response(self, execute_statement_response):
124+
return self.boto3_converter.ExecuteStatementOutput(execute_statement_response)
125+
126+
def execute_transaction_request(self, execute_transaction_request):
127+
return self.boto3_converter.ExecuteTransactionInput(execute_transaction_request)
128+
129+
def execute_transaction_response(self, execute_transaction_response):
130+
return self.boto3_converter.ExecuteTransactionOutput(execute_transaction_response)

DynamoDbEncryption/runtimes/python/src/aws_dbesdk_dynamodb/internal/resource_to_client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ def scan_request(self, scan_request):
9292
scan_request["TableName"] = self.table_name
9393
return self.boto3_converter.ScanInput(scan_request)
9494

95+
def update_item_request(self, update_item_request):
96+
# update_item requests on a boto3.resource.Table require a configured table name.
97+
if not self.table_name:
98+
raise ValueError("Table name must be provided to ResourceShapeToClientShapeConverter to use update_item")
99+
update_item_request["TableName"] = self.table_name
100+
return self.boto3_converter.UpdateItemInput(update_item_request)
101+
102+
def delete_item_request(self, delete_item_request):
103+
# delete_item requests on a boto3.resource.Table require a configured table name.
104+
if not self.table_name:
105+
raise ValueError("Table name must be provided to ResourceShapeToClientShapeConverter to use delete_item")
106+
delete_item_request["TableName"] = self.table_name
107+
return self.boto3_converter.DeleteItemInput(delete_item_request)
108+
95109
def transact_get_items_request(self, transact_get_items_request):
96110
return self.boto3_converter.TransactGetItemsInput(transact_get_items_request)
97111

@@ -115,6 +129,24 @@ def batch_write_item_request(self, batch_write_item_request):
115129

116130
def batch_write_item_response(self, batch_write_item_response):
117131
return self.boto3_converter.BatchWriteItemOutput(batch_write_item_response)
132+
133+
def batch_execute_statement_request(self, batch_execute_statement_request):
134+
return self.boto3_converter.BatchExecuteStatementInput(batch_execute_statement_request)
135+
136+
def batch_execute_statement_response(self, batch_execute_statement_response):
137+
return self.boto3_converter.BatchExecuteStatementOutput(batch_execute_statement_response)
138+
139+
def execute_statement_request(self, execute_statement_request):
140+
return self.boto3_converter.ExecuteStatementInput(execute_statement_request)
141+
142+
def execute_statement_response(self, execute_statement_response):
143+
return self.boto3_converter.ExecuteStatementOutput(execute_statement_response)
144+
145+
def execute_transaction_request(self, execute_transaction_request):
146+
return self.boto3_converter.ExecuteTransactionInput(execute_transaction_request)
147+
148+
def execute_transaction_response(self, execute_transaction_response):
149+
return self.boto3_converter.ExecuteTransactionOutput(execute_transaction_response)
118150

119151
def scan_response(self, scan_response):
120152
return self.boto3_converter.ScanOutput(scan_response)
@@ -127,3 +159,9 @@ def get_item_response(self, get_item_response):
127159

128160
def put_item_response(self, put_item_response):
129161
return self.boto3_converter.PutItemOutput(put_item_response)
162+
163+
def update_item_response(self, update_item_response):
164+
return self.boto3_converter.UpdateItemOutput(update_item_response)
165+
166+
def delete_item_response(self, delete_item_response):
167+
return self.boto3_converter.DeleteItemOutput(delete_item_response)

DynamoDbEncryption/runtimes/python/test/integ/encrypted/test_client.py

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
77
from aws_dbesdk_dynamodb.encrypted.paginator import EncryptedPaginator
8+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.errors import (
9+
DynamoDbEncryptionTransformsException,
10+
)
811

912
from ...constants import (
1013
INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME,
@@ -27,6 +30,8 @@
2730
basic_batch_write_item_delete_request_dict,
2831
basic_batch_write_item_put_request_ddb,
2932
basic_batch_write_item_put_request_dict,
33+
basic_delete_item_request_ddb,
34+
basic_delete_item_request_dict,
3035
basic_get_item_request_ddb,
3136
basic_get_item_request_dict,
3237
basic_put_item_request_ddb,
@@ -153,9 +158,15 @@ def get_item_request(expect_standard_dictionaries, test_item):
153158
return {**basic_get_item_request_dict(test_item), "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME}
154159
return basic_get_item_request_ddb(test_item)
155160

161+
@pytest.fixture
162+
def delete_item_request(expect_standard_dictionaries, test_item):
163+
if expect_standard_dictionaries:
164+
return {**basic_delete_item_request_dict(test_item), "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME}
165+
return basic_delete_item_request_ddb(test_item)
166+
156167

157-
def test_GIVEN_valid_put_and_get_requests_WHEN_put_and_get_THEN_round_trip_passes(
158-
client, put_item_request, get_item_request
168+
def test_GIVEN_valid_put_and_get_and_delete_requests_WHEN_put_and_get_and_delete_THEN_round_trip_passes(
169+
client, put_item_request, get_item_request, delete_item_request
159170
):
160171
# Given: Valid put_item request
161172
# When: put_item
@@ -174,6 +185,23 @@ def test_GIVEN_valid_put_and_get_requests_WHEN_put_and_get_THEN_round_trip_passe
174185
actual_item = sort_dynamodb_json_lists(get_response["Item"])
175186
assert expected_item == actual_item
176187

188+
# Given: Valid delete_item request for the same item
189+
# When: delete_item
190+
delete_response = client.delete_item(**{**delete_item_request, "ReturnValues": "ALL_OLD"})
191+
# Then: delete_item succeeds and contains the expected response
192+
assert delete_response["ResponseMetadata"]["HTTPStatusCode"] == 200
193+
# DynamoDB JSON uses lists to represent sets, so strict equality can fail.
194+
# Sort lists to ensure consistent ordering when comparing expected and actual items.
195+
expected_item = sort_dynamodb_json_lists(put_item_request["Item"])
196+
actual_item = sort_dynamodb_json_lists(delete_response["Attributes"])
197+
assert expected_item == actual_item
198+
199+
# Given: Valid get_item request for the same item
200+
# When: get_item
201+
get_response = client.get_item(**get_item_request)
202+
# Then: get_item is empty (i.e. the item was deleted)
203+
assert "Item" not in get_response
204+
177205

178206
@pytest.fixture
179207
def batch_write_item_put_request(expect_standard_dictionaries, multiple_test_items):
@@ -346,19 +374,58 @@ def test_GIVEN_valid_transact_write_and_get_requests_WHEN_transact_write_and_get
346374
assert transact_write_delete_response["ResponseMetadata"]["HTTPStatusCode"] == 200
347375

348376

349-
def test_WHEN_update_item_THEN_raises_not_implemented_error():
350-
"""Test that update_item raises NotImplementedError."""
377+
def test_WHEN_update_item_THEN_raises_DynamoDbEncryptionTransformsException():
378+
"""Test that update_item raises DynamoDbEncryptionTransformsException."""
351379
# Given: Encrypted client and update item parameters
352-
# When: Calling update_item
353-
with pytest.raises(NotImplementedError):
380+
# Then: DynamoDbEncryptionTransformsException is raised
381+
with pytest.raises(DynamoDbEncryptionTransformsException):
382+
# When: Calling update_item
354383
encrypted_client(expect_standard_dictionaries=False).update_item(
355384
TableName=INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME,
356-
Key={"partition_key": "test-key", "sort_key": 1},
385+
Key={"partition_key": {"S": "test-key"}, "sort_key": {"N": "1"}},
357386
UpdateExpression="SET attribute1 = :val",
358387
ExpressionAttributeValues={":val": {"S": "new value"}},
359388
)
360-
# Then: NotImplementedError is raised
361389

390+
def test_WHEN_execute_statement_THEN_raises_DynamoDbEncryptionTransformsException():
391+
"""Test that execute_statement raises DynamoDbEncryptionTransformsException."""
392+
# Given: Encrypted client and update item parameters
393+
# Then: DynamoDbEncryptionTransformsException is raised
394+
with pytest.raises(DynamoDbEncryptionTransformsException):
395+
# When: Calling update_item
396+
encrypted_client(expect_standard_dictionaries=False).execute_statement(
397+
Statement="SELECT * FROM " + INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME,
398+
)
399+
400+
def test_WHEN_execute_transaction_THEN_raises_DynamoDbEncryptionTransformsException():
401+
"""Test that execute_transaction raises DynamoDbEncryptionTransformsException."""
402+
# Given: Encrypted client and update item parameters
403+
# Then: DynamoDbEncryptionTransformsException is raised
404+
with pytest.raises(DynamoDbEncryptionTransformsException):
405+
# When: Calling update_item
406+
encrypted_client(expect_standard_dictionaries=False).execute_transaction(
407+
TransactStatements=[
408+
{
409+
"Statement": "SELECT * FROM " + INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME,
410+
"Parameters": [],
411+
}
412+
],
413+
)
414+
415+
def test_WHEN_batch_execute_statement_THEN_raises_DynamoDbEncryptionTransformsException():
416+
"""Test that batch_execute_statement raises DynamoDbEncryptionTransformsException."""
417+
# Given: Encrypted client and update item parameters
418+
# Then: DynamoDbEncryptionTransformsException is raised
419+
with pytest.raises(DynamoDbEncryptionTransformsException):
420+
# When: Calling update_item
421+
encrypted_client(expect_standard_dictionaries=False).batch_execute_statement(
422+
Statements=[
423+
{
424+
"Statement": "SELECT * FROM " + INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME,
425+
"Parameters": [],
426+
}
427+
],
428+
)
362429

363430
def test_WHEN_get_paginator_THEN_correct_paginator_is_returned():
364431
"""Test get_paginator for scan and query operations."""

0 commit comments

Comments
 (0)