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.
Files changed (69) hide show
  1. nuql-0.0.3/LICENSE +21 -0
  2. nuql-0.0.3/PKG-INFO +34 -0
  3. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/put_item.py +2 -2
  4. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/key_condition.py +34 -13
  5. {nuql-0.0.1 → nuql-0.0.3}/nuql/client.py +2 -2
  6. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/key.py +36 -50
  7. nuql-0.0.3/nuql/fields/list.py +52 -0
  8. nuql-0.0.3/nuql/fields/map.py +37 -0
  9. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/string.py +35 -53
  10. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/fields/field.py +19 -2
  11. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/fields/field_map.py +2 -2
  12. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/records/validator.py +1 -0
  13. {nuql-0.0.1 → nuql-0.0.3}/pyproject.toml +2 -1
  14. nuql-0.0.1/PKG-INFO +0 -12
  15. nuql-0.0.1/nuql/fields/list.py +0 -90
  16. nuql-0.0.1/nuql/fields/map.py +0 -67
  17. {nuql-0.0.1 → nuql-0.0.3}/.gitignore +0 -0
  18. {nuql-0.0.1 → nuql-0.0.3}/nuql/__init__.py +0 -0
  19. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/__init__.py +0 -0
  20. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/adapter.py +0 -0
  21. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/batch_get/__init__.py +0 -0
  22. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/batch_get/batch_get.py +0 -0
  23. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/batch_get/queue.py +0 -0
  24. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/batch_write.py +0 -0
  25. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/condition_check.py +0 -0
  26. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/create.py +0 -0
  27. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/delete.py +0 -0
  28. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/get.py +0 -0
  29. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/put_update.py +0 -0
  30. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/__init__.py +0 -0
  31. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/condition.py +0 -0
  32. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/condition_builder.py +0 -0
  33. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/query/query.py +0 -0
  34. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/transaction.py +0 -0
  35. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/update/__init__.py +0 -0
  36. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/update/expression_builder.py +0 -0
  37. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/update/update_item.py +0 -0
  38. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/update/utils.py +0 -0
  39. {nuql-0.0.1 → nuql-0.0.3}/nuql/api/upsert.py +0 -0
  40. {nuql-0.0.1 → nuql-0.0.3}/nuql/connection.py +0 -0
  41. {nuql-0.0.1 → nuql-0.0.3}/nuql/exceptions.py +0 -0
  42. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/__init__.py +0 -0
  43. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/boolean.py +0 -0
  44. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/datetime.py +0 -0
  45. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/datetime_timestamp.py +0 -0
  46. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/float.py +0 -0
  47. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/integer.py +0 -0
  48. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/ulid.py +0 -0
  49. {nuql-0.0.1 → nuql-0.0.3}/nuql/fields/uuid.py +0 -0
  50. {nuql-0.0.1 → nuql-0.0.3}/nuql/generators/__init__.py +0 -0
  51. {nuql-0.0.1 → nuql-0.0.3}/nuql/generators/datetime.py +0 -0
  52. {nuql-0.0.1 → nuql-0.0.3}/nuql/generators/ulid.py +0 -0
  53. {nuql-0.0.1 → nuql-0.0.3}/nuql/generators/uuid.py +0 -0
  54. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/__init__.py +0 -0
  55. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/fields/__init__.py +0 -0
  56. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/fields/value.py +0 -0
  57. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/records/__init__.py +0 -0
  58. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/records/projections.py +0 -0
  59. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/records/serialiser.py +0 -0
  60. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/tables/__init__.py +0 -0
  61. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/tables/indexes.py +0 -0
  62. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/tables/table.py +0 -0
  63. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/utils/__init__.py +0 -0
  64. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/utils/dict.py +0 -0
  65. {nuql-0.0.1 → nuql-0.0.3}/nuql/resources/utils/validators.py +0 -0
  66. {nuql-0.0.1 → nuql-0.0.3}/nuql/types/__init__.py +0 -0
  67. {nuql-0.0.1 → nuql-0.0.3}/nuql/types/config.py +0 -0
  68. {nuql-0.0.1 → nuql-0.0.3}/nuql/types/fields.py +0 -0
  69. {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['types.QueryWhere'] = None,
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
- if self.index['hash'] not in condition:
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
- key_name = self.index['hash'] if is_hash_key else self.index['sort']
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
- if key_name not in parsed_conditions:
104
- parsed_conditions[key_name] = ['eq', {}]
112
+ for key_name in projected_keys:
113
+ if key_name not in parsed_conditions:
114
+ parsed_conditions[key_name] = ['eq', {}]
105
115
 
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
- )
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
- if operand != 'eq':
114
- parsed_conditions[key_name][0] = operand
123
+ if operand != 'eq':
124
+ parsed_conditions[key_name][0] = operand
115
125
 
116
- parsed_conditions[key_name][1][key] = condition_value
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 table in schema.values():
41
- table.update(global_fields)
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
- projected_value = key_dict.get(projected_name)
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 | None:
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 not value:
95
+ if not has_value and self.default:
95
96
  value = self.default
96
97
 
97
98
  # Serialise the value
98
- value = self.serialise(value)
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(obj):
65
+ if not inspect.isclass(_obj):
66
66
  return False
67
67
 
68
- if not issubclass(obj, resources.FieldBase):
68
+ if not issubclass(_obj, resources.FieldBase):
69
69
  return False
70
70
 
71
71
  return True
@@ -15,6 +15,7 @@ class Validator:
15
15
  self.path = path
16
16
  self.children = []
17
17
  self._errors = []
18
+ self.partial_keys = []
18
19
 
19
20
  @property
20
21
  def errors(self):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nuql"
3
- version = "0.0.1"
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
@@ -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]
@@ -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