clear-skies-aws 1.10.2__py3-none-any.whl → 2.0.1__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 (73) hide show
  1. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.1.dist-info}/METADATA +36 -35
  2. clear_skies_aws-2.0.1.dist-info/RECORD +4 -0
  3. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.1.dist-info}/WHEEL +1 -1
  4. clear_skies_aws-2.0.1.dist-info/licenses/LICENSE +21 -0
  5. clear_skies_aws-1.10.2.dist-info/LICENSE +0 -7
  6. clear_skies_aws-1.10.2.dist-info/RECORD +0 -71
  7. clearskies_aws/__init__.py +0 -2
  8. clearskies_aws/actions/__init__.py +0 -108
  9. clearskies_aws/actions/action_aws.py +0 -118
  10. clearskies_aws/actions/assume_role.py +0 -102
  11. clearskies_aws/actions/assume_role_test.py +0 -72
  12. clearskies_aws/actions/ses.py +0 -194
  13. clearskies_aws/actions/ses_test.py +0 -89
  14. clearskies_aws/actions/sns.py +0 -64
  15. clearskies_aws/actions/sns_test.py +0 -77
  16. clearskies_aws/actions/sqs.py +0 -82
  17. clearskies_aws/actions/sqs_test.py +0 -127
  18. clearskies_aws/actions/step_function.py +0 -66
  19. clearskies_aws/actions/step_function_test.py +0 -103
  20. clearskies_aws/backends/__init__.py +0 -12
  21. clearskies_aws/backends/dynamo_db_backend.py +0 -614
  22. clearskies_aws/backends/dynamo_db_backend_test.py +0 -300
  23. clearskies_aws/backends/dynamo_db_condition_parser.py +0 -365
  24. clearskies_aws/backends/dynamo_db_condition_parser_test.py +0 -266
  25. clearskies_aws/backends/dynamo_db_parti_ql_backend.py +0 -1123
  26. clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +0 -544
  27. clearskies_aws/backends/sqs_backend.py +0 -80
  28. clearskies_aws/backends/sqs_backend_test.py +0 -31
  29. clearskies_aws/contexts/__init__.py +0 -10
  30. clearskies_aws/contexts/cli.py +0 -19
  31. clearskies_aws/contexts/cli_websocket_mock.py +0 -33
  32. clearskies_aws/contexts/lambda_api_gateway.py +0 -30
  33. clearskies_aws/contexts/lambda_api_gateway_web_socket.py +0 -30
  34. clearskies_aws/contexts/lambda_elb.py +0 -30
  35. clearskies_aws/contexts/lambda_http_gateway.py +0 -30
  36. clearskies_aws/contexts/lambda_invocation.py +0 -48
  37. clearskies_aws/contexts/lambda_sns.py +0 -43
  38. clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +0 -51
  39. clearskies_aws/contexts/lambda_sqs_standard_partial_batch_test.py +0 -66
  40. clearskies_aws/contexts/wsgi.py +0 -19
  41. clearskies_aws/di/__init__.py +0 -1
  42. clearskies_aws/di/standard_dependencies.py +0 -60
  43. clearskies_aws/handlers/__init__.py +0 -2
  44. clearskies_aws/handlers/secrets_manager_rotation.py +0 -174
  45. clearskies_aws/handlers/simple_body_routing.py +0 -39
  46. clearskies_aws/input_outputs/__init__.py +0 -8
  47. clearskies_aws/input_outputs/cli_websocket_mock.py +0 -12
  48. clearskies_aws/input_outputs/lambda_api_gateway.py +0 -105
  49. clearskies_aws/input_outputs/lambda_api_gateway_test.py +0 -87
  50. clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +0 -8
  51. clearskies_aws/input_outputs/lambda_elb.py +0 -21
  52. clearskies_aws/input_outputs/lambda_http_gateway.py +0 -12
  53. clearskies_aws/input_outputs/lambda_invocation.py +0 -34
  54. clearskies_aws/input_outputs/lambda_sns.py +0 -52
  55. clearskies_aws/input_outputs/lambda_sqs_standard.py +0 -54
  56. clearskies_aws/mocks/__init__.py +0 -1
  57. clearskies_aws/mocks/actions/__init__.py +0 -6
  58. clearskies_aws/mocks/actions/ses.py +0 -28
  59. clearskies_aws/mocks/actions/sns.py +0 -23
  60. clearskies_aws/mocks/actions/sqs.py +0 -23
  61. clearskies_aws/mocks/actions/step_function.py +0 -26
  62. clearskies_aws/secrets/__init__.py +0 -7
  63. clearskies_aws/secrets/additional_configs/__init__.py +0 -54
  64. clearskies_aws/secrets/additional_configs/iam_db_auth.py +0 -29
  65. clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +0 -92
  66. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +0 -81
  67. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +0 -141
  68. clearskies_aws/secrets/akeyless_with_ssm_cache.py +0 -46
  69. clearskies_aws/secrets/parameter_store.py +0 -50
  70. clearskies_aws/secrets/parameter_store_test.py +0 -18
  71. clearskies_aws/secrets/secrets_manager.py +0 -75
  72. clearskies_aws/secrets/secrets_manager_test.py +0 -18
  73. clearskies_aws/web_socket_connection_model.py +0 -43
@@ -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,365 +0,0 @@
1
- import base64
2
- import json
3
- import logging
4
- from decimal import Decimal, DecimalException
5
- from typing import Any, Dict, List, Tuple
6
-
7
- from clearskies import ConditionParser
8
-
9
- # Ensure AttributeValueTypeDef is imported from the correct boto3 types package
10
- # This is crucial for the "ideal fix".
11
- from types_boto3_dynamodb.type_defs import AttributeValueTypeDef
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- class DynamoDBConditionParser(ConditionParser):
17
- """
18
- Parses string conditions into a structured format suitable for DynamoDB PartiQL queries.
19
-
20
- This class handles various SQL-like operators and translates them into
21
- PartiQL compatible expressions and parameters. It also includes a utility
22
- to convert Python values into the DynamoDB AttributeValue format.
23
- """
24
-
25
- operator_lengths: Dict[str, int] = {
26
- "<>": 2,
27
- "<=": 2,
28
- ">=": 2,
29
- "!=": 2,
30
- ">": 1,
31
- "<": 1,
32
- "=": 1,
33
- "in": 4,
34
- "is not null": 12,
35
- "is null": 8,
36
- "is not": 8,
37
- "is": 4,
38
- "like": 6,
39
- "is not missing": 15,
40
- "is missing": 11,
41
- "contains": 9,
42
- "begins_with": 12,
43
- }
44
- operators: List[str] = [
45
- # Longer operators first to help with matching
46
- "is not null",
47
- "is not missing",
48
- "is null",
49
- "is missing",
50
- "begins_with",
51
- "contains",
52
- "<>",
53
- "!=",
54
- "<=",
55
- ">=",
56
- "is not",
57
- "is",
58
- "like",
59
- ">",
60
- "<",
61
- "=",
62
- "in",
63
- ]
64
- operators_for_matching: Dict[str, str] = {
65
- "like": " like ",
66
- "in": " in ",
67
- "is not missing": " is not missing",
68
- "is missing": " is missing",
69
- "is not null": " is not null",
70
- "is null": " is null",
71
- "is": " is ",
72
- "is not": " is not ",
73
- "begins_with": " begins_with",
74
- "contains": " contains",
75
- }
76
- operators_with_simple_placeholders: Dict[str, bool] = {
77
- "<>": True,
78
- "<=": True,
79
- ">=": True,
80
- "!=": True,
81
- "=": True,
82
- "<": True,
83
- ">": True,
84
- "is": True,
85
- "is not": True,
86
- }
87
- operators_without_placeholders: set[str] = {
88
- "is not missing",
89
- "is missing",
90
- }
91
- operator_needs_remap: Dict[str, str] = {
92
- "is not null": "is not missing",
93
- "is null": "is missing",
94
- }
95
- operators_with_special_placeholders: set[str] = {"begins_with", "contains"}
96
-
97
- def parse_condition(self, condition: str) -> Dict[str, Any]:
98
- """
99
- Parses a string condition into a structured dictionary.
100
-
101
- The "values" key in the returned dictionary will contain List[AttributeValueTypeDef].
102
-
103
- Args:
104
- condition: The condition string to parse.
105
-
106
- Returns:
107
- A dictionary with keys: "table", "column", "operator", "values" (DynamoDB formatted),
108
- and "parsed" (the SQL fragment).
109
- """
110
- lowercase_condition: str = condition.lower()
111
- matching_operator: str = ""
112
- matching_index: int = -1
113
- current_best_match_len: int = 0
114
-
115
- for operator in self.operators:
116
- try:
117
- operator_for_match: str = self.operators_for_matching.get(
118
- operator, operator
119
- )
120
- index: int = lowercase_condition.index(operator_for_match)
121
-
122
- if matching_index == -1 or index < matching_index:
123
- matching_index = index
124
- matching_operator = operator
125
- current_best_match_len = len(operator_for_match)
126
- elif index == matching_index:
127
- if len(operator_for_match) > current_best_match_len:
128
- matching_operator = operator
129
- current_best_match_len = len(operator_for_match)
130
- except ValueError:
131
- continue
132
-
133
- if not matching_operator:
134
- raise ValueError(f"No supported operators found in condition {condition}")
135
-
136
- column: str = condition[:matching_index].strip()
137
- value: str = condition[
138
- matching_index + self.operator_lengths[matching_operator] :
139
- ].strip()
140
-
141
- if len(value) >= 2:
142
- first_char = value[0]
143
- last_char = value[-1]
144
- if (first_char == "'" and last_char == "'") or (
145
- first_char == '"' and last_char == '"'
146
- ):
147
- value = value[1:-1]
148
-
149
- raw_values: List[str] = []
150
-
151
- if matching_operator == "in":
152
- raw_values = self._parse_condition_list(value) if value else []
153
- elif matching_operator not in self.operators_without_placeholders and not (
154
- matching_operator in self.operator_needs_remap
155
- and self.operator_needs_remap[matching_operator]
156
- in self.operators_without_placeholders
157
- ):
158
- raw_values = [value]
159
-
160
- if matching_operator.lower() == "like":
161
- if value.startswith("%") and value.endswith("%") and len(value) > 1:
162
- matching_operator = "contains"
163
- raw_values = [value[1:-1]]
164
- elif value.endswith("%") and not value.startswith("%"):
165
- matching_operator = "begins_with"
166
- raw_values = [value[:-1]]
167
- elif value.startswith("%") and not value.endswith("%"):
168
- raise ValueError(
169
- "DynamoDB PartiQL does not directly support 'ends_with'"
170
- )
171
- else:
172
- matching_operator = "="
173
- raw_values = [value]
174
-
175
- matching_operator = self.operator_needs_remap.get(
176
- matching_operator.lower(), matching_operator
177
- )
178
-
179
- table_name: str = ""
180
- final_column_name: str = column
181
- if "." in column:
182
- table_prefix, column_name_part = column.split(".", 1)
183
- table_name = table_prefix.strip().replace('"', "").replace("`", "")
184
- final_column_name = (
185
- column_name_part.strip().replace('"', "").replace("`", "")
186
- )
187
- else:
188
- final_column_name = column.replace('"', "").replace("`", "")
189
-
190
- # This list will now correctly be List[AttributeValueTypeDef]
191
- parameters: List[AttributeValueTypeDef] = []
192
- if matching_operator.lower() not in self.operators_without_placeholders:
193
- for val_item in raw_values:
194
- parameters.append(self.to_dynamodb_attribute_value(val_item))
195
-
196
- column_for_parsed: str = (
197
- f"{table_name}.{final_column_name}" if table_name else final_column_name
198
- )
199
-
200
- return {
201
- "table": table_name,
202
- "column": final_column_name,
203
- "operator": matching_operator.upper(),
204
- "values": parameters, # This is now correctly typed for MyPy
205
- "parsed": self._with_placeholders(
206
- column_for_parsed,
207
- matching_operator,
208
- parameters,
209
- ),
210
- }
211
-
212
- def _with_placeholders(
213
- self,
214
- column: str,
215
- operator: str,
216
- values: List[
217
- AttributeValueTypeDef
218
- ], # Parameter 'values' is List[AttributeValueTypeDef]
219
- escape: bool = True,
220
- escape_character: str = '"',
221
- ) -> str:
222
- """
223
- Formats a SQL fragment with placeholders for a given column, operator, and parameters.
224
- """
225
- quoted_column = column
226
- if escape:
227
- parts: List[str] = column.split(".", 1)
228
- cleaned_parts: List[str] = [part.strip('"`') for part in parts]
229
- if len(cleaned_parts) == 2:
230
- quoted_column = (
231
- f"{escape_character}{cleaned_parts[0]}{escape_character}"
232
- "."
233
- f"{escape_character}{cleaned_parts[1]}{escape_character}"
234
- )
235
- else:
236
- quoted_column = (
237
- f"{escape_character}{cleaned_parts[0]}{escape_character}"
238
- )
239
-
240
- upper_case_operator: str = operator.upper()
241
- lower_case_operator: str = operator.lower()
242
-
243
- if lower_case_operator in self.operators_with_simple_placeholders:
244
- return f"{quoted_column} {upper_case_operator} ?"
245
- if lower_case_operator in self.operators_without_placeholders:
246
- return f"{quoted_column} {upper_case_operator}"
247
- if lower_case_operator in self.operators_with_special_placeholders:
248
- return f"{lower_case_operator}({quoted_column}, ?)"
249
-
250
- if lower_case_operator == "in":
251
- placeholders_str: str = ", ".join(["?" for _ in values])
252
- return f"{quoted_column} IN ({placeholders_str})"
253
-
254
- raise ValueError(f"Unsupported operator for placeholder generation: {operator}")
255
-
256
- def to_dynamodb_attribute_value(
257
- self, value: Any
258
- ) -> AttributeValueTypeDef: # Return type changed
259
- """
260
- Converts a Python variable into a DynamoDB-formatted attribute value dictionary.
261
- """
262
- if isinstance(value, str):
263
- if value.lower() == "true":
264
- return {"BOOL": True}
265
- if value.lower() == "false":
266
- return {"BOOL": False}
267
- if value.lower() == "null":
268
- return {"NULL": True}
269
- try:
270
- if value.isdigit() or (value.startswith("-") and value[1:].isdigit()):
271
- return {"N": str(int(value))}
272
- return {"N": str(Decimal(value))}
273
- except (ValueError, TypeError, json.JSONDecodeError, DecimalException):
274
- return {"S": value}
275
- elif isinstance(value, bool):
276
- return {"BOOL": value}
277
- elif isinstance(value, (int, float, Decimal)):
278
- return {"N": str(value)}
279
- elif value is None:
280
- return {"NULL": True}
281
- elif isinstance(value, bytes):
282
- return {"B": base64.b64encode(value).decode("utf-8")}
283
- elif isinstance(value, list):
284
- # Each item will be AttributeValueTypeDef, so the list is List[AttributeValueTypeDef]
285
- return {"L": [self.to_dynamodb_attribute_value(item) for item in value]}
286
- elif isinstance(value, dict):
287
- # Each value in the map will be AttributeValueTypeDef
288
- return {
289
- "M": {
290
- str(k): self.to_dynamodb_attribute_value(v)
291
- for k, v in value.items()
292
- }
293
- }
294
- elif isinstance(value, set):
295
- if not value:
296
- raise ValueError(
297
- "Cannot determine DynamoDB Set type from an empty Python set."
298
- )
299
- if all(isinstance(item, str) for item in value):
300
- return {"SS": sorted(list(value))}
301
- elif all(isinstance(item, (int, float, Decimal)) for item in value):
302
- return {"NS": sorted([str(item) for item in value])}
303
- elif all(isinstance(item, bytes) for item in value):
304
- return {
305
- "BS": sorted(
306
- [base64.b64encode(item).decode("utf-8") for item in value]
307
- )
308
- }
309
- raise ValueError(
310
- "Set contains mixed types or unsupported types for DynamoDB Sets."
311
- )
312
- else:
313
- raise TypeError(
314
- f"Unsupported Python type for DynamoDB conversion: {type(value)}"
315
- )
316
-
317
- def _parse_condition_list(self, list_string: str) -> List[str]:
318
- """
319
- Parses a string representation of a list into a list of strings.
320
- """
321
- if not list_string.strip():
322
- return []
323
-
324
- if list_string.startswith("(") and list_string.endswith(")"):
325
- list_string = list_string[1:-1]
326
- if not list_string.strip():
327
- return []
328
-
329
- items: List[str] = []
330
- current_item: str = ""
331
- in_quotes: bool = False
332
- quote_char: str = ""
333
- for char in list_string:
334
- if char in ("'", '"'):
335
- if in_quotes and char == quote_char:
336
- in_quotes = False
337
- elif not in_quotes:
338
- in_quotes = True
339
- quote_char = char
340
- else:
341
- current_item += char
342
- elif char == "," and not in_quotes:
343
- stripped_item = current_item.strip()
344
- if stripped_item:
345
- items.append(stripped_item)
346
- current_item = ""
347
- else:
348
- current_item += char
349
-
350
- stripped_current_item = current_item.strip()
351
- if stripped_current_item:
352
- items.append(stripped_current_item)
353
-
354
- final_items = []
355
- for item in items:
356
- processed_item = item
357
- if len(processed_item) >= 2:
358
- if processed_item.startswith("'") and processed_item.endswith("'"):
359
- processed_item = processed_item[1:-1]
360
- elif processed_item.startswith('"') and processed_item.endswith('"'):
361
- processed_item = processed_item[1:-1]
362
-
363
- if processed_item:
364
- final_items.append(processed_item)
365
- return final_items