nuql 0.0.1__tar.gz → 0.0.3__tar.gz
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-0.0.3/LICENSE +21 -0
- nuql-0.0.3/PKG-INFO +34 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/put_item.py +2 -2
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/key_condition.py +34 -13
- {nuql-0.0.1 → nuql-0.0.3}/nuql/client.py +2 -2
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/key.py +36 -50
- nuql-0.0.3/nuql/fields/list.py +52 -0
- nuql-0.0.3/nuql/fields/map.py +37 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/string.py +35 -53
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/fields/field.py +19 -2
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/fields/field_map.py +2 -2
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/records/validator.py +1 -0
- {nuql-0.0.1 → nuql-0.0.3}/pyproject.toml +2 -1
- nuql-0.0.1/PKG-INFO +0 -12
- nuql-0.0.1/nuql/fields/list.py +0 -90
- nuql-0.0.1/nuql/fields/map.py +0 -67
- {nuql-0.0.1 → nuql-0.0.3}/.gitignore +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/adapter.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/batch_get/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/batch_get/batch_get.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/batch_get/queue.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/batch_write.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/condition_check.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/create.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/delete.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/get.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/put_update.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/condition.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/condition_builder.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/query.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/transaction.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/update/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/update/expression_builder.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/update/update_item.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/update/utils.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/api/upsert.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/connection.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/exceptions.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/boolean.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/datetime.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/datetime_timestamp.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/float.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/integer.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/ulid.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/uuid.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/generators/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/generators/datetime.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/generators/ulid.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/generators/uuid.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/fields/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/fields/value.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/records/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/records/projections.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/records/serialiser.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/tables/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/tables/indexes.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/tables/table.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/utils/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/utils/dict.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/utils/validators.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/types/__init__.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/types/config.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/types/fields.py +0 -0
- {nuql-0.0.1 → nuql-0.0.3}/nuql/types/serialisation.py +0 -0
nuql-0.0.3/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025, Github:lukeshortland
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
nuql-0.0.3/PKG-INFO
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: nuql
|
3
|
+
Version: 0.0.3
|
4
|
+
Summary: Nuql (pronounced 'nuckle') is a lightweight DynamoDB library for implementing the single table model pattern.
|
5
|
+
License: MIT License
|
6
|
+
|
7
|
+
Copyright (c) 2025, Github:lukeshortland
|
8
|
+
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
11
|
+
in the Software without restriction, including without limitation the rights
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
14
|
+
furnished to do so, subject to the following conditions:
|
15
|
+
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
17
|
+
copies or substantial portions of the Software.
|
18
|
+
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
25
|
+
SOFTWARE.
|
26
|
+
License-File: LICENSE
|
27
|
+
Classifier: License :: OSI Approved :: MIT License
|
28
|
+
Classifier: Operating System :: OS Independent
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
30
|
+
Requires-Python: >=3.13
|
31
|
+
Requires-Dist: boto3>=1.40.0
|
32
|
+
Requires-Dist: pyparsing>=3.2.3
|
33
|
+
Requires-Dist: python-ulid>=3.0.0
|
34
|
+
Requires-Dist: uuid-utils>=0.11.0
|
@@ -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[
|
19
|
+
condition: Optional[Dict[str, Any]] = None,
|
20
20
|
exclude_condition: bool = False,
|
21
21
|
**kwargs,
|
22
22
|
) -> Dict[str, Any]:
|
@@ -109,4 +109,4 @@ class PutItem(Boto3Adapter):
|
|
109
109
|
except ClientError as exc:
|
110
110
|
raise nuql.Boto3Error(exc, args)
|
111
111
|
|
112
|
-
return args['Item']
|
112
|
+
return self.table.serialiser.deserialise(args['Item'])
|
@@ -65,8 +65,13 @@ class KeyCondition:
|
|
65
65
|
else:
|
66
66
|
self.index = table.indexes.get_index(index_name)
|
67
67
|
|
68
|
-
|
68
|
+
pk_field = table.fields[self.index['hash']]
|
69
|
+
sk_field = table.fields.get(self.index.get('sort'))
|
70
|
+
|
71
|
+
if pk_field.auto_include_key_condition:
|
69
72
|
condition[self.index['hash']] = None
|
73
|
+
if sk_field and sk_field.auto_include_key_condition:
|
74
|
+
condition[self.index.get('sort')] = None
|
70
75
|
|
71
76
|
parsed_conditions = {}
|
72
77
|
|
@@ -98,22 +103,27 @@ class KeyCondition:
|
|
98
103
|
|
99
104
|
# Process projected field
|
100
105
|
else:
|
101
|
-
|
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'])
|
102
111
|
|
103
|
-
|
104
|
-
|
112
|
+
for key_name in projected_keys:
|
113
|
+
if key_name not in parsed_conditions:
|
114
|
+
parsed_conditions[key_name] = ['eq', {}]
|
105
115
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
116
|
+
if parsed_conditions[key_name][0] != 'eq' and operand != 'eq':
|
117
|
+
raise nuql.NuqlError(
|
118
|
+
code='KeyConditionError',
|
119
|
+
message=f'Multiple non-equals operators provided for the key \'{key_name}\' '
|
120
|
+
'will result in an ambiguous key condition.'
|
121
|
+
)
|
112
122
|
|
113
|
-
|
114
|
-
|
123
|
+
if operand != 'eq':
|
124
|
+
parsed_conditions[key_name][0] = operand
|
115
125
|
|
116
|
-
|
126
|
+
parsed_conditions[key_name][1][key] = condition_value
|
117
127
|
|
118
128
|
self.condition = None
|
119
129
|
validator = resources.Validator()
|
@@ -135,6 +145,17 @@ class KeyCondition:
|
|
135
145
|
serialised_value = field(value, 'query', validator)
|
136
146
|
key_condition = getattr(key_obj, operand)(serialised_value)
|
137
147
|
|
148
|
+
is_partial = key in validator.partial_keys
|
149
|
+
|
150
|
+
if key == self.index['hash'] and is_partial:
|
151
|
+
raise nuql.NuqlError(
|
152
|
+
code='KeyConditionError',
|
153
|
+
message=f'Partial key \'{key}\' cannot be used in a key condition on \'{index_name}\' index.'
|
154
|
+
)
|
155
|
+
|
156
|
+
if is_partial and operand != 'begins_with':
|
157
|
+
continue
|
158
|
+
|
138
159
|
if self.condition is None:
|
139
160
|
self.condition = key_condition
|
140
161
|
else:
|
@@ -37,8 +37,8 @@ class Nuql:
|
|
37
37
|
|
38
38
|
# Insert global fields on to all tables
|
39
39
|
if isinstance(global_fields, dict):
|
40
|
-
for
|
41
|
-
|
40
|
+
for table_name in list(schema.keys()):
|
41
|
+
schema[table_name].update(global_fields)
|
42
42
|
|
43
43
|
self.connection = Connection(name, session)
|
44
44
|
self.fields = custom_fields
|
@@ -5,58 +5,12 @@ from typing import Dict, Any
|
|
5
5
|
|
6
6
|
import nuql
|
7
7
|
from nuql import resources, types
|
8
|
+
from nuql.resources import EmptyValue
|
8
9
|
|
9
10
|
|
10
11
|
class Key(resources.FieldBase):
|
11
12
|
type = 'key'
|
12
13
|
|
13
|
-
def __call__(self, value: Any, action: 'types.SerialisationType', validator: 'resources.Validator') -> Any:
|
14
|
-
"""
|
15
|
-
Encapsulates the internal serialisation logic to prepare for
|
16
|
-
sending the record to DynamoDB.
|
17
|
-
|
18
|
-
:arg value: Deserialised value.
|
19
|
-
:arg action: SerialisationType (`create`, `update`, `write` or `query`).
|
20
|
-
:arg validator: Validator instance.
|
21
|
-
:return: Serialised value.
|
22
|
-
"""
|
23
|
-
has_value = not isinstance(value, resources.EmptyValue)
|
24
|
-
|
25
|
-
# Apply generators if applicable to the field to overwrite the value
|
26
|
-
if action in ['create', 'update', 'write']:
|
27
|
-
if action == 'create' and self.on_create:
|
28
|
-
value = self.on_create()
|
29
|
-
|
30
|
-
if action == 'update' and self.on_update:
|
31
|
-
value = self.on_update()
|
32
|
-
|
33
|
-
if self.on_write:
|
34
|
-
value = self.on_write()
|
35
|
-
|
36
|
-
# Set default value if applicable
|
37
|
-
if not has_value and not value:
|
38
|
-
value = self.default
|
39
|
-
|
40
|
-
# Serialise the value
|
41
|
-
value = self.serialise_template(value, action, validator)
|
42
|
-
|
43
|
-
# Validate required field
|
44
|
-
if self.required and action == 'create' and value is None:
|
45
|
-
validator.add(name=self.name, message='Field is required')
|
46
|
-
|
47
|
-
# Validate against enum
|
48
|
-
if self.enum and has_value and action in ['create', 'update', 'write'] and value not in self.enum:
|
49
|
-
validator.add(name=self.name, message=f'Value must be one of: {", ".join(self.enum)}')
|
50
|
-
|
51
|
-
# Run internal validation
|
52
|
-
self.internal_validation(value, action, validator)
|
53
|
-
|
54
|
-
# Run custom validation logic
|
55
|
-
if self.validator and action in ['create', 'update', 'write']:
|
56
|
-
self.validator(value, validator)
|
57
|
-
|
58
|
-
return value
|
59
|
-
|
60
14
|
def on_init(self) -> None:
|
61
15
|
"""Initialises the key field."""
|
62
16
|
# Validate the field has a value
|
@@ -69,6 +23,8 @@ class Key(resources.FieldBase):
|
|
69
23
|
# Callback fn handles configuring projected fields on the schema
|
70
24
|
def callback(field_map: dict) -> None:
|
71
25
|
"""Callback fn to configure projected fields on the schema."""
|
26
|
+
auto_include_map = {}
|
27
|
+
|
72
28
|
for key, value in self.value.items():
|
73
29
|
projected_name = self.parse_projected_name(value)
|
74
30
|
|
@@ -88,15 +44,38 @@ class Key(resources.FieldBase):
|
|
88
44
|
field_map[projected_name].projected_from.append(self.name)
|
89
45
|
self.projects_fields.append(projected_name)
|
90
46
|
|
47
|
+
auto_include_map[projected_name] = field_map[projected_name].default is not None
|
48
|
+
|
49
|
+
self.auto_include_key_condition = all(auto_include_map.values())
|
50
|
+
|
91
51
|
if self.init_callback is not None:
|
92
52
|
self.init_callback(callback)
|
93
53
|
|
54
|
+
def serialise_internal(
|
55
|
+
self,
|
56
|
+
value: Any,
|
57
|
+
action: 'types.SerialisationType',
|
58
|
+
validator: 'resources.Validator'
|
59
|
+
) -> Any:
|
60
|
+
"""
|
61
|
+
Internal serialisation override.
|
62
|
+
|
63
|
+
:arg value: Value to serialise.
|
64
|
+
:arg action: Serialisation action.
|
65
|
+
:arg validator: Validator instance.
|
66
|
+
:return: Serialised value.
|
67
|
+
"""
|
68
|
+
serialised = self.serialise_template(value, action, validator)
|
69
|
+
if serialised['is_partial']:
|
70
|
+
validator.partial_keys.append(self.name)
|
71
|
+
return serialised['value']
|
72
|
+
|
94
73
|
def serialise_template(
|
95
74
|
self,
|
96
75
|
key_dict: Dict[str, Any],
|
97
76
|
action: 'types.SerialisationType',
|
98
77
|
validator: 'resources.Validator'
|
99
|
-
) -> str:
|
78
|
+
) -> Dict[str, Any]:
|
100
79
|
"""
|
101
80
|
Serialises the key dict to a string.
|
102
81
|
|
@@ -108,6 +87,11 @@ class Key(resources.FieldBase):
|
|
108
87
|
output = ''
|
109
88
|
s = self.sanitise
|
110
89
|
|
90
|
+
if key_dict is None:
|
91
|
+
key_dict = {}
|
92
|
+
|
93
|
+
is_partial = False
|
94
|
+
|
111
95
|
for key, value in self.value.items():
|
112
96
|
projected_name = self.parse_projected_name(value)
|
113
97
|
|
@@ -121,7 +105,9 @@ class Key(resources.FieldBase):
|
|
121
105
|
f'\'{self.name}\') is not defined in the schema'
|
122
106
|
)
|
123
107
|
|
124
|
-
|
108
|
+
is_partial = is_partial or (key not in key_dict and not projected_field.default)
|
109
|
+
|
110
|
+
projected_value = key_dict.get(projected_name) or EmptyValue()
|
125
111
|
serialised_value = projected_field(projected_value, action, validator)
|
126
112
|
used_value = s(serialised_value) if serialised_value else None
|
127
113
|
else:
|
@@ -133,7 +119,7 @@ class Key(resources.FieldBase):
|
|
133
119
|
|
134
120
|
output += f'{s(key)}:{used_value if used_value else ""}|'
|
135
121
|
|
136
|
-
return output[:-1]
|
122
|
+
return {'value': output[:-1], 'is_partial': is_partial}
|
137
123
|
|
138
124
|
def deserialise(self, value: str) -> Dict[str, Any]:
|
139
125
|
"""
|
@@ -0,0 +1,52 @@
|
|
1
|
+
__all__ = ['List']
|
2
|
+
|
3
|
+
from typing import List as _List, Any
|
4
|
+
|
5
|
+
import nuql
|
6
|
+
from nuql import resources, types
|
7
|
+
from nuql.resources import FieldBase
|
8
|
+
|
9
|
+
|
10
|
+
class List(FieldBase):
|
11
|
+
type = 'list'
|
12
|
+
of: FieldBase
|
13
|
+
|
14
|
+
def on_init(self) -> None:
|
15
|
+
"""Defines the contents of the list."""
|
16
|
+
if 'of' not in self.config:
|
17
|
+
raise nuql.NuqlError(
|
18
|
+
code='SchemaError',
|
19
|
+
message='Config key \'of\' must be defined for the list field type'
|
20
|
+
)
|
21
|
+
|
22
|
+
# Initialise the configured 'of' field type
|
23
|
+
field_map = resources.create_field_map(
|
24
|
+
fields={'of': self.config['of']},
|
25
|
+
parent=self.parent,
|
26
|
+
field_types=self.parent.provider.fields
|
27
|
+
)
|
28
|
+
self.of = field_map['of']
|
29
|
+
|
30
|
+
def serialise_internal(
|
31
|
+
self,
|
32
|
+
value: Any,
|
33
|
+
action: 'types.SerialisationType',
|
34
|
+
validator: 'resources.Validator'
|
35
|
+
) -> Any:
|
36
|
+
"""Internal serialisation"""
|
37
|
+
if not isinstance(value, list):
|
38
|
+
return None
|
39
|
+
else:
|
40
|
+
return [self.of(item, action, validator) for item in value]
|
41
|
+
|
42
|
+
def deserialise(self, value: _List[Any] | None) -> _List[Any] | None:
|
43
|
+
"""
|
44
|
+
Deserialises a list of values.
|
45
|
+
|
46
|
+
:arg value: List or None.
|
47
|
+
:return: List or None.
|
48
|
+
"""
|
49
|
+
if not isinstance(value, list):
|
50
|
+
return None
|
51
|
+
|
52
|
+
return [self.of.deserialise(item) for item in value]
|
@@ -0,0 +1,37 @@
|
|
1
|
+
__all__ = ['Map']
|
2
|
+
|
3
|
+
from typing import Dict as _Dict, Any
|
4
|
+
|
5
|
+
import nuql
|
6
|
+
from nuql import resources, types
|
7
|
+
|
8
|
+
|
9
|
+
class Map(resources.FieldBase):
|
10
|
+
type = 'map'
|
11
|
+
fields: _Dict[str, Any] = {}
|
12
|
+
serialiser: 'resources.Serialiser' = None
|
13
|
+
|
14
|
+
def on_init(self) -> None:
|
15
|
+
"""Initialises the dict schema."""
|
16
|
+
if 'fields' not in self.config:
|
17
|
+
raise nuql.NuqlError(
|
18
|
+
code='SchemaError',
|
19
|
+
message='Config key \'fields\' must be defined for the dict field type'
|
20
|
+
)
|
21
|
+
|
22
|
+
self.fields = resources.create_field_map(self.config['fields'], self.parent, self.parent.provider.fields)
|
23
|
+
self.serialiser = resources.Serialiser(self)
|
24
|
+
|
25
|
+
def serialise_internal(
|
26
|
+
self,
|
27
|
+
value: Any,
|
28
|
+
action: 'types.SerialisationType',
|
29
|
+
validator: 'resources.Validator'
|
30
|
+
) -> Any:
|
31
|
+
"""Serialises the Map value"""
|
32
|
+
if value:
|
33
|
+
return self.serialiser.serialise(action, value, validator)
|
34
|
+
|
35
|
+
def deserialise(self, value: Any) -> Any:
|
36
|
+
"""Deserialises the Map value"""
|
37
|
+
return self.serialiser.deserialise(value)
|
@@ -6,7 +6,7 @@ from typing import List, Dict, Any
|
|
6
6
|
|
7
7
|
import nuql
|
8
8
|
from nuql import resources, types
|
9
|
-
|
9
|
+
from nuql.resources import EmptyValue
|
10
10
|
|
11
11
|
TEMPLATE_PATTERN = r'\$\{(\w+)}'
|
12
12
|
|
@@ -15,61 +15,14 @@ class String(resources.FieldBase):
|
|
15
15
|
type = 'string'
|
16
16
|
is_template = False
|
17
17
|
|
18
|
-
def __call__(self, value: Any, action: 'types.SerialisationType', validator: 'resources.Validator') -> Any:
|
19
|
-
"""
|
20
|
-
Encapsulates the internal serialisation logic to prepare for
|
21
|
-
sending the record to DynamoDB.
|
22
|
-
|
23
|
-
:arg value: Deserialised value.
|
24
|
-
:arg action: SerialisationType (`create`, `update`, `write` or `query`).
|
25
|
-
:arg validator: Validator instance.
|
26
|
-
:return: Serialised value.
|
27
|
-
"""
|
28
|
-
has_value = not isinstance(value, resources.EmptyValue)
|
29
|
-
|
30
|
-
# Apply generators if applicable to the field to overwrite the value
|
31
|
-
if action in ['create', 'update', 'write']:
|
32
|
-
if action == 'create' and self.on_create:
|
33
|
-
value = self.on_create()
|
34
|
-
|
35
|
-
if action == 'update' and self.on_update:
|
36
|
-
value = self.on_update()
|
37
|
-
|
38
|
-
if self.on_write:
|
39
|
-
value = self.on_write()
|
40
|
-
|
41
|
-
# Set default value if applicable
|
42
|
-
if not has_value and not value and not self.value and not self.is_template:
|
43
|
-
value = self.default
|
44
|
-
|
45
|
-
if self.value and not self.is_template:
|
46
|
-
value = self.value
|
47
|
-
|
48
|
-
# Serialise the value
|
49
|
-
if self.is_template:
|
50
|
-
value = self.serialise_template(value, action, validator)
|
51
|
-
else:
|
52
|
-
value = self.serialise(value)
|
53
|
-
|
54
|
-
# Validate required field
|
55
|
-
if self.required and action == 'create' and value is None:
|
56
|
-
validator.add(name=self.name, message='Field is required')
|
57
|
-
|
58
|
-
# Run internal validation
|
59
|
-
self.internal_validation(value, action, validator)
|
60
|
-
|
61
|
-
# Run custom validation logic
|
62
|
-
if self.validator and action in ['create', 'update', 'write']:
|
63
|
-
self.validator(value, validator)
|
64
|
-
|
65
|
-
return value
|
66
|
-
|
67
18
|
def on_init(self) -> None:
|
68
19
|
"""Initialises the string field when a template is defined."""
|
69
20
|
self.is_template = self.value is not None and bool(re.search(TEMPLATE_PATTERN, self.value))
|
70
21
|
|
71
22
|
def callback(field_map: dict) -> None:
|
72
23
|
"""Callback fn to configure projected fields on the schema."""
|
24
|
+
auto_include_map = {}
|
25
|
+
|
73
26
|
for key in self.find_projections(self.value):
|
74
27
|
if key not in field_map:
|
75
28
|
raise nuql.NuqlError(
|
@@ -82,9 +35,35 @@ class String(resources.FieldBase):
|
|
82
35
|
field_map[key].projected_from.append(self.name)
|
83
36
|
self.projects_fields.append(key)
|
84
37
|
|
38
|
+
auto_include_map[key] = field_map[key].default is not None
|
39
|
+
|
40
|
+
self.auto_include_key_condition = all(auto_include_map.values())
|
41
|
+
|
85
42
|
if self.init_callback is not None and self.is_template:
|
86
43
|
self.init_callback(callback)
|
87
44
|
|
45
|
+
def serialise_internal(
|
46
|
+
self,
|
47
|
+
value: Any,
|
48
|
+
action: 'types.SerialisationType',
|
49
|
+
validator: 'resources.Validator'
|
50
|
+
) -> Any:
|
51
|
+
"""
|
52
|
+
Internal serialisation override.
|
53
|
+
|
54
|
+
:arg value: Value to serialise.
|
55
|
+
:arg action: Serialisation action.
|
56
|
+
:arg validator: Validator instance.
|
57
|
+
:return: Serialised value.
|
58
|
+
"""
|
59
|
+
if self.is_template:
|
60
|
+
serialised = self.serialise_template(value, action, validator)
|
61
|
+
if serialised['is_partial']:
|
62
|
+
validator.partial_keys.append(self.name)
|
63
|
+
return serialised['value']
|
64
|
+
else:
|
65
|
+
return self.serialise(value)
|
66
|
+
|
88
67
|
def serialise(self, value: str | None) -> str | None:
|
89
68
|
"""
|
90
69
|
Serialises a string value.
|
@@ -108,7 +87,7 @@ class String(resources.FieldBase):
|
|
108
87
|
value: Dict[str, Any],
|
109
88
|
action: 'types.SerialisationType',
|
110
89
|
validator: 'resources.Validator'
|
111
|
-
) -> str
|
90
|
+
) -> Dict[str, Any]:
|
112
91
|
"""
|
113
92
|
Serialises a template string.
|
114
93
|
|
@@ -120,9 +99,12 @@ class String(resources.FieldBase):
|
|
120
99
|
if not isinstance(value, dict):
|
121
100
|
value = {}
|
122
101
|
|
102
|
+
is_partial = False
|
103
|
+
|
123
104
|
# Add not provided keys as empty strings
|
124
105
|
for key in self.find_projections(self.value):
|
125
106
|
if key not in value:
|
107
|
+
is_partial = True
|
126
108
|
value[key] = None
|
127
109
|
|
128
110
|
serialised = {}
|
@@ -138,11 +120,11 @@ class String(resources.FieldBase):
|
|
138
120
|
f'\'{self.name}\') is not defined in the schema'
|
139
121
|
)
|
140
122
|
|
141
|
-
serialised_value = field(deserialised_value, action, validator)
|
123
|
+
serialised_value = field(deserialised_value or EmptyValue(), action, validator)
|
142
124
|
serialised[key] = serialised_value if serialised_value else ''
|
143
125
|
|
144
126
|
template = Template(self.value)
|
145
|
-
return template.substitute(serialised)
|
127
|
+
return {'value': template.substitute(serialised), 'is_partial': is_partial}
|
146
128
|
|
147
129
|
def deserialise_template(self, value: str | None) -> Dict[str, Any]:
|
148
130
|
"""
|
@@ -28,6 +28,7 @@ class FieldBase:
|
|
28
28
|
self.config = config
|
29
29
|
self.parent = parent
|
30
30
|
self.init_callback = init_callback
|
31
|
+
self.auto_include_key_condition = False
|
31
32
|
|
32
33
|
# Handle 'KEY' field type
|
33
34
|
self.projected_from = []
|
@@ -91,11 +92,11 @@ class FieldBase:
|
|
91
92
|
value = self.on_write()
|
92
93
|
|
93
94
|
# Set default value if applicable
|
94
|
-
if not has_value and
|
95
|
+
if not has_value and self.default:
|
95
96
|
value = self.default
|
96
97
|
|
97
98
|
# Serialise the value
|
98
|
-
value = self.
|
99
|
+
value = self.serialise_internal(value, action, validator)
|
99
100
|
|
100
101
|
# Validate required field
|
101
102
|
if self.required and action == 'create' and value is None:
|
@@ -126,6 +127,22 @@ class FieldBase:
|
|
126
127
|
message='Serialisation has not been implemented for this field type.'
|
127
128
|
)
|
128
129
|
|
130
|
+
def serialise_internal(
|
131
|
+
self,
|
132
|
+
value: Any,
|
133
|
+
_action: 'types.SerialisationType',
|
134
|
+
_validator: 'resources.Validator'
|
135
|
+
) -> Any:
|
136
|
+
"""
|
137
|
+
Internal serialisation wrapper to allow overridable serialisation behaviour.
|
138
|
+
|
139
|
+
:arg value: Value to serialise.
|
140
|
+
:arg _action: Serialisation type.
|
141
|
+
:arg _validator: Validator instance.
|
142
|
+
:return: Serialised value.
|
143
|
+
"""
|
144
|
+
return self.serialise(value)
|
145
|
+
|
129
146
|
def deserialise(self, value: Any) -> Any:
|
130
147
|
"""
|
131
148
|
Deserialise/unmarshal the field value from DynamoDB format.
|
@@ -62,10 +62,10 @@ def get_field_types(field_types: List[Type['types.FieldType']] | None = None) ->
|
|
62
62
|
|
63
63
|
def is_valid(_obj: Any) -> bool:
|
64
64
|
"""Check the provided object is a valid field type."""
|
65
|
-
if not inspect.isclass(
|
65
|
+
if not inspect.isclass(_obj):
|
66
66
|
return False
|
67
67
|
|
68
|
-
if not issubclass(
|
68
|
+
if not issubclass(_obj, resources.FieldBase):
|
69
69
|
return False
|
70
70
|
|
71
71
|
return True
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "nuql"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.3"
|
4
4
|
description = "Nuql (pronounced 'nuckle') is a lightweight DynamoDB library for implementing the single table model pattern."
|
5
5
|
requires-python = ">=3.13"
|
6
6
|
dependencies = [
|
@@ -9,6 +9,7 @@ dependencies = [
|
|
9
9
|
"python-ulid>=3.0.0",
|
10
10
|
"uuid-utils>=0.11.0",
|
11
11
|
]
|
12
|
+
license = { file = "LICENSE" }
|
12
13
|
classifiers = [
|
13
14
|
"Programming Language :: Python :: 3",
|
14
15
|
"License :: OSI Approved :: MIT License",
|
nuql-0.0.1/PKG-INFO
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: nuql
|
3
|
-
Version: 0.0.1
|
4
|
-
Summary: Nuql (pronounced 'nuckle') is a lightweight DynamoDB library for implementing the single table model pattern.
|
5
|
-
Classifier: License :: OSI Approved :: MIT License
|
6
|
-
Classifier: Operating System :: OS Independent
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
8
|
-
Requires-Python: >=3.13
|
9
|
-
Requires-Dist: boto3>=1.40.0
|
10
|
-
Requires-Dist: pyparsing>=3.2.3
|
11
|
-
Requires-Dist: python-ulid>=3.0.0
|
12
|
-
Requires-Dist: uuid-utils>=0.11.0
|
nuql-0.0.1/nuql/fields/list.py
DELETED
@@ -1,90 +0,0 @@
|
|
1
|
-
__all__ = ['List']
|
2
|
-
|
3
|
-
from typing import List as _List, Any
|
4
|
-
|
5
|
-
import nuql
|
6
|
-
from nuql import resources, types
|
7
|
-
from nuql.resources import FieldBase
|
8
|
-
|
9
|
-
|
10
|
-
class List(FieldBase):
|
11
|
-
type = 'list'
|
12
|
-
of: FieldBase
|
13
|
-
|
14
|
-
def on_init(self) -> None:
|
15
|
-
"""Defines the contents of the list."""
|
16
|
-
if 'of' not in self.config:
|
17
|
-
raise nuql.NuqlError(
|
18
|
-
code='SchemaError',
|
19
|
-
message='Config key \'of\' must be defined for the list field type'
|
20
|
-
)
|
21
|
-
|
22
|
-
# Initialise the configured 'of' field type
|
23
|
-
field_map = resources.create_field_map(
|
24
|
-
fields={'of': self.config['of']},
|
25
|
-
parent=self.parent,
|
26
|
-
field_types=self.parent.provider.fields
|
27
|
-
)
|
28
|
-
self.of = field_map['of']
|
29
|
-
|
30
|
-
def __call__(self, value: Any, action: 'types.SerialisationType', validator: 'resources.Validator') -> Any:
|
31
|
-
"""
|
32
|
-
Encapsulates the internal serialisation logic to prepare for
|
33
|
-
sending the record to DynamoDB.
|
34
|
-
|
35
|
-
:arg value: Deserialised value.
|
36
|
-
:arg action: SerialisationType (`create`, `update`, `write` or `query`).
|
37
|
-
:arg validator: Validator instance.
|
38
|
-
:return: Serialised value.
|
39
|
-
"""
|
40
|
-
has_value = not isinstance(value, resources.EmptyValue)
|
41
|
-
|
42
|
-
# Apply generators if applicable to the field to overwrite the value
|
43
|
-
if action in ['create', 'update', 'write']:
|
44
|
-
if action == 'create' and self.on_create:
|
45
|
-
value = self.on_create()
|
46
|
-
|
47
|
-
if action == 'update' and self.on_update:
|
48
|
-
value = self.on_update()
|
49
|
-
|
50
|
-
if self.on_write:
|
51
|
-
value = self.on_write()
|
52
|
-
|
53
|
-
# Set default value if applicable
|
54
|
-
if not has_value and not value:
|
55
|
-
value = self.default
|
56
|
-
|
57
|
-
# Serialise the value
|
58
|
-
if not isinstance(value, list):
|
59
|
-
value = None
|
60
|
-
else:
|
61
|
-
value = [self.of(item, action, validator) for item in value]
|
62
|
-
|
63
|
-
# Validate required field
|
64
|
-
if self.required and action == 'create' and value is None:
|
65
|
-
validator.add(name=self.name, message='Field is required')
|
66
|
-
|
67
|
-
# Validate against enum
|
68
|
-
if self.enum and has_value and action in ['create', 'update', 'write'] and value not in self.enum:
|
69
|
-
validator.add(name=self.name, message=f'Value must be one of: {", ".join(self.enum)}')
|
70
|
-
|
71
|
-
# Run internal validation
|
72
|
-
self.internal_validation(value, action, validator)
|
73
|
-
|
74
|
-
# Run custom validation logic
|
75
|
-
if self.validator and action in ['create', 'update', 'write']:
|
76
|
-
self.validator(value, validator)
|
77
|
-
|
78
|
-
return value
|
79
|
-
|
80
|
-
def deserialise(self, value: _List[Any] | None) -> _List[Any] | None:
|
81
|
-
"""
|
82
|
-
Deserialises a list of values.
|
83
|
-
|
84
|
-
:arg value: List or None.
|
85
|
-
:return: List or None.
|
86
|
-
"""
|
87
|
-
if not isinstance(value, list):
|
88
|
-
return None
|
89
|
-
|
90
|
-
return [self.of.deserialise(item) for item in value]
|
nuql-0.0.1/nuql/fields/map.py
DELETED
@@ -1,67 +0,0 @@
|
|
1
|
-
__all__ = ['Map']
|
2
|
-
|
3
|
-
from typing import Dict as _Dict, Any
|
4
|
-
|
5
|
-
import nuql
|
6
|
-
from nuql import resources, types
|
7
|
-
|
8
|
-
|
9
|
-
class Map(resources.FieldBase):
|
10
|
-
type = 'map'
|
11
|
-
fields: _Dict[str, Any] = {}
|
12
|
-
serialiser: 'resources.Serialiser' = None
|
13
|
-
|
14
|
-
def on_init(self) -> None:
|
15
|
-
"""Initialises the dict schema."""
|
16
|
-
if 'fields' not in self.config:
|
17
|
-
raise nuql.NuqlError(
|
18
|
-
code='SchemaError',
|
19
|
-
message='Config key \'fields\' must be defined for the dict field type'
|
20
|
-
)
|
21
|
-
|
22
|
-
self.fields = resources.create_field_map(self.config['fields'], self.parent, self.parent.provider.fields)
|
23
|
-
self.serialiser = resources.Serialiser(self)
|
24
|
-
|
25
|
-
def __call__(self, value: Any, action: 'types.SerialisationType', validator: 'resources.Validator') -> Any:
|
26
|
-
"""
|
27
|
-
Encapsulates the internal serialisation logic to prepare for
|
28
|
-
sending the record to DynamoDB.
|
29
|
-
|
30
|
-
:arg value: Deserialised value.
|
31
|
-
:arg action: SerialisationType (`create`, `update`, `write` or `query`).
|
32
|
-
:arg validator: Validator instance.
|
33
|
-
:return: Serialised value.
|
34
|
-
"""
|
35
|
-
has_value = not isinstance(value, resources.EmptyValue)
|
36
|
-
|
37
|
-
# Apply generators if applicable to the field to overwrite the value
|
38
|
-
if action in ['create', 'update', 'write']:
|
39
|
-
if action == 'create' and self.on_create:
|
40
|
-
value = self.on_create()
|
41
|
-
|
42
|
-
if action == 'update' and self.on_update:
|
43
|
-
value = self.on_update()
|
44
|
-
|
45
|
-
if self.on_write:
|
46
|
-
value = self.on_write()
|
47
|
-
|
48
|
-
# Set default value if applicable
|
49
|
-
if not has_value and not value:
|
50
|
-
value = self.default
|
51
|
-
|
52
|
-
# Serialise the value
|
53
|
-
if value:
|
54
|
-
value = self.serialiser.serialise(action, value, validator)
|
55
|
-
|
56
|
-
# Validate required field
|
57
|
-
if self.required and action == 'create' and value is None:
|
58
|
-
validator.add(name=self.name, message='Field is required')
|
59
|
-
|
60
|
-
# Run internal validation
|
61
|
-
self.internal_validation(value, action, validator)
|
62
|
-
|
63
|
-
# Run custom validation logic
|
64
|
-
if self.validator and action in ['create', 'update', 'write']:
|
65
|
-
self.validator(value, validator)
|
66
|
-
|
67
|
-
return value
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|