nuql 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nuql/__init__.py +3 -0
- nuql/api/__init__.py +13 -0
- nuql/api/adapter.py +34 -0
- nuql/api/batch_get/__init__.py +2 -0
- nuql/api/batch_get/batch_get.py +40 -0
- nuql/api/batch_get/queue.py +120 -0
- nuql/api/batch_write.py +99 -0
- nuql/api/condition_check.py +39 -0
- nuql/api/create.py +25 -0
- nuql/api/delete.py +88 -0
- nuql/api/get.py +30 -0
- nuql/api/put_item.py +112 -0
- nuql/api/put_update.py +25 -0
- nuql/api/query/__init__.py +4 -0
- nuql/api/query/condition.py +157 -0
- nuql/api/query/condition_builder.py +211 -0
- nuql/api/query/key_condition.py +200 -0
- nuql/api/query/query.py +166 -0
- nuql/api/transaction.py +145 -0
- nuql/api/update/__init__.py +3 -0
- nuql/api/update/expression_builder.py +33 -0
- nuql/api/update/update_item.py +139 -0
- nuql/api/update/utils.py +126 -0
- nuql/api/upsert.py +32 -0
- nuql/client.py +88 -0
- nuql/connection.py +43 -0
- nuql/exceptions.py +66 -0
- nuql/fields/__init__.py +11 -0
- nuql/fields/boolean.py +29 -0
- nuql/fields/datetime.py +49 -0
- nuql/fields/datetime_timestamp.py +45 -0
- nuql/fields/float.py +40 -0
- nuql/fields/integer.py +40 -0
- nuql/fields/key.py +207 -0
- nuql/fields/list.py +90 -0
- nuql/fields/map.py +67 -0
- nuql/fields/string.py +184 -0
- nuql/fields/ulid.py +39 -0
- nuql/fields/uuid.py +42 -0
- nuql/generators/__init__.py +3 -0
- nuql/generators/datetime.py +37 -0
- nuql/generators/ulid.py +10 -0
- nuql/generators/uuid.py +19 -0
- nuql/resources/__init__.py +4 -0
- nuql/resources/fields/__init__.py +3 -0
- nuql/resources/fields/field.py +153 -0
- nuql/resources/fields/field_map.py +85 -0
- nuql/resources/fields/value.py +5 -0
- nuql/resources/records/__init__.py +3 -0
- nuql/resources/records/projections.py +49 -0
- nuql/resources/records/serialiser.py +144 -0
- nuql/resources/records/validator.py +48 -0
- nuql/resources/tables/__init__.py +2 -0
- nuql/resources/tables/indexes.py +140 -0
- nuql/resources/tables/table.py +151 -0
- nuql/resources/utils/__init__.py +2 -0
- nuql/resources/utils/dict.py +21 -0
- nuql/resources/utils/validators.py +165 -0
- nuql/types/__init__.py +3 -0
- nuql/types/config.py +27 -0
- nuql/types/fields.py +27 -0
- nuql/types/serialisation.py +10 -0
- nuql-0.0.1.dist-info/METADATA +12 -0
- nuql-0.0.1.dist-info/RECORD +65 -0
- nuql-0.0.1.dist-info/WHEEL +4 -0
nuql/client.py
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
__all__ = ['Nuql']
|
2
|
+
|
3
|
+
from typing import List, Type, Dict, Any
|
4
|
+
|
5
|
+
from boto3 import Session
|
6
|
+
|
7
|
+
from nuql import types, api, resources
|
8
|
+
from . import Connection, exceptions
|
9
|
+
|
10
|
+
|
11
|
+
class Nuql:
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
name: str,
|
15
|
+
indexes: List[Dict[str, Any]] | Dict[str, Any],
|
16
|
+
schema: Dict[str, Any],
|
17
|
+
session: Session | None = None,
|
18
|
+
custom_fields: List[Type['types.FieldType']] | None = None,
|
19
|
+
global_fields: Dict[str, Any] | None = None,
|
20
|
+
) -> None:
|
21
|
+
"""
|
22
|
+
Nuql - a lightweight DynamoDB library for implementing
|
23
|
+
the single table model pattern.
|
24
|
+
|
25
|
+
:arg name: DynamoDB table name.
|
26
|
+
:arg indexes: Table index definition.
|
27
|
+
:arg schema: Table design.
|
28
|
+
:param session: Boto3 Session instance.
|
29
|
+
:param custom_fields: List of custom field types.
|
30
|
+
:param global_fields: Additional field definitions to apply to all tables.
|
31
|
+
"""
|
32
|
+
if not isinstance(session, Session):
|
33
|
+
session = Session()
|
34
|
+
|
35
|
+
if custom_fields is None:
|
36
|
+
custom_fields = []
|
37
|
+
|
38
|
+
# Insert global fields on to all tables
|
39
|
+
if isinstance(global_fields, dict):
|
40
|
+
for table in schema.values():
|
41
|
+
table.update(global_fields)
|
42
|
+
|
43
|
+
self.connection = Connection(name, session)
|
44
|
+
self.fields = custom_fields
|
45
|
+
self.__schema = schema
|
46
|
+
self.__indexes = resources.Indexes(indexes)
|
47
|
+
|
48
|
+
resources.validate_schema(self.__schema, self.fields)
|
49
|
+
|
50
|
+
@property
|
51
|
+
def indexes(self) -> 'resources.Indexes':
|
52
|
+
return self.__indexes
|
53
|
+
|
54
|
+
@property
|
55
|
+
def schema(self) -> 'types.SchemaConfig':
|
56
|
+
return self.__schema
|
57
|
+
|
58
|
+
def batch_write(self) -> 'api.BatchWrite':
|
59
|
+
"""
|
60
|
+
Instantiates a `BatchWrite` object for performing batch writes to DynamoDB.
|
61
|
+
|
62
|
+
:return: BatchWrite instance.
|
63
|
+
"""
|
64
|
+
return api.BatchWrite(self)
|
65
|
+
|
66
|
+
def transaction(self) -> 'api.Transaction':
|
67
|
+
"""
|
68
|
+
Instantiates a `Transaction` object for performing transactions on a DynamoDB table.
|
69
|
+
|
70
|
+
:return: Transaction instance.
|
71
|
+
"""
|
72
|
+
return api.Transaction(self)
|
73
|
+
|
74
|
+
def get_table(self, name: str) -> 'resources.Table':
|
75
|
+
"""
|
76
|
+
Instantiates a `Table` object for the chosen table in the schema.
|
77
|
+
|
78
|
+
:arg name: Table name (in schema) to instantiate.
|
79
|
+
:return: Table instance.
|
80
|
+
"""
|
81
|
+
if name not in self.__schema:
|
82
|
+
raise exceptions.NuqlError(
|
83
|
+
code='TableNotDefined',
|
84
|
+
message=f'Table \'{name}\' is not defined in the schema.'
|
85
|
+
)
|
86
|
+
|
87
|
+
schema = self.__schema[name]
|
88
|
+
return resources.Table(name=name, provider=self, schema=schema, indexes=self.indexes)
|
nuql/connection.py
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
__all__ = ['Connection']
|
2
|
+
|
3
|
+
from boto3 import Session
|
4
|
+
|
5
|
+
|
6
|
+
class Connection:
|
7
|
+
def __init__(self, table_name: str, boto3_session: Session, **connection_args) -> None:
|
8
|
+
"""
|
9
|
+
Wrapper for the `boto3` DynamoDB resources/clients to be
|
10
|
+
shared throughout the library.
|
11
|
+
|
12
|
+
:arg table_name: DynamoDB table name.
|
13
|
+
:arg boto3_session: Session instance.
|
14
|
+
:param connection_args: Additional args to pass to the client/resource.
|
15
|
+
"""
|
16
|
+
self.table_name = table_name
|
17
|
+
self.session = boto3_session
|
18
|
+
self.__connection_args = connection_args
|
19
|
+
|
20
|
+
self.__resource = None
|
21
|
+
self.__client = None
|
22
|
+
self.__table = None
|
23
|
+
|
24
|
+
@property
|
25
|
+
def resource(self):
|
26
|
+
"""Creates a `boto3` DynamoDB resource if it doesn't exist."""
|
27
|
+
if self.__resource is None:
|
28
|
+
self.__resource = self.session.resource('dynamodb', **self.__connection_args)
|
29
|
+
return self.__resource
|
30
|
+
|
31
|
+
@property
|
32
|
+
def client(self):
|
33
|
+
"""Creates a `boto3` DynamoDB client if it doesn't exist."""
|
34
|
+
if self.__client is None:
|
35
|
+
self.__client = self.session.client('dynamodb', **self.__connection_args)
|
36
|
+
return self.__client
|
37
|
+
|
38
|
+
@property
|
39
|
+
def table(self):
|
40
|
+
"""Creates a `boto3` DynamoDB table if it doesn't exist."""
|
41
|
+
if self.__table is None:
|
42
|
+
self.__table = self.resource.Table(self.table_name)
|
43
|
+
return self.__table
|
nuql/exceptions.py
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
__all__ = ['NuqlError', 'ValidationError', 'Boto3Error', 'ItemNotFound']
|
2
|
+
|
3
|
+
from typing import List, Dict, Any
|
4
|
+
|
5
|
+
from botocore.exceptions import ClientError
|
6
|
+
|
7
|
+
from nuql import types
|
8
|
+
|
9
|
+
|
10
|
+
class NuqlError(Exception):
|
11
|
+
def __init__(self, code: str, message: str, **details) -> None:
|
12
|
+
"""
|
13
|
+
Base exception for Nuql.
|
14
|
+
|
15
|
+
:arg code: Error code.
|
16
|
+
:arg message: Error message.
|
17
|
+
:param details: Arbitrary details to add to the exception.
|
18
|
+
"""
|
19
|
+
self.code = code
|
20
|
+
self.message = message
|
21
|
+
self.details = details
|
22
|
+
|
23
|
+
super().__init__(f'[{self.code}] {self.message}')
|
24
|
+
|
25
|
+
|
26
|
+
class ValidationError(NuqlError):
|
27
|
+
def __init__(self, errors: List['types.ValidationErrorItem']):
|
28
|
+
"""
|
29
|
+
Exception for validation errors during the serialisation process.
|
30
|
+
|
31
|
+
:arg errors: List of ValidationErrorItem dicts.
|
32
|
+
"""
|
33
|
+
self.errors = errors
|
34
|
+
|
35
|
+
formatted_message = 'Schema validation errors occurred:\n\n'
|
36
|
+
|
37
|
+
for error in self.errors:
|
38
|
+
formatted_message += f' \'{error["name"]}\': {error["message"]}\n'
|
39
|
+
|
40
|
+
super().__init__('ValidationError', formatted_message)
|
41
|
+
|
42
|
+
|
43
|
+
class Boto3Error(NuqlError):
|
44
|
+
def __init__(self, exc: ClientError, request_args: Dict[str, Any]):
|
45
|
+
"""
|
46
|
+
Exception wrapper for boto3 ClientError.
|
47
|
+
|
48
|
+
:arg exc: ClientError instance.
|
49
|
+
"""
|
50
|
+
self.request_args = request_args
|
51
|
+
self.error_info = exc.response.get('Error', {})
|
52
|
+
self.code = self.error_info.get('Code', 'UnknownError')
|
53
|
+
self.message = self.error_info.get('Message', str(exc))
|
54
|
+
|
55
|
+
super().__init__(code=self.code, message=self.message)
|
56
|
+
|
57
|
+
|
58
|
+
class ItemNotFound(NuqlError):
|
59
|
+
def __init__(self, key: Dict[str, Any]) -> None:
|
60
|
+
"""
|
61
|
+
Thrown when an item is not found when doing a get_item request.
|
62
|
+
|
63
|
+
:arg key: Key used to retrieve the item.
|
64
|
+
"""
|
65
|
+
self.key = key
|
66
|
+
super().__init__('ItemNotFound', f'Item not found: {key}')
|
nuql/fields/__init__.py
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
from .string import *
|
2
|
+
from .key import *
|
3
|
+
from .boolean import *
|
4
|
+
from .datetime import *
|
5
|
+
from .datetime_timestamp import *
|
6
|
+
from .list import *
|
7
|
+
from .map import *
|
8
|
+
from .integer import *
|
9
|
+
from .float import *
|
10
|
+
from .uuid import *
|
11
|
+
from .ulid import *
|
nuql/fields/boolean.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
__all__ = ['Boolean']
|
2
|
+
|
3
|
+
from nuql.resources import FieldBase
|
4
|
+
|
5
|
+
|
6
|
+
class Boolean(FieldBase):
|
7
|
+
type = 'boolean'
|
8
|
+
|
9
|
+
def serialise(self, value: bool | None) -> bool | None:
|
10
|
+
"""
|
11
|
+
Serialises a boolean value (type checker).
|
12
|
+
|
13
|
+
:arg value: Boolean value.
|
14
|
+
:return: Boolean value.
|
15
|
+
"""
|
16
|
+
if isinstance(value, bool):
|
17
|
+
return value
|
18
|
+
return None
|
19
|
+
|
20
|
+
def deserialise(self, value: bool | None) -> bool | None:
|
21
|
+
"""
|
22
|
+
Deserialises a boolean value (type checker).
|
23
|
+
|
24
|
+
:arg value: Boolean value.
|
25
|
+
:return: Boolean value.
|
26
|
+
"""
|
27
|
+
if isinstance(value, bool):
|
28
|
+
return value
|
29
|
+
return None
|
nuql/fields/datetime.py
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
__all__ = ['Datetime']
|
2
|
+
|
3
|
+
from datetime import datetime, UTC
|
4
|
+
|
5
|
+
import nuql
|
6
|
+
from nuql.resources import FieldBase
|
7
|
+
|
8
|
+
|
9
|
+
class Datetime(FieldBase):
|
10
|
+
type = 'datetime'
|
11
|
+
date_format = '%Y-%m-%dT%H:%M:%S%z'
|
12
|
+
|
13
|
+
def serialise(self, value: datetime | None) -> str | None:
|
14
|
+
"""
|
15
|
+
Serialises a datetime value.
|
16
|
+
|
17
|
+
:arg value: datetime instance or None.
|
18
|
+
:return: String representation of the datetime instance.
|
19
|
+
"""
|
20
|
+
if not isinstance(value, datetime):
|
21
|
+
return None
|
22
|
+
|
23
|
+
# Validate that the datetime is timezone-aware
|
24
|
+
if not value.tzinfo:
|
25
|
+
raise nuql.NuqlError(
|
26
|
+
code='SerialisationError',
|
27
|
+
message='Datetime value must be timezone-aware.'
|
28
|
+
)
|
29
|
+
|
30
|
+
return str(value.astimezone(UTC).strftime(self.date_format))
|
31
|
+
|
32
|
+
def deserialise(self, value: str | None) -> datetime | None:
|
33
|
+
"""
|
34
|
+
Deserialises a datetime value.
|
35
|
+
|
36
|
+
:arg value: String representation of the datetime.
|
37
|
+
:return: datetime instance or None.
|
38
|
+
"""
|
39
|
+
if value is None:
|
40
|
+
return None
|
41
|
+
|
42
|
+
# Parse the datetime string
|
43
|
+
try:
|
44
|
+
dt = datetime.strptime(value, self.date_format)
|
45
|
+
dt = dt.replace(tzinfo=UTC)
|
46
|
+
return dt
|
47
|
+
|
48
|
+
except (ValueError, TypeError):
|
49
|
+
return None
|
@@ -0,0 +1,45 @@
|
|
1
|
+
__all__ = ['DatetimeTimestamp']
|
2
|
+
|
3
|
+
from datetime import datetime, UTC
|
4
|
+
from decimal import Decimal
|
5
|
+
|
6
|
+
import nuql
|
7
|
+
from nuql.resources import FieldBase
|
8
|
+
|
9
|
+
|
10
|
+
class DatetimeTimestamp(FieldBase):
|
11
|
+
type = 'datetime_timestamp'
|
12
|
+
|
13
|
+
def serialise(self, value: datetime | None) -> int | None:
|
14
|
+
"""
|
15
|
+
Serialises a datetime to a timestamp.
|
16
|
+
|
17
|
+
:arg value: datetime instance or None.
|
18
|
+
:return: int or None.
|
19
|
+
"""
|
20
|
+
if not isinstance(value, datetime):
|
21
|
+
return None
|
22
|
+
|
23
|
+
# Validate timezone-awareness
|
24
|
+
if value.tzinfo is None:
|
25
|
+
raise nuql.NuqlError(
|
26
|
+
code='SerialisationError',
|
27
|
+
message='Datetime value must be timezone-aware.'
|
28
|
+
)
|
29
|
+
|
30
|
+
return int(value.astimezone(UTC).timestamp() * 1000)
|
31
|
+
|
32
|
+
def deserialise(self, value: Decimal | None) -> datetime | None:
|
33
|
+
"""
|
34
|
+
Deserialises a timestamp to a datetime.
|
35
|
+
|
36
|
+
:arg value: Decimal instance or None.
|
37
|
+
:return: datetime instance or None.
|
38
|
+
"""
|
39
|
+
if not isinstance(value, Decimal):
|
40
|
+
return None
|
41
|
+
|
42
|
+
try:
|
43
|
+
return datetime.fromtimestamp(int(value) / 1000, UTC)
|
44
|
+
except (ValueError, TypeError):
|
45
|
+
return None
|
nuql/fields/float.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
__all__ = ['Float']
|
2
|
+
|
3
|
+
from decimal import Decimal
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from nuql import resources, types
|
7
|
+
from nuql.api import Incrementor
|
8
|
+
|
9
|
+
|
10
|
+
class Float(resources.FieldBase):
|
11
|
+
type = 'float'
|
12
|
+
|
13
|
+
def serialise(self, value: float | Incrementor | None) -> Decimal | Incrementor | None:
|
14
|
+
"""
|
15
|
+
Serialises a float value.
|
16
|
+
|
17
|
+
:arg value: Value as float, Incrementor or None.
|
18
|
+
:return: Value as Decimal, Incrementor or None.
|
19
|
+
"""
|
20
|
+
if not isinstance(value, (float, Incrementor)):
|
21
|
+
return None
|
22
|
+
if isinstance(value, Incrementor):
|
23
|
+
return value
|
24
|
+
return Decimal(str(value))
|
25
|
+
|
26
|
+
def deserialise(self, value: Decimal | None) -> float | None:
|
27
|
+
"""
|
28
|
+
Deserialises a float value.
|
29
|
+
|
30
|
+
:arg value: Value as Decimal or None.
|
31
|
+
:return: Value as float or None.
|
32
|
+
"""
|
33
|
+
if not isinstance(value, Decimal):
|
34
|
+
return None
|
35
|
+
return float(value)
|
36
|
+
|
37
|
+
def internal_validation(self, value: Any, action: 'types.SerialisationType', validator: 'resources.Validator'):
|
38
|
+
"""Validate the Incrementor type."""
|
39
|
+
if isinstance(value, Incrementor) and action != 'update':
|
40
|
+
validator.add(name=self.name, message='Incrementors can only be used for updates')
|
nuql/fields/integer.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
__all__ = ['Integer']
|
2
|
+
|
3
|
+
from decimal import Decimal
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from nuql.api import Incrementor
|
7
|
+
from nuql import resources, types
|
8
|
+
|
9
|
+
|
10
|
+
class Integer(resources.FieldBase):
|
11
|
+
type = 'int'
|
12
|
+
|
13
|
+
def serialise(self, value: int | Incrementor | None) -> Decimal | Incrementor | None:
|
14
|
+
"""
|
15
|
+
Serialises an integer value.
|
16
|
+
|
17
|
+
:arg value: Value as int, Incrementor or None.
|
18
|
+
:return: Value as Decimal, Incrementor or None.
|
19
|
+
"""
|
20
|
+
if not isinstance(value, (int, Incrementor)):
|
21
|
+
return None
|
22
|
+
if isinstance(value, Incrementor):
|
23
|
+
return value
|
24
|
+
return Decimal(str(value))
|
25
|
+
|
26
|
+
def deserialise(self, value: Decimal | None) -> int | None:
|
27
|
+
"""
|
28
|
+
Deserialises an integer value.
|
29
|
+
|
30
|
+
:arg value: Value as Decimal or None.
|
31
|
+
:return: Value as int or None.
|
32
|
+
"""
|
33
|
+
if not isinstance(value, Decimal):
|
34
|
+
return None
|
35
|
+
return int(value)
|
36
|
+
|
37
|
+
def internal_validation(self, value: Any, action: 'types.SerialisationType', validator: 'resources.Validator'):
|
38
|
+
"""Validate the Incrementor type."""
|
39
|
+
if isinstance(value, Incrementor) and action != 'update':
|
40
|
+
validator.add(name=self.name, message='Incrementors can only be used for updates')
|
nuql/fields/key.py
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
__all__ = ['Key']
|
2
|
+
|
3
|
+
import re
|
4
|
+
from typing import Dict, Any
|
5
|
+
|
6
|
+
import nuql
|
7
|
+
from nuql import resources, types
|
8
|
+
|
9
|
+
|
10
|
+
class Key(resources.FieldBase):
|
11
|
+
type = 'key'
|
12
|
+
|
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
|
+
def on_init(self) -> None:
|
61
|
+
"""Initialises the key field."""
|
62
|
+
# Validate the field has a value
|
63
|
+
if self.value is None:
|
64
|
+
raise nuql.NuqlError(
|
65
|
+
code='KeySchemaError',
|
66
|
+
message='\'value\' must be defined for a key field'
|
67
|
+
)
|
68
|
+
|
69
|
+
# Callback fn handles configuring projected fields on the schema
|
70
|
+
def callback(field_map: dict) -> None:
|
71
|
+
"""Callback fn to configure projected fields on the schema."""
|
72
|
+
for key, value in self.value.items():
|
73
|
+
projected_name = self.parse_projected_name(value)
|
74
|
+
|
75
|
+
# Skip fixed value fields
|
76
|
+
if not projected_name:
|
77
|
+
continue
|
78
|
+
|
79
|
+
# Validate projected key exists on the table
|
80
|
+
if projected_name not in field_map:
|
81
|
+
raise nuql.NuqlError(
|
82
|
+
code='KeySchemaError',
|
83
|
+
message=f'Field \'{projected_name}\' (projected on key '
|
84
|
+
f'\'{self.name}\') is not defined in the schema'
|
85
|
+
)
|
86
|
+
|
87
|
+
# Add reference to this field on the projected field
|
88
|
+
field_map[projected_name].projected_from.append(self.name)
|
89
|
+
self.projects_fields.append(projected_name)
|
90
|
+
|
91
|
+
if self.init_callback is not None:
|
92
|
+
self.init_callback(callback)
|
93
|
+
|
94
|
+
def serialise_template(
|
95
|
+
self,
|
96
|
+
key_dict: Dict[str, Any],
|
97
|
+
action: 'types.SerialisationType',
|
98
|
+
validator: 'resources.Validator'
|
99
|
+
) -> str:
|
100
|
+
"""
|
101
|
+
Serialises the key dict to a string.
|
102
|
+
|
103
|
+
:arg key_dict: Dict to serialise.
|
104
|
+
:arg action: Serialisation type.
|
105
|
+
:arg validator: Validator instance.
|
106
|
+
:return: Serialised representation.
|
107
|
+
"""
|
108
|
+
output = ''
|
109
|
+
s = self.sanitise
|
110
|
+
|
111
|
+
for key, value in self.value.items():
|
112
|
+
projected_name = self.parse_projected_name(value)
|
113
|
+
|
114
|
+
if projected_name in self.projects_fields:
|
115
|
+
projected_field = self.parent.fields.get(projected_name)
|
116
|
+
|
117
|
+
if projected_field is None:
|
118
|
+
raise nuql.NuqlError(
|
119
|
+
code='KeySchemaError',
|
120
|
+
message=f'Field \'{projected_name}\' (projected on key '
|
121
|
+
f'\'{self.name}\') is not defined in the schema'
|
122
|
+
)
|
123
|
+
|
124
|
+
projected_value = key_dict.get(projected_name)
|
125
|
+
serialised_value = projected_field(projected_value, action, validator)
|
126
|
+
used_value = s(serialised_value) if serialised_value else None
|
127
|
+
else:
|
128
|
+
used_value = s(value)
|
129
|
+
|
130
|
+
# A query might provide only a partial value
|
131
|
+
if projected_name is not None and projected_name not in value:
|
132
|
+
break
|
133
|
+
|
134
|
+
output += f'{s(key)}:{used_value if used_value else ""}|'
|
135
|
+
|
136
|
+
return output[:-1]
|
137
|
+
|
138
|
+
def deserialise(self, value: str) -> Dict[str, Any]:
|
139
|
+
"""
|
140
|
+
Deserialises the key string to a dict.
|
141
|
+
|
142
|
+
:arg value: String key value.
|
143
|
+
:return: Key dict.
|
144
|
+
"""
|
145
|
+
output = {}
|
146
|
+
|
147
|
+
if value is None:
|
148
|
+
return output
|
149
|
+
|
150
|
+
unmarshalled = {
|
151
|
+
key: serialised_value
|
152
|
+
if serialised_value else None
|
153
|
+
for key, serialised_value in [item.split(':') for item in value.split('|')]
|
154
|
+
}
|
155
|
+
|
156
|
+
for key, serialised_value in self.value.items():
|
157
|
+
provided_value = unmarshalled.get(key)
|
158
|
+
projected_name = self.parse_projected_name(serialised_value)
|
159
|
+
|
160
|
+
if projected_name in self.projects_fields:
|
161
|
+
projected_field = self.parent.fields.get(projected_name)
|
162
|
+
|
163
|
+
if projected_field is None:
|
164
|
+
raise nuql.NuqlError(
|
165
|
+
code='KeySchemaError',
|
166
|
+
message=f'Field \'{projected_name}\' (projected on key '
|
167
|
+
f'\'{self.name}\') is not defined in the schema'
|
168
|
+
)
|
169
|
+
|
170
|
+
deserialised_value = projected_field.deserialise(provided_value)
|
171
|
+
output[projected_name] = deserialised_value
|
172
|
+
else:
|
173
|
+
output[key] = provided_value
|
174
|
+
|
175
|
+
return output
|
176
|
+
|
177
|
+
@staticmethod
|
178
|
+
def parse_projected_name(value: str) -> str | None:
|
179
|
+
"""
|
180
|
+
Parses key name in the format '${field_name}'.
|
181
|
+
|
182
|
+
:arg value: Value to parse.
|
183
|
+
:return: Field name if it matches the format.
|
184
|
+
"""
|
185
|
+
if not isinstance(value, str):
|
186
|
+
return None
|
187
|
+
match = re.search(r'\$\{([a-zA-Z0-9_]+)}', value)
|
188
|
+
if not match:
|
189
|
+
return None
|
190
|
+
else:
|
191
|
+
return match.group(1)
|
192
|
+
|
193
|
+
@staticmethod
|
194
|
+
def sanitise(value: str) -> str:
|
195
|
+
"""
|
196
|
+
Sanitises the input to avoid conflict with serialisation/deserialisation.
|
197
|
+
|
198
|
+
:arg value: String value.
|
199
|
+
:return: Sanitised string value.
|
200
|
+
"""
|
201
|
+
if not isinstance(value, str):
|
202
|
+
value = str(value)
|
203
|
+
|
204
|
+
for character in [':', '|']:
|
205
|
+
value = value.replace(character, '')
|
206
|
+
|
207
|
+
return value
|