nuql 0.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.
- nuql/__init__.py +3 -0
- nuql/api/__init__.py +13 -0
- nuql/api/adapter.py +34 -0
- nuql/api/batch_get/__init__.py +2 -0
- nuql/api/batch_get/batch_get.py +40 -0
- nuql/api/batch_get/queue.py +120 -0
- nuql/api/batch_write.py +99 -0
- nuql/api/condition_check.py +39 -0
- nuql/api/create.py +25 -0
- nuql/api/delete.py +88 -0
- nuql/api/get.py +30 -0
- nuql/api/put_item.py +112 -0
- nuql/api/put_update.py +25 -0
- nuql/api/query/__init__.py +4 -0
- nuql/api/query/condition.py +157 -0
- nuql/api/query/condition_builder.py +211 -0
- nuql/api/query/key_condition.py +200 -0
- nuql/api/query/query.py +166 -0
- nuql/api/transaction.py +145 -0
- nuql/api/update/__init__.py +3 -0
- nuql/api/update/expression_builder.py +33 -0
- nuql/api/update/update_item.py +139 -0
- nuql/api/update/utils.py +126 -0
- nuql/api/upsert.py +32 -0
- nuql/client.py +88 -0
- nuql/connection.py +43 -0
- nuql/exceptions.py +66 -0
- nuql/fields/__init__.py +11 -0
- nuql/fields/boolean.py +29 -0
- nuql/fields/datetime.py +49 -0
- nuql/fields/datetime_timestamp.py +45 -0
- nuql/fields/float.py +40 -0
- nuql/fields/integer.py +40 -0
- nuql/fields/key.py +207 -0
- nuql/fields/list.py +90 -0
- nuql/fields/map.py +67 -0
- nuql/fields/string.py +184 -0
- nuql/fields/ulid.py +39 -0
- nuql/fields/uuid.py +42 -0
- nuql/generators/__init__.py +3 -0
- nuql/generators/datetime.py +37 -0
- nuql/generators/ulid.py +10 -0
- nuql/generators/uuid.py +19 -0
- nuql/resources/__init__.py +4 -0
- nuql/resources/fields/__init__.py +3 -0
- nuql/resources/fields/field.py +153 -0
- nuql/resources/fields/field_map.py +85 -0
- nuql/resources/fields/value.py +5 -0
- nuql/resources/records/__init__.py +3 -0
- nuql/resources/records/projections.py +49 -0
- nuql/resources/records/serialiser.py +144 -0
- nuql/resources/records/validator.py +48 -0
- nuql/resources/tables/__init__.py +2 -0
- nuql/resources/tables/indexes.py +140 -0
- nuql/resources/tables/table.py +151 -0
- nuql/resources/utils/__init__.py +2 -0
- nuql/resources/utils/dict.py +21 -0
- nuql/resources/utils/validators.py +165 -0
- nuql/types/__init__.py +3 -0
- nuql/types/config.py +27 -0
- nuql/types/fields.py +27 -0
- nuql/types/serialisation.py +10 -0
- nuql-0.0.1.dist-info/METADATA +12 -0
- nuql-0.0.1.dist-info/RECORD +65 -0
- nuql-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
__all__ = ['Condition']
|
2
|
+
|
3
|
+
from typing import Dict, Any, Literal, Optional
|
4
|
+
|
5
|
+
from boto3.dynamodb.conditions import ComparisonCondition, Attr, ConditionExpressionBuilder
|
6
|
+
|
7
|
+
import nuql
|
8
|
+
from nuql import resources, types
|
9
|
+
from . import condition_builder
|
10
|
+
|
11
|
+
|
12
|
+
class Condition:
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
table: 'resources.Table',
|
16
|
+
condition: Dict[str, Any] | None = None,
|
17
|
+
condition_type: Literal['FilterExpression', 'ConditionExpression'] = 'FilterExpression',
|
18
|
+
) -> None:
|
19
|
+
"""
|
20
|
+
Base condition builder helper to resolve queries.
|
21
|
+
|
22
|
+
:arg table: Table instance.
|
23
|
+
:param condition: Condition dict.
|
24
|
+
:param condition_type: Condition type (FilterExpression or ConditionExpression).
|
25
|
+
"""
|
26
|
+
|
27
|
+
self.table = table
|
28
|
+
self.variables = condition['variables'] if condition and condition.get('variables') else {}
|
29
|
+
self.type = condition_type
|
30
|
+
self.condition = None
|
31
|
+
self.validator = resources.Validator()
|
32
|
+
|
33
|
+
if condition:
|
34
|
+
query = condition_builder.build_query(condition['where'])
|
35
|
+
self.condition = self.resolve(query['condition'])
|
36
|
+
|
37
|
+
@property
|
38
|
+
def resource_args(self) -> Dict[str, Any]:
|
39
|
+
"""Boto3 resource args for the condition."""
|
40
|
+
args = {}
|
41
|
+
if self.condition:
|
42
|
+
args[self.type] = self.condition
|
43
|
+
return args
|
44
|
+
|
45
|
+
@property
|
46
|
+
def client_args(self) -> Dict[str, Any]:
|
47
|
+
"""Boto3 client args for the condition."""
|
48
|
+
if not self.condition:
|
49
|
+
return {}
|
50
|
+
|
51
|
+
builder = ConditionExpressionBuilder()
|
52
|
+
expression = builder.build_expression(self.condition, is_key_condition=False)
|
53
|
+
|
54
|
+
expression_string = getattr(expression, 'condition_expression')
|
55
|
+
attribute_name_placeholders = getattr(expression, 'attribute_name_placeholders')
|
56
|
+
attribute_value_placeholders = getattr(expression, 'attribute_value_placeholders')
|
57
|
+
|
58
|
+
return {
|
59
|
+
'ConditionExpression': expression_string,
|
60
|
+
'ExpressionAttributeNames': attribute_name_placeholders,
|
61
|
+
'ExpressionAttributeValues': attribute_value_placeholders,
|
62
|
+
}
|
63
|
+
|
64
|
+
def append(self, condition: str) -> None:
|
65
|
+
"""
|
66
|
+
Append a condition to the current condition.
|
67
|
+
|
68
|
+
:arg condition: Condition string.
|
69
|
+
"""
|
70
|
+
if isinstance(condition, str):
|
71
|
+
condition = condition_builder.build_query(condition)['condition']
|
72
|
+
condition = self.resolve(condition)
|
73
|
+
if self.condition:
|
74
|
+
self.condition &= condition
|
75
|
+
else:
|
76
|
+
self.condition = condition
|
77
|
+
|
78
|
+
def resolve(self, part: Any) -> ComparisonCondition:
|
79
|
+
"""
|
80
|
+
Recursively resolves condition parts.
|
81
|
+
|
82
|
+
:arg part: Part to resolve.
|
83
|
+
:return: ComparisonCondition instance.
|
84
|
+
"""
|
85
|
+
# Direct condition/function handling
|
86
|
+
if isinstance(part, dict) and part['type'] in ['condition', 'function']:
|
87
|
+
attr = Attr(part['field'])
|
88
|
+
field_name = part['field']
|
89
|
+
field = self.table.fields.get(field_name)
|
90
|
+
|
91
|
+
is_key = field_name in self.table.indexes.index_keys
|
92
|
+
is_projected_to_key = any([x in self.table.indexes.index_keys for x in field.projected_from])
|
93
|
+
|
94
|
+
# Validate that keys cannot be present in the query
|
95
|
+
if (is_key or is_projected_to_key) and self.type == 'FilterExpression':
|
96
|
+
raise nuql.NuqlError(
|
97
|
+
code='ConditionError',
|
98
|
+
message=f'Field \'{field_name}\' cannot be used in a condition query'
|
99
|
+
)
|
100
|
+
|
101
|
+
# Functions are called differently
|
102
|
+
if part['type'] == 'function':
|
103
|
+
expression = getattr(attr, part['function'])()
|
104
|
+
|
105
|
+
# Handle basic conditions
|
106
|
+
else:
|
107
|
+
if not field:
|
108
|
+
raise nuql.NuqlError(
|
109
|
+
code='ConditionError',
|
110
|
+
message=f'Field \'{field_name}\' is not defined in the schema'
|
111
|
+
)
|
112
|
+
|
113
|
+
# Variables provided outside of the query string
|
114
|
+
if part['value_type'] == 'variable':
|
115
|
+
if part['variable'] not in self.variables:
|
116
|
+
raise nuql.NuqlError(
|
117
|
+
code='ConditionError',
|
118
|
+
message=f'Variable \'{part["variable"]}\' is not defined in the condition'
|
119
|
+
)
|
120
|
+
value = self.variables[part['variable']]
|
121
|
+
|
122
|
+
# Rudimentary values passed in the query string
|
123
|
+
else:
|
124
|
+
value = part['value']
|
125
|
+
|
126
|
+
# Value is serialised for a query
|
127
|
+
expression = getattr(attr, part['operand'])(field(value, action='query', validator=self.validator))
|
128
|
+
|
129
|
+
return expression
|
130
|
+
|
131
|
+
# Handle grouped conditions
|
132
|
+
elif isinstance(part, dict) and part['type'] == 'parentheses':
|
133
|
+
condition = None
|
134
|
+
last_operator = None
|
135
|
+
|
136
|
+
for item in part['conditions']:
|
137
|
+
# Logical operator is stored outside of the loop so that it is used
|
138
|
+
if isinstance(item, dict) and item['type'] == 'logical_operator':
|
139
|
+
last_operator = item['operator']
|
140
|
+
|
141
|
+
else:
|
142
|
+
expression = self.resolve(item)
|
143
|
+
|
144
|
+
if last_operator is None:
|
145
|
+
condition = expression
|
146
|
+
elif last_operator == 'and':
|
147
|
+
condition &= expression
|
148
|
+
elif last_operator == 'or':
|
149
|
+
condition |= expression
|
150
|
+
|
151
|
+
return condition
|
152
|
+
|
153
|
+
raise nuql.NuqlError(
|
154
|
+
code='ConditionParsingError',
|
155
|
+
message='Unresolvable condition part was provided',
|
156
|
+
part=part
|
157
|
+
)
|
@@ -0,0 +1,211 @@
|
|
1
|
+
__all__ = ['build_query']
|
2
|
+
|
3
|
+
from typing import Dict, Any, List
|
4
|
+
|
5
|
+
from pyparsing import Word, alphas, alphanums, Regex, Group, Forward, oneOf, infixNotation, opAssoc, QuotedString, \
|
6
|
+
pyparsing_common, ParseException
|
7
|
+
|
8
|
+
import nuql
|
9
|
+
|
10
|
+
|
11
|
+
ATTR_OPERATORS = {
|
12
|
+
# Equals
|
13
|
+
'equals': 'eq',
|
14
|
+
'=': 'eq',
|
15
|
+
'==': 'eq',
|
16
|
+
'eq': 'eq',
|
17
|
+
'is': 'eq',
|
18
|
+
|
19
|
+
# Not equals
|
20
|
+
'not_equals': 'ne',
|
21
|
+
'!=': 'ne',
|
22
|
+
'<>': 'ne',
|
23
|
+
'ne': 'ne',
|
24
|
+
'is_not': 'ne',
|
25
|
+
|
26
|
+
# Less than
|
27
|
+
'less_than': 'lt',
|
28
|
+
'<': 'lt',
|
29
|
+
'lt': 'lt',
|
30
|
+
|
31
|
+
# Less than or equal to
|
32
|
+
'less_than_or_equals': 'lte',
|
33
|
+
'less_than_equals': 'lte',
|
34
|
+
'<=': 'lte',
|
35
|
+
'lte': 'lte',
|
36
|
+
|
37
|
+
# Greater than
|
38
|
+
'greater_than': 'gt',
|
39
|
+
'>': 'gt',
|
40
|
+
'gt': 'gt',
|
41
|
+
|
42
|
+
# Greater than or equal to
|
43
|
+
'greater_than_or_equals': 'gte',
|
44
|
+
'greater_than_equals': 'gte',
|
45
|
+
'>=': 'gte',
|
46
|
+
'gte': 'gte',
|
47
|
+
|
48
|
+
# Special
|
49
|
+
'begins_with': 'begins_with',
|
50
|
+
'between': 'between',
|
51
|
+
'attribute_type': 'attribute_type',
|
52
|
+
'contains': 'contains',
|
53
|
+
'match': 'contains',
|
54
|
+
'attribute_exists': 'exists',
|
55
|
+
'attribute_not_exists': 'not_exists',
|
56
|
+
'is_in': 'is_in',
|
57
|
+
'in': 'is_in',
|
58
|
+
}
|
59
|
+
|
60
|
+
|
61
|
+
field = Word(alphas + '_' + alphanums + '.').set_results_name('field')
|
62
|
+
|
63
|
+
# Operands and functions
|
64
|
+
operand = oneOf('''
|
65
|
+
= == equals eq is
|
66
|
+
!= <> not_equals ne is_not
|
67
|
+
< less_than lt
|
68
|
+
<= less_than_equals less_than_or_equals lte
|
69
|
+
> greater_than gt
|
70
|
+
>= greater_than_equals greater_than_or_equals gte
|
71
|
+
contains match
|
72
|
+
begins_with
|
73
|
+
between
|
74
|
+
is_in in
|
75
|
+
''', caseless=True).set_results_name('operand')
|
76
|
+
func = oneOf('attribute_exists attribute_not_exists', caseless=True).set_results_name('func')
|
77
|
+
|
78
|
+
# Value parsing
|
79
|
+
variable = Regex(r'\$\{[^}]*\}').set_results_name('variable')
|
80
|
+
quoted_string = (
|
81
|
+
QuotedString('"', unquote_results=False).set_results_name('string') |
|
82
|
+
QuotedString("'", unquote_results=False).set_results_name('string')
|
83
|
+
)
|
84
|
+
integer = pyparsing_common.integer.set_results_name('integer')
|
85
|
+
number = pyparsing_common.number.set_results_name('number')
|
86
|
+
boolean = oneOf('true false', caseless=True)
|
87
|
+
value = variable | quoted_string | number | integer | boolean
|
88
|
+
|
89
|
+
# Equation groups
|
90
|
+
condition_group = Group(field + operand + value)
|
91
|
+
function_group = Group(func + '(' + field + ')')
|
92
|
+
|
93
|
+
expression = Forward()
|
94
|
+
expression <<= infixNotation(
|
95
|
+
(condition_group | function_group), [
|
96
|
+
(oneOf('and', caseless=True), 2, opAssoc.LEFT),
|
97
|
+
(oneOf('or', caseless=True), 2, opAssoc.LEFT),
|
98
|
+
]
|
99
|
+
)
|
100
|
+
|
101
|
+
|
102
|
+
def build_query(query: str | None) -> Dict[str, Any]:
|
103
|
+
"""
|
104
|
+
Build query dict from string.
|
105
|
+
|
106
|
+
:arg query: Query string.
|
107
|
+
:return: Query dict payload.
|
108
|
+
"""
|
109
|
+
try:
|
110
|
+
parsed = expression.parse_string(query)[0].as_list()
|
111
|
+
except ParseException as e:
|
112
|
+
raise nuql.NuqlError(
|
113
|
+
code='ConditionParsingError',
|
114
|
+
message=f'Unable to parse condition: {e}',
|
115
|
+
)
|
116
|
+
|
117
|
+
variables = []
|
118
|
+
|
119
|
+
def recursive_unpack(part: Any, captured_variables: List[str]) -> Dict[str, Any]:
|
120
|
+
"""
|
121
|
+
Recursively unpacks condition parts.
|
122
|
+
|
123
|
+
:arg part: Condition part.
|
124
|
+
:arg captured_variables: List of variables captured in query.
|
125
|
+
:return: Dict representation of condition.
|
126
|
+
"""
|
127
|
+
if is_condition(part):
|
128
|
+
value_is_variable = isinstance(part[2], str) and part[2].startswith('${') and part[2].endswith('}')
|
129
|
+
if value_is_variable:
|
130
|
+
captured_variables.append(part[2].replace('${', '').replace('}', ''))
|
131
|
+
return {
|
132
|
+
'type': 'condition',
|
133
|
+
'field': part[0],
|
134
|
+
'operand': ATTR_OPERATORS[part[1].lower()],
|
135
|
+
**parse_value(part[2])
|
136
|
+
}
|
137
|
+
|
138
|
+
elif is_function(part):
|
139
|
+
return {
|
140
|
+
'type': 'function',
|
141
|
+
'field': part[2],
|
142
|
+
'function': ATTR_OPERATORS[part[0].lower()]
|
143
|
+
}
|
144
|
+
|
145
|
+
elif is_logical_operator(part):
|
146
|
+
return {'type': 'logical_operator', 'operator': part}
|
147
|
+
|
148
|
+
else:
|
149
|
+
return {
|
150
|
+
'type': 'parentheses',
|
151
|
+
'conditions': [recursive_unpack(item, captured_variables) for item in part]
|
152
|
+
}
|
153
|
+
|
154
|
+
result = recursive_unpack(parsed, variables)
|
155
|
+
|
156
|
+
return {'condition': result, 'variables': list(set(variables))}
|
157
|
+
|
158
|
+
|
159
|
+
def is_condition(part: Any):
|
160
|
+
return isinstance(part, list) and all(isinstance(item, str) for item in part[:-1]) and len(part) == 3
|
161
|
+
|
162
|
+
|
163
|
+
def is_function(part: Any):
|
164
|
+
return isinstance(part, list) and all(isinstance(item, str) for item in part) and len(part) == 4
|
165
|
+
|
166
|
+
|
167
|
+
def is_logical_operator(part: Any):
|
168
|
+
return isinstance(part, str) and part in ['and', 'AND', 'or', 'OR']
|
169
|
+
|
170
|
+
|
171
|
+
def parse_value(var: str) -> Dict[str, Any]:
|
172
|
+
"""
|
173
|
+
Parses provided variable.
|
174
|
+
|
175
|
+
:arg var: Variable to parse.
|
176
|
+
:return: Condition result keys.
|
177
|
+
"""
|
178
|
+
# Parse variable
|
179
|
+
try:
|
180
|
+
var = variable.parse_string(var)
|
181
|
+
return {'value_type': 'variable', 'variable': var.variable.replace('${', '').replace('}', '')}
|
182
|
+
except (ParseException, AttributeError):
|
183
|
+
pass
|
184
|
+
|
185
|
+
# Parse quoted string
|
186
|
+
try:
|
187
|
+
var = quoted_string.parse_string(var)
|
188
|
+
return {
|
189
|
+
'value_type': 'string',
|
190
|
+
'value': var.string.lstrip('"').rstrip('"')
|
191
|
+
if var.string.startswith('"')
|
192
|
+
else var.string.lstrip("'").rstrip("'")
|
193
|
+
}
|
194
|
+
except (ParseException, AttributeError):
|
195
|
+
pass
|
196
|
+
|
197
|
+
# Parse integer type
|
198
|
+
if isinstance(var, int):
|
199
|
+
return {'value_type': 'integer', 'value': var}
|
200
|
+
|
201
|
+
# Parse number
|
202
|
+
if isinstance(var, float):
|
203
|
+
return {'value_type': 'number', 'value': var}
|
204
|
+
|
205
|
+
if isinstance(var, str) and var.lower() == 'true':
|
206
|
+
return {'value_type': 'boolean', 'value': True}
|
207
|
+
|
208
|
+
if isinstance(var, str) and var.lower() == 'false':
|
209
|
+
return {'value_type': 'boolean', 'value': False}
|
210
|
+
|
211
|
+
raise nuql.NuqlError(code='ConditionParsingError', message=f'Unable to parse variable \'{var}\' in condition.')
|
@@ -0,0 +1,200 @@
|
|
1
|
+
__all__ = ['KeyCondition']
|
2
|
+
|
3
|
+
from typing import Dict, Any
|
4
|
+
|
5
|
+
from boto3.dynamodb.conditions import Key, ConditionExpressionBuilder
|
6
|
+
|
7
|
+
import nuql
|
8
|
+
from nuql import resources
|
9
|
+
|
10
|
+
|
11
|
+
KEY_OPERANDS = {
|
12
|
+
# Equals
|
13
|
+
'equals': 'eq',
|
14
|
+
'=': 'eq',
|
15
|
+
'==': 'eq',
|
16
|
+
'eq': 'eq',
|
17
|
+
|
18
|
+
# Less than
|
19
|
+
'less_than': 'lt',
|
20
|
+
'<': 'lt',
|
21
|
+
'lt': 'lt',
|
22
|
+
|
23
|
+
# Less than or equal to
|
24
|
+
'less_than_or_equals': 'lte',
|
25
|
+
'less_than_equals': 'lte',
|
26
|
+
'<=': 'lte',
|
27
|
+
'lte': 'lte',
|
28
|
+
|
29
|
+
# Greater than
|
30
|
+
'greater_than': 'gt',
|
31
|
+
'>': 'gt',
|
32
|
+
'gt': 'gt',
|
33
|
+
|
34
|
+
# Greater than or equal to
|
35
|
+
'greater_than_or_equals': 'gte',
|
36
|
+
'greater_than_equals': 'gte',
|
37
|
+
'>=': 'gte',
|
38
|
+
'gte': 'gte',
|
39
|
+
|
40
|
+
# Special
|
41
|
+
'begins_with': 'begins_with',
|
42
|
+
'between': 'between',
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
class KeyCondition:
|
47
|
+
def __init__(self, table: 'resources.Table', condition: Dict[str, Any] | None = None, index_name: str = 'primary'):
|
48
|
+
"""
|
49
|
+
DynamoDB key condition builder.
|
50
|
+
|
51
|
+
:arg table: Table instance.
|
52
|
+
:param condition: Condition dict.
|
53
|
+
:param index_name: Optional index name.
|
54
|
+
"""
|
55
|
+
if not isinstance(condition, (dict, type(None))):
|
56
|
+
raise nuql.NuqlError(code='KeyConditionError', message='Key condition must be a dict or None.')
|
57
|
+
|
58
|
+
if not condition:
|
59
|
+
condition = {}
|
60
|
+
|
61
|
+
self.index_name = index_name
|
62
|
+
|
63
|
+
if index_name == 'primary':
|
64
|
+
self.index = table.indexes.primary
|
65
|
+
else:
|
66
|
+
self.index = table.indexes.get_index(index_name)
|
67
|
+
|
68
|
+
if self.index['hash'] not in condition:
|
69
|
+
condition[self.index['hash']] = None
|
70
|
+
|
71
|
+
parsed_conditions = {}
|
72
|
+
|
73
|
+
for key, value in condition.items():
|
74
|
+
# Validate that the key exists
|
75
|
+
if key not in table.fields:
|
76
|
+
raise nuql.NuqlError(code='KeyConditionError', message=f'Field \'{key}\' is not defined in the schema.')
|
77
|
+
|
78
|
+
field = table.fields[key]
|
79
|
+
|
80
|
+
is_hash_key = key == self.index['hash']
|
81
|
+
is_sort_key = key == self.index.get('sort')
|
82
|
+
projects_to_hash = self.index['hash'] in field.projected_from
|
83
|
+
projects_to_sort = self.index.get('sort') in field.projected_from
|
84
|
+
|
85
|
+
# Validate that the key is available on the index
|
86
|
+
if not any([is_hash_key, is_sort_key, projects_to_hash, projects_to_sort]):
|
87
|
+
raise nuql.NuqlError(
|
88
|
+
code='KeyConditionError',
|
89
|
+
message=f'Field \'{key}\' cannot be used in a key condition on \'{index_name}\' index '
|
90
|
+
f'as it is not a hash/sort key and it doesn\'t project to the hash/sort key.'
|
91
|
+
)
|
92
|
+
|
93
|
+
operand, condition_value = self.extract_condition(key, value)
|
94
|
+
|
95
|
+
# Directly set the key field where not projected
|
96
|
+
if is_hash_key or is_sort_key:
|
97
|
+
parsed_conditions[key] = [operand, condition_value]
|
98
|
+
|
99
|
+
# Process projected field
|
100
|
+
else:
|
101
|
+
key_name = self.index['hash'] if is_hash_key else self.index['sort']
|
102
|
+
|
103
|
+
if key_name not in parsed_conditions:
|
104
|
+
parsed_conditions[key_name] = ['eq', {}]
|
105
|
+
|
106
|
+
if parsed_conditions[key_name][0] != 'eq' and operand != 'eq':
|
107
|
+
raise nuql.NuqlError(
|
108
|
+
code='KeyConditionError',
|
109
|
+
message=f'Multiple non-equals operators provided for the key \'{key_name}\' '
|
110
|
+
'will result in an ambiguous key condition.'
|
111
|
+
)
|
112
|
+
|
113
|
+
if operand != 'eq':
|
114
|
+
parsed_conditions[key_name][0] = operand
|
115
|
+
|
116
|
+
parsed_conditions[key_name][1][key] = condition_value
|
117
|
+
|
118
|
+
self.condition = None
|
119
|
+
validator = resources.Validator()
|
120
|
+
|
121
|
+
# Generate key condition
|
122
|
+
for key, (operand, value) in parsed_conditions.items():
|
123
|
+
field = table.fields[key]
|
124
|
+
key_obj = Key(key)
|
125
|
+
|
126
|
+
if operand == 'between':
|
127
|
+
if len(value) != 2:
|
128
|
+
raise nuql.NuqlError(
|
129
|
+
code='KeyConditionError',
|
130
|
+
message=f'Between operator requires exactly two values for the key \'{key}\'.'
|
131
|
+
)
|
132
|
+
serialised_value = (field(value[0], 'query', validator), field(value[1], 'query', validator))
|
133
|
+
key_condition = getattr(key_obj, operand)(*serialised_value)
|
134
|
+
else:
|
135
|
+
serialised_value = field(value, 'query', validator)
|
136
|
+
key_condition = getattr(key_obj, operand)(serialised_value)
|
137
|
+
|
138
|
+
if self.condition is None:
|
139
|
+
self.condition = key_condition
|
140
|
+
else:
|
141
|
+
self.condition &= key_condition
|
142
|
+
|
143
|
+
@property
|
144
|
+
def resource_args(self) -> Dict[str, Any]:
|
145
|
+
"""Query request args."""
|
146
|
+
if self.condition is None:
|
147
|
+
raise nuql.NuqlError(code='KeyConditionError', message='Key condition is empty.')
|
148
|
+
|
149
|
+
args: Dict[str, Any] = {'KeyConditionExpression': self.condition}
|
150
|
+
if self.index_name != 'primary':
|
151
|
+
args['IndexName'] = self.index_name
|
152
|
+
|
153
|
+
return args
|
154
|
+
|
155
|
+
@property
|
156
|
+
def client_args(self) -> Dict[str, Any]:
|
157
|
+
"""Boto3 client args for the condition."""
|
158
|
+
if self.condition is None:
|
159
|
+
raise nuql.NuqlError(code='KeyConditionError', message='Key condition is empty.')
|
160
|
+
|
161
|
+
args = {}
|
162
|
+
|
163
|
+
if self.index_name != 'primary':
|
164
|
+
args['IndexName'] = self.index_name
|
165
|
+
|
166
|
+
builder = ConditionExpressionBuilder()
|
167
|
+
expression = builder.build_expression(self.condition, is_key_condition=True)
|
168
|
+
|
169
|
+
args['KeyConditionExpression'] = getattr(expression, 'condition_expression')
|
170
|
+
args['ExpressionAttributeNames'] = getattr(expression, 'attribute_name_placeholders')
|
171
|
+
args['ExpressionAttributeValues'] = getattr(expression, 'attribute_value_placeholders')
|
172
|
+
|
173
|
+
return args
|
174
|
+
|
175
|
+
@staticmethod
|
176
|
+
def extract_condition(key: str, value: Any) -> (str, Any):
|
177
|
+
"""
|
178
|
+
Parses and extracts the operand and value from the condition.
|
179
|
+
|
180
|
+
:arg key: Condition key.
|
181
|
+
:arg value: Condition value or dict.
|
182
|
+
:return: Tuple containing the operand and value.
|
183
|
+
"""
|
184
|
+
value_keys = list(value.keys()) if isinstance(value, dict) else []
|
185
|
+
if isinstance(value, dict) and all([x.lower() in KEY_OPERANDS for x in value.keys()]):
|
186
|
+
if len(value_keys) > 1:
|
187
|
+
raise nuql.NuqlError(
|
188
|
+
code='KeyConditionError',
|
189
|
+
message=f'Multiple operators provided for the key \'{key}\' (' + ', '.join(value_keys) + ').'
|
190
|
+
)
|
191
|
+
|
192
|
+
condition_dict = next(iter(value.items()))
|
193
|
+
|
194
|
+
operand = KEY_OPERANDS[condition_dict[0].lower()]
|
195
|
+
condition_value = condition_dict[1]
|
196
|
+
else:
|
197
|
+
operand = 'eq'
|
198
|
+
condition_value = value
|
199
|
+
|
200
|
+
return operand, condition_value
|