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.
- nuql-0.0.1/.gitignore +4 -0
- nuql-0.0.1/PKG-INFO +12 -0
- nuql-0.0.1/nuql/__init__.py +3 -0
- nuql-0.0.1/nuql/api/__init__.py +13 -0
- nuql-0.0.1/nuql/api/adapter.py +34 -0
- nuql-0.0.1/nuql/api/batch_get/__init__.py +2 -0
- nuql-0.0.1/nuql/api/batch_get/batch_get.py +40 -0
- nuql-0.0.1/nuql/api/batch_get/queue.py +120 -0
- nuql-0.0.1/nuql/api/batch_write.py +99 -0
- nuql-0.0.1/nuql/api/condition_check.py +39 -0
- nuql-0.0.1/nuql/api/create.py +25 -0
- nuql-0.0.1/nuql/api/delete.py +88 -0
- nuql-0.0.1/nuql/api/get.py +30 -0
- nuql-0.0.1/nuql/api/put_item.py +112 -0
- nuql-0.0.1/nuql/api/put_update.py +25 -0
- nuql-0.0.1/nuql/api/query/__init__.py +4 -0
- nuql-0.0.1/nuql/api/query/condition.py +157 -0
- nuql-0.0.1/nuql/api/query/condition_builder.py +211 -0
- nuql-0.0.1/nuql/api/query/key_condition.py +200 -0
- nuql-0.0.1/nuql/api/query/query.py +166 -0
- nuql-0.0.1/nuql/api/transaction.py +145 -0
- nuql-0.0.1/nuql/api/update/__init__.py +3 -0
- nuql-0.0.1/nuql/api/update/expression_builder.py +33 -0
- nuql-0.0.1/nuql/api/update/update_item.py +139 -0
- nuql-0.0.1/nuql/api/update/utils.py +126 -0
- nuql-0.0.1/nuql/api/upsert.py +32 -0
- nuql-0.0.1/nuql/client.py +88 -0
- nuql-0.0.1/nuql/connection.py +43 -0
- nuql-0.0.1/nuql/exceptions.py +66 -0
- nuql-0.0.1/nuql/fields/__init__.py +11 -0
- nuql-0.0.1/nuql/fields/boolean.py +29 -0
- nuql-0.0.1/nuql/fields/datetime.py +49 -0
- nuql-0.0.1/nuql/fields/datetime_timestamp.py +45 -0
- nuql-0.0.1/nuql/fields/float.py +40 -0
- nuql-0.0.1/nuql/fields/integer.py +40 -0
- nuql-0.0.1/nuql/fields/key.py +207 -0
- nuql-0.0.1/nuql/fields/list.py +90 -0
- nuql-0.0.1/nuql/fields/map.py +67 -0
- nuql-0.0.1/nuql/fields/string.py +184 -0
- nuql-0.0.1/nuql/fields/ulid.py +39 -0
- nuql-0.0.1/nuql/fields/uuid.py +42 -0
- nuql-0.0.1/nuql/generators/__init__.py +3 -0
- nuql-0.0.1/nuql/generators/datetime.py +37 -0
- nuql-0.0.1/nuql/generators/ulid.py +10 -0
- nuql-0.0.1/nuql/generators/uuid.py +19 -0
- nuql-0.0.1/nuql/resources/__init__.py +4 -0
- nuql-0.0.1/nuql/resources/fields/__init__.py +3 -0
- nuql-0.0.1/nuql/resources/fields/field.py +153 -0
- nuql-0.0.1/nuql/resources/fields/field_map.py +85 -0
- nuql-0.0.1/nuql/resources/fields/value.py +5 -0
- nuql-0.0.1/nuql/resources/records/__init__.py +3 -0
- nuql-0.0.1/nuql/resources/records/projections.py +49 -0
- nuql-0.0.1/nuql/resources/records/serialiser.py +144 -0
- nuql-0.0.1/nuql/resources/records/validator.py +48 -0
- nuql-0.0.1/nuql/resources/tables/__init__.py +2 -0
- nuql-0.0.1/nuql/resources/tables/indexes.py +140 -0
- nuql-0.0.1/nuql/resources/tables/table.py +151 -0
- nuql-0.0.1/nuql/resources/utils/__init__.py +2 -0
- nuql-0.0.1/nuql/resources/utils/dict.py +21 -0
- nuql-0.0.1/nuql/resources/utils/validators.py +165 -0
- nuql-0.0.1/nuql/types/__init__.py +3 -0
- nuql-0.0.1/nuql/types/config.py +27 -0
- nuql-0.0.1/nuql/types/fields.py +27 -0
- nuql-0.0.1/nuql/types/serialisation.py +10 -0
- nuql-0.0.1/pyproject.toml +41 -0
nuql-0.0.1/.gitignore
ADDED
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,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,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)
|