nuql 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. nuql/__init__.py +3 -0
  2. nuql/api/__init__.py +13 -0
  3. nuql/api/adapter.py +34 -0
  4. nuql/api/batch_get/__init__.py +2 -0
  5. nuql/api/batch_get/batch_get.py +40 -0
  6. nuql/api/batch_get/queue.py +120 -0
  7. nuql/api/batch_write.py +99 -0
  8. nuql/api/condition_check.py +39 -0
  9. nuql/api/create.py +25 -0
  10. nuql/api/delete.py +88 -0
  11. nuql/api/get.py +30 -0
  12. nuql/api/put_item.py +112 -0
  13. nuql/api/put_update.py +25 -0
  14. nuql/api/query/__init__.py +4 -0
  15. nuql/api/query/condition.py +157 -0
  16. nuql/api/query/condition_builder.py +211 -0
  17. nuql/api/query/key_condition.py +200 -0
  18. nuql/api/query/query.py +166 -0
  19. nuql/api/transaction.py +145 -0
  20. nuql/api/update/__init__.py +3 -0
  21. nuql/api/update/expression_builder.py +33 -0
  22. nuql/api/update/update_item.py +139 -0
  23. nuql/api/update/utils.py +126 -0
  24. nuql/api/upsert.py +32 -0
  25. nuql/client.py +88 -0
  26. nuql/connection.py +43 -0
  27. nuql/exceptions.py +66 -0
  28. nuql/fields/__init__.py +11 -0
  29. nuql/fields/boolean.py +29 -0
  30. nuql/fields/datetime.py +49 -0
  31. nuql/fields/datetime_timestamp.py +45 -0
  32. nuql/fields/float.py +40 -0
  33. nuql/fields/integer.py +40 -0
  34. nuql/fields/key.py +207 -0
  35. nuql/fields/list.py +90 -0
  36. nuql/fields/map.py +67 -0
  37. nuql/fields/string.py +184 -0
  38. nuql/fields/ulid.py +39 -0
  39. nuql/fields/uuid.py +42 -0
  40. nuql/generators/__init__.py +3 -0
  41. nuql/generators/datetime.py +37 -0
  42. nuql/generators/ulid.py +10 -0
  43. nuql/generators/uuid.py +19 -0
  44. nuql/resources/__init__.py +4 -0
  45. nuql/resources/fields/__init__.py +3 -0
  46. nuql/resources/fields/field.py +153 -0
  47. nuql/resources/fields/field_map.py +85 -0
  48. nuql/resources/fields/value.py +5 -0
  49. nuql/resources/records/__init__.py +3 -0
  50. nuql/resources/records/projections.py +49 -0
  51. nuql/resources/records/serialiser.py +144 -0
  52. nuql/resources/records/validator.py +48 -0
  53. nuql/resources/tables/__init__.py +2 -0
  54. nuql/resources/tables/indexes.py +140 -0
  55. nuql/resources/tables/table.py +151 -0
  56. nuql/resources/utils/__init__.py +2 -0
  57. nuql/resources/utils/dict.py +21 -0
  58. nuql/resources/utils/validators.py +165 -0
  59. nuql/types/__init__.py +3 -0
  60. nuql/types/config.py +27 -0
  61. nuql/types/fields.py +27 -0
  62. nuql/types/serialisation.py +10 -0
  63. nuql-0.0.1.dist-info/METADATA +12 -0
  64. nuql-0.0.1.dist-info/RECORD +65 -0
  65. nuql-0.0.1.dist-info/WHEEL +4 -0
nuql/fields/list.py ADDED
@@ -0,0 +1,90 @@
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/fields/map.py ADDED
@@ -0,0 +1,67 @@
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
nuql/fields/string.py ADDED
@@ -0,0 +1,184 @@
1
+ __all__ = ['String']
2
+
3
+ import re
4
+ from string import Template
5
+ from typing import List, Dict, Any
6
+
7
+ import nuql
8
+ from nuql import resources, types
9
+
10
+
11
+ TEMPLATE_PATTERN = r'\$\{(\w+)}'
12
+
13
+
14
+ class String(resources.FieldBase):
15
+ type = 'string'
16
+ is_template = False
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
+ def on_init(self) -> None:
68
+ """Initialises the string field when a template is defined."""
69
+ self.is_template = self.value is not None and bool(re.search(TEMPLATE_PATTERN, self.value))
70
+
71
+ def callback(field_map: dict) -> None:
72
+ """Callback fn to configure projected fields on the schema."""
73
+ for key in self.find_projections(self.value):
74
+ if key not in field_map:
75
+ raise nuql.NuqlError(
76
+ code='TemplateStringError',
77
+ message=f'Field \'{key}\' (projected on string field '
78
+ f'\'{self.name}\') is not defined in the schema'
79
+ )
80
+
81
+ # Add reference to this field on the projected field
82
+ field_map[key].projected_from.append(self.name)
83
+ self.projects_fields.append(key)
84
+
85
+ if self.init_callback is not None and self.is_template:
86
+ self.init_callback(callback)
87
+
88
+ def serialise(self, value: str | None) -> str | None:
89
+ """
90
+ Serialises a string value.
91
+
92
+ :arg value: Value.
93
+ :return: Serialised value
94
+ """
95
+ return str(value) if value else None
96
+
97
+ def deserialise(self, value: str | None) -> str | None:
98
+ """
99
+ Deserialises a string value.
100
+
101
+ :arg value: String value.
102
+ :return: String value.
103
+ """
104
+ return str(value) if value else None
105
+
106
+ def serialise_template(
107
+ self,
108
+ value: Dict[str, Any],
109
+ action: 'types.SerialisationType',
110
+ validator: 'resources.Validator'
111
+ ) -> str | None:
112
+ """
113
+ Serialises a template string.
114
+
115
+ :arg value: Dict of projections.
116
+ :arg action: Serialisation type.
117
+ :arg validator: Validator instance.
118
+ :return: String value.
119
+ """
120
+ if not isinstance(value, dict):
121
+ value = {}
122
+
123
+ # Add not provided keys as empty strings
124
+ for key in self.find_projections(self.value):
125
+ if key not in value:
126
+ value[key] = None
127
+
128
+ serialised = {}
129
+
130
+ # Serialise values before substituting
131
+ for key, deserialised_value in value.items():
132
+ field = self.parent.fields.get(key)
133
+
134
+ if not field:
135
+ raise nuql.NuqlError(
136
+ code='TemplateStringError',
137
+ message=f'Field \'{key}\' (projected on string field '
138
+ f'\'{self.name}\') is not defined in the schema'
139
+ )
140
+
141
+ serialised_value = field(deserialised_value, action, validator)
142
+ serialised[key] = serialised_value if serialised_value else ''
143
+
144
+ template = Template(self.value)
145
+ return template.substitute(serialised)
146
+
147
+ def deserialise_template(self, value: str | None) -> Dict[str, Any]:
148
+ """
149
+ Deserialises a string template.
150
+
151
+ :arg value: String value or None.
152
+ :return: Dict of projections.
153
+ """
154
+ if not value:
155
+ return {}
156
+
157
+ pattern = re.sub(TEMPLATE_PATTERN, r'(?P<\1>[^&#]+)', self.value)
158
+ match = re.fullmatch(pattern, value)
159
+ output = {}
160
+
161
+ for key, serialised_value in (match.groupdict() if match else {}).items():
162
+ field = self.parent.fields.get(key)
163
+
164
+ if not field:
165
+ raise nuql.NuqlError(
166
+ code='TemplateStringError',
167
+ message=f'Field \'{key}\' (projected on string field '
168
+ f'\'{self.name}\') is not defined in the schema'
169
+ )
170
+
171
+ deserialised_value = field.deserialise(serialised_value)
172
+ output[key] = deserialised_value
173
+
174
+ return output
175
+
176
+ @staticmethod
177
+ def find_projections(value: str) -> List[str]:
178
+ """
179
+ Finds projections in the value provided as templates '${field_name}'.
180
+
181
+ :arg value: Value to parse.
182
+ :return: List of field names.
183
+ """
184
+ return re.findall(TEMPLATE_PATTERN, value)
nuql/fields/ulid.py ADDED
@@ -0,0 +1,39 @@
1
+ __all__ = ['Ulid']
2
+
3
+ from ulid import ULID
4
+
5
+ from nuql import resources
6
+
7
+
8
+ class Ulid(resources.FieldBase):
9
+ type = 'ulid'
10
+
11
+ def serialise(self, value: ULID | str | None) -> str | None:
12
+ """
13
+ Serialises a ULID value.
14
+
15
+ :arg value: ULID, str or None.
16
+ :return: str or None.
17
+ """
18
+ if isinstance(value, ULID):
19
+ return str(value)
20
+ if isinstance(value, str):
21
+ try:
22
+ return str(ULID.from_str(value))
23
+ except ValueError:
24
+ return None
25
+ return None
26
+
27
+ def deserialise(self, value: str | None) -> str | None:
28
+ """
29
+ Deserialises a ULID value.
30
+
31
+ :arg value: str or None.
32
+ :return: str or None.
33
+ """
34
+ if isinstance(value, str):
35
+ # try:
36
+ return str(ULID.from_str(value))
37
+ # except ValueError:
38
+ # return None
39
+ return None
nuql/fields/uuid.py ADDED
@@ -0,0 +1,42 @@
1
+ __all__ = ['Uuid']
2
+
3
+ from uuid import UUID as NATIVE_UUID
4
+ from uuid_utils import UUID
5
+
6
+ from nuql import resources
7
+
8
+
9
+ class Uuid(resources.FieldBase):
10
+ type = 'uuid'
11
+
12
+ def serialise(self, value: NATIVE_UUID | UUID | str | None) -> str | None:
13
+ """
14
+ Serialises a UUID value.
15
+
16
+ :arg value: UUID, str or None.
17
+ :return: str or None.
18
+ """
19
+ if isinstance(value, (NATIVE_UUID, UUID)):
20
+ return str(value)
21
+
22
+ if isinstance(value, str):
23
+ try:
24
+ return str(UUID(value))
25
+ except ValueError:
26
+ return None
27
+
28
+ return None
29
+
30
+ def deserialise(self, value: str | None) -> str | None:
31
+ """
32
+ Deserialises a UUID value (only to string).
33
+
34
+ :arg value: str or None.
35
+ :return: str or None.
36
+ """
37
+ if isinstance(value, str):
38
+ try:
39
+ return str(UUID(value))
40
+ except ValueError:
41
+ return None
42
+ return None
@@ -0,0 +1,3 @@
1
+ from .datetime import *
2
+ from .uuid import *
3
+ from .ulid import *
@@ -0,0 +1,37 @@
1
+ __all__ = ["Datetime"]
2
+
3
+ from datetime import datetime, timedelta, UTC
4
+
5
+
6
+ class Datetime:
7
+ @classmethod
8
+ def now(cls):
9
+ """Generates current UTC time"""
10
+ def generator():
11
+ return datetime.now(UTC)
12
+ return generator
13
+
14
+ @classmethod
15
+ def relative(
16
+ cls,
17
+ days: float = 0,
18
+ seconds: float = 0,
19
+ microseconds: float = 0,
20
+ milliseconds: float = 0,
21
+ minutes: float = 0,
22
+ hours: float = 0,
23
+ weeks: float = 0
24
+ ):
25
+ """Generates a UTC datetime relative to now."""
26
+ def generator():
27
+ return datetime.now(UTC) + timedelta(
28
+ days=days,
29
+ seconds=seconds,
30
+ microseconds=microseconds,
31
+ milliseconds=milliseconds,
32
+ minutes=minutes,
33
+ hours=hours,
34
+ weeks=weeks
35
+ )
36
+
37
+ return generator
@@ -0,0 +1,10 @@
1
+ from ulid import ULID
2
+
3
+
4
+ class Ulid:
5
+ @classmethod
6
+ def now(cls):
7
+ """Generates current ULID"""
8
+ def generator():
9
+ return ULID()
10
+ return generator
@@ -0,0 +1,19 @@
1
+ __all__ = ['Uuid']
2
+
3
+ from uuid_utils import uuid4, uuid7
4
+
5
+
6
+ class Uuid:
7
+ @classmethod
8
+ def v4(cls):
9
+ """Generates a random UUID v4"""
10
+ def generator():
11
+ return uuid4()
12
+ return generator
13
+
14
+ @classmethod
15
+ def v7(cls):
16
+ """Generates a random UUID v7"""
17
+ def generator():
18
+ return uuid7()
19
+ return generator
@@ -0,0 +1,4 @@
1
+ from .fields import *
2
+ from .tables import *
3
+ from .records import *
4
+ from .utils import *
@@ -0,0 +1,3 @@
1
+ from .value import *
2
+ from .field import *
3
+ from .field_map import *
@@ -0,0 +1,153 @@
1
+ __all__ = ['FieldBase']
2
+
3
+ from typing import Any, List, Optional, Callable
4
+
5
+ import nuql
6
+ from nuql import resources, types
7
+
8
+
9
+ class FieldBase:
10
+ type: str = None
11
+
12
+ def __init__(
13
+ self,
14
+ name: str,
15
+ config: 'types.FieldConfig',
16
+ parent: 'resources.Table',
17
+ init_callback: Callable[[Callable], None] | None = None
18
+ ) -> None:
19
+ """
20
+ Wrapper for the handling of field serialisation and deserialisation.
21
+
22
+ :arg name: Field name.
23
+ :arg config: Field config dict.
24
+ :arg parent: Parent instance.
25
+ :param init_callback: Optional init callback.
26
+ """
27
+ self.name = name
28
+ self.config = config
29
+ self.parent = parent
30
+ self.init_callback = init_callback
31
+
32
+ # Handle 'KEY' field type
33
+ self.projected_from = []
34
+ self.projects_fields = []
35
+
36
+ self.on_init()
37
+
38
+ @property
39
+ def required(self) -> bool:
40
+ return self.config.get('required', False)
41
+
42
+ @property
43
+ def default(self) -> Any:
44
+ return self.config.get('default', None)
45
+
46
+ @property
47
+ def value(self) -> Any:
48
+ return self.config.get('value', None)
49
+
50
+ @property
51
+ def on_create(self) -> Optional['types.GeneratorCallback']:
52
+ return self.config.get('on_create', None)
53
+
54
+ @property
55
+ def on_update(self) -> Optional['types.GeneratorCallback']:
56
+ return self.config.get('on_update', None)
57
+
58
+ @property
59
+ def on_write(self) -> Optional['types.GeneratorCallback']:
60
+ return self.config.get('on_write', None)
61
+
62
+ @property
63
+ def validator(self) -> Optional['types.ValidatorCallback']:
64
+ return self.config.get('validator', None)
65
+
66
+ @property
67
+ def enum(self) -> List[Any] | None:
68
+ return self.config.get('enum', None)
69
+
70
+ def __call__(self, value: Any, action: 'types.SerialisationType', validator: 'resources.Validator') -> Any:
71
+ """
72
+ Encapsulates the internal serialisation logic to prepare for
73
+ sending the record to DynamoDB.
74
+
75
+ :arg value: Deserialised value.
76
+ :arg action: SerialisationType (`create`, `update`, `write` or `query`).
77
+ :arg validator: Validator instance.
78
+ :return: Serialised value.
79
+ """
80
+ has_value = not isinstance(value, resources.EmptyValue)
81
+
82
+ # Apply generators if applicable to the field to overwrite the value
83
+ if action in ['create', 'update', 'write']:
84
+ if action == 'create' and self.on_create:
85
+ value = self.on_create()
86
+
87
+ if action == 'update' and self.on_update:
88
+ value = self.on_update()
89
+
90
+ if self.on_write:
91
+ value = self.on_write()
92
+
93
+ # Set default value if applicable
94
+ if not has_value and not value:
95
+ value = self.default
96
+
97
+ # Serialise the value
98
+ value = self.serialise(value)
99
+
100
+ # Validate required field
101
+ if self.required and action == 'create' and value is None:
102
+ validator.add(name=self.name, message='Field is required')
103
+
104
+ # Validate against enum
105
+ if self.enum and has_value and action in ['create', 'update', 'write'] and value not in self.enum:
106
+ validator.add(name=self.name, message=f'Value must be one of: {", ".join(self.enum)}')
107
+
108
+ # Run internal validation
109
+ self.internal_validation(value, action, validator)
110
+
111
+ # Run custom validation logic
112
+ if self.validator and action in ['create', 'update', 'write']:
113
+ self.validator(value, validator)
114
+
115
+ return value
116
+
117
+ def serialise(self, value: Any) -> Any:
118
+ """
119
+ Serialise/marshal the field value into DynamoDB format.
120
+
121
+ :arg value: Deserialised value.
122
+ :return: Serialised value.
123
+ """
124
+ raise nuql.NuqlError(
125
+ code='NotImplementedError',
126
+ message='Serialisation has not been implemented for this field type.'
127
+ )
128
+
129
+ def deserialise(self, value: Any) -> Any:
130
+ """
131
+ Deserialise/unmarshal the field value from DynamoDB format.
132
+
133
+ :arg value: Serialised value.
134
+ :return: Deserialised value.
135
+ """
136
+ raise nuql.NuqlError(
137
+ code='NotImplementedError',
138
+ message='Deserialisation has not been implemented for this field type.'
139
+ )
140
+
141
+ def on_init(self) -> None:
142
+ """Custom initialisation logic for the field."""
143
+ pass
144
+
145
+ def internal_validation(self, value: Any, action: 'types.SerialisationType', validator: 'resources.Validator'):
146
+ """
147
+ Perform internal validation on the field.
148
+
149
+ :arg value: Value.
150
+ :arg action: Serialisation action.
151
+ :arg validator: Validator instance.
152
+ """
153
+ pass