clear-skies-aws 1.10.2__py3-none-any.whl → 2.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/METADATA +36 -35
- clear_skies_aws-2.0.2.dist-info/RECORD +63 -0
- {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/WHEEL +1 -1
- clear_skies_aws-2.0.2.dist-info/licenses/LICENSE +21 -0
- clearskies_aws/__init__.py +15 -2
- clearskies_aws/actions/__init__.py +13 -106
- clearskies_aws/actions/action_aws.py +74 -57
- clearskies_aws/actions/assume_role.py +43 -30
- clearskies_aws/actions/ses.py +82 -73
- clearskies_aws/actions/sns.py +27 -30
- clearskies_aws/actions/sqs.py +32 -33
- clearskies_aws/actions/step_function.py +38 -31
- clearskies_aws/backends/__init__.py +11 -4
- clearskies_aws/backends/backend.py +106 -0
- clearskies_aws/backends/dynamo_db_backend.py +150 -155
- clearskies_aws/backends/dynamo_db_condition_parser.py +40 -80
- clearskies_aws/backends/dynamo_db_parti_ql_backend.py +179 -337
- clearskies_aws/backends/sqs_backend.py +32 -51
- clearskies_aws/configs/__init__.py +0 -0
- clearskies_aws/contexts/__init__.py +23 -10
- clearskies_aws/contexts/cli_web_socket_mock.py +19 -0
- clearskies_aws/contexts/lambda_alb.py +76 -0
- clearskies_aws/contexts/lambda_api_gateway.py +75 -28
- clearskies_aws/contexts/lambda_api_gateway_web_socket.py +56 -29
- clearskies_aws/contexts/lambda_invocation.py +15 -44
- clearskies_aws/contexts/lambda_sns.py +8 -33
- clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +14 -36
- clearskies_aws/di/__init__.py +6 -1
- clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
- clearskies_aws/di/inject/__init__.py +6 -0
- clearskies_aws/di/inject/boto3.py +15 -0
- clearskies_aws/di/inject/boto3_session.py +13 -0
- clearskies_aws/di/inject/parameter_store.py +15 -0
- clearskies_aws/{handlers → endpoints}/secrets_manager_rotation.py +76 -55
- clearskies_aws/endpoints/simple_body_routing.py +41 -0
- clearskies_aws/input_outputs/__init__.py +21 -8
- clearskies_aws/input_outputs/{cli_websocket_mock.py → cli_web_socket_mock.py} +9 -3
- clearskies_aws/input_outputs/lambda_alb.py +53 -0
- clearskies_aws/input_outputs/lambda_api_gateway.py +106 -88
- clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +69 -6
- clearskies_aws/input_outputs/lambda_input_output.py +87 -0
- clearskies_aws/input_outputs/lambda_invocation.py +77 -26
- clearskies_aws/input_outputs/lambda_sns.py +66 -39
- clearskies_aws/input_outputs/lambda_sqs_standard.py +70 -40
- clearskies_aws/mocks/actions/ses.py +25 -19
- clearskies_aws/mocks/actions/sns.py +18 -12
- clearskies_aws/mocks/actions/sqs.py +18 -12
- clearskies_aws/mocks/actions/step_function.py +19 -13
- clearskies_aws/models/__init__.py +0 -0
- clearskies_aws/models/web_socket_connection_model.py +182 -0
- clearskies_aws/secrets/__init__.py +13 -7
- clearskies_aws/secrets/additional_configs/__init__.py +10 -2
- clearskies_aws/secrets/additional_configs/iam_db_auth.py +26 -16
- clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +43 -39
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +30 -31
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +70 -49
- clearskies_aws/secrets/akeyless_with_ssm_cache.py +32 -18
- clearskies_aws/secrets/parameter_store.py +34 -32
- clearskies_aws/secrets/secrets.py +16 -0
- clearskies_aws/secrets/secrets_manager.py +78 -57
- clear_skies_aws-1.10.2.dist-info/LICENSE +0 -7
- clear_skies_aws-1.10.2.dist-info/RECORD +0 -71
- clearskies_aws/actions/assume_role_test.py +0 -72
- clearskies_aws/actions/ses_test.py +0 -89
- clearskies_aws/actions/sns_test.py +0 -77
- clearskies_aws/actions/sqs_test.py +0 -127
- clearskies_aws/actions/step_function_test.py +0 -103
- clearskies_aws/backends/dynamo_db_backend_test.py +0 -300
- clearskies_aws/backends/dynamo_db_condition_parser_test.py +0 -266
- clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +0 -544
- clearskies_aws/backends/sqs_backend_test.py +0 -31
- clearskies_aws/contexts/cli.py +0 -19
- clearskies_aws/contexts/cli_websocket_mock.py +0 -33
- clearskies_aws/contexts/lambda_elb.py +0 -30
- clearskies_aws/contexts/lambda_http_gateway.py +0 -30
- clearskies_aws/contexts/lambda_sqs_standard_partial_batch_test.py +0 -66
- clearskies_aws/contexts/wsgi.py +0 -19
- clearskies_aws/di/standard_dependencies.py +0 -60
- clearskies_aws/handlers/simple_body_routing.py +0 -39
- clearskies_aws/input_outputs/lambda_api_gateway_test.py +0 -87
- clearskies_aws/input_outputs/lambda_elb.py +0 -21
- clearskies_aws/input_outputs/lambda_http_gateway.py +0 -12
- clearskies_aws/secrets/parameter_store_test.py +0 -18
- clearskies_aws/secrets/secrets_manager_test.py +0 -18
- clearskies_aws/web_socket_connection_model.py +0 -43
- clearskies_aws/{handlers → endpoints}/__init__.py +1 -1
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
4
|
import json
|
|
3
5
|
from decimal import Decimal
|
|
4
|
-
from typing import Any, Callable
|
|
6
|
+
from typing import Any, Callable
|
|
5
7
|
|
|
8
|
+
import clearskies
|
|
6
9
|
from boto3.dynamodb import conditions as dynamodb_conditions
|
|
7
10
|
from clearskies import model
|
|
8
11
|
from clearskies.autodoc.schema import String as AutoDocString
|
|
9
|
-
from clearskies.
|
|
10
|
-
from clearskies.
|
|
11
|
-
from clearskies.
|
|
12
|
-
from
|
|
12
|
+
from clearskies.columns.boolean import Boolean
|
|
13
|
+
from clearskies.columns.float import Float
|
|
14
|
+
from clearskies.columns.integer import Integer
|
|
15
|
+
from types_boto3_dynamodb import DynamoDBServiceResource
|
|
16
|
+
|
|
17
|
+
from clearskies_aws.backends import backend
|
|
13
18
|
|
|
14
19
|
|
|
15
|
-
class DynamoDBBackend(Backend):
|
|
20
|
+
class DynamoDBBackend(backend.Backend):
|
|
16
21
|
"""
|
|
17
22
|
DynamoDB is complicated.
|
|
18
23
|
|
|
@@ -70,21 +75,19 @@ class DynamoDBBackend(Backend):
|
|
|
70
75
|
Any other filter/sort options will become unreliable as soon as the table grows past the maximum result size.
|
|
71
76
|
"""
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
_environment = None
|
|
75
|
-
_dynamodb = None
|
|
78
|
+
dynamodb: DynamoDBServiceResource
|
|
76
79
|
|
|
77
80
|
_allowed_configs = [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
"table_name",
|
|
82
|
+
"wheres",
|
|
83
|
+
"sorts",
|
|
84
|
+
"limit",
|
|
85
|
+
"pagination",
|
|
86
|
+
"model_columns",
|
|
84
87
|
]
|
|
85
88
|
|
|
86
89
|
_required_configs = [
|
|
87
|
-
|
|
90
|
+
"table_name",
|
|
88
91
|
]
|
|
89
92
|
|
|
90
93
|
_table_indexes = None
|
|
@@ -94,114 +97,111 @@ class DynamoDBBackend(Backend):
|
|
|
94
97
|
# this is the list of operators that we can use when querying a dynamodb index and their corresponding
|
|
95
98
|
# key method name in dynamodb
|
|
96
99
|
_index_operators = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
"=": "eq",
|
|
101
|
+
"<": "lt",
|
|
102
|
+
">": "gt",
|
|
103
|
+
">=": "gte",
|
|
104
|
+
"<=": "lte",
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
# this is a map from clearskies operators to the equivalent dynamodb attribute operators
|
|
105
108
|
_attribute_operators = {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
"!=": "ne",
|
|
110
|
+
"<=": "lte",
|
|
111
|
+
">=": "gte",
|
|
112
|
+
">": "gt",
|
|
113
|
+
"<": "lt",
|
|
114
|
+
"=": "eq",
|
|
115
|
+
"IS NOT NULL": "exists",
|
|
116
|
+
"IS NULL": "not_exists",
|
|
117
|
+
"IS NOT": "ne",
|
|
118
|
+
"IS": "eq",
|
|
119
|
+
"LIKE": "", # requires special handling
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
def __init__(self
|
|
120
|
-
self.
|
|
121
|
-
|
|
122
|
-
if not environment.get('AWS_REGION', True):
|
|
123
|
-
raise ValueError('To use DynamoDB you must use set AWS_REGION in the .env file or an environment variable')
|
|
122
|
+
def __init__(self):
|
|
123
|
+
if not self.environment.get("AWS_REGION", True):
|
|
124
|
+
raise ValueError("To use DynamoDB you must use set AWS_REGION in the .env file or an environment variable")
|
|
124
125
|
|
|
125
|
-
self.
|
|
126
|
+
self.dynamodb = self.boto3.resource("dynamodb", region_name=self.environment.get("AWS_REGION", True))
|
|
126
127
|
self._table_indexes = {}
|
|
127
128
|
self._model_columns_cache = {}
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
@classmethod
|
|
131
|
+
def clear_table_cache(cls):
|
|
132
|
+
cls._table_indexes = {}
|
|
133
|
+
cls._model_columns_cache = {}
|
|
131
134
|
|
|
132
135
|
def update(self, id, data, model):
|
|
133
136
|
# when we run an update column we must include the sort column on the primary
|
|
134
137
|
# index (if it exists)
|
|
135
138
|
sort_column_name = self._find_primary_sort_column(model)
|
|
136
|
-
key = {model.id_column_name: model.
|
|
139
|
+
key = {model.id_column_name: model.get_columns()[model.id_column_name]}
|
|
137
140
|
if sort_column_name:
|
|
138
141
|
key[sort_column_name] = data.get(
|
|
139
|
-
sort_column_name,
|
|
140
|
-
model.columns()[sort_column_name].to_backend(model._data)
|
|
142
|
+
sort_column_name, model.get_columns()[sort_column_name].to_backend(model._data)
|
|
141
143
|
)
|
|
142
|
-
table = self.
|
|
144
|
+
table = self.dynamodb.Table(model.destination_name())
|
|
143
145
|
|
|
144
146
|
data = self.excessive_type_casting(data)
|
|
145
147
|
|
|
146
148
|
updated = table.update_item(
|
|
147
149
|
Key=key,
|
|
148
|
-
UpdateExpression=
|
|
150
|
+
UpdateExpression="SET " + ", ".join([f"#{column_name} = :{column_name}" for column_name in data.keys()]),
|
|
149
151
|
ExpressionAttributeValues={
|
|
150
|
-
**{f
|
|
151
|
-
for (column_name, value) in data.items()},
|
|
152
|
+
**{f":{column_name}": value for (column_name, value) in data.items()},
|
|
152
153
|
},
|
|
153
154
|
ExpressionAttributeNames={
|
|
154
|
-
**{f
|
|
155
|
-
for column_name in data.keys()},
|
|
155
|
+
**{f"#{column_name}": column_name for column_name in data.keys()},
|
|
156
156
|
},
|
|
157
157
|
ReturnValues="ALL_NEW",
|
|
158
158
|
)
|
|
159
|
-
return self._map_from_boto3(updated[
|
|
159
|
+
return self._map_from_boto3(updated["Attributes"])
|
|
160
160
|
|
|
161
161
|
def create(self, data, model):
|
|
162
|
-
table = self.
|
|
162
|
+
table = self.dynamodb.Table(model.destination_name())
|
|
163
163
|
table.put_item(Item=data)
|
|
164
164
|
return {**data}
|
|
165
165
|
|
|
166
166
|
def excessive_type_casting(self, data):
|
|
167
|
-
for
|
|
167
|
+
for key, value in data.items():
|
|
168
168
|
if isinstance(value, float):
|
|
169
169
|
data[key] = Decimal(value)
|
|
170
170
|
return data
|
|
171
171
|
|
|
172
172
|
def delete(self, id, model):
|
|
173
|
-
table = self.
|
|
173
|
+
table = self.dynamodb.Table(model.table_name())
|
|
174
174
|
table.delete_item(Key={model.id_column_name: model.__getattr__(model.id_column_name)})
|
|
175
175
|
return True
|
|
176
176
|
|
|
177
177
|
def count(self, configuration, model):
|
|
178
|
-
response = self.
|
|
179
|
-
return response[
|
|
180
|
-
|
|
181
|
-
def records(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
response
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
self._map_from_boto3(response['LastEvaluatedKey'])
|
|
178
|
+
response = self.dynamodb_query(configuration, model, "COUNT")
|
|
179
|
+
return response["Count"]
|
|
180
|
+
|
|
181
|
+
def records(
|
|
182
|
+
self, configuration: dict[str, Any], model: model.Model, next_page_data: dict[str, str] = None
|
|
183
|
+
) -> list[dict[str, Any]]:
|
|
184
|
+
response = self.dynamodb_query(configuration, model, "ALL_ATTRIBUTES")
|
|
185
|
+
if "LastEvaluatedKey" in response and response["LastEvaluatedKey"] is not None and type(next_page_data) == dict:
|
|
186
|
+
next_page_data["next_token"] = self.serialize_next_token_for_response(
|
|
187
|
+
self._map_from_boto3(response["LastEvaluatedKey"])
|
|
189
188
|
)
|
|
190
|
-
return [self._map_from_boto3(item) for item in response[
|
|
189
|
+
return [self._map_from_boto3(item) for item in response["Items"]]
|
|
191
190
|
|
|
192
191
|
def _dynamodb_query(self, configuration, model, select_type):
|
|
193
|
-
[filter_expression, key_condition_expression, index_name,
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
[filter_expression, key_condition_expression, index_name, scan_index_forward] = (
|
|
193
|
+
self._create_dynamodb_query_parameters(configuration, model)
|
|
194
|
+
)
|
|
195
|
+
table = self.dynamodb.Table(model.table_name())
|
|
196
196
|
|
|
197
197
|
# so we want to put together the kwargs for scan/query:
|
|
198
198
|
kwargs = {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
"IndexName": index_name,
|
|
200
|
+
"KeyConditionExpression": key_condition_expression,
|
|
201
|
+
"FilterExpression": filter_expression,
|
|
202
|
+
"Select": select_type,
|
|
203
|
+
"ExclusiveStartKey": self.restore_next_token_from_config(configuration["pagination"].get("next_token")),
|
|
204
|
+
"Limit": configuration["limit"] if configuration["limit"] and select_type != "COUNT" else None,
|
|
205
205
|
}
|
|
206
206
|
# the trouble is that boto3 isn't okay with parameters of None.
|
|
207
207
|
# therefore, we need to remove any of the above keys that are None
|
|
@@ -209,7 +209,7 @@ class DynamoDBBackend(Backend):
|
|
|
209
209
|
|
|
210
210
|
if key_condition_expression:
|
|
211
211
|
# add the scan index forward setting for key conditions
|
|
212
|
-
kwargs[
|
|
212
|
+
kwargs["ScanIndexForward"] = scan_index_forward
|
|
213
213
|
return table.query(**kwargs)
|
|
214
214
|
return table.scan(**kwargs)
|
|
215
215
|
|
|
@@ -217,13 +217,13 @@ class DynamoDBBackend(Backend):
|
|
|
217
217
|
# DynamoDB only supports sorting by a single column, and only if we can find a supporting index
|
|
218
218
|
# figure out if and what we are sorting by.
|
|
219
219
|
sort_column = None
|
|
220
|
-
sort_direction =
|
|
221
|
-
if
|
|
222
|
-
sort_column = configuration[
|
|
223
|
-
sort_direction = configuration[
|
|
220
|
+
sort_direction = "asc"
|
|
221
|
+
if "sorts" in configuration and configuration["sorts"]:
|
|
222
|
+
sort_column = configuration["sorts"][0]["column"]
|
|
223
|
+
sort_direction = configuration["sorts"][0]["direction"]
|
|
224
224
|
|
|
225
225
|
# if we have neither sort nor a where then we have a simple query and can finish up now.
|
|
226
|
-
if not sort_column and not configuration[
|
|
226
|
+
if not sort_column and not configuration["wheres"]:
|
|
227
227
|
return [None, None, None, True]
|
|
228
228
|
|
|
229
229
|
# so the thing here is that if we find a condition that corresponds to an indexed
|
|
@@ -234,7 +234,7 @@ class DynamoDBBackend(Backend):
|
|
|
234
234
|
# the query operation in dynamodb, so searching on an indexed column doesn't guarantee
|
|
235
235
|
# that we can use a query.
|
|
236
236
|
[key_condition_expression, index_name, remaining_conditions] = self._find_key_condition_expressions(
|
|
237
|
-
configuration[
|
|
237
|
+
configuration["wheres"],
|
|
238
238
|
model.id_column_name,
|
|
239
239
|
sort_column,
|
|
240
240
|
model,
|
|
@@ -243,8 +243,8 @@ class DynamoDBBackend(Backend):
|
|
|
243
243
|
return [
|
|
244
244
|
self._as_attr_filter_expressions(remaining_conditions, model),
|
|
245
245
|
key_condition_expression,
|
|
246
|
-
index_name,
|
|
247
|
-
sort_direction.lower() ==
|
|
246
|
+
index_name, # we don't need to specify the name of the primary index
|
|
247
|
+
sort_direction.lower() == "asc",
|
|
248
248
|
]
|
|
249
249
|
|
|
250
250
|
def _find_key_condition_expressions(self, conditions, id_column_name, sort_column, model):
|
|
@@ -271,13 +271,13 @@ class DynamoDBBackend(Backend):
|
|
|
271
271
|
id_conditions = []
|
|
272
272
|
indexable_conditions = []
|
|
273
273
|
secondary_conditions = []
|
|
274
|
-
for
|
|
275
|
-
column_name = condition[
|
|
274
|
+
for index, condition in enumerate(conditions):
|
|
275
|
+
column_name = condition["column"]
|
|
276
276
|
# if the column isn't a hash index and isn't an equals search, then this condition "anchor" an index search.
|
|
277
|
-
if column_name not in indexes or condition[
|
|
277
|
+
if column_name not in indexes or condition["operator"] != "=":
|
|
278
278
|
# however, it may still contribute to a secondary condition in an index search, so record it
|
|
279
279
|
# if it uses a supporting operator
|
|
280
|
-
if condition[
|
|
280
|
+
if condition["operator"] in self._index_operators:
|
|
281
281
|
secondary_conditions.append(index)
|
|
282
282
|
|
|
283
283
|
# if we get here then we have an '=' condition on a hash attribute in an index - we can use an index!
|
|
@@ -352,31 +352,31 @@ class DynamoDBBackend(Backend):
|
|
|
352
352
|
"""
|
|
353
353
|
# the condition for the primary condition
|
|
354
354
|
index_condition = conditions[primary_condition_index]
|
|
355
|
-
index_data = indexes[index_condition[
|
|
355
|
+
index_data = indexes[index_condition["column"]]
|
|
356
356
|
|
|
357
357
|
# our secondary columns are just suggestions, so see if we can actually use any
|
|
358
358
|
index_condition_counts = {}
|
|
359
359
|
for condition_index in secondary_condition_indexes:
|
|
360
360
|
secondary_condition = conditions[condition_index]
|
|
361
|
-
secondary_column = secondary_condition[
|
|
362
|
-
if secondary_column not in index_data[
|
|
361
|
+
secondary_column = secondary_condition["column"]
|
|
362
|
+
if secondary_column not in index_data["sortable_columns"]:
|
|
363
363
|
continue
|
|
364
|
-
secondary_index = index_data[
|
|
364
|
+
secondary_index = index_data["sortable_columns"][secondary_column]
|
|
365
365
|
if secondary_index not in index_condition_counts:
|
|
366
|
-
index_condition_counts[secondary_index] = {
|
|
367
|
-
index_condition_counts[secondary_index][
|
|
368
|
-
index_condition_counts[secondary_index][
|
|
366
|
+
index_condition_counts[secondary_index] = {"count": 0, "condition_indexes": []}
|
|
367
|
+
index_condition_counts[secondary_index]["count"] += 1
|
|
368
|
+
index_condition_counts[secondary_index]["condition_indexes"].append(condition_index)
|
|
369
369
|
|
|
370
370
|
# now we can decide which index to use. Prefer an index that hits some secondary conditions,
|
|
371
371
|
# or an index that hits the sort column, or the default index.
|
|
372
372
|
used_condition_indexes = [primary_condition_index]
|
|
373
373
|
if index_condition_counts:
|
|
374
|
-
index_to_use = max(index_condition_counts, key=lambda key: index_condition_counts[key][
|
|
375
|
-
used_condition_indexes.extend(index_condition_counts[index_to_use][
|
|
376
|
-
elif sort_column in index_data[
|
|
377
|
-
index_to_use = index_data[
|
|
374
|
+
index_to_use = max(index_condition_counts, key=lambda key: index_condition_counts[key]["count"])
|
|
375
|
+
used_condition_indexes.extend(index_condition_counts[index_to_use]["condition_indexes"])
|
|
376
|
+
elif sort_column in index_data["sortable_columns"]:
|
|
377
|
+
index_to_use = index_data["sortable_columns"][sort_column]
|
|
378
378
|
else:
|
|
379
|
-
index_to_use = index_data[
|
|
379
|
+
index_to_use = index_data["default_index_name"]
|
|
380
380
|
|
|
381
381
|
# now build our key expression. For every condition in used_condition_indexes, add it to
|
|
382
382
|
# a key expression, and remove it from the conditions array. Do this backwards to make sure
|
|
@@ -386,11 +386,12 @@ class DynamoDBBackend(Backend):
|
|
|
386
386
|
key_condition_expression = None
|
|
387
387
|
for condition_index in used_condition_indexes:
|
|
388
388
|
condition = conditions[condition_index]
|
|
389
|
-
dynamodb_operator_method = self._index_operators[condition[
|
|
390
|
-
raw_search_value = condition[
|
|
391
|
-
value = self._value_for_condition_expression(raw_search_value, condition[
|
|
392
|
-
condition_expression = getattr(dynamodb_conditions.Key(condition[
|
|
393
|
-
|
|
389
|
+
dynamodb_operator_method = self._index_operators[condition["operator"]]
|
|
390
|
+
raw_search_value = condition["values"][0] if condition["values"] else None
|
|
391
|
+
value = self._value_for_condition_expression(raw_search_value, condition["column"], model)
|
|
392
|
+
condition_expression = getattr(dynamodb_conditions.Key(condition["column"]), dynamodb_operator_method)(
|
|
393
|
+
value
|
|
394
|
+
)
|
|
394
395
|
# add to our key condition expression
|
|
395
396
|
if key_condition_expression is None:
|
|
396
397
|
key_condition_expression = condition_expression
|
|
@@ -409,27 +410,27 @@ class DynamoDBBackend(Backend):
|
|
|
409
410
|
def _as_attr_filter_expressions(self, conditions, model):
|
|
410
411
|
filter_expression = None
|
|
411
412
|
for condition in conditions:
|
|
412
|
-
operator = condition[
|
|
413
|
-
value = condition[
|
|
414
|
-
column_name = condition[
|
|
413
|
+
operator = condition["operator"]
|
|
414
|
+
value = condition["values"][0] if condition["values"] else None
|
|
415
|
+
column_name = condition["column"]
|
|
415
416
|
if operator not in self._attribute_operators:
|
|
416
417
|
raise ValueError(
|
|
417
418
|
f"I was asked to filter by operator '{operator}' but this operator is not supported by DynamoDB"
|
|
418
419
|
)
|
|
419
420
|
|
|
420
421
|
# a couple of our operators require special handling
|
|
421
|
-
if operator ==
|
|
422
|
-
if value[0] !=
|
|
423
|
-
condition_expression = dynamodb_conditions.Attr(column_name).begins_with(value.rstrip(
|
|
424
|
-
elif value[0] ==
|
|
422
|
+
if operator == "LIKE":
|
|
423
|
+
if value[0] != "%" and value[-1] == "%":
|
|
424
|
+
condition_expression = dynamodb_conditions.Attr(column_name).begins_with(value.rstrip("%"))
|
|
425
|
+
elif value[0] == "%" and value[-1] != "%":
|
|
425
426
|
raise ValueError("DynamoDB doesn't support the 'ends_with' operator")
|
|
426
|
-
elif value[0] ==
|
|
427
|
-
condition_expression = dynamodb_conditions.Attr(column_name).contains(value.strip(
|
|
427
|
+
elif value[0] == "%" and value[-1] == "%":
|
|
428
|
+
condition_expression = dynamodb_conditions.Attr(column_name).contains(value.strip("%"))
|
|
428
429
|
else:
|
|
429
430
|
condition_expression = dynamodb_conditions.Attr(column_name).eq(value)
|
|
430
|
-
elif operator ==
|
|
431
|
+
elif operator == "IS NULL":
|
|
431
432
|
condition_expression = dynamodb_conditions.Attr(column_name).exists()
|
|
432
|
-
elif operator ==
|
|
433
|
+
elif operator == "IS NOT NULL":
|
|
433
434
|
condition_expression = dynamodb_conditions.Attr(column_name).not_exists()
|
|
434
435
|
else:
|
|
435
436
|
dynamodb_operator = self._attribute_operators[operator]
|
|
@@ -459,7 +460,7 @@ class DynamoDBBackend(Backend):
|
|
|
459
460
|
return value
|
|
460
461
|
|
|
461
462
|
def _get_indexes_for_model(self, model):
|
|
462
|
-
"""
|
|
463
|
+
"""Load indexes for the DynamoDB table for the given model."""
|
|
463
464
|
if model.table_name() in self._table_indexes:
|
|
464
465
|
return self._table_indexes[model.table_name()]
|
|
465
466
|
|
|
@@ -479,11 +480,11 @@ class DynamoDBBackend(Backend):
|
|
|
479
480
|
# and then is further subdivided for columns that have RANGE/Sort attributes, giving you
|
|
480
481
|
# the index name for that HASH+RANGE combination.
|
|
481
482
|
table_indexes = {}
|
|
482
|
-
table = self.
|
|
483
|
+
table = self.dynamodb.Table(model.table_name())
|
|
483
484
|
schemas = []
|
|
484
485
|
# the primary index for the table doesn't have a name, and it will be used by default
|
|
485
486
|
# if we don't specify an index name. Therefore, we just pass around None for it's name
|
|
486
|
-
schemas.append({
|
|
487
|
+
schemas.append({"IndexName": None, "KeySchema": table.key_schema})
|
|
487
488
|
global_secondary_indexes = table.global_secondary_indexes
|
|
488
489
|
local_secondary_indexes = table.local_secondary_indexes
|
|
489
490
|
if global_secondary_indexes is not None:
|
|
@@ -491,17 +492,17 @@ class DynamoDBBackend(Backend):
|
|
|
491
492
|
if local_secondary_indexes is not None:
|
|
492
493
|
schemas.extend(table.local_secondary_indexes)
|
|
493
494
|
for schema in schemas:
|
|
494
|
-
hash_column =
|
|
495
|
-
range_column =
|
|
496
|
-
for key in schema[
|
|
497
|
-
if key[
|
|
498
|
-
range_column = key[
|
|
499
|
-
if key[
|
|
500
|
-
hash_column = key[
|
|
495
|
+
hash_column = ""
|
|
496
|
+
range_column = ""
|
|
497
|
+
for key in schema["KeySchema"]:
|
|
498
|
+
if key["KeyType"] == "RANGE":
|
|
499
|
+
range_column = key["AttributeName"]
|
|
500
|
+
if key["KeyType"] == "HASH":
|
|
501
|
+
hash_column = key["AttributeName"]
|
|
501
502
|
if hash_column not in table_indexes:
|
|
502
|
-
table_indexes[hash_column] = {
|
|
503
|
+
table_indexes[hash_column] = {"default_index_name": schema["IndexName"], "sortable_columns": {}}
|
|
503
504
|
if range_column:
|
|
504
|
-
table_indexes[hash_column][
|
|
505
|
+
table_indexes[hash_column]["sortable_columns"][range_column] = schema["IndexName"]
|
|
505
506
|
|
|
506
507
|
self._table_indexes[model.table_name()] = table_indexes
|
|
507
508
|
return table_indexes
|
|
@@ -511,7 +512,7 @@ class DynamoDBBackend(Backend):
|
|
|
511
512
|
primary_indexes = indexes.get(model.id_column_name)
|
|
512
513
|
if not primary_indexes:
|
|
513
514
|
return None
|
|
514
|
-
for
|
|
515
|
+
for column_name, index_name in primary_indexes["sortable_columns"].items():
|
|
515
516
|
# the primary index doesn't have a name, so we want the record with a name of None
|
|
516
517
|
if index_name is None:
|
|
517
518
|
return column_name
|
|
@@ -532,32 +533,32 @@ class DynamoDBBackend(Backend):
|
|
|
532
533
|
|
|
533
534
|
for key in self._required_configs:
|
|
534
535
|
if key not in configuration:
|
|
535
|
-
raise KeyError(f
|
|
536
|
+
raise KeyError(f"Missing required configuration key {key}")
|
|
536
537
|
|
|
537
538
|
for key in self._allowed_configs:
|
|
538
|
-
if not
|
|
539
|
-
configuration[key] = [] if key[-1] ==
|
|
539
|
+
if key not in configuration:
|
|
540
|
+
configuration[key] = [] if key[-1] == "s" else ""
|
|
540
541
|
|
|
541
542
|
return configuration
|
|
542
543
|
|
|
543
|
-
def validate_pagination_kwargs(self, kwargs:
|
|
544
|
+
def validate_pagination_kwargs(self, kwargs: dict[str, Any], case_mapping: Callable) -> str:
|
|
544
545
|
extra_keys = set(kwargs.keys()) - set(self.allowed_pagination_keys())
|
|
545
546
|
if len(extra_keys):
|
|
546
|
-
key_name = case_mapping(
|
|
547
|
+
key_name = case_mapping("next_token")
|
|
547
548
|
return "Invalid pagination key(s): '" + "','".join(extra_keys) + f"'. Only '{key_name}' is allowed"
|
|
548
|
-
if
|
|
549
|
-
key_name = case_mapping(
|
|
549
|
+
if "next_token" not in kwargs:
|
|
550
|
+
key_name = case_mapping("next_token")
|
|
550
551
|
return f"You must specify '{key_name}' when setting pagination"
|
|
551
552
|
# the next token should be a urlsafe-base64 encoded JSON string
|
|
552
553
|
try:
|
|
553
|
-
json.loads(base64.urlsafe_b64decode(kwargs[
|
|
554
|
+
json.loads(base64.urlsafe_b64decode(kwargs["next_token"]))
|
|
554
555
|
except:
|
|
555
|
-
key_name = case_mapping(
|
|
556
|
+
key_name = case_mapping("next_token")
|
|
556
557
|
return "The provided '{key_name}' appears to be invalid."
|
|
557
|
-
return
|
|
558
|
+
return ""
|
|
558
559
|
|
|
559
|
-
def allowed_pagination_keys(self) ->
|
|
560
|
-
return [
|
|
560
|
+
def allowed_pagination_keys(self) -> list[str]:
|
|
561
|
+
return ["next_token"]
|
|
561
562
|
|
|
562
563
|
def restore_next_token_from_config(self, next_token):
|
|
563
564
|
if not next_token:
|
|
@@ -568,23 +569,19 @@ class DynamoDBBackend(Backend):
|
|
|
568
569
|
return None
|
|
569
570
|
|
|
570
571
|
def serialize_next_token_for_response(self, last_evaluated_key):
|
|
571
|
-
return base64.urlsafe_b64encode(json.dumps(last_evaluated_key).encode(
|
|
572
|
+
return base64.urlsafe_b64encode(json.dumps(last_evaluated_key).encode("utf-8")).decode("utf8")
|
|
572
573
|
|
|
573
|
-
def documentation_pagination_next_page_response(self, case_mapping: Callable) ->
|
|
574
|
-
return [AutoDocString(case_mapping(
|
|
574
|
+
def documentation_pagination_next_page_response(self, case_mapping: Callable) -> list[Any]:
|
|
575
|
+
return [AutoDocString(case_mapping("next_token"))]
|
|
575
576
|
|
|
576
|
-
def documentation_pagination_next_page_example(self, case_mapping: Callable) ->
|
|
577
|
-
return {case_mapping(
|
|
577
|
+
def documentation_pagination_next_page_example(self, case_mapping: Callable) -> dict[str, Any]:
|
|
578
|
+
return {case_mapping("next_token"): ""}
|
|
578
579
|
|
|
579
|
-
def documentation_pagination_parameters(self, case_mapping: Callable) ->
|
|
580
|
-
return [(
|
|
581
|
-
AutoDocString(case_mapping('next_token'), example=''), 'A token to fetch the next page of results'
|
|
582
|
-
)]
|
|
580
|
+
def documentation_pagination_parameters(self, case_mapping: Callable) -> list[tuple[Any]]:
|
|
581
|
+
return [(AutoDocString(case_mapping("next_token"), example=""), "A token to fetch the next page of results")]
|
|
583
582
|
|
|
584
583
|
def column_from_backend(self, column, value):
|
|
585
|
-
"""
|
|
586
|
-
We have a couple columns we want to override transformations for
|
|
587
|
-
"""
|
|
584
|
+
"""We have a couple columns we want to override transformations for."""
|
|
588
585
|
# We're pretty much ignoring the BOOL type for dynamodb, because it doesn't work in indexes
|
|
589
586
|
# (and 99% of the time when I have a boolean, it gets used in an index). Therefore,
|
|
590
587
|
# convert boolean values to "0", "1".
|
|
@@ -598,9 +595,7 @@ class DynamoDBBackend(Backend):
|
|
|
598
595
|
return super().column_from_backend(column, value)
|
|
599
596
|
|
|
600
597
|
def column_to_backend(self, column, backend_data):
|
|
601
|
-
"""
|
|
602
|
-
We have a couple columns we want to override transformations for
|
|
603
|
-
"""
|
|
598
|
+
"""We have a couple columns we want to override transformations for."""
|
|
604
599
|
# most importantly, there's no need to transform a JSON column in either direction
|
|
605
600
|
if isinstance(column, Boolean):
|
|
606
601
|
if column.name not in backend_data:
|