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,544 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import json
|
|
3
|
-
import re
|
|
4
|
-
import unittest
|
|
5
|
-
from decimal import Decimal
|
|
6
|
-
from unittest.mock import MagicMock, call, patch
|
|
7
|
-
|
|
8
|
-
from boto3.session import Session as Boto3Session
|
|
9
|
-
from botocore.exceptions import ClientError
|
|
10
|
-
from clearskies import Model
|
|
11
|
-
from clearskies.autodoc.schema import String as AutoDocString
|
|
12
|
-
|
|
13
|
-
from clearskies_aws.backends.dynamo_db_parti_ql_backend import (
|
|
14
|
-
DynamoDBPartiQLBackend,
|
|
15
|
-
DynamoDBPartiQLCursor,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@patch("clearskies_aws.backends.dynamo_db_parti_ql_backend.logger")
|
|
20
|
-
class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
21
|
-
|
|
22
|
-
def setUp(self):
|
|
23
|
-
"""Set up the test environment before each test method."""
|
|
24
|
-
self.mock_boto3_session = MagicMock(spec=Boto3Session)
|
|
25
|
-
self.mock_dynamodb_client = MagicMock()
|
|
26
|
-
self.mock_boto3_session.client.return_value = self.mock_dynamodb_client
|
|
27
|
-
|
|
28
|
-
self.cursor_under_test = DynamoDBPartiQLCursor(self.mock_boto3_session)
|
|
29
|
-
|
|
30
|
-
self.backend = DynamoDBPartiQLBackend(self.cursor_under_test)
|
|
31
|
-
self.mock_model = MagicMock(spec=Model)
|
|
32
|
-
self.mock_model.get_table_name = MagicMock(return_value="my_test_table")
|
|
33
|
-
self.mock_model.id_column_name = "id"
|
|
34
|
-
|
|
35
|
-
self.mock_model.schema = MagicMock()
|
|
36
|
-
self.mock_model.schema.return_value.indexes = {}
|
|
37
|
-
|
|
38
|
-
self.backend._get_table_description = MagicMock()
|
|
39
|
-
self.backend._get_table_description.return_value = {
|
|
40
|
-
"KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}],
|
|
41
|
-
"GlobalSecondaryIndexes": [],
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
def _get_base_config(self, table_name="test_table", **overrides):
|
|
45
|
-
"""Helper to create a base configuration dictionary with defaults."""
|
|
46
|
-
config = {
|
|
47
|
-
"table_name": table_name,
|
|
48
|
-
"wheres": [],
|
|
49
|
-
"sorts": [],
|
|
50
|
-
"limit": None,
|
|
51
|
-
"pagination": {},
|
|
52
|
-
"model_columns": [],
|
|
53
|
-
"select_all": False,
|
|
54
|
-
"selects": [],
|
|
55
|
-
"group_by_column": None,
|
|
56
|
-
"joins": [],
|
|
57
|
-
}
|
|
58
|
-
config.update(overrides)
|
|
59
|
-
return config
|
|
60
|
-
|
|
61
|
-
def test_as_sql_simple_select_all(self, mock_logger_arg):
|
|
62
|
-
"""Test SQL generation for a simple 'SELECT *' statement."""
|
|
63
|
-
config = self._get_base_config(table_name="users", select_all=True)
|
|
64
|
-
config["_chosen_index_name"] = None
|
|
65
|
-
statement, params, limit, next_token = self.backend.as_sql(config)
|
|
66
|
-
self.assertEqual('SELECT * FROM "users"', statement)
|
|
67
|
-
self.assertEqual([], params)
|
|
68
|
-
self.assertIsNone(limit)
|
|
69
|
-
self.assertEqual(next_token, config.get("pagination", {}).get("next_token"))
|
|
70
|
-
|
|
71
|
-
def test_as_sql_select_specific_columns(self, mock_logger_arg):
|
|
72
|
-
"""Test SQL generation for selecting specific columns."""
|
|
73
|
-
config = self._get_base_config(table_name="products", selects=["name", "price"])
|
|
74
|
-
config["_chosen_index_name"] = None
|
|
75
|
-
statement, params, limit, next_token = self.backend.as_sql(config)
|
|
76
|
-
self.assertEqual('SELECT "name", "price" FROM "products"', statement)
|
|
77
|
-
self.assertEqual([], params)
|
|
78
|
-
|
|
79
|
-
def test_as_sql_select_all_and_specific_columns_uses_specific(
|
|
80
|
-
self, mock_logger_arg
|
|
81
|
-
):
|
|
82
|
-
"""Test SQL generation uses specific columns if both select_all and selects are given."""
|
|
83
|
-
config = self._get_base_config(
|
|
84
|
-
table_name="inventory", select_all=True, selects=["item_id", "stock_count"]
|
|
85
|
-
)
|
|
86
|
-
config["_chosen_index_name"] = None
|
|
87
|
-
statement, params, limit, next_token = self.backend.as_sql(config)
|
|
88
|
-
expected_sql = 'SELECT "item_id", "stock_count" FROM "inventory"'
|
|
89
|
-
self.assertEqual(expected_sql, statement)
|
|
90
|
-
mock_logger_arg.warning.assert_any_call(
|
|
91
|
-
"Both 'select_all=True' and specific 'selects' were provided. Using specific 'selects'."
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
def test_as_sql_default_select_if_no_select_all_or_selects(self, mock_logger_arg):
|
|
95
|
-
"""Test SQL generation defaults to 'SELECT *' if no specific columns are given."""
|
|
96
|
-
config = self._get_base_config(table_name="orders")
|
|
97
|
-
config["_chosen_index_name"] = None
|
|
98
|
-
statement, params, limit, next_token = self.backend.as_sql(config)
|
|
99
|
-
self.assertEqual('SELECT * FROM "orders"', statement)
|
|
100
|
-
self.assertEqual([], params)
|
|
101
|
-
|
|
102
|
-
def test_as_sql_with_wheres(self, mock_logger_arg):
|
|
103
|
-
"""Test SQL generation with WHERE clauses."""
|
|
104
|
-
config = self._get_base_config(
|
|
105
|
-
table_name="customers",
|
|
106
|
-
select_all=True,
|
|
107
|
-
wheres=[
|
|
108
|
-
{"column": "city", "operator": "=", "values": ["New York"]},
|
|
109
|
-
{"column": "age", "operator": ">", "values": [30]},
|
|
110
|
-
],
|
|
111
|
-
)
|
|
112
|
-
config["_chosen_index_name"] = None
|
|
113
|
-
statement, params, limit, next_token = self.backend.as_sql(config)
|
|
114
|
-
expected_statement = 'SELECT * FROM "customers" WHERE "city" = ? AND "age" > ?'
|
|
115
|
-
expected_parameters = [{"S": "New York"}, {"N": "30"}]
|
|
116
|
-
self.assertEqual(expected_statement, statement)
|
|
117
|
-
self.assertEqual(expected_parameters, params)
|
|
118
|
-
|
|
119
|
-
def test_as_sql_with_sorts(self, mock_logger_arg):
|
|
120
|
-
"""Test SQL generation with ORDER BY clauses (no table prefix for columns)."""
|
|
121
|
-
config = self._get_base_config(
|
|
122
|
-
table_name="items",
|
|
123
|
-
select_all=True,
|
|
124
|
-
sorts=[
|
|
125
|
-
{"column": "name", "direction": "ASC"},
|
|
126
|
-
{"column": "created_at", "direction": "DESC"},
|
|
127
|
-
],
|
|
128
|
-
)
|
|
129
|
-
config["_chosen_index_name"] = None
|
|
130
|
-
statement, params, limit, next_token = self.backend.as_sql(config)
|
|
131
|
-
expected_statement = (
|
|
132
|
-
'SELECT * FROM "items" ORDER BY "name" ASC, "created_at" DESC'
|
|
133
|
-
)
|
|
134
|
-
self.assertEqual(expected_statement, statement)
|
|
135
|
-
|
|
136
|
-
def test_as_sql_with_index_name(self, mock_logger_arg):
|
|
137
|
-
"""Test SQL generation uses index name in FROM clause if provided."""
|
|
138
|
-
config = self._get_base_config(table_name="my_table", select_all=True)
|
|
139
|
-
config["_chosen_index_name"] = "my_gsi"
|
|
140
|
-
|
|
141
|
-
statement, params, limit, next_token = self.backend.as_sql(config)
|
|
142
|
-
self.assertEqual('SELECT * FROM "my_table"."my_gsi"', statement)
|
|
143
|
-
|
|
144
|
-
def test_as_sql_ignores_group_by_and_joins(self, mock_logger_arg):
|
|
145
|
-
"""Test that GROUP BY and JOIN configurations are ignored for SQL but logged."""
|
|
146
|
-
config = self._get_base_config(
|
|
147
|
-
table_name="log_data", group_by_column="level", joins=["some_join_info"]
|
|
148
|
-
)
|
|
149
|
-
config["_chosen_index_name"] = None
|
|
150
|
-
|
|
151
|
-
statement, _, _, _ = self.backend.as_sql(config)
|
|
152
|
-
self.assertNotIn("GROUP BY", statement.upper())
|
|
153
|
-
self.assertNotIn("JOIN", statement.upper())
|
|
154
|
-
mock_logger_arg.warning.assert_any_call(
|
|
155
|
-
"Configuration included 'group_by_column=level', "
|
|
156
|
-
"but GROUP BY is not supported by this DynamoDB PartiQL backend and will be ignored for SQL generation."
|
|
157
|
-
)
|
|
158
|
-
mock_logger_arg.warning.assert_any_call(
|
|
159
|
-
"Configuration included 'joins=['some_join_info']', "
|
|
160
|
-
"but JOINs are not supported by this DynamoDB PartiQL backend and will be ignored for SQL generation."
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
def test_check_query_configuration_sort_with_base_table_hash_key_equality(
|
|
164
|
-
self, mock_logger_arg
|
|
165
|
-
):
|
|
166
|
-
"""Test _check_query_configuration allows sort if base table hash key equality exists."""
|
|
167
|
-
self.backend._get_table_description.return_value = {
|
|
168
|
-
"KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}],
|
|
169
|
-
"GlobalSecondaryIndexes": [],
|
|
170
|
-
}
|
|
171
|
-
config = self._get_base_config(
|
|
172
|
-
table_name="my_test_table",
|
|
173
|
-
sorts=[{"column": "name", "direction": "ASC"}],
|
|
174
|
-
wheres=[{"column": "id", "operator": "=", "values": ["some_id"]}],
|
|
175
|
-
)
|
|
176
|
-
processed_config = self.backend._check_query_configuration(
|
|
177
|
-
config, self.mock_model
|
|
178
|
-
)
|
|
179
|
-
self.assertIsNone(processed_config.get("_chosen_index_name"))
|
|
180
|
-
self.assertEqual(processed_config.get("_partition_key_for_target"), "id")
|
|
181
|
-
|
|
182
|
-
def test_check_query_configuration_sort_raises_error_if_no_hash_key_equality(
|
|
183
|
-
self, mock_logger_arg
|
|
184
|
-
):
|
|
185
|
-
"""Test _check_query_configuration raises ValueError for sort without hash key equality."""
|
|
186
|
-
self.backend._get_table_description.return_value = {
|
|
187
|
-
"KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}],
|
|
188
|
-
"GlobalSecondaryIndexes": [],
|
|
189
|
-
}
|
|
190
|
-
config = self._get_base_config(
|
|
191
|
-
table_name="my_test_table",
|
|
192
|
-
sorts=[{"column": "name", "direction": "ASC"}],
|
|
193
|
-
wheres=[{"column": "status", "operator": "=", "values": ["active"]}],
|
|
194
|
-
)
|
|
195
|
-
expected_error_message = "DynamoDB PartiQL queries with ORDER BY on 'my_test_table' require an equality condition on its partition key ('id') in the WHERE clause."
|
|
196
|
-
with self.assertRaisesRegex(ValueError, re.escape(expected_error_message)):
|
|
197
|
-
self.backend._check_query_configuration(config, self.mock_model)
|
|
198
|
-
|
|
199
|
-
def test_check_query_configuration_sort_uses_gsi_if_partition_key_matches(
|
|
200
|
-
self, mock_logger_arg
|
|
201
|
-
):
|
|
202
|
-
"""Test _check_query_configuration selects GSI if its partition key matches WHERE and can sort."""
|
|
203
|
-
self.backend._get_table_description.return_value = {
|
|
204
|
-
"KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}],
|
|
205
|
-
"GlobalSecondaryIndexes": [
|
|
206
|
-
{
|
|
207
|
-
"IndexName": "domain-status-index",
|
|
208
|
-
"KeySchema": [
|
|
209
|
-
{"AttributeName": "domain", "KeyType": "HASH"},
|
|
210
|
-
{"AttributeName": "status", "KeyType": "RANGE"},
|
|
211
|
-
],
|
|
212
|
-
"Projection": {"ProjectionType": "ALL"},
|
|
213
|
-
}
|
|
214
|
-
],
|
|
215
|
-
}
|
|
216
|
-
config = self._get_base_config(
|
|
217
|
-
table_name="my_test_table",
|
|
218
|
-
sorts=[{"column": "status", "direction": "DESC"}],
|
|
219
|
-
wheres=[{"column": "domain", "operator": "=", "values": ["example.com"]}],
|
|
220
|
-
)
|
|
221
|
-
processed_config = self.backend._check_query_configuration(
|
|
222
|
-
config, self.mock_model
|
|
223
|
-
)
|
|
224
|
-
self.assertEqual(
|
|
225
|
-
processed_config.get("_chosen_index_name"), "domain-status-index"
|
|
226
|
-
)
|
|
227
|
-
self.assertEqual(processed_config.get("_partition_key_for_target"), "domain")
|
|
228
|
-
|
|
229
|
-
def test_count_uses_native_query_with_pk_condition(self, mock_logger_arg):
|
|
230
|
-
"""Test count() uses native DDB query when PK equality is present."""
|
|
231
|
-
self.backend._get_table_description.return_value = {
|
|
232
|
-
"KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}]
|
|
233
|
-
}
|
|
234
|
-
config = self._get_base_config(
|
|
235
|
-
table_name="users",
|
|
236
|
-
wheres=[{"column": "id", "operator": "=", "values": ["user123"]}],
|
|
237
|
-
)
|
|
238
|
-
self.mock_dynamodb_client.query.return_value = {"Count": 10, "Items": []}
|
|
239
|
-
|
|
240
|
-
count = self.backend.count(config, self.mock_model)
|
|
241
|
-
self.assertEqual(count, 10)
|
|
242
|
-
self.mock_dynamodb_client.query.assert_called_once()
|
|
243
|
-
self.mock_dynamodb_client.scan.assert_not_called()
|
|
244
|
-
called_args = self.mock_dynamodb_client.query.call_args[1]
|
|
245
|
-
self.assertEqual(called_args.get("TableName"), "users")
|
|
246
|
-
self.assertEqual(called_args.get("Select"), "COUNT")
|
|
247
|
-
self.assertIn("KeyConditionExpression", called_args)
|
|
248
|
-
|
|
249
|
-
def test_count_uses_native_scan_without_pk_condition(self, mock_logger_arg):
|
|
250
|
-
"""Test count() uses native DDB scan when PK equality is NOT present."""
|
|
251
|
-
self.backend._get_table_description.return_value = {
|
|
252
|
-
"KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}]
|
|
253
|
-
}
|
|
254
|
-
config = self._get_base_config(
|
|
255
|
-
table_name="users",
|
|
256
|
-
wheres=[{"column": "status", "operator": "=", "values": ["active"]}],
|
|
257
|
-
)
|
|
258
|
-
self.mock_dynamodb_client.scan.return_value = {"Count": 5, "Items": []}
|
|
259
|
-
|
|
260
|
-
count = self.backend.count(config, self.mock_model)
|
|
261
|
-
self.assertEqual(count, 5)
|
|
262
|
-
self.mock_dynamodb_client.scan.assert_called_once()
|
|
263
|
-
self.mock_dynamodb_client.query.assert_not_called()
|
|
264
|
-
called_args = self.mock_dynamodb_client.scan.call_args[1]
|
|
265
|
-
self.assertEqual(called_args.get("TableName"), "users")
|
|
266
|
-
self.assertEqual(called_args.get("Select"), "COUNT")
|
|
267
|
-
self.assertIn("FilterExpression", called_args)
|
|
268
|
-
|
|
269
|
-
def test_count_paginates_native_results(self, mock_logger_arg):
|
|
270
|
-
"""Test count() paginates and sums results from native DDB operations."""
|
|
271
|
-
self.backend._get_table_description.return_value = {
|
|
272
|
-
"KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}]
|
|
273
|
-
}
|
|
274
|
-
config = self._get_base_config(table_name="large_table")
|
|
275
|
-
|
|
276
|
-
self.mock_dynamodb_client.scan.side_effect = [
|
|
277
|
-
{"Count": 100, "LastEvaluatedKey": {"id": {"S": "page1_end"}}},
|
|
278
|
-
{"Count": 50, "LastEvaluatedKey": {"id": {"S": "page2_end"}}},
|
|
279
|
-
{"Count": 25},
|
|
280
|
-
]
|
|
281
|
-
count = self.backend.count(config, self.mock_model)
|
|
282
|
-
self.assertEqual(count, 175)
|
|
283
|
-
self.assertEqual(self.mock_dynamodb_client.scan.call_count, 3)
|
|
284
|
-
|
|
285
|
-
def test_records_simple_fetch(self, mock_logger_arg):
|
|
286
|
-
"""Test records() fetching a single page of results without limit or pagination."""
|
|
287
|
-
config = self._get_base_config(table_name="users", select_all=True)
|
|
288
|
-
expected_statement = 'SELECT * FROM "users"'
|
|
289
|
-
ddb_items = [
|
|
290
|
-
{"id": {"S": "user1"}, "name": {"S": "Alice"}, "age": {"N": "30"}},
|
|
291
|
-
{"id": {"S": "user2"}, "name": {"S": "Bob"}, "age": {"N": "24"}},
|
|
292
|
-
]
|
|
293
|
-
self.mock_dynamodb_client.execute_statement.return_value = {"Items": ddb_items}
|
|
294
|
-
|
|
295
|
-
results = list(self.backend.records(config, self.mock_model))
|
|
296
|
-
|
|
297
|
-
expected_call_kwargs = {
|
|
298
|
-
"Statement": expected_statement,
|
|
299
|
-
}
|
|
300
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
301
|
-
**expected_call_kwargs
|
|
302
|
-
)
|
|
303
|
-
self.assertEqual(len(results), 2)
|
|
304
|
-
# Assert based on what _map_from_boto3 currently does
|
|
305
|
-
self.assertEqual(
|
|
306
|
-
results[0], {"id": "user1", "name": "Alice", "age": Decimal("30")}
|
|
307
|
-
)
|
|
308
|
-
self.assertEqual(
|
|
309
|
-
results[1], {"id": "user2", "name": "Bob", "age": Decimal("24")}
|
|
310
|
-
)
|
|
311
|
-
self.assertIsNone(config["pagination"].get("next_page_token_for_response"))
|
|
312
|
-
|
|
313
|
-
def test_records_with_limit(self, mock_logger_arg):
|
|
314
|
-
"""Test records() respects the server-side limit passed to DynamoDB."""
|
|
315
|
-
config = self._get_base_config(table_name="products", limit=1, select_all=True)
|
|
316
|
-
expected_statement = 'SELECT * FROM "products"'
|
|
317
|
-
ddb_items = [{"id": {"S": "prod1"}, "price": {"N": "10.99"}}]
|
|
318
|
-
ddb_next_token = "fakeDDBNextToken"
|
|
319
|
-
|
|
320
|
-
self.mock_dynamodb_client.execute_statement.return_value = {
|
|
321
|
-
"Items": ddb_items,
|
|
322
|
-
"NextToken": ddb_next_token,
|
|
323
|
-
}
|
|
324
|
-
next_page_data = {}
|
|
325
|
-
results = list(self.backend.records(config, self.mock_model, next_page_data))
|
|
326
|
-
|
|
327
|
-
expected_call_kwargs = {
|
|
328
|
-
"Statement": expected_statement,
|
|
329
|
-
"Limit": 1,
|
|
330
|
-
}
|
|
331
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
332
|
-
**expected_call_kwargs
|
|
333
|
-
)
|
|
334
|
-
self.assertEqual(len(results), 1)
|
|
335
|
-
self.assertEqual(results[0], {"id": "prod1", "price": Decimal("10.99")})
|
|
336
|
-
expected_client_token = self.backend.serialize_next_token_for_response(
|
|
337
|
-
ddb_next_token
|
|
338
|
-
)
|
|
339
|
-
self.assertEqual(
|
|
340
|
-
next_page_data["next_token"],
|
|
341
|
-
expected_client_token,
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
def test_records_pagination_flow(self, mock_logger_arg):
|
|
345
|
-
"""Test records() handling of client-provided token and returning DDB's next token."""
|
|
346
|
-
initial_ddb_token = "start_token_from_ddb_previously"
|
|
347
|
-
client_sends_this_token = self.backend.serialize_next_token_for_response(
|
|
348
|
-
initial_ddb_token
|
|
349
|
-
)
|
|
350
|
-
config1 = self._get_base_config(
|
|
351
|
-
table_name="events",
|
|
352
|
-
select_all=True,
|
|
353
|
-
pagination={"next_token": client_sends_this_token},
|
|
354
|
-
)
|
|
355
|
-
expected_statement = 'SELECT * FROM "events"'
|
|
356
|
-
ddb_items_page1 = [{"event_id": {"S": "evt1"}}]
|
|
357
|
-
ddb_next_token_page1 = "ddb_token_for_page2"
|
|
358
|
-
|
|
359
|
-
self.mock_dynamodb_client.execute_statement.return_value = {
|
|
360
|
-
"Items": ddb_items_page1,
|
|
361
|
-
"NextToken": ddb_next_token_page1,
|
|
362
|
-
}
|
|
363
|
-
next_page_data = {}
|
|
364
|
-
results_page1 = list(
|
|
365
|
-
self.backend.records(config1, self.mock_model, next_page_data)
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
expected_call_kwargs1 = {
|
|
369
|
-
"Statement": expected_statement,
|
|
370
|
-
"NextToken": initial_ddb_token,
|
|
371
|
-
}
|
|
372
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
373
|
-
**expected_call_kwargs1
|
|
374
|
-
)
|
|
375
|
-
self.assertEqual(len(results_page1), 1)
|
|
376
|
-
self.assertEqual(results_page1[0], {"event_id": "evt1"})
|
|
377
|
-
client_token_for_next_call = next_page_data["next_token"]
|
|
378
|
-
self.assertIsNotNone(client_token_for_next_call)
|
|
379
|
-
self.assertEqual(
|
|
380
|
-
self.backend.restore_next_token_from_config(client_token_for_next_call),
|
|
381
|
-
ddb_next_token_page1,
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
self.mock_dynamodb_client.execute_statement.reset_mock()
|
|
385
|
-
config2 = self._get_base_config(
|
|
386
|
-
table_name="events",
|
|
387
|
-
select_all=True,
|
|
388
|
-
pagination={"next_token": client_token_for_next_call},
|
|
389
|
-
)
|
|
390
|
-
ddb_items_page2 = [{"event_id": {"S": "evt2"}}]
|
|
391
|
-
self.mock_dynamodb_client.execute_statement.return_value = {
|
|
392
|
-
"Items": ddb_items_page2
|
|
393
|
-
}
|
|
394
|
-
next_page_data = {}
|
|
395
|
-
results_page2 = list(
|
|
396
|
-
self.backend.records(config2, self.mock_model, next_page_data)
|
|
397
|
-
)
|
|
398
|
-
expected_call_kwargs2 = {
|
|
399
|
-
"Statement": expected_statement,
|
|
400
|
-
"NextToken": ddb_next_token_page1,
|
|
401
|
-
}
|
|
402
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
403
|
-
**expected_call_kwargs2
|
|
404
|
-
)
|
|
405
|
-
self.assertEqual(len(results_page2), 1)
|
|
406
|
-
self.assertEqual(results_page2[0], {"event_id": "evt2"})
|
|
407
|
-
self.assertEqual(next_page_data, {})
|
|
408
|
-
|
|
409
|
-
def test_records_no_items_returned_with_next_token(self, mock_logger_arg):
|
|
410
|
-
"""Test records() when DDB returns no items but provides a NextToken."""
|
|
411
|
-
config = self._get_base_config(table_name="filtered_items", select_all=True)
|
|
412
|
-
expected_statement = 'SELECT * FROM "filtered_items"'
|
|
413
|
-
ddb_next_token = "ddb_has_more_but_current_page_empty_after_filter"
|
|
414
|
-
|
|
415
|
-
self.mock_dynamodb_client.execute_statement.return_value = {
|
|
416
|
-
"Items": [],
|
|
417
|
-
"NextToken": ddb_next_token,
|
|
418
|
-
}
|
|
419
|
-
next_page_data = {}
|
|
420
|
-
results = list(self.backend.records(config, self.mock_model, next_page_data))
|
|
421
|
-
|
|
422
|
-
expected_call_kwargs = {"Statement": expected_statement}
|
|
423
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
424
|
-
**expected_call_kwargs
|
|
425
|
-
)
|
|
426
|
-
self.assertEqual(len(results), 0)
|
|
427
|
-
expected_client_token = self.backend.serialize_next_token_for_response(
|
|
428
|
-
ddb_next_token
|
|
429
|
-
)
|
|
430
|
-
self.assertEqual(
|
|
431
|
-
next_page_data["next_token"],
|
|
432
|
-
expected_client_token,
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
def test_records_limit_cuts_off_ddb_page(self, mock_logger_arg):
|
|
436
|
-
"""Test when server-side limit means fewer items are returned than a full DDB page."""
|
|
437
|
-
config = self._get_base_config(
|
|
438
|
-
table_name="many_items", limit=1, select_all=True
|
|
439
|
-
)
|
|
440
|
-
expected_statement = 'SELECT * FROM "many_items"'
|
|
441
|
-
ddb_items_returned_by_limit = [{"id": {"S": "item1"}}]
|
|
442
|
-
ddb_next_token_after_limit = "ddb_still_has_more_after_limit"
|
|
443
|
-
|
|
444
|
-
self.mock_dynamodb_client.execute_statement.return_value = {
|
|
445
|
-
"Items": ddb_items_returned_by_limit,
|
|
446
|
-
"NextToken": ddb_next_token_after_limit,
|
|
447
|
-
}
|
|
448
|
-
next_page_data = {}
|
|
449
|
-
results = list(self.backend.records(config, self.mock_model, next_page_data))
|
|
450
|
-
|
|
451
|
-
expected_call_kwargs = {
|
|
452
|
-
"Statement": expected_statement,
|
|
453
|
-
"Limit": 1,
|
|
454
|
-
}
|
|
455
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
456
|
-
**expected_call_kwargs
|
|
457
|
-
)
|
|
458
|
-
self.assertEqual(len(results), 1)
|
|
459
|
-
self.assertEqual(results[0], {"id": "item1"})
|
|
460
|
-
expected_client_token = self.backend.serialize_next_token_for_response(
|
|
461
|
-
ddb_next_token_after_limit
|
|
462
|
-
)
|
|
463
|
-
self.assertEqual(
|
|
464
|
-
next_page_data["next_token"],
|
|
465
|
-
expected_client_token,
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
def test_create_record(self, mock_logger_arg):
|
|
469
|
-
"""Test create() inserts a record and returns the input data."""
|
|
470
|
-
data_to_create = {"id": "new_user_123", "name": "Jane Doe", "age": 28}
|
|
471
|
-
# Updated expected statement and parameters to match the new PartiQL format
|
|
472
|
-
expected_statement = (
|
|
473
|
-
"INSERT INTO \"my_test_table\" VALUE {'id': ?, 'name': ?, 'age': ?}"
|
|
474
|
-
)
|
|
475
|
-
expected_ddb_parameters = [
|
|
476
|
-
{"S": "new_user_123"},
|
|
477
|
-
{"S": "Jane Doe"},
|
|
478
|
-
{"N": "28"},
|
|
479
|
-
]
|
|
480
|
-
|
|
481
|
-
self.mock_dynamodb_client.execute_statement.return_value = {}
|
|
482
|
-
|
|
483
|
-
created_data = self.backend.create(data_to_create, self.mock_model)
|
|
484
|
-
|
|
485
|
-
self.assertEqual(created_data, data_to_create)
|
|
486
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
487
|
-
Statement=expected_statement,
|
|
488
|
-
Parameters=expected_ddb_parameters,
|
|
489
|
-
)
|
|
490
|
-
|
|
491
|
-
def test_update_record(self, mock_logger_arg):
|
|
492
|
-
"""Test update() modifies a record and returns the updated data."""
|
|
493
|
-
record_id = "user_to_update"
|
|
494
|
-
update_data = {"age": 35, "status": "active"}
|
|
495
|
-
|
|
496
|
-
expected_set_params = [{"N": "35"}, {"S": "active"}]
|
|
497
|
-
expected_id_param = {"S": "user_to_update"}
|
|
498
|
-
expected_ddb_parameters = expected_set_params + [expected_id_param]
|
|
499
|
-
|
|
500
|
-
updated_item_from_db = {
|
|
501
|
-
"id": {"S": record_id},
|
|
502
|
-
"name": {"S": "Original Name"},
|
|
503
|
-
"age": {"N": "35"},
|
|
504
|
-
"status": {"S": "active"},
|
|
505
|
-
}
|
|
506
|
-
self.mock_dynamodb_client.execute_statement.return_value = {
|
|
507
|
-
"Items": [updated_item_from_db]
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
updated_data_response = self.backend.update(
|
|
511
|
-
record_id, update_data, self.mock_model
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
expected_statement = 'UPDATE "my_test_table" SET "age" = ?, "status" = ? WHERE "id" = ? RETURNING ALL NEW *'
|
|
515
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
516
|
-
Statement=expected_statement, Parameters=expected_ddb_parameters
|
|
517
|
-
)
|
|
518
|
-
self.assertEqual(
|
|
519
|
-
updated_data_response,
|
|
520
|
-
{
|
|
521
|
-
"id": "user_to_update",
|
|
522
|
-
"name": "Original Name",
|
|
523
|
-
"age": Decimal("35"),
|
|
524
|
-
"status": "active",
|
|
525
|
-
},
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
def test_delete_record(self, mock_logger_arg):
|
|
529
|
-
"""Test delete() removes a record."""
|
|
530
|
-
record_id = "user_to_delete"
|
|
531
|
-
expected_ddb_parameters = [{"S": "user_to_delete"}]
|
|
532
|
-
self.mock_dynamodb_client.execute_statement.return_value = {}
|
|
533
|
-
|
|
534
|
-
result = self.backend.delete(record_id, self.mock_model)
|
|
535
|
-
|
|
536
|
-
self.assertTrue(result)
|
|
537
|
-
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
538
|
-
Statement='DELETE FROM "my_test_table" WHERE "id" = ?',
|
|
539
|
-
Parameters=expected_ddb_parameters,
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if __name__ == "__main__":
|
|
544
|
-
unittest.main(argv=["first-arg-is-ignored"], exit=False)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
import json
|
|
3
|
-
from unittest.mock import MagicMock
|
|
4
|
-
from collections import OrderedDict
|
|
5
|
-
from types import SimpleNamespace
|
|
6
|
-
from .sqs_backend import SqsBackend
|
|
7
|
-
import clearskies
|
|
8
|
-
from ..di import StandardDependencies
|
|
9
|
-
class User(clearskies.Model):
|
|
10
|
-
def __init__(self, sqs_backend, columns):
|
|
11
|
-
super().__init__(sqs_backend, columns)
|
|
12
|
-
|
|
13
|
-
id_column_name = 'name'
|
|
14
|
-
|
|
15
|
-
def columns_configuration(self):
|
|
16
|
-
return OrderedDict([
|
|
17
|
-
clearskies.column_types.string('name'),
|
|
18
|
-
])
|
|
19
|
-
class SqsBackendTest(unittest.TestCase):
|
|
20
|
-
def setUp(self):
|
|
21
|
-
self.di = StandardDependencies()
|
|
22
|
-
self.di.bind('environment', {'AWS_REGION': 'us-east-2'})
|
|
23
|
-
self.sqs = SimpleNamespace(send_message=MagicMock())
|
|
24
|
-
self.boto3 = SimpleNamespace(client=MagicMock(return_value=self.sqs))
|
|
25
|
-
self.di.bind('boto3', self.boto3)
|
|
26
|
-
|
|
27
|
-
def test_send(self):
|
|
28
|
-
user = self.di.build(User)
|
|
29
|
-
user.save({'name': 'sup'})
|
|
30
|
-
self.boto3.client.assert_called_with('sqs', region_name='us-east-2')
|
|
31
|
-
self.sqs.send_message.assert_called_with(QueueUrl='users', MessageBody=json.dumps({"name": "sup"}))
|
clearskies_aws/contexts/cli.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from ..di import StandardDependencies
|
|
2
|
-
from clearskies.contexts.cli import CLI, build_context
|
|
3
|
-
def cli(
|
|
4
|
-
application,
|
|
5
|
-
di_class=StandardDependencies,
|
|
6
|
-
bindings=None,
|
|
7
|
-
binding_classes=None,
|
|
8
|
-
binding_modules=None,
|
|
9
|
-
additional_configs=None,
|
|
10
|
-
):
|
|
11
|
-
return build_context(
|
|
12
|
-
CLI,
|
|
13
|
-
application,
|
|
14
|
-
di_class=di_class,
|
|
15
|
-
bindings=bindings,
|
|
16
|
-
binding_classes=binding_classes,
|
|
17
|
-
binding_modules=binding_modules,
|
|
18
|
-
additional_configs=additional_configs,
|
|
19
|
-
)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import clearskies
|
|
2
|
-
import clearskies_aws
|
|
3
|
-
from ..input_outputs import CLIWebsocketMock as CLIWebsocketMockInputOutput
|
|
4
|
-
from clearskies_aws.contexts.cli import CLI
|
|
5
|
-
class CLIWebsocketMock(CLI):
|
|
6
|
-
def __init__(self, di):
|
|
7
|
-
super().__init__(di)
|
|
8
|
-
|
|
9
|
-
def __call__(self):
|
|
10
|
-
if self.handler is None:
|
|
11
|
-
raise ValueError("Cannot execute CLIWebsocketMock context without first configuring it")
|
|
12
|
-
|
|
13
|
-
try:
|
|
14
|
-
return self.handler(self.di.build(CLIWebsocketMockInputOutput))
|
|
15
|
-
except clearskies.input_outputs.exceptions.CLINotFound:
|
|
16
|
-
print("help (aka 404 not found)!")
|
|
17
|
-
def cli_websocket_mock(
|
|
18
|
-
application,
|
|
19
|
-
di_class=clearskies_aws.di.StandardDependencies,
|
|
20
|
-
bindings=None,
|
|
21
|
-
binding_classes=None,
|
|
22
|
-
binding_modules=None,
|
|
23
|
-
additional_configs=None,
|
|
24
|
-
):
|
|
25
|
-
return clearskies.contexts.build_context(
|
|
26
|
-
CLIWebsocketMock,
|
|
27
|
-
application,
|
|
28
|
-
di_class=di_class,
|
|
29
|
-
bindings=bindings,
|
|
30
|
-
binding_classes=binding_classes,
|
|
31
|
-
binding_modules=binding_modules,
|
|
32
|
-
additional_configs=additional_configs,
|
|
33
|
-
)
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from ..input_outputs import LambdaELB as LambdaELBInputOutput
|
|
2
|
-
from ..di import StandardDependencies
|
|
3
|
-
from clearskies.contexts.build_context import build_context
|
|
4
|
-
from clearskies.contexts.context import Context
|
|
5
|
-
class LambdaELB(Context):
|
|
6
|
-
def __init__(self, di):
|
|
7
|
-
super().__init__(di)
|
|
8
|
-
|
|
9
|
-
def __call__(self, event, context):
|
|
10
|
-
if self.handler is None:
|
|
11
|
-
raise ValueError("Cannot execute LambdaELB context without first configuring it")
|
|
12
|
-
|
|
13
|
-
return self.handler(LambdaELBInputOutput(event, context))
|
|
14
|
-
def lambda_elb(
|
|
15
|
-
application,
|
|
16
|
-
di_class=StandardDependencies,
|
|
17
|
-
bindings=None,
|
|
18
|
-
binding_classes=None,
|
|
19
|
-
binding_modules=None,
|
|
20
|
-
additional_configs=None,
|
|
21
|
-
):
|
|
22
|
-
return build_context(
|
|
23
|
-
LambdaELB,
|
|
24
|
-
application,
|
|
25
|
-
di_class=di_class,
|
|
26
|
-
bindings=bindings,
|
|
27
|
-
binding_classes=binding_classes,
|
|
28
|
-
binding_modules=binding_modules,
|
|
29
|
-
additional_configs=additional_configs,
|
|
30
|
-
)
|