nuql 0.0.6__py3-none-any.whl → 0.0.7__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/api/batch_write.py +22 -18
- nuql/api/condition_check.py +1 -1
- nuql/api/delete.py +1 -1
- nuql/api/put_item.py +3 -3
- nuql/api/query/condition.py +4 -11
- nuql/api/query/key_condition.py +148 -83
- nuql/api/query/query.py +3 -3
- nuql/api/transaction.py +4 -4
- nuql/fields/key.py +10 -3
- nuql/fields/string.py +37 -14
- nuql/fields/ulid.py +25 -8
- nuql/fields/uuid.py +7 -14
- nuql/generators/ulid.py +11 -2
- nuql/generators/uuid.py +8 -2
- nuql/resources/fields/field.py +1 -4
- nuql/resources/records/serialiser.py +6 -0
- {nuql-0.0.6.dist-info → nuql-0.0.7.dist-info}/METADATA +1 -3
- {nuql-0.0.6.dist-info → nuql-0.0.7.dist-info}/RECORD +20 -20
- {nuql-0.0.6.dist-info → nuql-0.0.7.dist-info}/WHEEL +0 -0
- {nuql-0.0.6.dist-info → nuql-0.0.7.dist-info}/licenses/LICENSE +0 -0
nuql/api/batch_write.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
__all__ = ['BatchWrite']
|
2
2
|
|
3
|
-
from typing import Dict, Any,
|
3
|
+
from typing import Dict, Any, Literal
|
4
4
|
|
5
5
|
from botocore.exceptions import ClientError
|
6
6
|
|
@@ -51,35 +51,39 @@ class BatchWrite:
|
|
51
51
|
message='Batch write context manager has not been started'
|
52
52
|
)
|
53
53
|
|
54
|
-
def
|
54
|
+
def put(
|
55
|
+
self,
|
55
56
|
table: 'resources.Table',
|
56
57
|
data: Dict[str, Any],
|
58
|
+
serialisation_type: Literal['create', 'update', 'write'] = 'write'
|
57
59
|
) -> None:
|
58
60
|
"""
|
59
61
|
Create a new item on the table as part of a batch write.
|
60
62
|
|
61
63
|
:arg table: Table instance.
|
62
64
|
:arg data: Data to create.
|
65
|
+
:param serialisation_type: Data serialisation type.
|
63
66
|
"""
|
64
67
|
self._validate_started()
|
65
|
-
create = api.Create(self.client, table)
|
66
|
-
args = create.prepare_args(data=data, exclude_condition=True)
|
67
|
-
self._actions['put_item'].append(args)
|
68
68
|
|
69
|
-
|
70
|
-
self,
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
69
|
+
if serialisation_type == 'create':
|
70
|
+
create = api.Create(self.client, table)
|
71
|
+
args = create.prepare_args(data=data, exclude_condition=True)
|
72
|
+
|
73
|
+
elif serialisation_type == 'update':
|
74
|
+
put_update = api.PutUpdate(self.client, table)
|
75
|
+
args = put_update.prepare_args(data=data, exclude_condition=True)
|
76
|
+
|
77
|
+
elif serialisation_type == 'write':
|
78
|
+
put = api.PutItem(self.client, table)
|
79
|
+
args = put.prepare_args(data=data, exclude_condition=True)
|
80
|
+
|
81
|
+
else:
|
82
|
+
raise nuql.NuqlError(
|
83
|
+
code='BatchWriteError',
|
84
|
+
message=f'Invalid serialisation type: {serialisation_type}'
|
85
|
+
)
|
76
86
|
|
77
|
-
:arg table: Table instance.
|
78
|
-
:arg data: Data to update.
|
79
|
-
"""
|
80
|
-
self._validate_started()
|
81
|
-
put_update = api.PutUpdate(self.client, table)
|
82
|
-
args = put_update.prepare_args(data=data, exclude_condition=True)
|
83
87
|
self._actions['put_item'].append(args)
|
84
88
|
|
85
89
|
def delete(
|
nuql/api/condition_check.py
CHANGED
@@ -9,7 +9,7 @@ from nuql.api import Boto3Adapter
|
|
9
9
|
|
10
10
|
|
11
11
|
class ConditionCheck(Boto3Adapter):
|
12
|
-
def prepare_client_args(self, key: Dict[str, Any], condition: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
12
|
+
def prepare_client_args(self, key: Dict[str, Any], condition: str | Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
13
13
|
"""
|
14
14
|
Prepare the request args for a condition check operation against the table (client API).
|
15
15
|
|
nuql/api/delete.py
CHANGED
nuql/api/put_item.py
CHANGED
@@ -16,7 +16,7 @@ class PutItem(Boto3Adapter):
|
|
16
16
|
def prepare_client_args(
|
17
17
|
self,
|
18
18
|
data: Dict[str, Any],
|
19
|
-
condition: Optional[Dict[str, Any]] = None,
|
19
|
+
condition: Optional[str | Dict[str, Any]] = None,
|
20
20
|
exclude_condition: bool = False,
|
21
21
|
**kwargs,
|
22
22
|
) -> Dict[str, Any]:
|
@@ -59,7 +59,7 @@ class PutItem(Boto3Adapter):
|
|
59
59
|
def prepare_args(
|
60
60
|
self,
|
61
61
|
data: Dict[str, Any],
|
62
|
-
condition: Dict[str, Any] | None = None,
|
62
|
+
condition: str | Dict[str, Any] | None = None,
|
63
63
|
exclude_condition: bool = False,
|
64
64
|
**kwargs,
|
65
65
|
) -> Dict[str, Any]:
|
@@ -94,7 +94,7 @@ class PutItem(Boto3Adapter):
|
|
94
94
|
"""
|
95
95
|
pass
|
96
96
|
|
97
|
-
def invoke_sync(self, data: Dict[str, Any], condition: Dict[str, Any] | None = None) -> Dict[str, Any]:
|
97
|
+
def invoke_sync(self, data: Dict[str, Any], condition: str | Dict[str, Any] | None = None) -> Dict[str, Any]:
|
98
98
|
"""
|
99
99
|
Perform a put operation against the table.
|
100
100
|
|
nuql/api/query/condition.py
CHANGED
@@ -13,7 +13,7 @@ class Condition:
|
|
13
13
|
def __init__(
|
14
14
|
self,
|
15
15
|
table: 'resources.Table',
|
16
|
-
condition: Dict[str, Any] | None = None,
|
16
|
+
condition: str | Dict[str, Any] | None = None,
|
17
17
|
condition_type: Literal['FilterExpression', 'ConditionExpression'] = 'FilterExpression',
|
18
18
|
) -> None:
|
19
19
|
"""
|
@@ -23,6 +23,9 @@ class Condition:
|
|
23
23
|
:param condition: Condition dict.
|
24
24
|
:param condition_type: Condition type (FilterExpression or ConditionExpression).
|
25
25
|
"""
|
26
|
+
# Initialise the condition even when it is a string
|
27
|
+
if isinstance(condition, str):
|
28
|
+
condition = {'where': condition, 'variables': {}}
|
26
29
|
|
27
30
|
self.table = table
|
28
31
|
self.variables = condition['variables'] if condition and condition.get('variables') else {}
|
@@ -88,16 +91,6 @@ class Condition:
|
|
88
91
|
field_name = part['field']
|
89
92
|
field = self.table.fields.get(field_name)
|
90
93
|
|
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
94
|
# Functions are called differently
|
102
95
|
if part['type'] == 'function':
|
103
96
|
expression = getattr(attr, part['function'])()
|
nuql/api/query/key_condition.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
__all__ = ['KeyCondition']
|
2
2
|
|
3
|
-
from typing import Dict, Any
|
3
|
+
from typing import Dict, Any, Tuple, Union
|
4
4
|
|
5
|
-
from boto3.dynamodb.conditions import Key, ConditionExpressionBuilder
|
5
|
+
from boto3.dynamodb.conditions import Key, ConditionExpressionBuilder, ComparisonCondition
|
6
6
|
|
7
7
|
import nuql
|
8
|
-
from nuql import resources
|
8
|
+
from nuql import resources, types
|
9
9
|
|
10
10
|
|
11
11
|
KEY_OPERANDS = {
|
@@ -59,20 +59,88 @@ class KeyCondition:
|
|
59
59
|
condition = {}
|
60
60
|
|
61
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)
|
62
|
+
self.index = self._resolve_index(table, index_name)
|
67
63
|
|
68
64
|
pk_field = table.fields[self.index['hash']]
|
69
65
|
sk_field = table.fields.get(self.index.get('sort'))
|
70
66
|
|
67
|
+
# Key fields that only contain fixed values should always be included
|
71
68
|
if pk_field.auto_include_key_condition:
|
72
69
|
condition[self.index['hash']] = None
|
73
|
-
if sk_field
|
70
|
+
if sk_field.auto_include_key_condition:
|
74
71
|
condition[self.index.get('sort')] = None
|
75
72
|
|
73
|
+
parsed_conditions = self.parse_conditions(table, condition, index_name)
|
74
|
+
self.condition = self.build_condition_expression(table, parsed_conditions, index_name)
|
75
|
+
|
76
|
+
@property
|
77
|
+
def resource_args(self) -> Dict[str, Any]:
|
78
|
+
"""Query request args."""
|
79
|
+
if self.condition is None:
|
80
|
+
raise nuql.NuqlError(code='KeyConditionError', message='Key condition is empty.')
|
81
|
+
|
82
|
+
args: Dict[str, Any] = {'KeyConditionExpression': self.condition}
|
83
|
+
if self.index_name != 'primary':
|
84
|
+
args['IndexName'] = self.index_name
|
85
|
+
|
86
|
+
return args
|
87
|
+
|
88
|
+
@property
|
89
|
+
def client_args(self) -> Dict[str, Any]:
|
90
|
+
"""Boto3 client args for the condition."""
|
91
|
+
if self.condition is None:
|
92
|
+
raise nuql.NuqlError(code='KeyConditionError', message='Key condition is empty.')
|
93
|
+
|
94
|
+
args = {}
|
95
|
+
|
96
|
+
if self.index_name != 'primary':
|
97
|
+
args['IndexName'] = self.index_name
|
98
|
+
|
99
|
+
builder = ConditionExpressionBuilder()
|
100
|
+
expression = builder.build_expression(self.condition, is_key_condition=True)
|
101
|
+
|
102
|
+
args['KeyConditionExpression'] = getattr(expression, 'condition_expression')
|
103
|
+
args['ExpressionAttributeNames'] = getattr(expression, 'attribute_name_placeholders')
|
104
|
+
args['ExpressionAttributeValues'] = getattr(expression, 'attribute_value_placeholders')
|
105
|
+
|
106
|
+
return args
|
107
|
+
|
108
|
+
@staticmethod
|
109
|
+
def extract_condition(key: str, value: Any) -> Tuple[str, Any]:
|
110
|
+
"""
|
111
|
+
Parses and extracts the operand and value from the condition.
|
112
|
+
|
113
|
+
:arg key: Condition key.
|
114
|
+
:arg value: Condition value or dict.
|
115
|
+
:return: Tuple containing the operand and value.
|
116
|
+
"""
|
117
|
+
value_keys = list(value.keys()) if isinstance(value, dict) else []
|
118
|
+
if isinstance(value, dict) and all([x.lower() in KEY_OPERANDS for x in value.keys()]):
|
119
|
+
if len(value_keys) > 1:
|
120
|
+
raise nuql.NuqlError(
|
121
|
+
code='KeyConditionError',
|
122
|
+
message=f'Multiple operators provided for the key \'{key}\' (' + ', '.join(value_keys) + ').'
|
123
|
+
)
|
124
|
+
|
125
|
+
condition_dict = next(iter(value.items()))
|
126
|
+
|
127
|
+
operand = KEY_OPERANDS[condition_dict[0].lower()]
|
128
|
+
condition_value = condition_dict[1]
|
129
|
+
else:
|
130
|
+
operand = 'eq'
|
131
|
+
condition_value = value
|
132
|
+
|
133
|
+
return operand, condition_value
|
134
|
+
|
135
|
+
def parse_conditions(self, table: 'resources.Table', condition: Dict[str, Any], index_name: str) -> Dict[str, Any]:
|
136
|
+
"""
|
137
|
+
Parse the condition dict handling projected fields.
|
138
|
+
|
139
|
+
:arg table: Table instance.
|
140
|
+
:arg condition: Condition dict.
|
141
|
+
:arg index_name: Index name.
|
142
|
+
:return: Parsed condition dict.
|
143
|
+
"""
|
76
144
|
parsed_conditions = {}
|
77
145
|
|
78
146
|
for key, value in condition.items():
|
@@ -87,8 +155,15 @@ class KeyCondition:
|
|
87
155
|
projects_to_hash = self.index['hash'] in field.projected_from
|
88
156
|
projects_to_sort = self.index.get('sort') in field.projected_from
|
89
157
|
|
158
|
+
# Resolve projected fields to their respective key(s)
|
159
|
+
projected_keys = []
|
160
|
+
if projects_to_hash:
|
161
|
+
projected_keys.append(self.index['hash'])
|
162
|
+
if projects_to_sort:
|
163
|
+
projected_keys.append(self.index['sort'])
|
164
|
+
|
90
165
|
# Validate that the key is available on the index
|
91
|
-
if not
|
166
|
+
if not is_hash_key and not is_sort_key and not projects_to_hash and not projects_to_sort:
|
92
167
|
raise nuql.NuqlError(
|
93
168
|
code='KeyConditionError',
|
94
169
|
message=f'Field \'{key}\' cannot be used in a key condition on \'{index_name}\' index '
|
@@ -103,16 +178,14 @@ class KeyCondition:
|
|
103
178
|
|
104
179
|
# Process projected field
|
105
180
|
else:
|
106
|
-
projected_keys = []
|
107
|
-
if projects_to_hash:
|
108
|
-
projected_keys.append(self.index['hash'])
|
109
|
-
if projects_to_sort:
|
110
|
-
projected_keys.append(self.index['sort'])
|
111
|
-
|
112
181
|
for key_name in projected_keys:
|
113
182
|
if key_name not in parsed_conditions:
|
114
183
|
parsed_conditions[key_name] = ['eq', {}]
|
115
184
|
|
185
|
+
parsed_conditions[key_name][1][key] = condition_value
|
186
|
+
|
187
|
+
# Allow a key with projections to have a greater_than or begins_with operator
|
188
|
+
# without disrupting the integrity of the condition
|
116
189
|
if parsed_conditions[key_name][0] != 'eq' and operand != 'eq':
|
117
190
|
raise nuql.NuqlError(
|
118
191
|
code='KeyConditionError',
|
@@ -120,102 +193,94 @@ class KeyCondition:
|
|
120
193
|
'will result in an ambiguous key condition.'
|
121
194
|
)
|
122
195
|
|
196
|
+
# The first non-equals operand becomes the winner
|
123
197
|
if operand != 'eq':
|
124
198
|
parsed_conditions[key_name][0] = operand
|
125
199
|
|
126
|
-
|
200
|
+
if self.index['hash'] not in parsed_conditions:
|
201
|
+
raise nuql.NuqlError(
|
202
|
+
code='KeyConditionError',
|
203
|
+
message=f'Hash key \'{self.index["hash"]}\' is required in the key condition '
|
204
|
+
f'but was not provided nor could be inferred from the schema.'
|
205
|
+
)
|
206
|
+
|
207
|
+
return parsed_conditions
|
208
|
+
|
209
|
+
def build_condition_expression(
|
210
|
+
self,
|
211
|
+
table: 'resources.Table',
|
212
|
+
parsed_conditions: Dict[str, Any],
|
213
|
+
index_name: str
|
214
|
+
) -> ComparisonCondition:
|
215
|
+
"""
|
216
|
+
Builds the final condition expression to be used in the query.
|
127
217
|
|
128
|
-
|
218
|
+
:arg table: Table instance.
|
219
|
+
:arg parsed_conditions: Parsed conditions dict.
|
220
|
+
:arg index_name: Index name.
|
221
|
+
:return: ComparisonCondition instance.
|
222
|
+
"""
|
223
|
+
condition = None
|
129
224
|
validator = resources.Validator()
|
130
225
|
|
131
226
|
# Generate key condition
|
132
227
|
for key, (operand, value) in parsed_conditions.items():
|
228
|
+
|
133
229
|
field = table.fields[key]
|
230
|
+
|
134
231
|
key_obj = Key(key)
|
232
|
+
key_condition_args = set()
|
135
233
|
|
234
|
+
# Special case for the between operator
|
136
235
|
if operand == 'between':
|
137
236
|
if len(value) != 2:
|
138
237
|
raise nuql.NuqlError(
|
139
238
|
code='KeyConditionError',
|
140
239
|
message=f'Between operator requires exactly two values for the key \'{key}\'.'
|
141
240
|
)
|
142
|
-
|
143
|
-
|
241
|
+
key_condition_args.add(field(value[0], 'query', validator))
|
242
|
+
key_condition_args.add(field(value[1], 'query', validator))
|
243
|
+
|
244
|
+
# All other operators use a single value
|
144
245
|
else:
|
145
|
-
|
146
|
-
key_condition = getattr(key_obj, operand)(serialised_value)
|
246
|
+
key_condition_args.add(field(value, 'query', validator))
|
147
247
|
|
148
248
|
is_partial = key in validator.partial_keys
|
149
249
|
|
250
|
+
# Disallow partial keys on hash keys
|
150
251
|
if key == self.index['hash'] and is_partial:
|
151
252
|
raise nuql.NuqlError(
|
152
253
|
code='KeyConditionError',
|
153
|
-
message=f'Partial key \'{key}\' cannot be used in a key condition on \'{index_name}\'
|
254
|
+
message=f'Partial key \'{key}\' cannot be used in a key condition on \'{index_name}\' '
|
255
|
+
f'index as it is the hash key for the index.'
|
154
256
|
)
|
155
257
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
self.condition = key_condition
|
161
|
-
else:
|
162
|
-
self.condition &= key_condition
|
163
|
-
|
164
|
-
@property
|
165
|
-
def resource_args(self) -> Dict[str, Any]:
|
166
|
-
"""Query request args."""
|
167
|
-
if self.condition is None:
|
168
|
-
raise nuql.NuqlError(code='KeyConditionError', message='Key condition is empty.')
|
169
|
-
|
170
|
-
args: Dict[str, Any] = {'KeyConditionExpression': self.condition}
|
171
|
-
if self.index_name != 'primary':
|
172
|
-
args['IndexName'] = self.index_name
|
173
|
-
|
174
|
-
return args
|
175
|
-
|
176
|
-
@property
|
177
|
-
def client_args(self) -> Dict[str, Any]:
|
178
|
-
"""Boto3 client args for the condition."""
|
179
|
-
if self.condition is None:
|
180
|
-
raise nuql.NuqlError(code='KeyConditionError', message='Key condition is empty.')
|
181
|
-
|
182
|
-
args = {}
|
183
|
-
|
184
|
-
if self.index_name != 'primary':
|
185
|
-
args['IndexName'] = self.index_name
|
186
|
-
|
187
|
-
builder = ConditionExpressionBuilder()
|
188
|
-
expression = builder.build_expression(self.condition, is_key_condition=True)
|
189
|
-
|
190
|
-
args['KeyConditionExpression'] = getattr(expression, 'condition_expression')
|
191
|
-
args['ExpressionAttributeNames'] = getattr(expression, 'attribute_name_placeholders')
|
192
|
-
args['ExpressionAttributeValues'] = getattr(expression, 'attribute_value_placeholders')
|
193
|
-
|
194
|
-
return args
|
195
|
-
|
196
|
-
@staticmethod
|
197
|
-
def extract_condition(key: str, value: Any) -> (str, Any):
|
198
|
-
"""
|
199
|
-
Parses and extracts the operand and value from the condition.
|
200
|
-
|
201
|
-
:arg key: Condition key.
|
202
|
-
:arg value: Condition value or dict.
|
203
|
-
:return: Tuple containing the operand and value.
|
204
|
-
"""
|
205
|
-
value_keys = list(value.keys()) if isinstance(value, dict) else []
|
206
|
-
if isinstance(value, dict) and all([x.lower() in KEY_OPERANDS for x in value.keys()]):
|
207
|
-
if len(value_keys) > 1:
|
258
|
+
# Partial sort key is allowed, but we must switch the operand to begins_with
|
259
|
+
if is_partial and operand == 'eq':
|
260
|
+
operand = 'begins_with'
|
261
|
+
elif is_partial and operand != 'begins_with':
|
208
262
|
raise nuql.NuqlError(
|
209
263
|
code='KeyConditionError',
|
210
|
-
message=f'
|
264
|
+
message=f'Operator \'{operand}\' is not supported for the key \'{key}\' '
|
265
|
+
f'as it results in a partial key. Only \'begins_with\' is supported.'
|
211
266
|
)
|
212
267
|
|
213
|
-
|
268
|
+
key_condition = getattr(key_obj, operand)(*key_condition_args)
|
214
269
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
condition_value = value
|
270
|
+
if condition is None:
|
271
|
+
condition = key_condition
|
272
|
+
else:
|
273
|
+
condition &= key_condition
|
220
274
|
|
221
|
-
return
|
275
|
+
return condition
|
276
|
+
|
277
|
+
@staticmethod
|
278
|
+
def _resolve_index(
|
279
|
+
table: 'resources.Table',
|
280
|
+
index_name: str
|
281
|
+
) -> Union['types.PrimaryIndex', 'types.SecondaryIndex',]:
|
282
|
+
"""Resolves the index to query against"""
|
283
|
+
if index_name == 'primary':
|
284
|
+
return table.indexes.primary
|
285
|
+
else:
|
286
|
+
return table.indexes.get_index(index_name)
|
nuql/api/query/query.py
CHANGED
@@ -12,7 +12,7 @@ class Query(api.Boto3Adapter):
|
|
12
12
|
def prepare_client_args(
|
13
13
|
self,
|
14
14
|
key_condition: Dict[str, Any] | None = None,
|
15
|
-
condition: Dict[str, Any] | None = None,
|
15
|
+
condition: str | Dict[str, Any] | None = None,
|
16
16
|
index_name: str = 'primary',
|
17
17
|
limit: int | None = None,
|
18
18
|
scan_index_forward: bool = True,
|
@@ -52,7 +52,7 @@ class Query(api.Boto3Adapter):
|
|
52
52
|
def prepare_args(
|
53
53
|
self,
|
54
54
|
key_condition: Dict[str, Any] | None = None,
|
55
|
-
condition: Dict[str, Any] | None = None,
|
55
|
+
condition: str | Dict[str, Any] | None = None,
|
56
56
|
index_name: str = 'primary',
|
57
57
|
limit: int | None = None,
|
58
58
|
scan_index_forward: bool = True,
|
@@ -92,7 +92,7 @@ class Query(api.Boto3Adapter):
|
|
92
92
|
def invoke_sync(
|
93
93
|
self,
|
94
94
|
key_condition: Dict[str, Any] | None = None,
|
95
|
-
condition: Dict[str, Any] | None = None,
|
95
|
+
condition: str | Dict[str, Any] | None = None,
|
96
96
|
index_name: str = 'primary',
|
97
97
|
limit: int | None = None,
|
98
98
|
scan_index_forward: bool = True,
|
nuql/api/transaction.py
CHANGED
@@ -66,7 +66,7 @@ class Transaction:
|
|
66
66
|
self,
|
67
67
|
table: 'resources.Table',
|
68
68
|
data: Dict[str, Any],
|
69
|
-
condition: Dict[str, Any] | None = None,
|
69
|
+
condition: str | Dict[str, Any] | None = None,
|
70
70
|
) -> None:
|
71
71
|
"""
|
72
72
|
Create a new item on a table as part of a transaction.
|
@@ -86,7 +86,7 @@ class Transaction:
|
|
86
86
|
self,
|
87
87
|
table: 'resources.Table',
|
88
88
|
data: Dict[str, Any],
|
89
|
-
condition: Dict[str, Any] | None = None,
|
89
|
+
condition: str | Dict[str, Any] | None = None,
|
90
90
|
shallow: bool = False,
|
91
91
|
) -> None:
|
92
92
|
"""
|
@@ -108,7 +108,7 @@ class Transaction:
|
|
108
108
|
self,
|
109
109
|
table: 'resources.Table',
|
110
110
|
key: Dict[str, Any],
|
111
|
-
condition: Dict[str, Any] | None = None,
|
111
|
+
condition: str | Dict[str, Any] | None = None,
|
112
112
|
) -> None:
|
113
113
|
"""
|
114
114
|
Delete an item on a table as part of a transaction.
|
@@ -128,7 +128,7 @@ class Transaction:
|
|
128
128
|
self,
|
129
129
|
table: 'resources.Table',
|
130
130
|
key: Dict[str, Any],
|
131
|
-
condition: Dict[str, Any],
|
131
|
+
condition: str | Dict[str, Any],
|
132
132
|
) -> None:
|
133
133
|
"""
|
134
134
|
Perform a condition check on an item as part of a transaction.
|
nuql/fields/key.py
CHANGED
@@ -30,6 +30,7 @@ class Key(resources.FieldBase):
|
|
30
30
|
|
31
31
|
# Skip fixed value fields
|
32
32
|
if not projected_name:
|
33
|
+
auto_include_map[key] = True
|
33
34
|
continue
|
34
35
|
|
35
36
|
# Validate projected key exists on the table
|
@@ -105,11 +106,17 @@ class Key(resources.FieldBase):
|
|
105
106
|
f'\'{self.name}\') is not defined in the schema'
|
106
107
|
)
|
107
108
|
|
108
|
-
is_partial = is_partial or (key not in key_dict and not projected_field.default)
|
109
|
-
|
110
109
|
projected_value = key_dict.get(projected_name) or EmptyValue()
|
111
110
|
serialised_value = projected_field(projected_value, action, validator)
|
112
|
-
|
111
|
+
|
112
|
+
is_partial = (is_partial or
|
113
|
+
(key not in key_dict and not projected_field.default) or
|
114
|
+
isinstance(projected_value, EmptyValue))
|
115
|
+
|
116
|
+
if isinstance(projected_value, EmptyValue):
|
117
|
+
break
|
118
|
+
|
119
|
+
used_value = s(serialised_value) if not isinstance(serialised_value, (type(None), EmptyValue)) else None
|
113
120
|
else:
|
114
121
|
used_value = s(value)
|
115
122
|
|
nuql/fields/string.py
CHANGED
@@ -91,28 +91,30 @@ class String(resources.FieldBase):
|
|
91
91
|
"""
|
92
92
|
Serialises a template string.
|
93
93
|
|
94
|
+
If a required projected value is missing (no user value and no default),
|
95
|
+
the output is truncated right before that placeholder and the result is marked partial.
|
96
|
+
|
94
97
|
:arg value: Dict of projections.
|
95
98
|
:arg action: Serialisation type.
|
96
99
|
:arg validator: Validator instance.
|
97
|
-
:return:
|
100
|
+
:return: Dict with 'value' and 'is_partial'.
|
98
101
|
"""
|
99
102
|
if not isinstance(value, dict):
|
100
103
|
value = {}
|
101
104
|
|
102
105
|
is_partial = False
|
106
|
+
template_str = self.value or ""
|
107
|
+
output_parts: list[str] = []
|
103
108
|
|
104
|
-
|
105
|
-
for key in self.find_projections(self.value):
|
106
|
-
if key not in value:
|
107
|
-
is_partial = True
|
108
|
-
value[key] = None
|
109
|
+
last_idx = 0
|
109
110
|
|
110
|
-
|
111
|
+
# Walk through template pieces and placeholders in order
|
112
|
+
for match in re.finditer(TEMPLATE_PATTERN, template_str):
|
113
|
+
key = match.group(1)
|
114
|
+
# Append literal chunk before this placeholder
|
115
|
+
literal_chunk = template_str[last_idx:match.start()]
|
111
116
|
|
112
|
-
# Serialise values before substituting
|
113
|
-
for key, deserialised_value in value.items():
|
114
117
|
field = self.parent.fields.get(key)
|
115
|
-
|
116
118
|
if not field:
|
117
119
|
raise nuql.NuqlError(
|
118
120
|
code='TemplateStringError',
|
@@ -120,11 +122,32 @@ class String(resources.FieldBase):
|
|
120
122
|
f'\'{self.name}\') is not defined in the schema'
|
121
123
|
)
|
122
124
|
|
123
|
-
|
124
|
-
|
125
|
+
provided = key in value
|
126
|
+
provided_value = value.get(key)
|
127
|
+
|
128
|
+
# If not provided and no default, we mark partial and stop right before this placeholder
|
129
|
+
if (not provided) and (field.default is None):
|
130
|
+
is_partial = True
|
131
|
+
# Keep only what we have so far and the literal up to this point
|
132
|
+
output_parts.append(literal_chunk)
|
133
|
+
break
|
134
|
+
|
135
|
+
# Serialise the projected value (use EmptyValue to allow defaults)
|
136
|
+
serialised_value = field(provided_value or EmptyValue(), action, validator)
|
137
|
+
serialised_text = serialised_value if serialised_value else ''
|
138
|
+
|
139
|
+
# Append literal + substituted value
|
140
|
+
output_parts.append(literal_chunk)
|
141
|
+
output_parts.append(str(serialised_text))
|
142
|
+
|
143
|
+
# Advance past this placeholder
|
144
|
+
last_idx = match.end()
|
145
|
+
|
146
|
+
# If not partial, append the remaining tail literal
|
147
|
+
if not is_partial:
|
148
|
+
output_parts.append(template_str[last_idx:])
|
125
149
|
|
126
|
-
|
127
|
-
return {'value': template.substitute(serialised), 'is_partial': is_partial}
|
150
|
+
return {'value': ''.join(output_parts), 'is_partial': is_partial}
|
128
151
|
|
129
152
|
def deserialise_template(self, value: str | None) -> Dict[str, Any]:
|
130
153
|
"""
|
nuql/fields/ulid.py
CHANGED
@@ -1,25 +1,34 @@
|
|
1
1
|
__all__ = ['Ulid']
|
2
2
|
|
3
|
-
from
|
3
|
+
from typing import Any
|
4
4
|
|
5
|
+
import nuql
|
5
6
|
from nuql import resources
|
6
7
|
|
7
8
|
|
8
9
|
class Ulid(resources.FieldBase):
|
9
10
|
type = 'ulid'
|
10
11
|
|
11
|
-
def serialise(self, value:
|
12
|
+
def serialise(self, value: Any) -> str | None:
|
12
13
|
"""
|
13
14
|
Serialises a ULID value.
|
14
15
|
|
15
16
|
:arg value: ULID, str or None.
|
16
17
|
:return: str or None.
|
17
18
|
"""
|
18
|
-
|
19
|
+
try:
|
20
|
+
import ulid
|
21
|
+
except ImportError:
|
22
|
+
raise nuql.NuqlError(
|
23
|
+
code='DependencyError',
|
24
|
+
message='The "python-ulid" package is required to use the ULID field.'
|
25
|
+
) from None
|
26
|
+
|
27
|
+
if isinstance(value, ulid.ULID):
|
19
28
|
return str(value)
|
20
29
|
if isinstance(value, str):
|
21
30
|
try:
|
22
|
-
return str(ULID.from_str(value))
|
31
|
+
return str(ulid.ULID.from_str(value))
|
23
32
|
except ValueError:
|
24
33
|
return None
|
25
34
|
return None
|
@@ -31,9 +40,17 @@ class Ulid(resources.FieldBase):
|
|
31
40
|
:arg value: str or None.
|
32
41
|
:return: str or None.
|
33
42
|
"""
|
43
|
+
try:
|
44
|
+
import ulid
|
45
|
+
except ImportError:
|
46
|
+
raise nuql.NuqlError(
|
47
|
+
code='DependencyError',
|
48
|
+
message='The "python-ulid" package is required to use the ULID field.'
|
49
|
+
) from None
|
50
|
+
|
34
51
|
if isinstance(value, str):
|
35
|
-
|
36
|
-
return str(ULID.from_str(value))
|
37
|
-
|
38
|
-
|
52
|
+
try:
|
53
|
+
return str(ulid.ULID.from_str(value))
|
54
|
+
except ValueError:
|
55
|
+
return None
|
39
56
|
return None
|
nuql/fields/uuid.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
__all__ = ['Uuid']
|
2
2
|
|
3
|
-
from uuid import UUID
|
4
|
-
from uuid_utils import UUID
|
3
|
+
from uuid import UUID
|
5
4
|
|
6
5
|
from nuql import resources
|
7
6
|
|
@@ -9,23 +8,17 @@ from nuql import resources
|
|
9
8
|
class Uuid(resources.FieldBase):
|
10
9
|
type = 'uuid'
|
11
10
|
|
12
|
-
def serialise(self, value:
|
11
|
+
def serialise(self, value: UUID | str | None) -> str | None:
|
13
12
|
"""
|
14
13
|
Serialises a UUID value.
|
15
14
|
|
16
15
|
:arg value: UUID, str or None.
|
17
16
|
:return: str or None.
|
18
17
|
"""
|
19
|
-
|
20
|
-
return str(value)
|
21
|
-
|
22
|
-
|
23
|
-
try:
|
24
|
-
return str(UUID(value))
|
25
|
-
except ValueError:
|
26
|
-
return None
|
27
|
-
|
28
|
-
return None
|
18
|
+
try:
|
19
|
+
return str(UUID(value))
|
20
|
+
except (AttributeError, ValueError, TypeError):
|
21
|
+
return None
|
29
22
|
|
30
23
|
def deserialise(self, value: str | None) -> str | None:
|
31
24
|
"""
|
@@ -37,6 +30,6 @@ class Uuid(resources.FieldBase):
|
|
37
30
|
if isinstance(value, str):
|
38
31
|
try:
|
39
32
|
return str(UUID(value))
|
40
|
-
except ValueError:
|
33
|
+
except (AttributeError, ValueError, TypeError):
|
41
34
|
return None
|
42
35
|
return None
|
nuql/generators/ulid.py
CHANGED
@@ -1,10 +1,19 @@
|
|
1
|
-
from
|
1
|
+
from nuql import NuqlError
|
2
2
|
|
3
3
|
|
4
4
|
class Ulid:
|
5
5
|
@classmethod
|
6
6
|
def now(cls):
|
7
7
|
"""Generates current ULID"""
|
8
|
+
try:
|
9
|
+
import ulid
|
10
|
+
except ImportError:
|
11
|
+
raise NuqlError(
|
12
|
+
code='DependencyError',
|
13
|
+
message='The "python-ulid" package is required to use the ULID field.'
|
14
|
+
)
|
15
|
+
|
8
16
|
def generator():
|
9
|
-
return ULID()
|
17
|
+
return ulid.ULID()
|
18
|
+
|
10
19
|
return generator
|
nuql/generators/uuid.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
__all__ = ['Uuid']
|
2
2
|
|
3
|
-
from uuid_utils import uuid4, uuid7
|
4
|
-
|
5
3
|
|
6
4
|
class Uuid:
|
7
5
|
@classmethod
|
8
6
|
def v4(cls):
|
9
7
|
"""Generates a random UUID v4"""
|
8
|
+
from uuid import uuid4
|
9
|
+
|
10
10
|
def generator():
|
11
11
|
return uuid4()
|
12
12
|
return generator
|
@@ -14,6 +14,12 @@ class Uuid:
|
|
14
14
|
@classmethod
|
15
15
|
def v7(cls):
|
16
16
|
"""Generates a random UUID v7"""
|
17
|
+
try:
|
18
|
+
from uuid_utils import uuid7
|
19
|
+
except ImportError:
|
20
|
+
raise ImportError('Dependency "uuid_utils" must be installed to use UUID v7 generation.')
|
21
|
+
|
17
22
|
def generator():
|
18
23
|
return uuid7()
|
24
|
+
|
19
25
|
return generator
|
nuql/resources/fields/field.py
CHANGED
@@ -96,10 +96,7 @@ class FieldBase:
|
|
96
96
|
value = self.default
|
97
97
|
|
98
98
|
# Serialise the value
|
99
|
-
|
100
|
-
value = self.serialise_internal(value, action, validator)
|
101
|
-
else:
|
102
|
-
value = None
|
99
|
+
value = self.serialise_internal(value, action, validator)
|
103
100
|
|
104
101
|
# Validate required field
|
105
102
|
if self.required and action == 'create' and value is None:
|
@@ -45,6 +45,9 @@ class Serialiser:
|
|
45
45
|
projections = resources.Projections(self.parent, self)
|
46
46
|
output = {}
|
47
47
|
|
48
|
+
if not isinstance(data, dict):
|
49
|
+
data = {}
|
50
|
+
|
48
51
|
# Serialise provided fields
|
49
52
|
for key, deserialised_value in data.items():
|
50
53
|
field = self.get_field(key)
|
@@ -123,6 +126,9 @@ class Serialiser:
|
|
123
126
|
"""
|
124
127
|
record = {}
|
125
128
|
|
129
|
+
if not isinstance(data, dict):
|
130
|
+
data = {}
|
131
|
+
|
126
132
|
for name, field in self.parent.fields.items():
|
127
133
|
# Special Case: string templates
|
128
134
|
if hasattr(field, 'deserialise_template') and getattr(field, 'is_template', True):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: nuql
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.7
|
4
4
|
Summary: Nuql (pronounced 'nuckle') is a lightweight DynamoDB library for implementing the single table model pattern.
|
5
5
|
License: MIT License
|
6
6
|
|
@@ -30,5 +30,3 @@ Classifier: Programming Language :: Python :: 3
|
|
30
30
|
Requires-Python: >=3.13
|
31
31
|
Requires-Dist: boto3>=1.40.0
|
32
32
|
Requires-Dist: pyparsing>=3.2.3
|
33
|
-
Requires-Dist: python-ulid>=3.0.0
|
34
|
-
Requires-Dist: uuid-utils>=0.11.0
|
@@ -4,23 +4,23 @@ nuql/connection.py,sha256=6PusMgUwPoOT8X0CqWLKdgsdvdAEwmb_dfelz0WpJE8,1471
|
|
4
4
|
nuql/exceptions.py,sha256=1dhtHoLJZyP-njCPC2hNTK48tSllbGV7amlwq3tJQps,2044
|
5
5
|
nuql/api/__init__.py,sha256=DKxuIAjckS1vjMIPykbI3h81HszlBB9LdwQuFf53-wo,324
|
6
6
|
nuql/api/adapter.py,sha256=VIrItHyAW87xGgjavXRiN2-99shRjDn0kik_iLHeEl0,1355
|
7
|
-
nuql/api/batch_write.py,sha256=
|
8
|
-
nuql/api/condition_check.py,sha256
|
7
|
+
nuql/api/batch_write.py,sha256=QqtFtLVpZvcy8eyizW3jSjVO_B7D_ev3TJxnd8ppU-c,3233
|
8
|
+
nuql/api/condition_check.py,sha256=-okjA4NQPNMjA2If48X_Rxe3RKFN0AWMH9L5QT9Ln1Y,1594
|
9
9
|
nuql/api/create.py,sha256=SUfsZtRrp1WWILsdwvSGXKdSGIiiRFsKlttX8uS8AMw,793
|
10
|
-
nuql/api/delete.py,sha256=
|
10
|
+
nuql/api/delete.py,sha256=YWHCwYbnx27PE1ZUjn_19w3gTHsmoZRWcDila9eux7c,3044
|
11
11
|
nuql/api/get.py,sha256=8K29b-wRW4szYInPvdgyTe7lq3SsvQf1VaiABxsaw_0,934
|
12
|
-
nuql/api/put_item.py,sha256=
|
12
|
+
nuql/api/put_item.py,sha256=fBW0rsQ6qIHyKUrkbc5rINtBUQtQ9x79w_DksausT_A,4158
|
13
13
|
nuql/api/put_update.py,sha256=ZkNUjX3GkBrymVenoI4RHmjn5Xzh4u1YcRxTte9X22Q,795
|
14
|
-
nuql/api/transaction.py,sha256=
|
14
|
+
nuql/api/transaction.py,sha256=C9gF4i2HmPDoIHIBQUbVP2bfW0NaYoHTIMnc_1IAavg,4711
|
15
15
|
nuql/api/upsert.py,sha256=X7Qwg_1x8-RUUlQZUq6qLKzlzapOLyKMlFBiDMN8QOg,1133
|
16
16
|
nuql/api/batch_get/__init__.py,sha256=N1NQ_WFov4bANyCdS1HBPaPYnlDhZ4QEJSMiEN0sL0k,48
|
17
17
|
nuql/api/batch_get/batch_get.py,sha256=DgGCYLQqiCUzwZdrf1XyWHEecY4nzQ-mweWhLPe68RQ,1069
|
18
18
|
nuql/api/batch_get/queue.py,sha256=6S70p0i4xXlh8pU8X0-F8-U8P3uzoxJDzbUNaMEhrzU,4032
|
19
19
|
nuql/api/query/__init__.py,sha256=_YhXEUvd82bBhOlkBHtPYC8a8VgCHskeg4l95X8w2O0,112
|
20
|
-
nuql/api/query/condition.py,sha256=
|
20
|
+
nuql/api/query/condition.py,sha256=9FxV8rtAyuD_v1tPN20UZztY21PqJkgv3loJc-_6Aqc,5615
|
21
21
|
nuql/api/query/condition_builder.py,sha256=kHlpSYcrEX-vQvkqjOG6GVC2_a63zJSjJgIs2_zhGIo,6201
|
22
|
-
nuql/api/query/key_condition.py,sha256=
|
23
|
-
nuql/api/query/query.py,sha256=
|
22
|
+
nuql/api/query/key_condition.py,sha256=UlxYjvgH9iVors7oEToHkl4vK_IDqbnQKro1bk6LBp0,10845
|
23
|
+
nuql/api/query/query.py,sha256=90ieaZGwWXhpamJBX67y3SDA0oRaNf5YMyxjLWhz8W4,6383
|
24
24
|
nuql/api/update/__init__.py,sha256=15mjrAPhrOM74c3Q6NQXTGVjFFgwPCiTAlBLHIDmPQ8,85
|
25
25
|
nuql/api/update/expression_builder.py,sha256=jFkp0EASDx7uK2b0TanRALGusXqtXwvOVy02WW2OfGM,1147
|
26
26
|
nuql/api/update/update_item.py,sha256=KCfrAZiDPmQc0k20UAA9H3dw_8WFYJ8fDOqHb_xSYNo,5295
|
@@ -31,24 +31,24 @@ nuql/fields/datetime.py,sha256=4K28yydnzUc1HsMH-lgSSNOx2NbEL4qXC3kTpuRBVis,1386
|
|
31
31
|
nuql/fields/datetime_timestamp.py,sha256=drJLpmGhL8ANrfGsArKeVLb4v7maiignhzC4qpQzMHY,1275
|
32
32
|
nuql/fields/float.py,sha256=x48Spo_GNku0djO0Ogs8usZTfwOY-27Mh5OPOQBO3OY,1316
|
33
33
|
nuql/fields/integer.py,sha256=pHs3CJM4xKN60IYg5aTe4QjSQQBR4jp08HyANtidHgs,1312
|
34
|
-
nuql/fields/key.py,sha256=
|
34
|
+
nuql/fields/key.py,sha256=vG_yj4Lkiz3bsD2f31HEVgU5lEde_gTYk4awQ78gko8,6912
|
35
35
|
nuql/fields/list.py,sha256=m7wTIb4-VRRKO5C8XZJFeGOPHIqrvc3xNbWw-vHppQM,1508
|
36
36
|
nuql/fields/map.py,sha256=jFbJ9YhvVMrkLlzktMEQOrZw7uqOLgdfmwxuw9Zue-Y,1169
|
37
|
-
nuql/fields/string.py,sha256
|
38
|
-
nuql/fields/ulid.py,sha256=
|
39
|
-
nuql/fields/uuid.py,sha256=
|
37
|
+
nuql/fields/string.py,sha256=ITzRSuDGomBfRhcbHAVkfRQ7j2kDwBBFPdkzQzVNI1w,6629
|
38
|
+
nuql/fields/ulid.py,sha256=GSaEbi31_gUnZzx-MD-pCR8H_fNCEaLwwnWLxRg4JbI,1498
|
39
|
+
nuql/fields/uuid.py,sha256=iv-_P8W3MRBrt41R8gMsZTEoFF1eS3deoueCgmznUz4,881
|
40
40
|
nuql/generators/__init__.py,sha256=YgTwNlvZYM4EYo2u3gSN4bIuXz7f8SQF7Y7jnpXN_Ls,67
|
41
41
|
nuql/generators/datetime.py,sha256=eX10E9mbCaFVJb3xsVaNxTCpBpl1lsd6HTGYS6hsqNs,981
|
42
|
-
nuql/generators/ulid.py,sha256=
|
43
|
-
nuql/generators/uuid.py,sha256=
|
42
|
+
nuql/generators/ulid.py,sha256=la23skED6VbkqcrxZ16j0RPw8vgy8EM1n33jC2ztf4o,450
|
43
|
+
nuql/generators/uuid.py,sha256=ZxATeYevYhrJu6MceEK-CtNjRj0eBzoacv95icHh-2s,581
|
44
44
|
nuql/resources/__init__.py,sha256=8HRJ_H5yTdQtfeVZ29ikdbLQSPKiZRyNkoocQd5QtZ8,92
|
45
45
|
nuql/resources/fields/__init__.py,sha256=bajUBAvrK8kIMyb2vD_0NCwibRHOLo4Yoq1sac0Dobc,70
|
46
|
-
nuql/resources/fields/field.py,sha256=
|
46
|
+
nuql/resources/fields/field.py,sha256=NSynnwHwrKk7SaCvzpb2Zsjeu1LFNCfk0QR6cIKVvHo,5473
|
47
47
|
nuql/resources/fields/field_map.py,sha256=bOzoBFbB2XOyLfnqaVEL_BDwt3S9_oUGIl1N0awgZ7g,2519
|
48
48
|
nuql/resources/fields/value.py,sha256=CG3PnGeSMHDQciIMR6jkMhogCJZhpEfipLdyk9gnjJw,59
|
49
49
|
nuql/resources/records/__init__.py,sha256=gD8-xt4uIOpx_ZTnw1QTzOiwUpjKhR3nhkZw4AmEvaI,81
|
50
50
|
nuql/resources/records/projections.py,sha256=PTwoc8-XXieXsYw0H-3Fv7G8SzSN__j35QsqempXBj8,1618
|
51
|
-
nuql/resources/records/serialiser.py,sha256=
|
51
|
+
nuql/resources/records/serialiser.py,sha256=5vqLjNzY07Rrl-u_-SufImjAGCxpF8txwzlhDW0OHiE,5169
|
52
52
|
nuql/resources/records/validator.py,sha256=d8GQN_rwnuKnV6tkEDCqswE_ARso7ZNH0vB1L2eVrj0,1558
|
53
53
|
nuql/resources/tables/__init__.py,sha256=Bk8ewpPb_myliQPmyIarzma_Vezd4XR-Z1lABsTUMPQ,46
|
54
54
|
nuql/resources/tables/indexes.py,sha256=5e5bQ_yRgyLmnoXuKAqEw0mJJhY_NE4Sk0vavvMG2m4,5076
|
@@ -60,7 +60,7 @@ nuql/types/__init__.py,sha256=Ea4KR4mUs1RNUEskKF9sXfGpoQw2V9cnIYeE20wkChs,76
|
|
60
60
|
nuql/types/config.py,sha256=lFfPPe62l4DkfLXR8ecw5AiSRAFzsTYy_3a0d_1M7hY,659
|
61
61
|
nuql/types/fields.py,sha256=LjGgXM84WO54gRDkgvnwF3pjFvFxErdGYOvs0BskF_o,883
|
62
62
|
nuql/types/serialisation.py,sha256=XAy_gARcwYmfEuxWAJox2ClWK9ow0F5WXKVbMUVBq9w,242
|
63
|
-
nuql-0.0.
|
64
|
-
nuql-0.0.
|
65
|
-
nuql-0.0.
|
66
|
-
nuql-0.0.
|
63
|
+
nuql-0.0.7.dist-info/METADATA,sha256=gLqBg1Yagk-W7bcv9Ccir4ljUXLhGT7XylyA0MTkPl8,1667
|
64
|
+
nuql-0.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
65
|
+
nuql-0.0.7.dist-info/licenses/LICENSE,sha256=AS8DF6oGYsvk781m40Nec9eCkj_S_oUVAWaFakB2LMs,1097
|
66
|
+
nuql-0.0.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|