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.
Files changed (86) hide show
  1. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/METADATA +36 -35
  2. clear_skies_aws-2.0.2.dist-info/RECORD +63 -0
  3. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/WHEEL +1 -1
  4. clear_skies_aws-2.0.2.dist-info/licenses/LICENSE +21 -0
  5. clearskies_aws/__init__.py +15 -2
  6. clearskies_aws/actions/__init__.py +13 -106
  7. clearskies_aws/actions/action_aws.py +74 -57
  8. clearskies_aws/actions/assume_role.py +43 -30
  9. clearskies_aws/actions/ses.py +82 -73
  10. clearskies_aws/actions/sns.py +27 -30
  11. clearskies_aws/actions/sqs.py +32 -33
  12. clearskies_aws/actions/step_function.py +38 -31
  13. clearskies_aws/backends/__init__.py +11 -4
  14. clearskies_aws/backends/backend.py +106 -0
  15. clearskies_aws/backends/dynamo_db_backend.py +150 -155
  16. clearskies_aws/backends/dynamo_db_condition_parser.py +40 -80
  17. clearskies_aws/backends/dynamo_db_parti_ql_backend.py +179 -337
  18. clearskies_aws/backends/sqs_backend.py +32 -51
  19. clearskies_aws/configs/__init__.py +0 -0
  20. clearskies_aws/contexts/__init__.py +23 -10
  21. clearskies_aws/contexts/cli_web_socket_mock.py +19 -0
  22. clearskies_aws/contexts/lambda_alb.py +76 -0
  23. clearskies_aws/contexts/lambda_api_gateway.py +75 -28
  24. clearskies_aws/contexts/lambda_api_gateway_web_socket.py +56 -29
  25. clearskies_aws/contexts/lambda_invocation.py +15 -44
  26. clearskies_aws/contexts/lambda_sns.py +8 -33
  27. clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +14 -36
  28. clearskies_aws/di/__init__.py +6 -1
  29. clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
  30. clearskies_aws/di/inject/__init__.py +6 -0
  31. clearskies_aws/di/inject/boto3.py +15 -0
  32. clearskies_aws/di/inject/boto3_session.py +13 -0
  33. clearskies_aws/di/inject/parameter_store.py +15 -0
  34. clearskies_aws/{handlers → endpoints}/secrets_manager_rotation.py +76 -55
  35. clearskies_aws/endpoints/simple_body_routing.py +41 -0
  36. clearskies_aws/input_outputs/__init__.py +21 -8
  37. clearskies_aws/input_outputs/{cli_websocket_mock.py → cli_web_socket_mock.py} +9 -3
  38. clearskies_aws/input_outputs/lambda_alb.py +53 -0
  39. clearskies_aws/input_outputs/lambda_api_gateway.py +106 -88
  40. clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +69 -6
  41. clearskies_aws/input_outputs/lambda_input_output.py +87 -0
  42. clearskies_aws/input_outputs/lambda_invocation.py +77 -26
  43. clearskies_aws/input_outputs/lambda_sns.py +66 -39
  44. clearskies_aws/input_outputs/lambda_sqs_standard.py +70 -40
  45. clearskies_aws/mocks/actions/ses.py +25 -19
  46. clearskies_aws/mocks/actions/sns.py +18 -12
  47. clearskies_aws/mocks/actions/sqs.py +18 -12
  48. clearskies_aws/mocks/actions/step_function.py +19 -13
  49. clearskies_aws/models/__init__.py +0 -0
  50. clearskies_aws/models/web_socket_connection_model.py +182 -0
  51. clearskies_aws/secrets/__init__.py +13 -7
  52. clearskies_aws/secrets/additional_configs/__init__.py +10 -2
  53. clearskies_aws/secrets/additional_configs/iam_db_auth.py +26 -16
  54. clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +43 -39
  55. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +30 -31
  56. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +70 -49
  57. clearskies_aws/secrets/akeyless_with_ssm_cache.py +32 -18
  58. clearskies_aws/secrets/parameter_store.py +34 -32
  59. clearskies_aws/secrets/secrets.py +16 -0
  60. clearskies_aws/secrets/secrets_manager.py +78 -57
  61. clear_skies_aws-1.10.2.dist-info/LICENSE +0 -7
  62. clear_skies_aws-1.10.2.dist-info/RECORD +0 -71
  63. clearskies_aws/actions/assume_role_test.py +0 -72
  64. clearskies_aws/actions/ses_test.py +0 -89
  65. clearskies_aws/actions/sns_test.py +0 -77
  66. clearskies_aws/actions/sqs_test.py +0 -127
  67. clearskies_aws/actions/step_function_test.py +0 -103
  68. clearskies_aws/backends/dynamo_db_backend_test.py +0 -300
  69. clearskies_aws/backends/dynamo_db_condition_parser_test.py +0 -266
  70. clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +0 -544
  71. clearskies_aws/backends/sqs_backend_test.py +0 -31
  72. clearskies_aws/contexts/cli.py +0 -19
  73. clearskies_aws/contexts/cli_websocket_mock.py +0 -33
  74. clearskies_aws/contexts/lambda_elb.py +0 -30
  75. clearskies_aws/contexts/lambda_http_gateway.py +0 -30
  76. clearskies_aws/contexts/lambda_sqs_standard_partial_batch_test.py +0 -66
  77. clearskies_aws/contexts/wsgi.py +0 -19
  78. clearskies_aws/di/standard_dependencies.py +0 -60
  79. clearskies_aws/handlers/simple_body_routing.py +0 -39
  80. clearskies_aws/input_outputs/lambda_api_gateway_test.py +0 -87
  81. clearskies_aws/input_outputs/lambda_elb.py +0 -21
  82. clearskies_aws/input_outputs/lambda_http_gateway.py +0 -12
  83. clearskies_aws/secrets/parameter_store_test.py +0 -18
  84. clearskies_aws/secrets/secrets_manager_test.py +0 -18
  85. clearskies_aws/web_socket_connection_model.py +0 -43
  86. clearskies_aws/{handlers → endpoints}/__init__.py +1 -1
@@ -1,300 +0,0 @@
1
- import unittest
2
- from collections import OrderedDict
3
- from decimal import Decimal
4
- from types import SimpleNamespace
5
- from unittest.mock import MagicMock
6
-
7
- import clearskies
8
- from boto3.dynamodb import conditions as dynamodb_conditions
9
-
10
- from ..di import StandardDependencies
11
-
12
-
13
- class User(clearskies.Model):
14
- def __init__(self, dynamo_db_backend, columns):
15
- super().__init__(dynamo_db_backend, columns)
16
-
17
- def columns_configuration(self):
18
- return OrderedDict([
19
- clearskies.column_types.string('name'),
20
- clearskies.column_types.string('category_id'),
21
- clearskies.column_types.integer('age'),
22
- ])
23
- class Users(clearskies.Models):
24
- def __init__(self, dynamo_db_backend, columns):
25
- super().__init__(dynamo_db_backend, columns)
26
-
27
- def model_class(self):
28
- return User
29
- class DynamoDBBackendTest(unittest.TestCase):
30
- def setUp(self):
31
- self.di = StandardDependencies()
32
- self.di.bind('environment', {'AWS_REGION': 'us-east-2'})
33
- self.dynamo_db_table = SimpleNamespace(
34
- key_schema=[{
35
- 'KeyType': 'HASH',
36
- 'AttributeName': 'id'
37
- }],
38
- global_secondary_indexes=[
39
- {
40
- 'IndexName':
41
- 'category_id-name-index',
42
- 'KeySchema': [
43
- {
44
- 'KeyType': 'HASH',
45
- 'AttributeName': 'category_id'
46
- },
47
- {
48
- 'KeyType': 'RANGE',
49
- 'AttributeName': 'name'
50
- },
51
- ]
52
- },
53
- {
54
- 'IndexName':
55
- 'category_id-age-index',
56
- 'KeySchema': [
57
- {
58
- 'KeyType': 'RANGE',
59
- 'AttributeName': 'age'
60
- },
61
- {
62
- 'KeyType': 'HASH',
63
- 'AttributeName': 'category_id'
64
- },
65
- ]
66
- },
67
- ],
68
- local_secondary_indexes=[{
69
- 'IndexName':
70
- 'id-category_id-index',
71
- 'KeySchema': [
72
- {
73
- 'KeyType': 'RANGE',
74
- 'AttributeName': 'category_id'
75
- },
76
- {
77
- 'KeyType': 'HASH',
78
- 'AttributeName': 'id'
79
- },
80
- ]
81
- }],
82
- )
83
- self.dynamo_db = SimpleNamespace(Table=MagicMock(return_value=self.dynamo_db_table), )
84
- self.boto3 = SimpleNamespace(resource=MagicMock(return_value=self.dynamo_db), )
85
- self.di.bind('boto3', self.boto3)
86
-
87
- def test_create(self):
88
- self.dynamo_db_table.put_item = MagicMock()
89
- user = self.di.build(User)
90
- user.save({'name': 'sup', 'age': 5, 'category_id': '1-2-3-4'})
91
- self.boto3.resource.assert_called_with('dynamodb', region_name='us-east-2')
92
- self.dynamo_db.Table.assert_called_with('users')
93
- self.assertEqual(1, len(self.dynamo_db_table.put_item.call_args_list))
94
- call = self.dynamo_db_table.put_item.call_args_list[0]
95
- self.assertEqual((), call.args)
96
- self.assertEqual(1, len(call.kwargs))
97
- self.assertTrue('Item' in call.kwargs)
98
- saved_data = call.kwargs['Item']
99
- # we're doing this a bit weird because the UUIDs will generate random values.
100
- # I could mock it, or I could just be lazy and grab it from the data I was given.
101
- self.assertEqual(
102
- {
103
- "id": saved_data["id"],
104
- "name": "sup",
105
- "age": 5,
106
- "category_id": "1-2-3-4",
107
- },
108
- saved_data,
109
- )
110
- self.assertEqual(saved_data["id"], user.id)
111
- self.assertEqual(5, user.age)
112
- self.assertEqual("1-2-3-4", user.category_id)
113
- self.assertEqual("sup", user.name)
114
-
115
- def test_update(self):
116
- self.dynamo_db_table.update_item = MagicMock(
117
- return_value={
118
- 'Attributes': {
119
- 'id': '1-2-3-4',
120
- 'name': 'hello',
121
- 'age': Decimal('10'),
122
- 'category_id': '1-2-3-5',
123
- }
124
- }
125
- )
126
- user = self.di.build(User)
127
- user.data = {'id': '1-2-3-4', 'name': 'sup', 'age': 5, 'category_id': '1-2-3-5'}
128
- user.save({'name': 'hello', 'age': 10})
129
- self.boto3.resource.assert_called_with('dynamodb', region_name='us-east-2')
130
- self.dynamo_db.Table.assert_called_with('users')
131
- self.dynamo_db_table.update_item.assert_called_with(
132
- Key={'id': '1-2-3-4'},
133
- UpdateExpression='SET #name = :name, #age = :age',
134
- ExpressionAttributeValues={
135
- ':name': 'hello',
136
- ':age': 10
137
- },
138
- ExpressionAttributeNames={
139
- '#name': 'name',
140
- '#age': 'age'
141
- },
142
- ReturnValues="ALL_NEW",
143
- )
144
- self.assertEqual("1-2-3-4", user.id)
145
- self.assertEqual("hello", user.name)
146
- self.assertEqual(10, user.age)
147
- self.assertEqual("1-2-3-5", user.category_id)
148
-
149
- def test_delete(self):
150
- self.dynamo_db_table.delete_item = MagicMock()
151
- user = self.di.build(User)
152
- user.data = {'id': '1-2-3-4', 'name': 'sup', 'age': 5, 'category_id': '1-2-3-5'}
153
- user.delete()
154
- self.dynamo_db_table.delete_item.assert_called_with(Key={'id': '1-2-3-4'}, )
155
-
156
- def test_fetch_by_id(self):
157
- self.dynamo_db_table.query = MagicMock(
158
- return_value={'Items': [{
159
- 'id': '1-2-3-4',
160
- 'age': Decimal(10),
161
- 'category_id': '4-5-6'
162
- }]}
163
- )
164
- users = self.di.build(Users)
165
- user = users.where('id=1-2-3-4').first()
166
- self.assertTrue(user.exists)
167
- self.assertEqual("1-2-3-4", user.id)
168
- self.assertEqual(10, user.age)
169
- self.assertEqual("4-5-6", user.category_id)
170
- self.assertEqual(1, len(self.dynamo_db_table.query.call_args_list))
171
- call = self.dynamo_db_table.query.call_args_list[0]
172
- self.assertEqual(3, len(call.kwargs))
173
- self.assertEqual("ALL_ATTRIBUTES", call.kwargs["Select"])
174
- self.assertEqual(True, call.kwargs["ScanIndexForward"])
175
- key_condition = call.kwargs['KeyConditionExpression']
176
-
177
- # dynamodb does not make it easy to verify that the key expression was built properly,
178
- # and I don't think that mocking will make this any better. Hoepfully they don't
179
- # change their library often...
180
- key_column = key_condition.get_expression()['values'][0]
181
- self.assertEqual("id", key_column.name)
182
- self.assertTrue(isinstance(key_column, dynamodb_conditions.Key))
183
- self.assertEqual("=", key_condition.expression_operator)
184
- self.assertEqual("1-2-3-4", key_condition.get_expression()["values"][1])
185
-
186
- def test_fetch_by_id_with_sort(self):
187
- self.dynamo_db_table.query = MagicMock(
188
- return_value={'Items': [{
189
- 'id': '1-2-3-4',
190
- 'age': Decimal(10),
191
- 'category_id': '4-5-6'
192
- }]}
193
- )
194
- users = self.di.build(Users)
195
- users = users.where('id=1-2-3-4').sort_by('category_id', 'desc').__iter__()
196
- self.assertEqual(1, len(self.dynamo_db_table.query.call_args_list))
197
- call = self.dynamo_db_table.query.call_args_list[0]
198
- self.assertEqual(4, len(call.kwargs))
199
- self.assertEqual("ALL_ATTRIBUTES", call.kwargs["Select"])
200
- self.assertEqual("id-category_id-index", call.kwargs["IndexName"])
201
- self.assertEqual(False, call.kwargs["ScanIndexForward"])
202
- key_condition = call.kwargs['KeyConditionExpression']
203
-
204
- # dynamodb does not make it easy to verify that the key expression was built properly,
205
- # and I don't think that mocking will make this any better. Hoepfully they don't
206
- # change their library often...
207
- key_column = key_condition.get_expression()['values'][0]
208
- self.assertEqual("id", key_column.name)
209
- self.assertTrue(isinstance(key_column, dynamodb_conditions.Key))
210
- self.assertEqual("=", key_condition.expression_operator)
211
- self.assertEqual("1-2-3-4", key_condition.get_expression()["values"][1])
212
-
213
- def test_fetch_by_secondary_index_twice(self):
214
- self.dynamo_db_table.query = MagicMock(
215
- return_value={'Items': [{
216
- 'id': '1-2-3-4',
217
- 'age': Decimal(10),
218
- 'category_id': '4-5-6'
219
- }]}
220
- )
221
- users = self.di.build(Users)
222
- users = users.where('category_id=1-2-3-4').where('age>10').__iter__()
223
- self.assertEqual(1, len(self.dynamo_db_table.query.call_args_list))
224
- call = self.dynamo_db_table.query.call_args_list[0]
225
- self.assertEqual(4, len(call.kwargs))
226
- self.assertEqual("ALL_ATTRIBUTES", call.kwargs["Select"])
227
- self.assertEqual("category_id-age-index", call.kwargs["IndexName"])
228
- self.assertEqual(True, call.kwargs["ScanIndexForward"])
229
- key_condition = call.kwargs['KeyConditionExpression']
230
- self.assertTrue(isinstance(key_condition, dynamodb_conditions.And))
231
- gt_condition = key_condition.get_expression()['values'][0]
232
- equal_condition = key_condition.get_expression()['values'][1]
233
-
234
- self.assertTrue(isinstance(equal_condition, dynamodb_conditions.Equals))
235
- key_column = equal_condition.get_expression()['values'][0]
236
- self.assertEqual("category_id", key_column.name)
237
- self.assertTrue(isinstance(key_column, dynamodb_conditions.Key))
238
- self.assertEqual("=", equal_condition.expression_operator)
239
- self.assertEqual("1-2-3-4", equal_condition.get_expression()["values"][1])
240
-
241
- self.assertTrue(isinstance(gt_condition, dynamodb_conditions.GreaterThan))
242
- key_column = gt_condition.get_expression()['values'][0]
243
- self.assertEqual("age", key_column.name)
244
- self.assertTrue(isinstance(key_column, dynamodb_conditions.Key))
245
- self.assertEqual(">", gt_condition.expression_operator)
246
- self.assertEqual(Decimal("10"), gt_condition.get_expression()["values"][1])
247
-
248
- def test_index_and_scan(self):
249
- self.dynamo_db_table.query = MagicMock(
250
- return_value={'Items': [{
251
- 'id': '1-2-3-4',
252
- 'age': Decimal(10),
253
- 'category_id': '4-5-6'
254
- }]}
255
- )
256
- users = self.di.build(Users)
257
- users = users.where('category_id=1-2-3-4').where('age is not null').__iter__()
258
- self.assertEqual(1, len(self.dynamo_db_table.query.call_args_list))
259
- call = self.dynamo_db_table.query.call_args_list[0]
260
- self.assertEqual(5, len(call.kwargs))
261
- self.assertEqual("ALL_ATTRIBUTES", call.kwargs["Select"])
262
- self.assertEqual("category_id-name-index", call.kwargs["IndexName"])
263
- self.assertEqual(True, call.kwargs["ScanIndexForward"])
264
-
265
- # our key condition should be an equal search on category_id
266
- key_condition = call.kwargs['KeyConditionExpression']
267
- key_column = key_condition.get_expression()['values'][0]
268
- self.assertEqual("category_id", key_column.name)
269
- self.assertTrue(isinstance(key_column, dynamodb_conditions.Key))
270
- self.assertEqual("=", key_condition.expression_operator)
271
- self.assertEqual("1-2-3-4", key_condition.get_expression()["values"][1])
272
-
273
- # and we should have a FilterExpression which is an 'is not null' condition
274
- filter_condition = call.kwargs['FilterExpression']
275
- key_column = filter_condition.get_expression()['values'][0]
276
- self.assertEqual("age", key_column.name)
277
- self.assertTrue(isinstance(key_column, dynamodb_conditions.Attr))
278
- self.assertEqual("attribute_not_exists", filter_condition.expression_operator)
279
-
280
- def test_scan_only(self):
281
- self.dynamo_db_table.scan = MagicMock(
282
- return_value={'Items': [{
283
- 'id': '1-2-3-4',
284
- 'age': Decimal(10),
285
- 'category_id': '4-5-6'
286
- }]}
287
- )
288
- users = self.di.build(Users)
289
- users = users.where('category_id>1-2-3-4').__iter__()
290
- self.assertEqual(1, len(self.dynamo_db_table.scan.call_args_list))
291
- call = self.dynamo_db_table.scan.call_args_list[0]
292
- self.assertEqual(2, len(call.kwargs))
293
- self.assertEqual("ALL_ATTRIBUTES", call.kwargs["Select"])
294
-
295
- # and we should have a FilterExpression which is an 'is not null' condition
296
- filter_condition = call.kwargs['FilterExpression']
297
- key_column = filter_condition.get_expression()['values'][0]
298
- self.assertEqual("category_id", key_column.name)
299
- self.assertTrue(isinstance(key_column, dynamodb_conditions.Attr))
300
- self.assertEqual(">", filter_condition.expression_operator)
@@ -1,266 +0,0 @@
1
- import unittest
2
- from decimal import Decimal
3
-
4
- from clearskies_aws.backends.dynamo_db_condition_parser import DynamoDBConditionParser
5
-
6
-
7
- class TestDynamoDBConditionParser(unittest.TestCase):
8
- def setUp(self):
9
- """Set up the parser for each test."""
10
- self.parser = DynamoDBConditionParser()
11
-
12
- def test_to_dynamodb_attribute_value_string(self):
13
- """Test conversion of a simple string."""
14
- self.assertEqual(
15
- self.parser.to_dynamodb_attribute_value("hello"), {"S": "hello"}
16
- )
17
-
18
- def test_to_dynamodb_attribute_value_int(self):
19
- """Test conversion of an integer."""
20
- self.assertEqual(self.parser.to_dynamodb_attribute_value(123), {"N": "123"})
21
-
22
- def test_to_dynamodb_attribute_value_float(self):
23
- """Test conversion of a float."""
24
- self.assertEqual(
25
- self.parser.to_dynamodb_attribute_value(123.45), {"N": "123.45"}
26
- )
27
-
28
- def test_to_dynamodb_attribute_value_decimal(self):
29
- """Test conversion of a Decimal object."""
30
- self.assertEqual(
31
- self.parser.to_dynamodb_attribute_value(Decimal("99.01")), {"N": "99.01"}
32
- )
33
-
34
- def test_to_dynamodb_attribute_value_bool_true(self):
35
- """Test conversion of a boolean True."""
36
- self.assertEqual(self.parser.to_dynamodb_attribute_value(True), {"BOOL": True})
37
-
38
- def test_to_dynamodb_attribute_value_bool_false(self):
39
- """Test conversion of a boolean False."""
40
- self.assertEqual(
41
- self.parser.to_dynamodb_attribute_value(False), {"BOOL": False}
42
- )
43
-
44
- def test_to_dynamodb_attribute_value_none(self):
45
- """Test conversion of None."""
46
- self.assertEqual(self.parser.to_dynamodb_attribute_value(None), {"NULL": True})
47
-
48
- def test_to_dynamodb_attribute_value_string_true_false_null(self):
49
- """Test conversion of string representations of boolean, null, and numbers."""
50
- self.assertEqual(
51
- self.parser.to_dynamodb_attribute_value("true"), {"BOOL": True}
52
- )
53
- self.assertEqual(
54
- self.parser.to_dynamodb_attribute_value("FALSE"), {"BOOL": False}
55
- )
56
- self.assertEqual(
57
- self.parser.to_dynamodb_attribute_value("NuLl"), {"NULL": True}
58
- )
59
- self.assertEqual(self.parser.to_dynamodb_attribute_value("123"), {"N": "123"})
60
- self.assertEqual(self.parser.to_dynamodb_attribute_value("-45"), {"N": "-45"})
61
- self.assertEqual(
62
- self.parser.to_dynamodb_attribute_value("123.45"), {"N": "123.45"}
63
- )
64
- self.assertEqual(self.parser.to_dynamodb_attribute_value("text"), {"S": "text"})
65
-
66
- def test_to_dynamodb_attribute_value_list(self):
67
- """Test conversion of a list with mixed data types."""
68
- val = ["a", 1, True, None, Decimal("2.3")]
69
- expected = {
70
- "L": [
71
- {"S": "a"},
72
- {"N": "1"},
73
- {"BOOL": True},
74
- {"NULL": True},
75
- {"N": "2.3"},
76
- ]
77
- }
78
- self.assertEqual(self.parser.to_dynamodb_attribute_value(val), expected)
79
-
80
- def test_to_dynamodb_attribute_value_map(self):
81
- """Test conversion of a dictionary (map)."""
82
- val = {"key_s": "val", "key_n": 100}
83
- expected = {"M": {"key_s": {"S": "val"}, "key_n": {"N": "100"}}}
84
- self.assertEqual(self.parser.to_dynamodb_attribute_value(val), expected)
85
-
86
- def test_to_dynamodb_attribute_value_set_string(self):
87
- """Test conversion of a set of strings."""
88
- val = {"a", "b", "a"}
89
- expected = {"SS": sorted(["a", "b"])}
90
- self.assertEqual(self.parser.to_dynamodb_attribute_value(val), expected)
91
-
92
- def test_to_dynamodb_attribute_value_set_number(self):
93
- """Test conversion of a set of numbers (int and Decimal)."""
94
- val = {1, 2, Decimal("3.0")}
95
- expected = {"NS": sorted(["1", "2", "3.0"])}
96
- self.assertEqual(self.parser.to_dynamodb_attribute_value(val), expected)
97
-
98
- def test_to_dynamodb_attribute_value_unsupported(self):
99
- """Test conversion of an unsupported data type raises TypeError."""
100
-
101
- class MyObject:
102
- pass
103
-
104
- with self.assertRaises(TypeError):
105
- self.parser.to_dynamodb_attribute_value(MyObject())
106
-
107
- def test_parse_condition_list_simple(self):
108
- """Test the internal _parse_condition_list helper method."""
109
- self.assertEqual(
110
- self.parser._parse_condition_list("'a', 'b', 'c'"), ["a", "b", "c"]
111
- )
112
- self.assertEqual(self.parser._parse_condition_list("1, 2, 3"), ["1", "2", "3"])
113
- self.assertEqual(self.parser._parse_condition_list("('a', \"b\")"), ["a", "b"])
114
- self.assertEqual(self.parser._parse_condition_list(""), [])
115
- self.assertEqual(self.parser._parse_condition_list(" "), [])
116
- self.assertEqual(self.parser._parse_condition_list(" 'item1' "), ["item1"])
117
-
118
- def test_parse_condition_equals_string(self):
119
- """Test parsing an equality condition with a string value."""
120
- result = self.parser.parse_condition("name = 'John Doe'")
121
- self.assertEqual(result["column"], "name")
122
- self.assertEqual(result["operator"], "=")
123
- self.assertEqual(result["values"], [{"S": "John Doe"}])
124
- self.assertEqual(result["parsed"], '"name" = ?')
125
- self.assertEqual(result["table"], "")
126
-
127
- def test_parse_condition_equals_number(self):
128
- """Test parsing an equality condition with a numeric value."""
129
- result = self.parser.parse_condition("age = 30")
130
- self.assertEqual(result["column"], "age")
131
- self.assertEqual(result["operator"], "=")
132
- self.assertEqual(result["values"], [{"N": "30"}])
133
- self.assertEqual(result["parsed"], '"age" = ?')
134
-
135
- def test_parse_condition_greater_than(self):
136
- """Test parsing a greater than condition."""
137
- result = self.parser.parse_condition("price > 10.5")
138
- self.assertEqual(result["column"], "price")
139
- self.assertEqual(result["operator"], ">")
140
- self.assertEqual(result["values"], [{"N": "10.5"}])
141
- self.assertEqual(result["parsed"], '"price" > ?')
142
-
143
- def test_parse_condition_table_column(self):
144
- """Test parsing a condition with a table-prefixed column."""
145
- result = self.parser.parse_condition("user.id = 'user123'")
146
- self.assertEqual(result["table"], "user")
147
- self.assertEqual(result["column"], "id")
148
- self.assertEqual(result["operator"], "=")
149
- self.assertEqual(result["values"], [{"S": "user123"}])
150
- self.assertEqual(result["parsed"], '"user"."id" = ?')
151
-
152
- def test_parse_condition_is_null(self):
153
- """Test parsing an 'IS NULL' condition (remapped to IS MISSING)."""
154
- result = self.parser.parse_condition("email is null")
155
- self.assertEqual(result["column"], "email")
156
- self.assertEqual(result["operator"], "IS MISSING")
157
- self.assertEqual(result["values"], [])
158
- self.assertEqual(result["parsed"], '"email" IS MISSING')
159
-
160
- def test_parse_condition_is_not_null(self):
161
- """Test parsing an 'IS NOT NULL' condition (remapped to IS NOT MISSING)."""
162
- result = self.parser.parse_condition("address is not null")
163
- self.assertEqual(result["column"], "address")
164
- self.assertEqual(result["operator"], "IS NOT MISSING")
165
- self.assertEqual(result["values"], [])
166
- self.assertEqual(result["parsed"], '"address" IS NOT MISSING')
167
-
168
- def test_parse_condition_like_begins_with(self):
169
- """Test parsing a 'LIKE value%' condition (becomes BEGINS_WITH)."""
170
- result = self.parser.parse_condition("name LIKE 'Jo%'")
171
- self.assertEqual(result["column"], "name")
172
- self.assertEqual(result["operator"], "BEGINS_WITH")
173
- self.assertEqual(result["values"], [{"S": "Jo"}])
174
- self.assertEqual(result["parsed"], 'begins_with("name", ?)')
175
-
176
- def test_parse_condition_like_contains(self):
177
- """Test parsing a 'LIKE %value%' condition (becomes CONTAINS)."""
178
- result = self.parser.parse_condition("description LIKE '%word%'")
179
- self.assertEqual(result["column"], "description")
180
- self.assertEqual(result["operator"], "CONTAINS")
181
- self.assertEqual(result["values"], [{"S": "word"}])
182
- self.assertEqual(result["parsed"], 'contains("description", ?)')
183
-
184
- def test_parse_condition_like_exact(self):
185
- """Test parsing a 'LIKE value' condition (no wildcards, becomes =)."""
186
- result = self.parser.parse_condition("tag LIKE 'exactmatch'")
187
- self.assertEqual(result["column"], "tag")
188
- self.assertEqual(result["operator"], "=")
189
- self.assertEqual(result["values"], [{"S": "exactmatch"}])
190
- self.assertEqual(result["parsed"], '"tag" = ?')
191
-
192
- def test_parse_condition_like_ends_with_error(self):
193
- """Test that 'LIKE %value' (ends with) raises an error."""
194
- with self.assertRaisesRegex(
195
- ValueError, "DynamoDB PartiQL does not directly support 'ends_with'"
196
- ):
197
- self.parser.parse_condition("filename LIKE '%doc'")
198
-
199
- def test_parse_condition_in_list_strings(self):
200
- """Test parsing an 'IN' condition with a list of strings."""
201
- result = self.parser.parse_condition("status IN ('active', 'pending')")
202
- self.assertEqual(result["column"], "status")
203
- self.assertEqual(result["operator"], "IN")
204
- self.assertEqual(result["values"], [{"S": "active"}, {"S": "pending"}])
205
- self.assertEqual(result["parsed"], '"status" IN (?, ?)')
206
-
207
- def test_parse_condition_in_list_numbers(self):
208
- """Test parsing an 'IN' condition with a list of numbers."""
209
- result = self.parser.parse_condition("id IN (1, 2, 3)")
210
- self.assertEqual(result["column"], "id")
211
- self.assertEqual(result["operator"], "IN")
212
- self.assertEqual(result["values"], [{"N": "1"}, {"N": "2"}, {"N": "3"}])
213
- self.assertEqual(result["parsed"], '"id" IN (?, ?, ?)')
214
-
215
- def test_parse_condition_in_list_single_value(self):
216
- """Test parsing an 'IN' condition with a single value in the list."""
217
- result = self.parser.parse_condition("id IN (1)")
218
- self.assertEqual(result["column"], "id")
219
- self.assertEqual(result["operator"], "IN")
220
- self.assertEqual(result["values"], [{"N": "1"}])
221
- self.assertEqual(result["parsed"], '"id" IN (?)')
222
-
223
- def test_parse_condition_contains_function(self):
224
- """Test parsing a 'CONTAINS' function call."""
225
- result = self.parser.parse_condition("tags CONTAINS 'important'")
226
- self.assertEqual(result["column"], "tags")
227
- self.assertEqual(result["operator"], "CONTAINS")
228
- self.assertEqual(result["values"], [{"S": "important"}])
229
- self.assertEqual(result["parsed"], 'contains("tags", ?)')
230
-
231
- def test_parse_condition_begins_with_function(self):
232
- """Test parsing a 'BEGINS_WITH' function call."""
233
- result = self.parser.parse_condition("sku BEGINS_WITH 'ABC-'")
234
- self.assertEqual(result["column"], "sku")
235
- self.assertEqual(result["operator"], "BEGINS_WITH")
236
- self.assertEqual(result["values"], [{"S": "ABC-"}])
237
- self.assertEqual(result["parsed"], 'begins_with("sku", ?)')
238
-
239
- def test_parse_condition_quoted_column(self):
240
- """Test parsing a condition with a double-quoted column name."""
241
- result = self.parser.parse_condition('"my-column" = "test value"')
242
- self.assertEqual(result["column"], "my-column")
243
- self.assertEqual(result["operator"], "=")
244
- self.assertEqual(result["values"], [{"S": "test value"}])
245
- self.assertEqual(result["parsed"], '"my-column" = ?')
246
-
247
- def test_parse_condition_no_operator(self):
248
- """Test that parsing a condition without a valid operator raises an error."""
249
- with self.assertRaisesRegex(ValueError, "No supported operators found"):
250
- self.parser.parse_condition("column value")
251
-
252
- def test_parse_condition_is_operator(self):
253
- """Test parsing an 'IS' condition."""
254
- result = self.parser.parse_condition("status IS 'active'")
255
- self.assertEqual(result["column"], "status")
256
- self.assertEqual(result["operator"], "IS")
257
- self.assertEqual(result["values"], [{"S": "active"}])
258
- self.assertEqual(result["parsed"], '"status" IS ?')
259
-
260
- def test_parse_condition_is_not_operator(self):
261
- """Test parsing an 'IS NOT' condition."""
262
- result = self.parser.parse_condition("type IS NOT 'internal'")
263
- self.assertEqual(result["column"], "type")
264
- self.assertEqual(result["operator"], "IS NOT")
265
- self.assertEqual(result["values"], [{"S": "internal"}])
266
- self.assertEqual(result["parsed"], '"type" IS NOT ?')