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
@@ -0,0 +1,166 @@
1
+ __all__ = ['Query']
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from botocore.exceptions import ClientError
6
+
7
+ import nuql
8
+ from nuql import types, api, resources
9
+
10
+
11
+ class Query(api.Boto3Adapter):
12
+ def prepare_client_args(
13
+ self,
14
+ key_condition: Dict[str, Any] | None = None,
15
+ condition: Dict[str, Any] | None = None,
16
+ index_name: str = 'primary',
17
+ limit: int | None = None,
18
+ scan_index_forward: bool = True,
19
+ exclusive_start_key: Dict[str, Any] | None = None,
20
+ consistent_read: bool = False,
21
+ ) -> Dict[str, Any]:
22
+ """
23
+ Prepares args for performing a query against the table (client API).
24
+
25
+ :param key_condition: Key condition expression as a dict.
26
+ :param condition: Filter condition expression as a dict.
27
+ :param index_name: Index to perform query against.
28
+ :param limit: Number of items to retrieve.
29
+ :param scan_index_forward: Direction of scan.
30
+ :param exclusive_start_key: Exclusive start key.
31
+ :param consistent_read: Perform query as a consistent read.
32
+ :return: Query result.
33
+ """
34
+ # Key condition is parsed from a dict and validated
35
+ key_condition = api.KeyCondition(self.table, key_condition, index_name)
36
+
37
+ # Filter condition is parsed from a string and validated
38
+ resources.validate_condition_dict(condition)
39
+ filter_condition = api.Condition(
40
+ table=self.table,
41
+ condition=condition,
42
+ condition_type='FilterExpression'
43
+ )
44
+
45
+ return {
46
+ **key_condition.client_args,
47
+ **filter_condition.client_args,
48
+ 'ScanIndexForward': scan_index_forward,
49
+ 'ConsistentRead': consistent_read,
50
+ }
51
+
52
+ def prepare_args(
53
+ self,
54
+ key_condition: Dict[str, Any] | None = None,
55
+ condition: Dict[str, Any] | None = None,
56
+ index_name: str = 'primary',
57
+ limit: int | None = None,
58
+ scan_index_forward: bool = True,
59
+ exclusive_start_key: Dict[str, Any] | None = None,
60
+ consistent_read: bool = False,
61
+ ) -> Dict[str, Any]:
62
+ """
63
+ Prepares args for performing a query against the table (resource API).
64
+
65
+ :param key_condition: Key condition expression as a dict.
66
+ :param condition: Filter condition expression as a dict.
67
+ :param index_name: Index to perform query against.
68
+ :param limit: Number of items to retrieve.
69
+ :param scan_index_forward: Direction of scan.
70
+ :param exclusive_start_key: Exclusive start key.
71
+ :param consistent_read: Perform query as a consistent read.
72
+ :return: Query result.
73
+ """
74
+ # Key condition is parsed from a dict and validated
75
+ key_condition = api.KeyCondition(self.table, key_condition, index_name)
76
+
77
+ # Filter condition is parsed from a string and validated
78
+ resources.validate_condition_dict(condition)
79
+ filter_condition = api.Condition(
80
+ table=self.table,
81
+ condition=condition,
82
+ condition_type='FilterExpression'
83
+ )
84
+
85
+ return {
86
+ **key_condition.resource_args,
87
+ **filter_condition.resource_args,
88
+ 'ScanIndexForward': scan_index_forward,
89
+ 'ConsistentRead': consistent_read,
90
+ }
91
+
92
+ def invoke_sync(
93
+ self,
94
+ key_condition: Dict[str, Any] | None = None,
95
+ condition: Dict[str, Any] | None = None,
96
+ index_name: str = 'primary',
97
+ limit: int | None = None,
98
+ scan_index_forward: bool = True,
99
+ exclusive_start_key: Dict[str, Any] | None = None,
100
+ consistent_read: bool = False,
101
+ ) -> Dict[str, Any]:
102
+ """
103
+ Synchronously invokes a query against the table.
104
+
105
+ :param key_condition: Key condition expression as a dict.
106
+ :param condition: Filter condition expression as a dict.
107
+ :param index_name: Index to perform query against.
108
+ :param limit: Number of items to retrieve.
109
+ :param scan_index_forward: Direction of scan.
110
+ :param exclusive_start_key: Exclusive start key.
111
+ :param consistent_read: Perform query as a consistent read.
112
+ :return: Query result.
113
+ """
114
+ args = self.prepare_args(
115
+ key_condition=key_condition,
116
+ condition=condition,
117
+ index_name=index_name,
118
+ limit=limit,
119
+ scan_index_forward=scan_index_forward,
120
+ exclusive_start_key=exclusive_start_key,
121
+ consistent_read=consistent_read,
122
+ )
123
+
124
+ data = []
125
+ last_evaluated_key = exclusive_start_key
126
+ fulfilled = False
127
+
128
+ while not fulfilled:
129
+ # Subtract processed records from limit
130
+ if isinstance(limit, int):
131
+ args['Limit'] = limit - len(data)
132
+
133
+ # Break when limit is reached
134
+ if 'Limit' in args and args['Limit'] == 0:
135
+ break
136
+
137
+ # Pagination is achieved by using LEK as exclusive start key
138
+ if last_evaluated_key:
139
+ args['ExclusiveStartKey'] = last_evaluated_key
140
+
141
+ try:
142
+ response = self.connection.table.query(**args)
143
+ except ClientError as exc:
144
+ raise nuql.Boto3Error(exc, args)
145
+
146
+ data.extend(response.get('Items', []))
147
+ last_evaluated_key = response.get('LastEvaluatedKey')
148
+
149
+ if not last_evaluated_key:
150
+ fulfilled = True
151
+
152
+ # Deserialise the data
153
+ data = [self.table.serialiser.deserialise(item) for item in data]
154
+
155
+ output = {'items': [], 'last_evaluated_key': last_evaluated_key}
156
+
157
+ # Follow functionality on local/global indexes - batch gets all retrieved items
158
+ index = self.table.indexes.get_index(index_name) if index_name != 'primary' else self.table.indexes.primary
159
+
160
+ if 'follow' in index and index['follow'] is True:
161
+ batch_get = api.BatchGet(self.client, self.table)
162
+ output.update(batch_get.invoke_sync(data))
163
+ else:
164
+ output['items'] = data
165
+
166
+ return output
@@ -0,0 +1,145 @@
1
+ __all__ = ['Transaction']
2
+
3
+ from typing import Dict, Any
4
+
5
+ from botocore.exceptions import ClientError
6
+
7
+ import nuql
8
+ from nuql import types, api, resources
9
+
10
+
11
+ # Maximum number of actions in a transaction
12
+ MAX_TRANSACTION_ACTIONS = 100
13
+
14
+
15
+ class Transaction:
16
+ def __init__(self, client: 'nuql.Nuql') -> None:
17
+ """
18
+ Context manager for executing DynamoDB transactions.
19
+
20
+ :arg client: Nuql instance.
21
+ """
22
+ self.client = client
23
+
24
+ self._actions = []
25
+ self._started = False
26
+
27
+ def __enter__(self):
28
+ """Enter the context manager."""
29
+ self._actions = []
30
+ self._started = True
31
+
32
+ return self
33
+
34
+ def __exit__(self, exc_type, exc_val, exc_tb):
35
+ """Dispatch the transaction to DynamoDB."""
36
+ if not self._actions:
37
+ raise nuql.NuqlError(
38
+ code='TransactionError',
39
+ message='Transaction has no actions'
40
+ )
41
+
42
+ args = {'TransactItems': self._actions}
43
+
44
+ try:
45
+ self.client.connection.client.transact_write_items(**args)
46
+ except ClientError as exc:
47
+ raise nuql.Boto3Error(exc, args)
48
+
49
+ self._started = False
50
+ return False
51
+
52
+ def _validate(self) -> None:
53
+ """Validates that the context manager has been started and maximum has not been exceeded."""
54
+ if not self._started:
55
+ raise nuql.NuqlError(
56
+ code='TransactionError',
57
+ message='Transaction context manager has not been started'
58
+ )
59
+ if len(self._actions) > MAX_TRANSACTION_ACTIONS:
60
+ raise nuql.NuqlError(
61
+ code='TransactionError',
62
+ message=f'Maximum number of actions exceeded in transaction (limit {MAX_TRANSACTION_ACTIONS})'
63
+ )
64
+
65
+ def create(
66
+ self,
67
+ table: 'resources.Table',
68
+ data: Dict[str, Any],
69
+ condition: Dict[str, Any] | None = None,
70
+ ) -> None:
71
+ """
72
+ Create a new item on a table as part of a transaction.
73
+
74
+ :arg table: Table instance.
75
+ :arg data: Data to create.
76
+ :param condition: Optional condition expression dict.
77
+ """
78
+ self._validate()
79
+
80
+ create = api.Create(self.client, table)
81
+ args = create.prepare_client_args(data=data, condition=condition)
82
+
83
+ self._actions.append({'Put': {'TableName': self.client.connection.table_name, **args}})
84
+
85
+ def update(
86
+ self,
87
+ table: 'resources.Table',
88
+ data: Dict[str, Any],
89
+ condition: Dict[str, Any] | None = None,
90
+ shallow: bool = False,
91
+ ) -> None:
92
+ """
93
+ Update an item on a table as part of a transaction.
94
+
95
+ :arg table: Table instance.
96
+ :arg data: Data to update.
97
+ :param condition: Optional condition expression dict.
98
+ :param shallow: Activates shallow update mode (so that whole nested items are updated at once).
99
+ """
100
+ self._validate()
101
+
102
+ update = api.UpdateItem(self.client, table)
103
+ args = update.prepare_client_args(data=data, condition=condition, shallow=shallow)
104
+
105
+ self._actions.append({'Update': {'TableName': self.client.connection.table_name, **args}})
106
+
107
+ def delete(
108
+ self,
109
+ table: 'resources.Table',
110
+ key: Dict[str, Any],
111
+ condition: Dict[str, Any] | None = None,
112
+ ) -> None:
113
+ """
114
+ Delete an item on a table as part of a transaction.
115
+
116
+ :arg table: Table instance.
117
+ :arg key: Record key as a dict.
118
+ :param condition: Optional condition expression dict.
119
+ """
120
+ self._validate()
121
+
122
+ delete = api.Delete(self.client, table)
123
+ args = delete.prepare_client_args(key=key, condition=condition)
124
+
125
+ self._actions.append({'Delete': {'TableName': self.client.connection.table_name, **args}})
126
+
127
+ def condition_check(
128
+ self,
129
+ table: 'resources.Table',
130
+ key: Dict[str, Any],
131
+ condition: Dict[str, Any],
132
+ ) -> None:
133
+ """
134
+ Perform a condition check on an item as part of a transaction.
135
+
136
+ :arg table: Table instance.
137
+ :arg key: Record key as a dict.
138
+ :arg condition: Condition expression dict.
139
+ """
140
+ self._validate()
141
+
142
+ condition_check = api.ConditionCheck(self.client, table)
143
+ args = condition_check.prepare_client_args(key=key, condition=condition)
144
+
145
+ self._actions.append({'ConditionCheck': {'TableName': self.client.connection.table_name, **args}})
@@ -0,0 +1,3 @@
1
+ from .utils import *
2
+ from .expression_builder import *
3
+ from .update_item import *
@@ -0,0 +1,33 @@
1
+ __all__ = ['UpdateExpressionBuilder']
2
+
3
+ from typing import Dict, Any
4
+
5
+ from .utils import flatten_dict, UpdateKeys, UpdateValues
6
+
7
+
8
+ class UpdateExpressionBuilder:
9
+ def __init__(self, data: Dict[str, Any], shallow: bool = False) -> None:
10
+ """
11
+ DynamoDB update expression builder.
12
+
13
+ :arg data: Serialised data to build expression from.
14
+ :param shallow: Activates shallow update mode (so that whole nested items are updated at once).
15
+ """
16
+ if not shallow:
17
+ data = flatten_dict(data)
18
+
19
+ self.keys = UpdateKeys()
20
+ self.update_expression = UpdateValues()
21
+
22
+ for index, (key, value) in enumerate(data.items()):
23
+ key = self.keys.add(key)
24
+ self.update_expression.add(key, value)
25
+
26
+ @property
27
+ def args(self):
28
+ """Returns the arguments for the boto3 API call."""
29
+ return {
30
+ 'ExpressionAttributeNames': self.keys.expression_names,
31
+ 'ExpressionAttributeValues': self.update_expression.values,
32
+ 'UpdateExpression': 'SET ' + ', '.join(self.update_expression.expressions),
33
+ }
@@ -0,0 +1,139 @@
1
+ __all__ = ['UpdateItem']
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
+
11
+
12
+ class UpdateItem(api.Boto3Adapter):
13
+ serialisation_action: Literal['create', 'update'] = 'update'
14
+
15
+ def prepare_client_args(
16
+ self,
17
+ data: Dict[str, Any],
18
+ condition: Dict[str, Any] | None = None,
19
+ shallow: bool = False,
20
+ **kwargs,
21
+ ) -> Dict[str, Any]:
22
+ """
23
+ Prepares the request args for updating an item in the table (client API).
24
+
25
+ :arg data: Data to update.
26
+ :param condition: Optional condition expression.
27
+ :param shallow: Activates shallow update mode (so that whole nested items are updated at once).
28
+ :param kwargs: Additional args to pass to the request.
29
+ :return: New item dict.
30
+ """
31
+ # Serialise the data for update
32
+ key = self.table.serialiser.serialise_key(data)
33
+ serialised_data = {k: v for k, v in self.table.serialiser.serialise('update', data).items() if k not in key}
34
+
35
+ # Marshall the key into DynamoDB format
36
+ serialiser = TypeSerializer()
37
+ marshalled_key = {k: serialiser.serialize(v) for k, v in key.items()}
38
+
39
+ # Generate the update condition
40
+ resources.validate_condition_dict(condition)
41
+ condition = api.Condition(
42
+ table=self.table,
43
+ condition=condition,
44
+ condition_type='ConditionExpression'
45
+ )
46
+ self.on_condition(condition)
47
+
48
+ # Generate the update expression
49
+ update = api.UpdateExpressionBuilder(serialised_data, shallow=shallow)
50
+ args = {
51
+ 'Key': marshalled_key,
52
+ **resources.merge_dicts(update.args, condition.client_args),
53
+ **kwargs
54
+ }
55
+
56
+ # Serialise ExpressionAttributeValues into DynamoDB format
57
+ if 'ExpressionAttributeValues' in args:
58
+ for key, value in args['ExpressionAttributeValues'].items():
59
+ args['ExpressionAttributeValues'][key] = serialiser.serialize(value)
60
+
61
+ # Remove empty ExpressionAttributeValues
62
+ if 'ExpressionAttributeValues' in args and not args['ExpressionAttributeValues']:
63
+ args.pop('ExpressionAttributeValues')
64
+
65
+ return args
66
+
67
+ def prepare_args(
68
+ self,
69
+ data: Dict[str, Any],
70
+ condition: Dict[str, Any] | None = None,
71
+ shallow: bool = False,
72
+ **kwargs,
73
+ ) -> Dict[str, Any]:
74
+ """
75
+ Prepares the request args for updating an item in the table (resource API).
76
+
77
+ :arg data: Data to update.
78
+ :param condition: Optional condition expression.
79
+ :param shallow: Activates shallow update mode (so that whole nested items are updated at once).
80
+ :param kwargs: Additional args to pass to the request.
81
+ :return: New item dict.
82
+ """
83
+ # Serialise the data for update
84
+ key = self.table.serialiser.serialise_key(data)
85
+ serialised_data = {k: v for k, v in self.table.serialiser.serialise('update', data).items() if k not in key}
86
+
87
+ # Generate the update condition
88
+ resources.validate_condition_dict(condition)
89
+ condition = api.Condition(
90
+ table=self.table,
91
+ condition=condition,
92
+ condition_type='ConditionExpression'
93
+ )
94
+ self.on_condition(condition)
95
+
96
+ # Generate the update expression
97
+ update = api.UpdateExpressionBuilder(serialised_data, shallow=shallow)
98
+ return {'Key': key, **update.args, **condition.resource_args, **kwargs}
99
+
100
+ def on_condition(self, condition: 'api.Condition') -> None:
101
+ """
102
+ Make changes to the condition expression before request.
103
+
104
+ :arg condition: Condition instance.
105
+ """
106
+ index = self.table.indexes.primary
107
+ keys = [index['hash']]
108
+
109
+ # Append sort key if defined in primary index, but only if it exists in the schema
110
+ if 'sort' in index and index['sort'] and index['sort'] in self.table.fields:
111
+ keys.append(index['sort'])
112
+
113
+ expression = ' and '.join([f'attribute_exists({key})' for key in keys])
114
+
115
+ # Add the expression to the existing condition
116
+ condition.append(expression)
117
+
118
+ def invoke_sync(
119
+ self,
120
+ data: Dict[str, Any],
121
+ condition: Dict[str, Any] | None = None,
122
+ shallow: bool = False
123
+ ) -> Dict[str, Any]:
124
+ """
125
+ Updates an item in the table.
126
+
127
+ :arg data: Data to update.
128
+ :param condition: Optional condition expression.
129
+ :param shallow: Activates shallow update mode (so that whole nested items are updated at once).
130
+ :return: New item dict.
131
+ """
132
+ args = self.prepare_args(data=data, condition=condition, shallow=shallow, ReturnValues='ALL_NEW')
133
+
134
+ try:
135
+ response = self.connection.table.update_item(**args)
136
+ except ClientError as exc:
137
+ raise nuql.Boto3Error(exc, args)
138
+
139
+ return self.table.serialiser.deserialise(response['Attributes'])
@@ -0,0 +1,126 @@
1
+ __all__ = ['flatten_dict', 'UpdateKeys', 'UpdateValues', 'Incrementor', 'decrement', 'increment']
2
+
3
+ from decimal import Decimal
4
+ from typing import Dict, Any
5
+
6
+
7
+ def flatten_dict(data: Dict[str, Any], parent: str = None) -> Dict[str, Any]:
8
+ """
9
+ Flattens a nested dict with dot notation keys.
10
+
11
+ :arg data: Dict to flatten.
12
+ :param parent: Parent key name if recursive.
13
+ :return: Flattened dict.
14
+ """
15
+ # Default initialiser
16
+ if parent is None:
17
+ parent = ''
18
+
19
+ items = []
20
+
21
+ for key, value in data.items():
22
+ new_key = (parent + '.' + key) if parent else key
23
+
24
+ # Process nested dict
25
+ if isinstance(value, dict):
26
+ if not value:
27
+ items.append((new_key, value))
28
+ else:
29
+ items.extend(flatten_dict(value, new_key).items())
30
+
31
+ else:
32
+ items.append((new_key, value))
33
+
34
+ return dict(items)
35
+
36
+
37
+ class UpdateKeys:
38
+ def __init__(self) -> None:
39
+ """State manager for keys in an update expression."""
40
+ self.current_index = 0
41
+ self.key_dict = {}
42
+
43
+ @property
44
+ def expression_names(self):
45
+ return {value: key for key, value in self.key_dict.items()}
46
+
47
+ def add(self, key: str) -> str:
48
+ """
49
+ Parse a key for the update expression.
50
+
51
+ :arg key: Key to parse.
52
+ :return: Resulting keys for the update expression.
53
+ """
54
+ keys = []
55
+
56
+ # Parse nested parts and see if the key exists in the dictionary
57
+ for part in key.split('.'):
58
+ if part in self.key_dict:
59
+ keys.append(self.key_dict[part])
60
+ else:
61
+ keys.append(f'#key_{self.current_index}')
62
+ self.key_dict[part] = f'#key_{self.current_index}'
63
+ self.current_index += 1
64
+
65
+ return '.'.join(keys)
66
+
67
+
68
+ class UpdateValues:
69
+ def __init__(self) -> None:
70
+ """State manager for values in an update expression."""
71
+ self.current_index = 0
72
+ self.values = {}
73
+ self.expressions = []
74
+
75
+ def add(self, key: str, value: Any) -> None:
76
+ """
77
+ Add a value to the update expression.
78
+
79
+ :arg key: Key in the update expression.
80
+ :arg value: Any value.
81
+ :return: Resulting value key for the update expression.
82
+ """
83
+ value_key = f':val_{self.current_index}'
84
+
85
+ if isinstance(value, Incrementor):
86
+ self.values[value_key] = value.value
87
+ self.expressions.append(f'{key} = {key} {"-" if value.negative else "+"} {value_key}')
88
+ else:
89
+ self.values[value_key] = value
90
+ self.expressions.append(f'{key} = {value_key}')
91
+
92
+ self.current_index += 1
93
+
94
+
95
+ class Incrementor:
96
+ def __init__(self, value: int | float | Decimal, negative: bool = False) -> None:
97
+ """
98
+ Data class for increment/decrement operations.
99
+ :arg value: Value to increment/decrement.
100
+ :param negative: Set to true to decrement the value.
101
+ """
102
+ if not isinstance(value, Decimal):
103
+ value = Decimal(str(value))
104
+
105
+ self.value = value
106
+ self.negative = negative
107
+
108
+
109
+ def increment(value: int | float | Decimal) -> Incrementor:
110
+ """
111
+ Increment a value on an update expression.
112
+
113
+ :arg value: Value to increment.
114
+ :return: Incrementor instance.
115
+ """
116
+ return Incrementor(value, negative=False)
117
+
118
+
119
+ def decrement(value: int | float | Decimal) -> Incrementor:
120
+ """
121
+ Decrement a value on an update expression.
122
+
123
+ :arg value: Value to decrement.
124
+ :return: Incrementor instance.
125
+ """
126
+ return Incrementor(value, negative=True)
nuql/api/upsert.py ADDED
@@ -0,0 +1,32 @@
1
+ __all__ = ['Upsert']
2
+
3
+ from typing import Any, Dict
4
+
5
+ import nuql
6
+ from nuql import api
7
+
8
+
9
+ class Upsert(api.Boto3Adapter):
10
+ def invoke_sync(self, data: Dict[str, Any], shallow: bool = False) -> Dict[str, Any]:
11
+ """
12
+ Updates an item in the table if it exists, otherwise creates a new one.
13
+
14
+ [NOTE]
15
+ Conditions aren't allowed for this API to avoid ambiguous
16
+ ConditionCheckFailedException (as this is a catch-all for any condition).
17
+
18
+ :arg data: Data to upsert.
19
+ :param shallow: Activates shallow update mode (so that whole nested items are updated at once).
20
+ :return: New item dict.
21
+ """
22
+ try:
23
+ update = api.UpdateItem(self.client, self.table)
24
+ return update.invoke_sync(data=data, shallow=shallow)
25
+
26
+ except nuql.Boto3Error as exc:
27
+ # When condition failed the create API will be used instead.
28
+ if exc.code == 'ConditionalCheckFailedException':
29
+ create = api.Create(self.client, self.table)
30
+ return create.invoke_sync(data=data)
31
+
32
+ raise exc