Skip to content

Commit 41c4d92

Browse files
authoredJun 3, 2025
feat(Python): DynamoDB Resource/Client shape transforms (#1907)
1 parent e9b623f commit 41c4d92

File tree

9 files changed

+3081
-0
lines changed

9 files changed

+3081
-0
lines changed
 
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from aws_cryptography_internal_dynamodb.smithygenerated.com_amazonaws_dynamodb.boto3_conversions import (
4+
InternalBoto3DynamoDBFormatConverter,
5+
)
6+
from boto3.dynamodb.types import TypeDeserializer
7+
8+
9+
class ClientShapeToResourceShapeConverter:
10+
11+
def __init__(self, delete_table_name=True):
12+
# Some callers expect the TableName kwarg to be removed from the outputs of this class.
13+
# (EncryptedResource, EncryptedTable.)
14+
# These callers' boto3 shapes do not include TableName.
15+
# Other callers expect the TableName kwarg to be included in the outputs of this class.
16+
# (EncryptedClient, EncryptedPaginator.)
17+
# These callers' boto3 shapes include TableName.
18+
self.delete_table_name = delete_table_name
19+
self.boto3_converter = InternalBoto3DynamoDBFormatConverter(
20+
item_handler=TypeDeserializer().deserialize, condition_handler=self.condition_handler
21+
)
22+
23+
def condition_handler(self, expression_key, request):
24+
"""Returns the input condition/names/values as-is."""
25+
# Conditions do not need to be converted from strings to boto3 Attrs.
26+
# Resources accept either strings or Attrs.
27+
# Return the provided condition string.
28+
condition = request[expression_key]
29+
30+
# This conversion in client_to_resource does not update ExpressionAttributeNames or ExpressionAttributeValues.
31+
# However, resource_to_client condition_handler may add new ExpressionAttributeNames and
32+
# ExpressionAttributeValues.
33+
# Smithy-generated code expects condition_handlers to return ExpressionAttributeNames and
34+
# ExpressionAttributeValues,
35+
# expecting empty dicts if there are none.
36+
try:
37+
names = request["ExpressionAttributeNames"]
38+
except KeyError:
39+
names = {}
40+
41+
try:
42+
values = request["ExpressionAttributeValues"]
43+
except KeyError:
44+
values = {}
45+
return condition, names, values
46+
47+
def put_item_request(self, put_item_request):
48+
out = self.boto3_converter.PutItemInput(put_item_request)
49+
# put_item requests on resources do not have a table name.
50+
if self.delete_table_name:
51+
del out["TableName"]
52+
return out
53+
54+
def put_item_response(self, put_item_response):
55+
return self.boto3_converter.PutItemOutput(put_item_response)
56+
57+
def get_item_request(self, get_item_request):
58+
out = self.boto3_converter.GetItemInput(get_item_request)
59+
# get_item requests on resources do not have a table name.
60+
if self.delete_table_name:
61+
del out["TableName"]
62+
return out
63+
64+
def get_item_response(self, get_item_response):
65+
return self.boto3_converter.GetItemOutput(get_item_response)
66+
67+
def query_request(self, query_request):
68+
out = self.boto3_converter.QueryInput(query_request)
69+
# query requests on resources do not have a table name.
70+
if self.delete_table_name:
71+
del out["TableName"]
72+
return out
73+
74+
def query_response(self, query_response):
75+
return self.boto3_converter.QueryOutput(query_response)
76+
77+
def scan_request(self, scan_request):
78+
out = self.boto3_converter.ScanInput(scan_request)
79+
# scan requests on resources do not have a table name.
80+
if self.delete_table_name:
81+
del out["TableName"]
82+
return out
83+
84+
def scan_response(self, scan_response):
85+
return self.boto3_converter.ScanOutput(scan_response)
86+
87+
def delete_item_request(self, delete_item_request):
88+
out = self.boto3_converter.DeleteItemInput(delete_item_request)
89+
# delete_item requests on resources do not have a table name.
90+
if self.delete_table_name:
91+
del out["TableName"]
92+
return out
93+
94+
def delete_item_response(self, delete_item_response):
95+
return self.boto3_converter.DeleteItemOutput(delete_item_response)
96+
97+
def update_item_request(self, update_item_request):
98+
out = self.boto3_converter.UpdateItemInput(update_item_request)
99+
# update_item requests on resources do not have a table name.
100+
if self.delete_table_name:
101+
del out["TableName"]
102+
return out
103+
104+
def update_item_response(self, update_item_response):
105+
return self.boto3_converter.UpdateItemOutput(update_item_response)
106+
107+
def transact_get_items_request(self, transact_get_items_request):
108+
return self.boto3_converter.TransactGetItemsInput(transact_get_items_request)
109+
110+
def transact_get_items_response(self, transact_get_items_response):
111+
return self.boto3_converter.TransactGetItemsOutput(transact_get_items_response)
112+
113+
def transact_write_items_request(self, transact_write_items_request):
114+
return self.boto3_converter.TransactWriteItemsInput(transact_write_items_request)
115+
116+
def transact_write_items_response(self, transact_write_items_response):
117+
return self.boto3_converter.TransactWriteItemsOutput(transact_write_items_response)
118+
119+
def batch_get_item_request(self, batch_get_item_request):
120+
return self.boto3_converter.BatchGetItemInput(batch_get_item_request)
121+
122+
def batch_get_item_response(self, batch_get_item_response):
123+
return self.boto3_converter.BatchGetItemOutput(batch_get_item_response)
124+
125+
def batch_write_item_request(self, batch_write_item_request):
126+
return self.boto3_converter.BatchWriteItemInput(batch_write_item_request)
127+
128+
def batch_write_item_response(self, batch_write_item_response):
129+
return self.boto3_converter.BatchWriteItemOutput(batch_write_item_response)
130+
131+
def batch_execute_statement_request(self, batch_execute_statement_request):
132+
return self.boto3_converter.BatchExecuteStatementInput(batch_execute_statement_request)
133+
134+
def batch_execute_statement_response(self, batch_execute_statement_response):
135+
return self.boto3_converter.BatchExecuteStatementOutput(batch_execute_statement_response)
136+
137+
def execute_statement_request(self, execute_statement_request):
138+
return self.boto3_converter.ExecuteStatementInput(execute_statement_request)
139+
140+
def execute_statement_response(self, execute_statement_response):
141+
return self.boto3_converter.ExecuteStatementOutput(execute_statement_response)
142+
143+
def execute_transaction_request(self, execute_transaction_request):
144+
return self.boto3_converter.ExecuteTransactionInput(execute_transaction_request)
145+
146+
def execute_transaction_response(self, execute_transaction_response):
147+
return self.boto3_converter.ExecuteTransactionOutput(execute_transaction_response)
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from aws_cryptography_internal_dynamodb.smithygenerated.com_amazonaws_dynamodb.boto3_conversions import (
4+
InternalBoto3DynamoDBFormatConverter,
5+
)
6+
from boto3.dynamodb.conditions import ConditionExpressionBuilder
7+
from boto3.dynamodb.types import TypeSerializer
8+
9+
10+
class ResourceShapeToClientShapeConverter:
11+
12+
def __init__(self, table_name=None):
13+
self.boto3_converter = InternalBoto3DynamoDBFormatConverter(
14+
item_handler=TypeSerializer().serialize, condition_handler=self.condition_handler
15+
)
16+
# TableName is optional;
17+
# Some requests require it (ex. put_item, update_item, delete_item),
18+
# but others do not (ex. transact_get_items, batch_write_item).
19+
self.table_name = table_name
20+
self.expression_builder = ConditionExpressionBuilder()
21+
22+
def condition_handler(self, expression_key, request):
23+
"""
24+
Converts an object from boto3.dynamodb.conditions to a string
25+
and updates ExpressionAttributeNames and ExpressionAttributeValues with any new names/values.
26+
The ExpressionAttributeValues are returned in resource format (Python dictionaries).
27+
"""
28+
condition_expression = request[expression_key]
29+
30+
try:
31+
existing_expression_attribute_names = request["ExpressionAttributeNames"]
32+
except KeyError:
33+
existing_expression_attribute_names = {}
34+
try:
35+
existing_expression_attribute_values = request["ExpressionAttributeValues"]
36+
except KeyError:
37+
existing_expression_attribute_values = {}
38+
39+
# Only convert if the condition expression is a boto3.dynamodb.conditions object.
40+
# Resources also accept strings.
41+
# If condition is not from boto3.dynamodb.conditions, assume the condition is string-like, and return as-is.
42+
if (
43+
hasattr(condition_expression, "__module__")
44+
and condition_expression.__module__ == "boto3.dynamodb.conditions"
45+
):
46+
built_condition_expression = self.expression_builder.build_expression(condition_expression)
47+
return (
48+
built_condition_expression.condition_expression,
49+
built_condition_expression.attribute_name_placeholders,
50+
built_condition_expression.attribute_value_placeholders,
51+
)
52+
else:
53+
return condition_expression, existing_expression_attribute_names, existing_expression_attribute_values
54+
55+
def put_item_request(self, put_item_request):
56+
# put_item requests on a boto3.resource.Table require a configured table name.
57+
if not self.table_name:
58+
raise ValueError("Table name must be provided to ResourceShapeToClientShapeConverter to use put_item")
59+
put_item_request["TableName"] = self.table_name
60+
return self.boto3_converter.PutItemInput(put_item_request)
61+
62+
def get_item_request(self, get_item_request):
63+
# get_item requests on a boto3.resource.Table require a configured table name.
64+
if not self.table_name:
65+
raise ValueError("Table name must be provided to ResourceShapeToClientShapeConverter to use get_item")
66+
get_item_request["TableName"] = self.table_name
67+
return self.boto3_converter.GetItemInput(get_item_request)
68+
69+
def query_request(self, query_request):
70+
# query requests on a boto3.resource.Table require a configured table name.
71+
if not self.table_name:
72+
raise ValueError("Table name must be provided to ResourceShapeToClientShapeConverter to use query")
73+
query_request["TableName"] = self.table_name
74+
return self.boto3_converter.QueryInput(query_request)
75+
76+
def scan_request(self, scan_request):
77+
# scan requests on a boto3.resource.Table require a configured table name.
78+
if not self.table_name:
79+
raise ValueError("Table name must be provided to ResourceShapeToClientShapeConverter to use scan")
80+
scan_request["TableName"] = self.table_name
81+
return self.boto3_converter.ScanInput(scan_request)
82+
83+
def update_item_request(self, update_item_request):
84+
# update_item requests on a boto3.resource.Table require a configured table name.
85+
if not self.table_name:
86+
raise ValueError("Table name must be provided to ResourceShapeToClientShapeConverter to use update_item")
87+
update_item_request["TableName"] = self.table_name
88+
return self.boto3_converter.UpdateItemInput(update_item_request)
89+
90+
def delete_item_request(self, delete_item_request):
91+
# delete_item requests on a boto3.resource.Table require a configured table name.
92+
if not self.table_name:
93+
raise ValueError("Table name must be provided to ResourceShapeToClientShapeConverter to use delete_item")
94+
delete_item_request["TableName"] = self.table_name
95+
return self.boto3_converter.DeleteItemInput(delete_item_request)
96+
97+
def transact_get_items_request(self, transact_get_items_request):
98+
return self.boto3_converter.TransactGetItemsInput(transact_get_items_request)
99+
100+
def transact_get_items_response(self, transact_get_items_response):
101+
return self.boto3_converter.TransactGetItemsOutput(transact_get_items_response)
102+
103+
def transact_write_items_request(self, transact_write_items_request):
104+
return self.boto3_converter.TransactWriteItemsInput(transact_write_items_request)
105+
106+
def transact_write_items_response(self, transact_write_items_response):
107+
return self.boto3_converter.TransactWriteItemsOutput(transact_write_items_response)
108+
109+
def batch_get_item_request(self, batch_get_item_request):
110+
return self.boto3_converter.BatchGetItemInput(batch_get_item_request)
111+
112+
def batch_get_item_response(self, batch_get_item_response):
113+
return self.boto3_converter.BatchGetItemOutput(batch_get_item_response)
114+
115+
def batch_write_item_request(self, batch_write_item_request):
116+
return self.boto3_converter.BatchWriteItemInput(batch_write_item_request)
117+
118+
def batch_write_item_response(self, batch_write_item_response):
119+
return self.boto3_converter.BatchWriteItemOutput(batch_write_item_response)
120+
121+
def batch_execute_statement_request(self, batch_execute_statement_request):
122+
return self.boto3_converter.BatchExecuteStatementInput(batch_execute_statement_request)
123+
124+
def batch_execute_statement_response(self, batch_execute_statement_response):
125+
return self.boto3_converter.BatchExecuteStatementOutput(batch_execute_statement_response)
126+
127+
def execute_statement_request(self, execute_statement_request):
128+
return self.boto3_converter.ExecuteStatementInput(execute_statement_request)
129+
130+
def execute_statement_response(self, execute_statement_response):
131+
return self.boto3_converter.ExecuteStatementOutput(execute_statement_response)
132+
133+
def execute_transaction_request(self, execute_transaction_request):
134+
return self.boto3_converter.ExecuteTransactionInput(execute_transaction_request)
135+
136+
def execute_transaction_response(self, execute_transaction_response):
137+
return self.boto3_converter.ExecuteTransactionOutput(execute_transaction_response)
138+
139+
def scan_response(self, scan_response):
140+
return self.boto3_converter.ScanOutput(scan_response)
141+
142+
def query_response(self, query_response):
143+
return self.boto3_converter.QueryOutput(query_response)
144+
145+
def get_item_response(self, get_item_response):
146+
return self.boto3_converter.GetItemOutput(get_item_response)
147+
148+
def put_item_response(self, put_item_response):
149+
return self.boto3_converter.PutItemOutput(put_item_response)
150+
151+
def update_item_response(self, update_item_response):
152+
return self.boto3_converter.UpdateItemOutput(update_item_response)
153+
154+
def delete_item_response(self, delete_item_response):
155+
return self.boto3_converter.DeleteItemOutput(delete_item_response)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from decimal import Decimal
4+
5+
simple_item_ddb = {
6+
"partition_key": {"S": "test-key"},
7+
"sort_key": {"N": "1"},
8+
"attribute1": {"S": "encrypted value"},
9+
"attribute2": {"S": "signed value"},
10+
":attribute3": {"S": "unsigned value"},
11+
}
12+
13+
simple_key_ddb = {"partition_key": simple_item_ddb["partition_key"], "sort_key": simple_item_ddb["sort_key"]}
14+
15+
simple_item_dict = {
16+
"partition_key": "test-key",
17+
"sort_key": 1,
18+
"attribute1": "encrypted value",
19+
"attribute2": "signed value",
20+
":attribute3": "unsigned value",
21+
}
22+
23+
simple_key_dict = {"partition_key": simple_item_dict["partition_key"], "sort_key": simple_item_dict["sort_key"]}
24+
25+
complex_item_ddb = {
26+
"partition_key": {"S": "all-types-test"},
27+
"sort_key": {"N": "1"},
28+
"attribute1": {
29+
"M": {
30+
"string": {"S": "string value"},
31+
"number": {"N": "123.45"},
32+
"binary": {"B": b"binary data"},
33+
"string_set": {"SS": ["value1", "value2"]},
34+
"number_set": {"NS": ["1", "2", "3"]},
35+
"binary_set": {"BS": [b"binary1", b"binary2"]},
36+
"list": {"L": [{"S": "list item 1"}, {"N": "42"}, {"B": b"list binary"}]},
37+
"map": {"M": {"nested_string": {"S": "nested value"}, "nested_number": {"N": "42"}}},
38+
}
39+
},
40+
"attribute2": {"S": "signed value"},
41+
":attribute3": {"S": "unsigned value"},
42+
}
43+
44+
complex_key_ddb = {"partition_key": complex_item_ddb["partition_key"], "sort_key": complex_item_ddb["sort_key"]}
45+
46+
complex_item_dict = {
47+
"partition_key": "all-types-test",
48+
"sort_key": 1,
49+
"attribute1": {
50+
"string": "string value",
51+
"number": Decimal("123.45"),
52+
"binary": b"binary data",
53+
"string_set": {"value1", "value2"},
54+
"number_set": {Decimal("1"), 2, Decimal("3")},
55+
"binary_set": {b"binary1", b"binary2"},
56+
"list": ["list item 1", 42, b"list binary"],
57+
"map": {"nested_string": "nested value", "nested_number": 42},
58+
},
59+
"attribute2": "signed value",
60+
":attribute3": "unsigned value",
61+
}
62+
63+
complex_key_dict = {"partition_key": complex_item_dict["partition_key"], "sort_key": complex_item_dict["sort_key"]}

‎DynamoDbEncryption/runtimes/python/test/requests.py

Lines changed: 585 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from test.integ.encrypted.test_resource import INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME
4+
5+
6+
def basic_put_item_response(item):
7+
"""Get a put_item response in resource (ddb) format for any item."""
8+
return {"Attributes": item}
9+
10+
11+
def exhaustive_put_item_response(item):
12+
"""
13+
Get a put_item response in resource (ddb) format for any item.
14+
This is not intended to be a real response that DynamoDB would return,
15+
but the response should contain additional attributes that DynamoDB could return.
16+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
17+
"""
18+
base = basic_put_item_response(item)
19+
additional_keys = {
20+
"ConsumedCapacity": {"CapacityUnits": 1, "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME},
21+
"ItemCollectionMetrics": {
22+
"TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME,
23+
"ItemCollectionKey": {"partition_key": item["partition_key"]},
24+
},
25+
"SequenceNumber": "1234567890",
26+
"SizeEstimateRangeGB": [0.5, 1.0],
27+
}
28+
return {**base, **additional_keys}
29+
30+
31+
def basic_get_item_response(item):
32+
"""Get a get_item response in resource (ddb) format for any item."""
33+
return {"Item": item}
34+
35+
36+
def exhaustive_get_item_response(item):
37+
"""
38+
Get a get_item response in resource (ddb) format for any item.
39+
This is not intended to be a real response that DynamoDB would return,
40+
but the response should contain additional attributes that DynamoDB could return.
41+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
42+
"""
43+
base = basic_get_item_response(item)
44+
additional_keys = {
45+
"ConsumedCapacity": {"CapacityUnits": 1, "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME},
46+
}
47+
return {**base, **additional_keys}
48+
49+
50+
def basic_query_response(items):
51+
"""Get a query response in resource (ddb) format for any items."""
52+
return {
53+
"Items": items,
54+
"Count": len(items),
55+
"ScannedCount": len(items),
56+
"ConsumedCapacity": {"CapacityUnits": 1, "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME},
57+
}
58+
59+
60+
def exhaustive_query_response(items):
61+
"""
62+
Get a query response in resource (ddb) format for any items.
63+
This is not intended to be a real response that DynamoDB would return,
64+
but the response should contain additional attributes that DynamoDB could return.
65+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
66+
"""
67+
base = basic_query_response(items)
68+
additional_keys = {
69+
"LastEvaluatedKey": {"partition_key": items[-1]["partition_key"]},
70+
}
71+
return {**base, **additional_keys}
72+
73+
74+
def basic_scan_response(items, keys):
75+
"""Get a scan response in resource (ddb) format for any items."""
76+
return {
77+
"Items": items,
78+
}
79+
80+
81+
def exhaustive_scan_response(items, keys):
82+
"""
83+
Get a scan response in resource (ddb) format for any items.
84+
This is not intended to be a real response that DynamoDB would return,
85+
but the response should contain additional attributes that DynamoDB could return.
86+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
87+
"""
88+
base = basic_scan_response(items, keys)
89+
additional_keys = {
90+
"ConsumedCapacity": {"CapacityUnits": 1, "TableName": INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME},
91+
"Count": len(items),
92+
"ScannedCount": len(items),
93+
"LastEvaluatedKey": keys[-1],
94+
}
95+
return {**base, **additional_keys}
96+
97+
98+
def basic_batch_get_item_response(items):
99+
"""Get a batch_get_item response in resource (ddb) format for any items."""
100+
return {"Responses": {INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME: items}}
101+
102+
103+
def exhaustive_batch_get_item_response(items):
104+
"""
105+
Get a batch_get_item response in resource (ddb) format for any items.
106+
This is not intended to be a real response that DynamoDB would return,
107+
but the response should contain additional attributes that DynamoDB could return.
108+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
109+
"""
110+
base = basic_batch_get_item_response(items)
111+
additional_keys = {
112+
"UnprocessedKeys": {
113+
INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME: {
114+
"Keys": [{"partition_key": item["partition_key"]} for item in items]
115+
}
116+
},
117+
}
118+
return {**base, **additional_keys}
119+
120+
121+
def basic_batch_write_item_put_response(items):
122+
"""Get a batch_write_item response in resource (ddb) format for any items."""
123+
return {
124+
"UnprocessedItems": {INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME: [{"PutRequest": {"Item": item}} for item in items]}
125+
}
126+
127+
128+
def exhaustive_batch_write_item_put_response(items):
129+
"""
130+
Get a batch_write_item response in resource (ddb) format for any items.
131+
This is not intended to be a real response that DynamoDB would return,
132+
but the response should contain additional attributes that DynamoDB could return.
133+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
134+
"""
135+
base = basic_batch_write_item_put_response(items)
136+
additional_keys = {
137+
"ItemCollectionMetrics": {
138+
INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME: [
139+
{"ItemCollectionKey": {"partition_key": items[-1]["partition_key"]}}
140+
]
141+
},
142+
}
143+
return {**base, **additional_keys}
144+
145+
146+
def basic_transact_write_items_response(items):
147+
"""Get a transact_write_items response in resource (ddb) format for any items."""
148+
return {
149+
"ItemCollectionMetrics": {
150+
INTEG_TEST_DEFAULT_DYNAMODB_TABLE_NAME: [
151+
{"ItemCollectionKey": {"partition_key": items[-1]["partition_key"]}}
152+
]
153+
},
154+
}
155+
156+
157+
# No exhaustive response for transact_write_items;
158+
# The basic_transact_write_items_response is sufficient
159+
160+
161+
def basic_transact_get_items_response(items):
162+
"""Get a transact_get_items response in resource (ddb) format for any items."""
163+
return {"Responses": [{"Item": item} for item in items]}
164+
165+
166+
# No exhaustive response for transact_get_items;
167+
# The basic_transact_get_items_response is sufficient
168+
169+
170+
def basic_update_item_response(item):
171+
"""Get an update_item response in resource (ddb) format for any item."""
172+
return {"Attributes": item}
173+
174+
175+
def exhaustive_update_item_response(item):
176+
"""
177+
Get an update_item response in resource (ddb) format for any item.
178+
This is not intended to be a real response that DynamoDB would return,
179+
but the response should contain additional attributes that DynamoDB could return.
180+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
181+
"""
182+
base = basic_update_item_response(item)
183+
additional_keys = {
184+
"ItemCollectionMetrics": {
185+
"ItemCollectionKey": {"partition_key": item["partition_key"]},
186+
},
187+
}
188+
return {**base, **additional_keys}
189+
190+
191+
def basic_delete_item_response(item):
192+
"""Get a delete_item response in resource (ddb) format for any item."""
193+
return {"Attributes": item}
194+
195+
196+
def exhaustive_delete_item_response(item):
197+
"""
198+
Get a delete_item response in resource (ddb) format for any item.
199+
This is not intended to be a real response that DynamoDB would return,
200+
but the response should contain additional attributes that DynamoDB could return.
201+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
202+
"""
203+
base = basic_delete_item_response(item)
204+
additional_keys = {
205+
"ItemCollectionMetrics": {
206+
"ItemCollectionKey": {"partition_key": item["partition_key"]},
207+
},
208+
}
209+
return {**base, **additional_keys}
210+
211+
212+
def basic_execute_statement_response(items):
213+
"""Get an execute_statement response in resource (ddb) format for any items."""
214+
return {"Items": items}
215+
216+
217+
def exhaustive_execute_statement_response(items):
218+
"""
219+
Get an execute_statement response in resource (ddb) format for any items.
220+
This is not intended to be a real response that DynamoDB would return,
221+
but the response should contain additional attributes that DynamoDB could return.
222+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
223+
"""
224+
base = basic_execute_statement_response(items)
225+
additional_keys = {
226+
"LastEvaluatedKey": {
227+
"partition_key": items[-1]["partition_key"],
228+
"sort_key": items[-1]["sort_key"],
229+
},
230+
}
231+
return {**base, **additional_keys}
232+
233+
234+
def basic_execute_transaction_response(items):
235+
"""Get an execute_transaction response in resource (ddb) format for any items."""
236+
return {"Responses": [{"Item": item} for item in items]}
237+
238+
239+
# No exhaustive response for execute_transaction;
240+
# The basic_execute_transaction_response is sufficient
241+
242+
243+
def basic_batch_execute_statement_response(items):
244+
"""Get a batch_execute_statement response in resource (ddb) format for any items."""
245+
return {"Responses": [{"Item": item} for item in items]}
246+
247+
248+
def exhaustive_batch_execute_statement_response(items):
249+
"""
250+
Get a batch_execute_statement response in resource (ddb) format for any items.
251+
This is not intended to be a real response that DynamoDB would return,
252+
but the response should contain additional attributes that DynamoDB could return.
253+
This is only intended to exhaustively test the conversion of the request between client and resource formats.
254+
"""
255+
base = basic_batch_execute_statement_response(items)
256+
base["Responses"][0]["Error"] = {
257+
"Item": {
258+
"partition_key": items[0]["partition_key"],
259+
"sort_key": items[0]["sort_key"],
260+
}
261+
}
262+
return base
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
The `test_client_to_resource.py` and `test_resource_to_client.py` files
2+
in this directory verify that DBESDK's boto3 resource/client conversion methods
3+
correctly convert between resource/client shapes for all operations
4+
supported by DBESDK.
5+
6+
The only shapes that require conversion are
7+
8+
- `AttributeValue`s (DDB items or keys)
9+
- Client format example: `{"S": "some string"}`
10+
- Resource format example: `"some string"`
11+
- ConditionExpressions (`KeyConditionExpression` or `FilterExpression`; only resource-to-client)
12+
- Client shape ex.:
13+
- KeyConditionExpression: `"attr : :value"`
14+
- ExpressionAttributeValues: `{":value" : {"S" : "some value}}`
15+
- Resource shape ex.:
16+
- KeyConditionExpression: `Attr("attr").eq("some value")`
17+
- (Resources also support the client-style string expression)
18+
19+
The conversion logic will recursively traverse inpuyt/output shapes to find shapes that require conversion, then convert them.
20+
(ex. for boto3 Table [put_item](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/put_item.html)),
21+
the following kwargs MUST be converted from resource to client format:
22+
23+
- `["Item"]`
24+
- `["Expected"][<key>]["Value"]`
25+
- `["Expected"][<key>]["AttributeValueList"]`
26+
- `["ConditionExpression"]`
27+
- `["ExpressionAttributeValues"]`
28+
29+
The requests, responses, and items in the parent directory are shared between the integ tests and these unit tests.
30+
The integ tests send the exact request that whose client/resource conversion is tested in the unit tests,
31+
and the integ tests receive the exact response whose conversion is tested in the unit tests.
32+
33+
The integration tests verify that the basic forms of these requests and responses are authoritative. The unit tests verify that DBESDK’s conversion logic exactly transforms one shape format into the other.
34+
35+
Note: The conversion logic is generated by Smithy-Dafny Python
36+
and the shape traversals are derived from the MPL's DynamoDB Smithy model.
37+
As a result, the correctness of this conversion logic is primarily depends on the correctness of the Smithy codegen logic and the correctness of the DynamoDB Smithy model.
38+
39+
Originally, the conversion logic was hand-written,
40+
so these tests go beyond smoke testing to provide extra guarantees,
41+
even though basic smoke testing should suffice now that the logic is machine-generated.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0

‎DynamoDbEncryption/runtimes/python/test/unit/internal/test_client_to_resource.py

Lines changed: 745 additions & 0 deletions
Large diffs are not rendered by default.

‎DynamoDbEncryption/runtimes/python/test/unit/internal/test_resource_to_client.py

Lines changed: 1081 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.