nuql 0.0.1__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 (65) hide show
  1. nuql-0.0.1/.gitignore +4 -0
  2. nuql-0.0.1/PKG-INFO +12 -0
  3. nuql-0.0.1/nuql/__init__.py +3 -0
  4. nuql-0.0.1/nuql/api/__init__.py +13 -0
  5. nuql-0.0.1/nuql/api/adapter.py +34 -0
  6. nuql-0.0.1/nuql/api/batch_get/__init__.py +2 -0
  7. nuql-0.0.1/nuql/api/batch_get/batch_get.py +40 -0
  8. nuql-0.0.1/nuql/api/batch_get/queue.py +120 -0
  9. nuql-0.0.1/nuql/api/batch_write.py +99 -0
  10. nuql-0.0.1/nuql/api/condition_check.py +39 -0
  11. nuql-0.0.1/nuql/api/create.py +25 -0
  12. nuql-0.0.1/nuql/api/delete.py +88 -0
  13. nuql-0.0.1/nuql/api/get.py +30 -0
  14. nuql-0.0.1/nuql/api/put_item.py +112 -0
  15. nuql-0.0.1/nuql/api/put_update.py +25 -0
  16. nuql-0.0.1/nuql/api/query/__init__.py +4 -0
  17. nuql-0.0.1/nuql/api/query/condition.py +157 -0
  18. nuql-0.0.1/nuql/api/query/condition_builder.py +211 -0
  19. nuql-0.0.1/nuql/api/query/key_condition.py +200 -0
  20. nuql-0.0.1/nuql/api/query/query.py +166 -0
  21. nuql-0.0.1/nuql/api/transaction.py +145 -0
  22. nuql-0.0.1/nuql/api/update/__init__.py +3 -0
  23. nuql-0.0.1/nuql/api/update/expression_builder.py +33 -0
  24. nuql-0.0.1/nuql/api/update/update_item.py +139 -0
  25. nuql-0.0.1/nuql/api/update/utils.py +126 -0
  26. nuql-0.0.1/nuql/api/upsert.py +32 -0
  27. nuql-0.0.1/nuql/client.py +88 -0
  28. nuql-0.0.1/nuql/connection.py +43 -0
  29. nuql-0.0.1/nuql/exceptions.py +66 -0
  30. nuql-0.0.1/nuql/fields/__init__.py +11 -0
  31. nuql-0.0.1/nuql/fields/boolean.py +29 -0
  32. nuql-0.0.1/nuql/fields/datetime.py +49 -0
  33. nuql-0.0.1/nuql/fields/datetime_timestamp.py +45 -0
  34. nuql-0.0.1/nuql/fields/float.py +40 -0
  35. nuql-0.0.1/nuql/fields/integer.py +40 -0
  36. nuql-0.0.1/nuql/fields/key.py +207 -0
  37. nuql-0.0.1/nuql/fields/list.py +90 -0
  38. nuql-0.0.1/nuql/fields/map.py +67 -0
  39. nuql-0.0.1/nuql/fields/string.py +184 -0
  40. nuql-0.0.1/nuql/fields/ulid.py +39 -0
  41. nuql-0.0.1/nuql/fields/uuid.py +42 -0
  42. nuql-0.0.1/nuql/generators/__init__.py +3 -0
  43. nuql-0.0.1/nuql/generators/datetime.py +37 -0
  44. nuql-0.0.1/nuql/generators/ulid.py +10 -0
  45. nuql-0.0.1/nuql/generators/uuid.py +19 -0
  46. nuql-0.0.1/nuql/resources/__init__.py +4 -0
  47. nuql-0.0.1/nuql/resources/fields/__init__.py +3 -0
  48. nuql-0.0.1/nuql/resources/fields/field.py +153 -0
  49. nuql-0.0.1/nuql/resources/fields/field_map.py +85 -0
  50. nuql-0.0.1/nuql/resources/fields/value.py +5 -0
  51. nuql-0.0.1/nuql/resources/records/__init__.py +3 -0
  52. nuql-0.0.1/nuql/resources/records/projections.py +49 -0
  53. nuql-0.0.1/nuql/resources/records/serialiser.py +144 -0
  54. nuql-0.0.1/nuql/resources/records/validator.py +48 -0
  55. nuql-0.0.1/nuql/resources/tables/__init__.py +2 -0
  56. nuql-0.0.1/nuql/resources/tables/indexes.py +140 -0
  57. nuql-0.0.1/nuql/resources/tables/table.py +151 -0
  58. nuql-0.0.1/nuql/resources/utils/__init__.py +2 -0
  59. nuql-0.0.1/nuql/resources/utils/dict.py +21 -0
  60. nuql-0.0.1/nuql/resources/utils/validators.py +165 -0
  61. nuql-0.0.1/nuql/types/__init__.py +3 -0
  62. nuql-0.0.1/nuql/types/config.py +27 -0
  63. nuql-0.0.1/nuql/types/fields.py +27 -0
  64. nuql-0.0.1/nuql/types/serialisation.py +10 -0
  65. nuql-0.0.1/pyproject.toml +41 -0
nuql-0.0.1/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .venv
2
+ .idea
3
+ local.py
4
+ .coverage
nuql-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,3 @@
1
+ from .exceptions import *
2
+ from .connection import *
3
+ from .client import *
@@ -0,0 +1,13 @@
1
+ from .adapter import *
2
+ from .query import *
3
+ from .batch_write import *
4
+ from .get import *
5
+ from .delete import *
6
+ from .update import *
7
+ from .put_item import *
8
+ from .create import *
9
+ from .upsert import *
10
+ from .put_update import *
11
+ from .condition_check import *
12
+ from .transaction import *
13
+ from .batch_get import *
@@ -0,0 +1,34 @@
1
+ __all__ = ['Boto3Adapter']
2
+
3
+ from typing import Any, Dict
4
+
5
+ import nuql
6
+ from nuql import resources
7
+
8
+
9
+ class Boto3Adapter:
10
+ def __init__(self, client: 'nuql.Nuql', table: 'resources.Table'):
11
+ """
12
+ Wrapper around API actions against boto3.
13
+
14
+ :arg client: Nuql instance.
15
+ """
16
+ self.client = client
17
+ self.connection = client.connection
18
+ self.table = table
19
+
20
+ def prepare_client_args(self, *args, **kwargs) -> Dict[str, Any]:
21
+ """Prepares the arguments for boto3 API invocation (for the client API)."""
22
+ raise NotImplementedError('Argument preparation for the client API has not been implemented for this method.')
23
+
24
+ def prepare_args(self, *args, **kwargs) -> Dict[str, Any]:
25
+ """Prepares the arguments for boto3 API invocation."""
26
+ raise NotImplementedError('Argument preparation has not been implemented for this method.')
27
+
28
+ def invoke_sync(self, *args, **kwargs) -> Any:
29
+ """Synchronously invokes boto3 API."""
30
+ raise NotImplementedError('Synchronous API invocation has not been implemented for this method.')
31
+
32
+ async def invoke_async(self, *args, **kwargs) -> Any:
33
+ """Asynchronously invokes boto3 API."""
34
+ raise NotImplementedError('Asynchronous API invocation has not been implemented for this method.')
@@ -0,0 +1,2 @@
1
+ from .queue import *
2
+ from .batch_get import *
@@ -0,0 +1,40 @@
1
+ __all__ = ['BatchGet']
2
+
3
+ from typing import Any, List, Dict
4
+
5
+ from botocore.exceptions import ClientError
6
+
7
+ import nuql
8
+ from nuql import api
9
+ from nuql.api import Boto3Adapter
10
+
11
+
12
+ class BatchGet(Boto3Adapter):
13
+ def invoke_sync(self, keys: List[Dict[str, Any]]) -> Dict[str, Any]:
14
+ """
15
+ Performs a batch get operation against the table.
16
+
17
+ :arg keys: Keys to get.
18
+ :return: Batch get result.
19
+ """
20
+ queue = api.BatchGetQueue(self.table, keys)
21
+ fulfilled = False
22
+
23
+ # Loop through until all keys have been processed
24
+ while not fulfilled:
25
+ batch = queue.get_batch()
26
+
27
+ if batch is None:
28
+ fulfilled = True
29
+ continue
30
+
31
+ args = {'RequestItems': batch}
32
+
33
+ try:
34
+ response = self.client.connection.client.batch_get_item(**args)
35
+ queue.process_response(response)
36
+
37
+ except ClientError as exc:
38
+ raise nuql.Boto3Error(exc, args)
39
+
40
+ return queue.result
@@ -0,0 +1,120 @@
1
+ __all__ = ['BatchGetQueue']
2
+
3
+ from typing import List, Dict, Any
4
+
5
+ from boto3.dynamodb.types import TypeSerializer, TypeDeserializer
6
+
7
+ from nuql import resources
8
+
9
+
10
+ class BatchGetQueue:
11
+ def __init__(self, table: 'resources.Table', keys: List[Dict[str, Any]]):
12
+ """
13
+ Queue helper for performing batch get operations.
14
+
15
+ :arg table: Table instance.
16
+ :arg keys: List of record keys to retrieve.
17
+ """
18
+ self.table = table
19
+ self.db_table_name = self.table.provider.connection.table_name
20
+
21
+ self._store = self.prepare(keys)
22
+
23
+ @staticmethod
24
+ def get_key_hash(key: Dict[str, Any]) -> str:
25
+ """
26
+ Simple hash function to make the store accessible by key.
27
+
28
+ :arg key: Key dict.
29
+ :return: Str hash of the key.
30
+ """
31
+ return ','.join([str(key[sorted_key]) for sorted_key in sorted(key.keys())])
32
+
33
+ @property
34
+ def result(self) -> Dict[str, Any]:
35
+ return {
36
+ 'items': [x['item'] for x in self._store.values() if x['item'] is not None],
37
+ 'unprocessed_keys': [
38
+ x['deserialised_key'] for x in self._store.values() if x['item'] is None
39
+ ]
40
+ }
41
+
42
+ def prepare(self, keys: List[Dict[str, Any]]) -> Dict[str, Any]:
43
+ """
44
+ Prepare the initial queue.
45
+
46
+ :arg keys: Full list of record keys.
47
+ :return: Queue dict.
48
+ """
49
+ output = {}
50
+ serialiser = TypeSerializer()
51
+
52
+ for key in keys:
53
+ serialised_key = self.table.serialiser.serialise_key(key)
54
+ key_hash = self.get_key_hash(serialised_key)
55
+
56
+ marshalled_key = {k: serialiser.serialize(v) for k, v in serialised_key.items()}
57
+
58
+ output[key_hash] = {
59
+ 'key': marshalled_key,
60
+ 'item': None,
61
+ 'dispatched': False,
62
+ 'processed': False,
63
+ 'deserialised_key': key
64
+ }
65
+
66
+ return output
67
+
68
+ def process_response(self, response: Dict[str, Any]) -> None:
69
+ """
70
+ Process the raw response from DynamoDB.
71
+
72
+ :arg response: Response dict.
73
+ """
74
+ processed = response.get('Responses', {}).get(self.db_table_name, [])
75
+ unprocessed = response.get('UnprocessedKeys', {}).get(self.db_table_name, {}).get('Keys', [])
76
+ deserialiser = TypeDeserializer()
77
+
78
+ # Handle successful keys
79
+ for item in processed:
80
+ item = {k: deserialiser.deserialize(v) for k, v in item.items()}
81
+ data = self.table.serialiser.deserialise(item)
82
+ key_hash = self.get_key_hash(self.table.serialiser.serialise_key(data))
83
+
84
+ self._store[key_hash]['item'] = data
85
+ self._store[key_hash]['processed'] = True
86
+
87
+ # Handle unprocessed keys (throttled)
88
+ for key in unprocessed:
89
+ data = self.table.serialiser.deserialise(key)
90
+ key_hash = self.get_key_hash(data)
91
+
92
+ # Reset the item
93
+ self._store[key_hash]['item'] = None
94
+ self._store[key_hash]['processed'] = False
95
+ self._store[key_hash]['dispatched'] = False
96
+
97
+ def get_batch(self, size: int = 100) -> Dict[str, Any] | None:
98
+ """
99
+ Get a batch of unprocessed keys.
100
+
101
+ :param size: Max number of keys to return.
102
+ :return: List of key dicts or None.
103
+ """
104
+ # Collect items not yet processed or dispatched
105
+ items = [
106
+ (key_hash, entry)
107
+ for key_hash, entry in self._store.items()
108
+ if not entry['processed'] and not entry['dispatched']
109
+ ]
110
+ selected = items[:size]
111
+
112
+ if not selected:
113
+ return None
114
+
115
+ # Mark dispatched to avoid re-dispatching
116
+ for key_hash, entry in selected:
117
+ entry['dispatched'] = True
118
+
119
+ batch_keys = [entry['key'] for _, entry in selected]
120
+ return {self.db_table_name: {'Keys': batch_keys}}
@@ -0,0 +1,99 @@
1
+ __all__ = ['BatchWrite']
2
+
3
+ from typing import Dict, Any, Optional
4
+
5
+ from botocore.exceptions import ClientError
6
+
7
+ import nuql
8
+ from nuql import resources, types, api
9
+
10
+
11
+ class BatchWrite:
12
+ def __init__(self, client: 'nuql.Nuql') -> None:
13
+ """
14
+ Batch writer context manager.
15
+
16
+ :arg client: Nuql instance.
17
+ """
18
+ self.client = client
19
+
20
+ self._actions = {'put_item': [], 'delete_item': []}
21
+ self._started = False
22
+
23
+ def __enter__(self):
24
+ """Enter the context manager."""
25
+ self._actions = {'put_item': [], 'delete_item': []}
26
+ self._started = True
27
+
28
+ return self
29
+
30
+ def __exit__(self, exc_type, exc_val, exc_tb):
31
+ """Dispatch batch write to DynamoDB."""
32
+ try:
33
+ with self.client.connection.table.batch_writer() as batch:
34
+ for args in self._actions['put_item']:
35
+ batch.put_item(**args)
36
+
37
+ for args in self._actions['delete_item']:
38
+ batch.delete_item(**args)
39
+
40
+ except ClientError as exc:
41
+ raise nuql.Boto3Error(exc, self._actions)
42
+
43
+ self._started = False
44
+ return False
45
+
46
+ def _validate_started(self) -> None:
47
+ """Validates that the context manager has been started."""
48
+ if not self._started:
49
+ raise nuql.NuqlError(
50
+ code='BatchWriteError',
51
+ message='Batch write context manager has not been started'
52
+ )
53
+
54
+ def create(self,
55
+ table: 'resources.Table',
56
+ data: Dict[str, Any],
57
+ ) -> None:
58
+ """
59
+ Create a new item on the table as part of a batch write.
60
+
61
+ :arg table: Table instance.
62
+ :arg data: Data to create.
63
+ """
64
+ self._validate_started()
65
+ create = api.Create(self.client, table)
66
+ args = create.prepare_args(data=data, exclude_condition=True)
67
+ self._actions['put_item'].append(args)
68
+
69
+ def update(
70
+ self,
71
+ table: 'resources.Table',
72
+ data: Dict[str, Any],
73
+ ) -> None:
74
+ """
75
+ Update an existing item on the table as part of a batch write.
76
+
77
+ :arg table: Table instance.
78
+ :arg data: Data to update.
79
+ """
80
+ self._validate_started()
81
+ put_update = api.PutUpdate(self.client, table)
82
+ args = put_update.prepare_args(data=data, exclude_condition=True)
83
+ self._actions['put_item'].append(args)
84
+
85
+ def delete(
86
+ self,
87
+ table: 'resources.Table',
88
+ key: Dict[str, Any],
89
+ ) -> None:
90
+ """
91
+ Delete an item on the table as part of a batch write.
92
+
93
+ :arg table: Table instance.
94
+ :arg key: Item key.
95
+ """
96
+ self._validate_started()
97
+ delete = api.Delete(self.client, table)
98
+ args = delete.prepare_args(key=key, exclude_condition=True)
99
+ self._actions['delete_item'].append(args)
@@ -0,0 +1,39 @@
1
+ __all__ = ['ConditionCheck']
2
+
3
+ from typing import Dict, Any
4
+
5
+ from boto3.dynamodb.types import TypeSerializer
6
+
7
+ from nuql import types, api, resources
8
+ from nuql.api import Boto3Adapter
9
+
10
+
11
+ class ConditionCheck(Boto3Adapter):
12
+ def prepare_client_args(self, key: Dict[str, Any], condition: Dict[str, Any], **kwargs) -> Dict[str, Any]:
13
+ """
14
+ Prepare the request args for a condition check operation against the table (client API).
15
+
16
+ :arg key: Record key as a dict.
17
+ :arg condition: Condition expression.
18
+ :param kwargs: Optional parameters to add to the request.
19
+ :return: Client request args.
20
+ """
21
+ serialised_data = self.table.serialiser.serialise('query', key)
22
+ resources.validate_condition_dict(condition, required=True)
23
+ condition = api.Condition(self.table, condition, 'ConditionExpression')
24
+
25
+ # Marshall into the DynamoDB format
26
+ serialiser = TypeSerializer()
27
+ marshalled_data = {k: serialiser.serialize(v) for k, v in serialised_data.items()}
28
+
29
+ args = {'Key': marshalled_data, **condition.client_args, **kwargs}
30
+
31
+ # Serialise ExpressionAttributeValues into DynamoDB format
32
+ if 'ExpressionAttributeValues' in args:
33
+ for key, value in args['ExpressionAttributeValues'].items():
34
+ args['ExpressionAttributeValues'][key] = serialiser.serialize(value)
35
+
36
+ if 'ExpressionAttributeValues' in args and not args['ExpressionAttributeValues']:
37
+ args.pop('ExpressionAttributeValues')
38
+
39
+ return args
@@ -0,0 +1,25 @@
1
+ __all__ = ['Create']
2
+
3
+ from nuql import api
4
+
5
+
6
+ class Create(api.PutItem):
7
+ serialisation_action = 'create'
8
+
9
+ def on_condition(self, condition: 'api.Condition') -> None:
10
+ """
11
+ Sets the condition expression to assert creation.
12
+
13
+ :arg condition: Condition instance.
14
+ """
15
+ index = self.table.indexes.primary
16
+ keys = [index['hash']]
17
+
18
+ # Append sort key if defined in primary index, but only if it exists in the schema
19
+ if 'sort' in index and index['sort'] and index['sort'] in self.table.fields:
20
+ keys.append(index['sort'])
21
+
22
+ expression = ' and '.join([f'attribute_not_exists({key})' for key in keys])
23
+
24
+ # Add the expression to the existing condition
25
+ condition.append(expression)
@@ -0,0 +1,88 @@
1
+ __all__ = ['Delete']
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from boto3.dynamodb.types import TypeSerializer
6
+ from botocore.exceptions import ClientError
7
+
8
+ import nuql
9
+ from nuql import types, api, resources
10
+ from nuql.api import Boto3Adapter, Condition
11
+
12
+
13
+ class Delete(Boto3Adapter):
14
+ def prepare_client_args(
15
+ self,
16
+ key: Dict[str, Any],
17
+ condition: Dict[str, Any] | None = None,
18
+ exclude_condition: bool = False,
19
+ **kwargs,
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Prepares the request args for a delete operation of an item on the table (client API).
23
+
24
+ :arg key: Record key as a dict.
25
+ :param condition: Condition expression as a dict.
26
+ :param exclude_condition: Exclude condition from request (i.e. for BatchWrite).
27
+ :param kwargs: Additional args to pass to the request.
28
+ """
29
+ serialised_data = self.table.serialiser.serialise('query', key)
30
+ resources.validate_condition_dict(condition)
31
+ condition = api.Condition(self.table, condition, 'ConditionExpression')
32
+
33
+ # Marshall into the DynamoDB format
34
+ serialiser = TypeSerializer()
35
+ marshalled_data = {k: serialiser.serialize(v) for k, v in serialised_data.items()}
36
+
37
+ args = {'Key': marshalled_data, **kwargs}
38
+
39
+ if not exclude_condition:
40
+ args.update(condition.client_args)
41
+
42
+ return args
43
+
44
+ def prepare_args(
45
+ self,
46
+ key: Dict[str, Any],
47
+ condition: Dict[str, Any] | None = None,
48
+ exclude_condition: bool = False,
49
+ **kwargs,
50
+ ) -> Dict[str, Any]:
51
+ """
52
+ Prepares the request args for a delete operation of an item on the table (resource API).
53
+
54
+ :arg key: Record key as a dict.
55
+ :param condition: Condition expression as a dict.
56
+ :param exclude_condition: Exclude condition from request (i.e. for BatchWrite).
57
+ :param kwargs: Additional args to pass to the request.
58
+ """
59
+ resources.validate_condition_dict(condition)
60
+ condition_expression = Condition(
61
+ table=self.table,
62
+ condition=condition,
63
+ condition_type='ConditionExpression'
64
+ )
65
+ args = {'Key': self.table.serialiser.serialise_key(key), **kwargs}
66
+
67
+ if not exclude_condition:
68
+ args.update(condition_expression.resource_args)
69
+
70
+ return args
71
+
72
+ def invoke_sync(
73
+ self,
74
+ key: Dict[str, Any],
75
+ condition: Dict[str, Any] | None = None,
76
+ ) -> None:
77
+ """
78
+ Performs a delete operation for an item on the table.
79
+
80
+ :arg key: Record key as a dict.
81
+ :param condition: Condition expression as a dict.
82
+ """
83
+ args = self.prepare_args(key=key, condition=condition)
84
+
85
+ try:
86
+ self.client.connection.table.delete_item(**args)
87
+ except ClientError as exc:
88
+ raise nuql.Boto3Error(exc, args)
@@ -0,0 +1,30 @@
1
+ __all__ = ['Get']
2
+
3
+ from typing import Any, Dict
4
+
5
+ from botocore.exceptions import ClientError
6
+
7
+ import nuql
8
+ from nuql.api import Boto3Adapter
9
+
10
+
11
+ class Get(Boto3Adapter):
12
+ def invoke_sync(self, key: Dict[str, Any], consistent_read: bool = False) -> Dict[str, Any]:
13
+ """
14
+ Retrieves a record from the table using the key.
15
+
16
+ :arg key: Record key as a dict.
17
+ :param consistent_read: Perform a consistent read.
18
+ :return: Deserialised record dict.
19
+ """
20
+ args = {'Key': self.table.serialiser.serialise_key(key), 'ConsistentRead': consistent_read}
21
+
22
+ try:
23
+ response = self.client.connection.table.get_item(**args)
24
+ except ClientError as exc:
25
+ raise nuql.Boto3Error(exc, args)
26
+
27
+ if 'Item' not in response:
28
+ raise nuql.ItemNotFound(args['Key'])
29
+
30
+ return self.table.serialiser.deserialise(response['Item'])
@@ -0,0 +1,112 @@
1
+ __all__ = ['PutItem']
2
+
3
+ from typing import Any, Dict, Optional, Literal
4
+
5
+ from boto3.dynamodb.types import TypeSerializer
6
+ from botocore.exceptions import ClientError
7
+
8
+ import nuql
9
+ from nuql import types, api, resources
10
+ from nuql.api import Boto3Adapter
11
+
12
+
13
+ class PutItem(Boto3Adapter):
14
+ serialisation_action: Literal['create', 'update', 'write'] = 'write'
15
+
16
+ def prepare_client_args(
17
+ self,
18
+ data: Dict[str, Any],
19
+ condition: Optional['types.QueryWhere'] = None,
20
+ exclude_condition: bool = False,
21
+ **kwargs,
22
+ ) -> Dict[str, Any]:
23
+ """
24
+ Prepare the request args for a put operation against the table (client API).
25
+
26
+ :arg data: Data to put.
27
+ :param condition: Optional condition expression dict.
28
+ :param exclude_condition: Exclude condition from request (i.e. for BatchWrite).
29
+ :param kwargs: Additional args to pass to the request.
30
+ :return: New item dict.
31
+ """
32
+ serialised_data = self.table.serialiser.serialise(self.serialisation_action, data)
33
+ condition = api.Condition(self.table, condition, 'ConditionExpression')
34
+ condition_args = condition.client_args
35
+
36
+ # Marshall into the DynamoDB format
37
+ serialiser = TypeSerializer()
38
+ marshalled_data = {k: serialiser.serialize(v) for k, v in serialised_data.items()}
39
+
40
+ # Implement ability to modify condition before the request
41
+ self.on_condition(condition)
42
+
43
+ # Serialise ExpressionAttributeValues into DynamoDB format
44
+ if 'ExpressionAttributeValues' in condition_args:
45
+ condition_args = {**condition_args}
46
+ for k, v in condition_args['ExpressionAttributeValues'].items():
47
+ condition_args['ExpressionAttributeValues'][k] = serialiser.serialize(v)
48
+
49
+ args: Dict[str, Any] = {'Item': marshalled_data, **kwargs}
50
+
51
+ if not exclude_condition:
52
+ args.update(condition.client_args)
53
+
54
+ if 'ExpressionAttributeValues' in args and not args['ExpressionAttributeValues']:
55
+ args.pop('ExpressionAttributeValues')
56
+
57
+ return args
58
+
59
+ def prepare_args(
60
+ self,
61
+ data: Dict[str, Any],
62
+ condition: Dict[str, Any] | None = None,
63
+ exclude_condition: bool = False,
64
+ **kwargs,
65
+ ) -> Dict[str, Any]:
66
+ """
67
+ Prepare the request args for a put operation against the table (resource API).
68
+
69
+ :arg data: Data to put.
70
+ :param condition: Optional condition expression dict.
71
+ :param exclude_condition: Exclude condition from request (i.e. for BatchWrite).
72
+ :param kwargs: Additional args to pass to the request.
73
+ :return: New item dict.
74
+ """
75
+ serialised_data = self.table.serialiser.serialise(self.serialisation_action, data)
76
+ resources.validate_condition_dict(condition, required=False)
77
+ condition = api.Condition(self.table, condition, 'ConditionExpression')
78
+
79
+ # Implement ability to modify condition before the request
80
+ self.on_condition(condition)
81
+
82
+ args = {'Item': serialised_data, **kwargs}
83
+
84
+ if not exclude_condition:
85
+ args.update(condition.resource_args)
86
+
87
+ return args
88
+
89
+ def on_condition(self, condition: 'api.Condition') -> None:
90
+ """
91
+ Make changes to the condition expression before request.
92
+
93
+ :arg condition: Condition instance.
94
+ """
95
+ pass
96
+
97
+ def invoke_sync(self, data: Dict[str, Any], condition: Dict[str, Any] | None = None) -> Dict[str, Any]:
98
+ """
99
+ Perform a put operation against the table.
100
+
101
+ :arg data: Data to put.
102
+ :param condition: Optional condition expression dict.
103
+ :return: New item dict.
104
+ """
105
+ args = self.prepare_args(data=data, condition=condition)
106
+
107
+ try:
108
+ self.connection.table.put_item(**args)
109
+ except ClientError as exc:
110
+ raise nuql.Boto3Error(exc, args)
111
+
112
+ return args['Item']
@@ -0,0 +1,25 @@
1
+ __all__ = ['PutUpdate']
2
+
3
+ from nuql import api
4
+
5
+
6
+ class PutUpdate(api.PutItem):
7
+ serialisation_action = 'create'
8
+
9
+ def on_condition(self, condition: 'api.Condition') -> None:
10
+ """
11
+ Sets the condition expression to assert creation.
12
+
13
+ :arg condition: Condition instance.
14
+ """
15
+ index = self.table.indexes.primary
16
+ keys = [index['hash']]
17
+
18
+ # Append sort key if defined in primary index, but only if it exists in the schema
19
+ if 'sort' in index and index['sort'] and index['sort'] in self.table.fields:
20
+ keys.append(index['sort'])
21
+
22
+ expression = ' and '.join([f'attribute_exists({key})' for key in keys])
23
+
24
+ # Add the expression to the existing condition
25
+ condition.append(expression)
@@ -0,0 +1,4 @@
1
+ from .key_condition import *
2
+ from .condition_builder import *
3
+ from .condition import *
4
+ from .query import *