Skip to content

Commit 41c4d92

Browse files
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"]}

0 commit comments

Comments
 (0)