clear-skies-aws 1.10.0__py3-none-any.whl → 1.10.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.0.dist-info → clear_skies_aws-1.10.2.dist-info}/METADATA +1 -1
- {clear_skies_aws-1.10.0.dist-info → clear_skies_aws-1.10.2.dist-info}/RECORD +7 -7
- clearskies_aws/backends/dynamo_db_parti_ql_backend.py +233 -110
- clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +13 -9
- clearskies_aws/secrets/akeyless_with_ssm_cache.py +15 -7
- {clear_skies_aws-1.10.0.dist-info → clear_skies_aws-1.10.2.dist-info}/LICENSE +0 -0
- {clear_skies_aws-1.10.0.dist-info → clear_skies_aws-1.10.2.dist-info}/WHEEL +0 -0
|
@@ -16,8 +16,8 @@ clearskies_aws/backends/dynamo_db_backend.py,sha256=i-Na3McQDbyCYmQewBPKgbWEd3A3
|
|
|
16
16
|
clearskies_aws/backends/dynamo_db_backend_test.py,sha256=wtymsOEVzO9y7_Whu9cCRyioRTmSimbLmRQSt3J4cik,13106
|
|
17
17
|
clearskies_aws/backends/dynamo_db_condition_parser.py,sha256=v-DO4ijdjiHwBsSDF3dUgqQw5twCcG-5NA1aq7kUYQ0,13239
|
|
18
18
|
clearskies_aws/backends/dynamo_db_condition_parser_test.py,sha256=n7snumB8GxyMBVlj-HL7JrsNj-ZJ91c5gdik0UNM5Ag,12664
|
|
19
|
-
clearskies_aws/backends/dynamo_db_parti_ql_backend.py,sha256=
|
|
20
|
-
clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py,sha256=
|
|
19
|
+
clearskies_aws/backends/dynamo_db_parti_ql_backend.py,sha256=pVd_N7XFf9PcTAm6rhMxih_sj9Ix6RkRj3LwmFy_Mr4,48544
|
|
20
|
+
clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py,sha256=7gH-RDh7JeaiEGLXsOB512SLmOp0hjdtWQ1cMmexvDM,23649
|
|
21
21
|
clearskies_aws/backends/sqs_backend.py,sha256=hT1JCvCMU76Q4Ir7OeX8U8eAMLIu1_tMwGgaN9rpsPk,2726
|
|
22
22
|
clearskies_aws/backends/sqs_backend_test.py,sha256=iCuHVVqZIR_PDGUUMZIkpir8yyJy3dcPR00AWR2N25U,1138
|
|
23
23
|
clearskies_aws/contexts/__init__.py,sha256=YjwRaSoAqC-nmo3aFB8rzyuiHSP9mi77FVM-8RF0hRE,472
|
|
@@ -59,13 +59,13 @@ clearskies_aws/secrets/additional_configs/iam_db_auth.py,sha256=K6eLjo_D0uSxtCfq
|
|
|
59
59
|
clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py,sha256=hzvR_WBwSoLcMdGXwhqkvKMKmjXzhZgimPWfm2MWSZQ,3467
|
|
60
60
|
clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py,sha256=L2-E8Tm6BDHV-yJJ_M_Lo72jNY7uBaTp8SPrRuekcUE,3776
|
|
61
61
|
clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py,sha256=Llvg8uQW8J-qndAlDDtg9TY_Tvu2h9tUzchxxUtaRik,6444
|
|
62
|
-
clearskies_aws/secrets/akeyless_with_ssm_cache.py,sha256=
|
|
62
|
+
clearskies_aws/secrets/akeyless_with_ssm_cache.py,sha256=MFAnajwO_XtZWMZHxsHFZBZa8V9vQGH0NSK4I6uVaow,1559
|
|
63
63
|
clearskies_aws/secrets/parameter_store.py,sha256=lxBlp_9d2-vVjGNfl5859XzZCcLVMMrZtlJG8lKGVPA,1810
|
|
64
64
|
clearskies_aws/secrets/parameter_store_test.py,sha256=35fTNau4tq_D4elMwyyByIiLesnmn05QhC_X1FVQXsM,763
|
|
65
65
|
clearskies_aws/secrets/secrets_manager.py,sha256=jlpfAFC23EeSpm50L8B-yrXg4IROQq-M_90zzXDp_ak,3056
|
|
66
66
|
clearskies_aws/secrets/secrets_manager_test.py,sha256=__YSe-YRbbE1S1SBvZZFQd3brIX5DPX2_wE9MI_Ezx0,788
|
|
67
67
|
clearskies_aws/web_socket_connection_model.py,sha256=d_Au_Pu7YXBfc7_lbuI7zz4MZ8ZOOwGM0oooppEofcI,1776
|
|
68
|
-
clear_skies_aws-1.10.
|
|
69
|
-
clear_skies_aws-1.10.
|
|
70
|
-
clear_skies_aws-1.10.
|
|
71
|
-
clear_skies_aws-1.10.
|
|
68
|
+
clear_skies_aws-1.10.2.dist-info/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
|
|
69
|
+
clear_skies_aws-1.10.2.dist-info/METADATA,sha256=jgygnKXGfTw4rejQAglNblRFraGfcX0VeRcwG7NYGyk,8784
|
|
70
|
+
clear_skies_aws-1.10.2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
71
|
+
clear_skies_aws-1.10.2.dist-info/RECORD,,
|
|
@@ -2,7 +2,8 @@ import base64
|
|
|
2
2
|
import binascii
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
|
|
5
|
+
import re
|
|
6
|
+
from decimal import Decimal, InvalidOperation
|
|
6
7
|
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union
|
|
7
8
|
|
|
8
9
|
from boto3.session import Session as Boto3Session
|
|
@@ -68,7 +69,10 @@ class DynamoDBPartiQLCursor:
|
|
|
68
69
|
"""
|
|
69
70
|
try:
|
|
70
71
|
call_args: ExecuteStatementInputTypeDef = {"Statement": statement}
|
|
71
|
-
if
|
|
72
|
+
# Only include 'Parameters' if it's not None AND not empty
|
|
73
|
+
if (
|
|
74
|
+
parameters
|
|
75
|
+
): # This implies parameters is not None and parameters is not an empty list
|
|
72
76
|
call_args["Parameters"] = parameters
|
|
73
77
|
if Limit is not None:
|
|
74
78
|
call_args["Limit"] = Limit
|
|
@@ -81,7 +85,7 @@ class DynamoDBPartiQLCursor:
|
|
|
81
85
|
**call_args
|
|
82
86
|
)
|
|
83
87
|
except ClientError as err:
|
|
84
|
-
error_response: Dict[str, Any] = err.response.get("Error", {})
|
|
88
|
+
error_response: Dict[str, Any] = err.response.get("Error", {}) # type: ignore
|
|
85
89
|
error_code: str = error_response.get("Code", "UnknownCode")
|
|
86
90
|
error_message: str = error_response.get("Message", "Unknown error")
|
|
87
91
|
|
|
@@ -114,6 +118,7 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
114
118
|
The count() method uses native DynamoDB Query/Scan operations for accuracy.
|
|
115
119
|
"""
|
|
116
120
|
|
|
121
|
+
_cursor: DynamoDBPartiQLCursor
|
|
117
122
|
_allowed_configs: List[str] = [
|
|
118
123
|
"table_name",
|
|
119
124
|
"wheres",
|
|
@@ -165,7 +170,7 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
165
170
|
if not table_name:
|
|
166
171
|
return ""
|
|
167
172
|
esc: str = self._table_escape_character()
|
|
168
|
-
final_name = f"{esc}{table_name.strip(esc)}{esc}"
|
|
173
|
+
final_name = f"{esc}{str(table_name).strip(esc)}{esc}"
|
|
169
174
|
if index_name:
|
|
170
175
|
final_name += f".{esc}{index_name.strip(esc)}{esc}"
|
|
171
176
|
return final_name
|
|
@@ -279,13 +284,13 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
279
284
|
|
|
280
285
|
if configuration.get("group_by_column"):
|
|
281
286
|
logger.warning(
|
|
282
|
-
|
|
287
|
+
"Configuration included 'group_by_column=" + configuration.get("group_by_column") + "', " +
|
|
283
288
|
"but GROUP BY is not supported by this DynamoDB PartiQL backend and will be ignored for SQL generation."
|
|
284
289
|
)
|
|
285
290
|
|
|
286
291
|
if configuration.get("joins"):
|
|
287
292
|
logger.warning(
|
|
288
|
-
|
|
293
|
+
"Configuration included 'joins=" + str(configuration.get("joins")) + "', " +
|
|
289
294
|
"but JOINs are not supported by this DynamoDB PartiQL backend and will be ignored for SQL generation."
|
|
290
295
|
)
|
|
291
296
|
|
|
@@ -363,8 +368,8 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
363
368
|
) -> Dict[str, Any]:
|
|
364
369
|
"""
|
|
365
370
|
Converts a list of 'where' condition dictionaries into DynamoDB native
|
|
366
|
-
expression strings and attribute maps.
|
|
367
|
-
This is
|
|
371
|
+
expression strings and attribute maps for Query/Scan operations.
|
|
372
|
+
This implementation is more comprehensive than the previous one.
|
|
368
373
|
"""
|
|
369
374
|
expression_attribute_names: Dict[str, str] = {}
|
|
370
375
|
expression_attribute_values: Dict[str, AttributeValueTypeDef] = {}
|
|
@@ -374,65 +379,98 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
374
379
|
name_counter = 0
|
|
375
380
|
value_counter = 0
|
|
376
381
|
|
|
377
|
-
|
|
382
|
+
# Helper to get unique placeholder names
|
|
383
|
+
def get_name_placeholder(column_name: str) -> str:
|
|
384
|
+
nonlocal name_counter
|
|
385
|
+
# Sanitize column name for placeholder if it contains special characters
|
|
386
|
+
sanitized_column_name = re.sub(r"[^a-zA-Z0-9_]", "", column_name)
|
|
387
|
+
placeholder = f"#{sanitized_column_name}_{name_counter}"
|
|
388
|
+
expression_attribute_names[placeholder] = column_name
|
|
389
|
+
name_counter += 1
|
|
390
|
+
return placeholder
|
|
391
|
+
|
|
392
|
+
# Helper to get unique value placeholders and add values
|
|
393
|
+
def get_value_placeholder(value: Any) -> str:
|
|
394
|
+
nonlocal value_counter
|
|
395
|
+
placeholder = f":val{value_counter}"
|
|
396
|
+
expression_attribute_values[placeholder] = (
|
|
397
|
+
self.condition_parser.to_dynamodb_attribute_value(value)
|
|
398
|
+
)
|
|
399
|
+
value_counter += 1
|
|
400
|
+
return placeholder
|
|
401
|
+
|
|
402
|
+
processed_condition_indices = set()
|
|
403
|
+
|
|
404
|
+
# First, try to build KeyConditionExpression for Partition Key and Sort Key
|
|
405
|
+
# Find partition key equality condition
|
|
406
|
+
pk_condition_index = -1
|
|
378
407
|
if partition_key_name:
|
|
379
|
-
processed_indices = set() # To avoid processing a condition twice
|
|
380
408
|
for i, cond in enumerate(conditions):
|
|
381
|
-
if i in processed_indices:
|
|
382
|
-
continue
|
|
383
409
|
if (
|
|
384
410
|
cond.get("column") == partition_key_name
|
|
385
411
|
and cond.get("operator") == "="
|
|
386
412
|
and cond.get("values")
|
|
387
413
|
):
|
|
388
|
-
|
|
389
|
-
value_placeholder = f":pk_val{value_counter}"
|
|
390
|
-
expression_attribute_names[name_placeholder] = partition_key_name
|
|
391
|
-
expression_attribute_values[value_placeholder] = (
|
|
392
|
-
self.condition_parser.to_dynamodb_attribute_value(
|
|
393
|
-
cond["values"][0]
|
|
394
|
-
)
|
|
395
|
-
)
|
|
396
|
-
key_condition_parts.append(
|
|
397
|
-
f"{name_placeholder} = {value_placeholder}"
|
|
398
|
-
)
|
|
399
|
-
name_counter += 1
|
|
400
|
-
value_counter += 1
|
|
401
|
-
partition_key_condition_found = True
|
|
402
|
-
processed_indices.add(i)
|
|
414
|
+
pk_condition_index = i
|
|
403
415
|
break
|
|
404
416
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
417
|
+
if pk_condition_index != -1:
|
|
418
|
+
pk_cond = conditions[pk_condition_index]
|
|
419
|
+
pk_name_ph = get_name_placeholder(pk_cond["column"])
|
|
420
|
+
pk_value_ph = get_value_placeholder(pk_cond["values"][0])
|
|
421
|
+
key_condition_parts.append(f"{pk_name_ph} = {pk_value_ph}")
|
|
422
|
+
processed_condition_indices.add(pk_condition_index)
|
|
423
|
+
|
|
424
|
+
# If partition key found, check for sort key condition
|
|
425
|
+
if sort_key_name:
|
|
409
426
|
for i, cond in enumerate(conditions):
|
|
410
|
-
if i in
|
|
427
|
+
if i in processed_condition_indices:
|
|
411
428
|
continue
|
|
412
429
|
if cond.get("column") == sort_key_name and cond.get("values"):
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
430
|
+
op_lower = cond["operator"].lower()
|
|
431
|
+
sk_name_ph = get_name_placeholder(cond["column"])
|
|
432
|
+
|
|
433
|
+
if op_lower == "=":
|
|
434
|
+
sk_value_ph = get_value_placeholder(cond["values"][0])
|
|
435
|
+
key_condition_parts.append(f"{sk_name_ph} = {sk_value_ph}")
|
|
436
|
+
elif op_lower == ">":
|
|
437
|
+
sk_value_ph = get_value_placeholder(cond["values"][0])
|
|
438
|
+
key_condition_parts.append(f"{sk_name_ph} > {sk_value_ph}")
|
|
439
|
+
elif op_lower == "<":
|
|
440
|
+
sk_value_ph = get_value_placeholder(cond["values"][0])
|
|
441
|
+
key_condition_parts.append(f"{sk_name_ph} < {sk_value_ph}")
|
|
442
|
+
elif op_lower == ">=":
|
|
443
|
+
sk_value_ph = get_value_placeholder(cond["values"][0])
|
|
444
|
+
key_condition_parts.append(f"{sk_name_ph} >= {sk_value_ph}")
|
|
445
|
+
elif op_lower == "<=":
|
|
446
|
+
sk_value_ph = get_value_placeholder(cond["values"][0])
|
|
447
|
+
key_condition_parts.append(f"{sk_name_ph} <= {sk_value_ph}")
|
|
448
|
+
elif op_lower == "between":
|
|
449
|
+
if len(cond["values"]) == 2:
|
|
450
|
+
sk_value1_ph = get_value_placeholder(cond["values"][0])
|
|
451
|
+
sk_value2_ph = get_value_placeholder(cond["values"][1])
|
|
452
|
+
key_condition_parts.append(
|
|
453
|
+
f"{sk_name_ph} BETWEEN {sk_value1_ph} AND {sk_value2_ph}"
|
|
421
454
|
)
|
|
422
|
-
|
|
455
|
+
else:
|
|
456
|
+
logger.warning(
|
|
457
|
+
f"Skipping malformed BETWEEN condition for sort key: {cond}"
|
|
458
|
+
)
|
|
459
|
+
elif op_lower == "begins_with":
|
|
460
|
+
sk_value_ph = get_value_placeholder(cond["values"][0])
|
|
423
461
|
key_condition_parts.append(
|
|
424
|
-
f"{
|
|
462
|
+
f"begins_with({sk_name_ph}, {sk_value_ph})"
|
|
425
463
|
)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
464
|
+
else:
|
|
465
|
+
# Other operators for sort key are not part of KeyConditionExpression
|
|
466
|
+
# They will be handled in FilterExpression below
|
|
467
|
+
continue
|
|
468
|
+
processed_condition_indices.add(i)
|
|
469
|
+
break # Assume only one sort key condition for KeyConditionExpression
|
|
430
470
|
|
|
431
|
-
#
|
|
471
|
+
# Process all remaining conditions for FilterExpression
|
|
432
472
|
for i, cond in enumerate(conditions):
|
|
433
|
-
if i in
|
|
434
|
-
locals().get("processed_indices") or set()
|
|
435
|
-
): # Check if already processed for KeyCondition
|
|
473
|
+
if i in processed_condition_indices:
|
|
436
474
|
continue
|
|
437
475
|
|
|
438
476
|
col_name = cond.get("column")
|
|
@@ -442,31 +480,88 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
442
480
|
if not col_name or not op or vals is None:
|
|
443
481
|
continue
|
|
444
482
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
)
|
|
483
|
+
name_ph = get_name_placeholder(col_name)
|
|
484
|
+
op_lower = op.lower()
|
|
485
|
+
|
|
486
|
+
if op_lower == "=":
|
|
487
|
+
value_ph = get_value_placeholder(vals[0])
|
|
488
|
+
filter_expression_parts.append(f"{name_ph} = {value_ph}")
|
|
489
|
+
elif op_lower == "!=":
|
|
490
|
+
value_ph = get_value_placeholder(vals[0])
|
|
491
|
+
filter_expression_parts.append(f"{name_ph} <> {value_ph}")
|
|
492
|
+
elif op_lower == ">":
|
|
493
|
+
value_ph = get_value_placeholder(vals[0])
|
|
494
|
+
filter_expression_parts.append(f"{name_ph} > {value_ph}")
|
|
495
|
+
elif op_lower == "<":
|
|
496
|
+
value_ph = get_value_placeholder(vals[0])
|
|
497
|
+
filter_expression_parts.append(f"{name_ph} < {value_ph}")
|
|
498
|
+
elif op_lower == ">=":
|
|
499
|
+
value_ph = get_value_placeholder(vals[0])
|
|
500
|
+
filter_expression_parts.append(f"{name_ph} >= {value_ph}")
|
|
501
|
+
elif op_lower == "<=":
|
|
502
|
+
value_ph = get_value_placeholder(vals[0])
|
|
503
|
+
filter_expression_parts.append(f"{name_ph} <= {value_ph}")
|
|
504
|
+
elif op_lower == "between":
|
|
505
|
+
if len(vals) == 2:
|
|
506
|
+
value1_ph = get_value_placeholder(vals[0])
|
|
507
|
+
value2_ph = get_value_placeholder(vals[1])
|
|
508
|
+
filter_expression_parts.append(
|
|
509
|
+
f"{name_ph} BETWEEN {value1_ph} AND {value2_ph}"
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
logger.warning(f"Skipping malformed BETWEEN condition: {cond}")
|
|
513
|
+
elif op_lower == "in":
|
|
514
|
+
value_placeholders = ", ".join([get_value_placeholder(v) for v in vals])
|
|
515
|
+
filter_expression_parts.append(f"{name_ph} IN ({value_placeholders})")
|
|
516
|
+
elif op_lower == "contains":
|
|
517
|
+
value_ph = get_value_placeholder(vals[0])
|
|
518
|
+
filter_expression_parts.append(f"contains({name_ph}, {value_ph})")
|
|
519
|
+
elif op_lower == "not contains":
|
|
520
|
+
value_ph = get_value_placeholder(vals[0])
|
|
521
|
+
filter_expression_parts.append(f"NOT contains({name_ph}, {value_ph})")
|
|
522
|
+
elif op_lower == "begins_with":
|
|
523
|
+
value_ph = get_value_placeholder(vals[0])
|
|
524
|
+
filter_expression_parts.append(f"begins_with({name_ph}, {value_ph})")
|
|
525
|
+
elif op_lower == "not begins_with":
|
|
526
|
+
value_ph = get_value_placeholder(vals[0])
|
|
453
527
|
filter_expression_parts.append(
|
|
454
|
-
f"{
|
|
528
|
+
f"NOT begins_with({name_ph}, {value_ph})"
|
|
529
|
+
)
|
|
530
|
+
elif op_lower == "is null":
|
|
531
|
+
filter_expression_parts.append(f"attribute_not_exists({name_ph})")
|
|
532
|
+
elif op_lower == "is not null":
|
|
533
|
+
filter_expression_parts.append(f"attribute_exists({name_ph})")
|
|
534
|
+
elif (
|
|
535
|
+
op_lower == "like"
|
|
536
|
+
): # Clearskies 'like' usually translates to begins_with or contains
|
|
537
|
+
# This is a simplification. A full implementation might need to inspect '%' position.
|
|
538
|
+
# For now, if it contains '%', assume 'contains'. If it ends with '%', assume 'begins_with'.
|
|
539
|
+
# If no '%', it's an equality.
|
|
540
|
+
if len(vals) > 0 and isinstance(vals[0], str):
|
|
541
|
+
like_value = vals[0]
|
|
542
|
+
if like_value.startswith("%") and like_value.endswith("%"):
|
|
543
|
+
value_ph = get_value_placeholder(like_value.strip("%"))
|
|
544
|
+
filter_expression_parts.append(
|
|
545
|
+
f"contains({name_ph}, {value_ph})"
|
|
546
|
+
)
|
|
547
|
+
elif like_value.endswith("%"):
|
|
548
|
+
value_ph = get_value_placeholder(like_value.rstrip("%"))
|
|
549
|
+
filter_expression_parts.append(
|
|
550
|
+
f"begins_with({name_ph}, {value_ph})"
|
|
551
|
+
)
|
|
552
|
+
else: # Treat as equality if no wildcards or complex pattern
|
|
553
|
+
value_ph = get_value_placeholder(like_value)
|
|
554
|
+
filter_expression_parts.append(f"{name_ph} = {value_ph}")
|
|
555
|
+
else:
|
|
556
|
+
logger.warning(f"Skipping unsupported LIKE condition: {cond}")
|
|
557
|
+
else:
|
|
558
|
+
logger.warning(
|
|
559
|
+
f"Skipping unsupported operator '{op}' for native DynamoDB expressions: {cond}"
|
|
455
560
|
)
|
|
456
|
-
name_counter += 1
|
|
457
|
-
value_counter += 1
|
|
458
|
-
# Add more operator handling here for a complete filter expression builder
|
|
459
561
|
|
|
460
562
|
result: Dict[str, Any] = {}
|
|
461
|
-
if
|
|
462
|
-
key_condition_parts and partition_key_condition_found
|
|
463
|
-
): # Only add KeyConditionExpression if PK equality was found
|
|
563
|
+
if key_condition_parts:
|
|
464
564
|
result["KeyConditionExpression"] = " AND ".join(key_condition_parts)
|
|
465
|
-
elif (
|
|
466
|
-
key_condition_parts
|
|
467
|
-
): # If we built key_condition_parts but PK equality wasn't the one, move to filter
|
|
468
|
-
filter_expression_parts.extend(key_condition_parts)
|
|
469
|
-
|
|
470
565
|
if filter_expression_parts:
|
|
471
566
|
result["FilterExpression"] = " AND ".join(filter_expression_parts)
|
|
472
567
|
if expression_attribute_names:
|
|
@@ -495,8 +590,8 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
495
590
|
table_description.get("GlobalSecondaryIndexes", [])
|
|
496
591
|
)
|
|
497
592
|
for gsi in gsi_definitions:
|
|
498
|
-
if gsi
|
|
499
|
-
for key_element in gsi
|
|
593
|
+
if gsi.get("IndexName", "") == chosen_index_name:
|
|
594
|
+
for key_element in gsi.get("KeySchema", []):
|
|
500
595
|
if key_element["KeyType"] == "RANGE":
|
|
501
596
|
sort_key_for_target = key_element["AttributeName"]
|
|
502
597
|
break
|
|
@@ -516,7 +611,7 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
516
611
|
wheres_config, partition_key_for_target, sort_key_for_target
|
|
517
612
|
)
|
|
518
613
|
|
|
519
|
-
params_for_native_call: Dict[str, Any] = {
|
|
614
|
+
params_for_native_call: Dict[str, Any] = {
|
|
520
615
|
"TableName": table_name,
|
|
521
616
|
"Select": "COUNT",
|
|
522
617
|
}
|
|
@@ -524,36 +619,37 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
524
619
|
params_for_native_call["IndexName"] = chosen_index_name
|
|
525
620
|
|
|
526
621
|
can_use_query_for_count = False
|
|
622
|
+
# A Query operation can be used for count if there is a KeyConditionExpression
|
|
623
|
+
# that includes an equality condition on the partition key of the target (table or GSI).
|
|
624
|
+
# We check if the partition key condition was successfully extracted into KeyConditionExpression.
|
|
527
625
|
if (
|
|
528
|
-
|
|
529
|
-
and partition_key_for_target
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
626
|
+
partition_key_for_target
|
|
627
|
+
and f"#{re.sub(r'[^a-zA-Z0-9_]', '', partition_key_for_target)}_0"
|
|
628
|
+
in native_expressions.get("ExpressionAttributeNames", {})
|
|
629
|
+
and native_expressions.get("KeyConditionExpression")
|
|
630
|
+
and f"#{re.sub(r'[^a-zA-Z0-9_]', '', partition_key_for_target)}_0 = :val0"
|
|
631
|
+
in native_expressions[
|
|
632
|
+
"KeyConditionExpression"
|
|
633
|
+
] # Simplified check, assumes first value is PK
|
|
535
634
|
):
|
|
536
635
|
can_use_query_for_count = True
|
|
537
636
|
params_for_native_call["KeyConditionExpression"] = native_expressions[
|
|
538
637
|
"KeyConditionExpression"
|
|
539
638
|
]
|
|
540
|
-
if native_expressions.get(
|
|
541
|
-
"FilterExpression"
|
|
542
|
-
): # Query can also have FilterExpression
|
|
639
|
+
if native_expressions.get("FilterExpression"):
|
|
543
640
|
params_for_native_call["FilterExpression"] = native_expressions[
|
|
544
641
|
"FilterExpression"
|
|
545
642
|
]
|
|
546
|
-
else:
|
|
547
|
-
#
|
|
548
|
-
|
|
549
|
-
if native_expressions.get("KeyConditionExpression"):
|
|
550
|
-
all_filter_parts.append(native_expressions["KeyConditionExpression"])
|
|
643
|
+
else:
|
|
644
|
+
# Fall back to Scan, and all conditions (including any potential key conditions that
|
|
645
|
+
# couldn't be used for a Query) go into FilterExpression.
|
|
551
646
|
if native_expressions.get("FilterExpression"):
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
647
|
+
params_for_native_call["FilterExpression"] = native_expressions[
|
|
648
|
+
"FilterExpression"
|
|
649
|
+
]
|
|
650
|
+
# If there's a KeyConditionExpression but no PK equality, it should also be part of the filter for scan.
|
|
651
|
+
# This logic is now handled more robustly within _wheres_to_native_dynamo_expressions
|
|
652
|
+
# by ensuring only true PK/SK conditions go to KeyConditionExpression initially.
|
|
557
653
|
|
|
558
654
|
if native_expressions.get("ExpressionAttributeNames"):
|
|
559
655
|
params_for_native_call["ExpressionAttributeNames"] = native_expressions[
|
|
@@ -599,18 +695,31 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
599
695
|
"""
|
|
600
696
|
Creates a new record in DynamoDB using PartiQL INSERT.
|
|
601
697
|
"""
|
|
602
|
-
table_name: str = self._finalize_table_name(model.
|
|
698
|
+
table_name: str = self._finalize_table_name(model.get_table_name())
|
|
603
699
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
700
|
+
if not data:
|
|
701
|
+
logger.warning("Create called with empty data. Nothing to insert.")
|
|
702
|
+
return {}
|
|
703
|
+
|
|
704
|
+
# Prepare parameters
|
|
705
|
+
parameters: List[AttributeValueTypeDef] = []
|
|
706
|
+
|
|
707
|
+
# Build the 'VALUE {key: ?, key: ?}' part and collect parameters
|
|
708
|
+
value_struct_parts: List[str] = []
|
|
709
|
+
for key, value in data.items():
|
|
710
|
+
# Use single quotes around the key to match PartiQL documentation examples
|
|
711
|
+
value_struct_parts.append(f"'{key}': ?")
|
|
712
|
+
parameters.append(self.condition_parser.to_dynamodb_attribute_value(value))
|
|
713
|
+
value_struct_clause = ", ".join(value_struct_parts)
|
|
608
714
|
|
|
609
|
-
|
|
610
|
-
statement = f"INSERT INTO {table_name} VALUE
|
|
715
|
+
# Construct the INSERT statement with explicit struct format
|
|
716
|
+
statement = f"INSERT INTO {table_name} VALUE {{{value_struct_clause}}}"
|
|
611
717
|
|
|
612
718
|
try:
|
|
613
|
-
self._cursor.execute(
|
|
719
|
+
self._cursor.execute(
|
|
720
|
+
statement=statement,
|
|
721
|
+
parameters=parameters,
|
|
722
|
+
)
|
|
614
723
|
return data
|
|
615
724
|
except Exception as e:
|
|
616
725
|
logger.error(
|
|
@@ -624,8 +733,8 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
624
733
|
"""
|
|
625
734
|
Updates an existing record in DynamoDB using PartiQL UPDATE.
|
|
626
735
|
"""
|
|
627
|
-
table_name: str = self._finalize_table_name(model.
|
|
628
|
-
id_column_name: str = model.id_column_name
|
|
736
|
+
table_name: str = self._finalize_table_name(model.get_table_name())
|
|
737
|
+
id_column_name: str = model.id_column_name
|
|
629
738
|
escaped_id_column: str = (
|
|
630
739
|
f"{self._column_escape_character()}{id_column_name}{self._column_escape_character()}"
|
|
631
740
|
)
|
|
@@ -679,13 +788,15 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
679
788
|
"""
|
|
680
789
|
Deletes a record from DynamoDB using PartiQL DELETE.
|
|
681
790
|
"""
|
|
682
|
-
table_name: str = self._finalize_table_name(model.
|
|
683
|
-
id_column_name: str = model.id_column_name
|
|
791
|
+
table_name: str = self._finalize_table_name(model.get_table_name())
|
|
792
|
+
id_column_name: str = model.id_column_name
|
|
684
793
|
escaped_id_column: str = (
|
|
685
794
|
f"{self._column_escape_character()}{id_column_name}{self._column_escape_character()}"
|
|
686
795
|
)
|
|
687
796
|
|
|
688
|
-
parameters: List[AttributeValueTypeDef] = [
|
|
797
|
+
parameters: List[AttributeValueTypeDef] = [
|
|
798
|
+
self.condition_parser.to_dynamodb_attribute_value(id_value)
|
|
799
|
+
]
|
|
689
800
|
statement: str = f"DELETE FROM {table_name} WHERE {escaped_id_column} = ?"
|
|
690
801
|
|
|
691
802
|
try:
|
|
@@ -731,7 +842,7 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
731
842
|
if "N" in attribute_value:
|
|
732
843
|
try:
|
|
733
844
|
return Decimal(attribute_value["N"])
|
|
734
|
-
except DecimalException
|
|
845
|
+
except InvalidOperation: # Changed from DecimalException
|
|
735
846
|
logger.warning(
|
|
736
847
|
f"Could not convert N value '{attribute_value['N']}' to Decimal."
|
|
737
848
|
)
|
|
@@ -741,7 +852,13 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
741
852
|
if "NULL" in attribute_value:
|
|
742
853
|
return None
|
|
743
854
|
if "B" in attribute_value:
|
|
744
|
-
|
|
855
|
+
try:
|
|
856
|
+
return base64.b64decode(attribute_value["B"])
|
|
857
|
+
except (binascii.Error, TypeError) as e:
|
|
858
|
+
logger.warning(
|
|
859
|
+
f"Failed to decode base64 binary value: {attribute_value['B']}, error: {e}"
|
|
860
|
+
)
|
|
861
|
+
return attribute_value["B"] # Return raw if decoding fails
|
|
745
862
|
if "L" in attribute_value:
|
|
746
863
|
return [self._map_from_boto3_value(item) for item in attribute_value["L"]]
|
|
747
864
|
if "M" in attribute_value:
|
|
@@ -754,13 +871,19 @@ class DynamoDBPartiQLBackend(CursorBackend):
|
|
|
754
871
|
if "NS" in attribute_value:
|
|
755
872
|
try:
|
|
756
873
|
return set(Decimal(n_val) for n_val in attribute_value["NS"])
|
|
757
|
-
except DecimalException
|
|
874
|
+
except InvalidOperation: # Changed from DecimalException
|
|
758
875
|
logger.warning(
|
|
759
876
|
f"Could not convert one or more NS values in '{attribute_value['NS']}' to Decimal."
|
|
760
877
|
)
|
|
761
878
|
return set(attribute_value["NS"])
|
|
762
879
|
if "BS" in attribute_value:
|
|
763
|
-
|
|
880
|
+
try:
|
|
881
|
+
return set(base64.b64decode(b_val) for b_val in attribute_value["BS"])
|
|
882
|
+
except (binascii.Error, TypeError) as e:
|
|
883
|
+
logger.warning(
|
|
884
|
+
f"Failed to decode one or more base64 binary values in '{attribute_value['BS']}', error: {e}"
|
|
885
|
+
)
|
|
886
|
+
return set(attribute_value["BS"]) # Return raw if decoding fails
|
|
764
887
|
|
|
765
888
|
logger.warning(f"Unrecognized DynamoDB attribute type: {attribute_value}")
|
|
766
889
|
return attribute_value
|
|
@@ -29,7 +29,7 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
29
29
|
|
|
30
30
|
self.backend = DynamoDBPartiQLBackend(self.cursor_under_test)
|
|
31
31
|
self.mock_model = MagicMock(spec=Model)
|
|
32
|
-
self.mock_model.
|
|
32
|
+
self.mock_model.get_table_name = MagicMock(return_value="my_test_table")
|
|
33
33
|
self.mock_model.id_column_name = "id"
|
|
34
34
|
|
|
35
35
|
self.mock_model.schema = MagicMock()
|
|
@@ -294,7 +294,9 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
294
294
|
|
|
295
295
|
results = list(self.backend.records(config, self.mock_model))
|
|
296
296
|
|
|
297
|
-
expected_call_kwargs = {
|
|
297
|
+
expected_call_kwargs = {
|
|
298
|
+
"Statement": expected_statement,
|
|
299
|
+
}
|
|
298
300
|
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
299
301
|
**expected_call_kwargs
|
|
300
302
|
)
|
|
@@ -324,7 +326,6 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
324
326
|
|
|
325
327
|
expected_call_kwargs = {
|
|
326
328
|
"Statement": expected_statement,
|
|
327
|
-
"Parameters": [],
|
|
328
329
|
"Limit": 1,
|
|
329
330
|
}
|
|
330
331
|
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
@@ -366,7 +367,6 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
366
367
|
|
|
367
368
|
expected_call_kwargs1 = {
|
|
368
369
|
"Statement": expected_statement,
|
|
369
|
-
"Parameters": [],
|
|
370
370
|
"NextToken": initial_ddb_token,
|
|
371
371
|
}
|
|
372
372
|
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
@@ -397,7 +397,6 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
397
397
|
)
|
|
398
398
|
expected_call_kwargs2 = {
|
|
399
399
|
"Statement": expected_statement,
|
|
400
|
-
"Parameters": [],
|
|
401
400
|
"NextToken": ddb_next_token_page1,
|
|
402
401
|
}
|
|
403
402
|
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
@@ -420,7 +419,7 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
420
419
|
next_page_data = {}
|
|
421
420
|
results = list(self.backend.records(config, self.mock_model, next_page_data))
|
|
422
421
|
|
|
423
|
-
expected_call_kwargs = {"Statement": expected_statement
|
|
422
|
+
expected_call_kwargs = {"Statement": expected_statement}
|
|
424
423
|
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
425
424
|
**expected_call_kwargs
|
|
426
425
|
)
|
|
@@ -451,7 +450,6 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
451
450
|
|
|
452
451
|
expected_call_kwargs = {
|
|
453
452
|
"Statement": expected_statement,
|
|
454
|
-
"Parameters": [],
|
|
455
453
|
"Limit": 1,
|
|
456
454
|
}
|
|
457
455
|
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
@@ -470,8 +468,14 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
470
468
|
def test_create_record(self, mock_logger_arg):
|
|
471
469
|
"""Test create() inserts a record and returns the input data."""
|
|
472
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
|
+
)
|
|
473
475
|
expected_ddb_parameters = [
|
|
474
|
-
{"
|
|
476
|
+
{"S": "new_user_123"},
|
|
477
|
+
{"S": "Jane Doe"},
|
|
478
|
+
{"N": "28"},
|
|
475
479
|
]
|
|
476
480
|
|
|
477
481
|
self.mock_dynamodb_client.execute_statement.return_value = {}
|
|
@@ -480,7 +484,7 @@ class TestDynamoDBPartiQLBackend(unittest.TestCase):
|
|
|
480
484
|
|
|
481
485
|
self.assertEqual(created_data, data_to_create)
|
|
482
486
|
self.mock_dynamodb_client.execute_statement.assert_called_once_with(
|
|
483
|
-
Statement=
|
|
487
|
+
Statement=expected_statement,
|
|
484
488
|
Parameters=expected_ddb_parameters,
|
|
485
489
|
)
|
|
486
490
|
|
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
1
3
|
from clearskies.secrets import AKeyless
|
|
4
|
+
|
|
5
|
+
|
|
2
6
|
class AkeylessWithSsmCache(AKeyless):
|
|
3
7
|
_boto3 = None
|
|
8
|
+
|
|
4
9
|
def __init__(self, requests, environment, boto3):
|
|
5
10
|
super().__init__(requests, environment)
|
|
6
11
|
self._boto3 = boto3
|
|
7
|
-
if not self._environment.get(
|
|
8
|
-
raise ValueError(
|
|
12
|
+
if not self._environment.get("AWS_REGION", True):
|
|
13
|
+
raise ValueError(
|
|
14
|
+
"To use parameter store you must use set the 'AWS_REGION' environment variable"
|
|
15
|
+
)
|
|
9
16
|
|
|
10
17
|
def get(self, path, refresh=False):
|
|
11
|
-
ssm = self._boto3.client(
|
|
12
|
-
#
|
|
13
|
-
|
|
18
|
+
ssm = self._boto3.client("ssm", region_name="us-east-1")
|
|
19
|
+
# AWS SSM parameter paths only allow a-z, A-Z, 0-9, -, _, ., /, @, and :
|
|
20
|
+
# Replace any disallowed characters with hyphens
|
|
21
|
+
ssm_name = re.sub(r"[^a-zA-Z0-9\-_\./@:]", "-", path)
|
|
14
22
|
# if we're not forcing a refresh, then see if it is in paramater store
|
|
15
23
|
if not refresh:
|
|
16
24
|
missing = False
|
|
@@ -19,7 +27,7 @@ class AkeylessWithSsmCache(AKeyless):
|
|
|
19
27
|
except ssm.exceptions.ParameterNotFound:
|
|
20
28
|
missing = True
|
|
21
29
|
if not missing:
|
|
22
|
-
value = response[
|
|
30
|
+
value = response["Parameter"]["Value"]
|
|
23
31
|
if value:
|
|
24
32
|
return value
|
|
25
33
|
|
|
@@ -31,7 +39,7 @@ class AkeylessWithSsmCache(AKeyless):
|
|
|
31
39
|
ssm.put_parameter(
|
|
32
40
|
Name=ssm_name,
|
|
33
41
|
Value=value,
|
|
34
|
-
Type=
|
|
42
|
+
Type="SecureString",
|
|
35
43
|
Overwrite=True,
|
|
36
44
|
)
|
|
37
45
|
|
|
File without changes
|
|
File without changes
|