statikk 0.0.9__tar.gz → 0.0.10__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.
- {statikk-0.0.9 → statikk-0.0.10}/PKG-INFO +1 -1
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk/engine.py +30 -8
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk/models.py +7 -3
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/PKG-INFO +1 -1
- {statikk-0.0.9 → statikk-0.0.10}/tests/test_engine.py +73 -1
- {statikk-0.0.9 → statikk-0.0.10}/.coveragerc +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/.gitignore +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/.readthedocs.yml +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/AUTHORS.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/CHANGELOG.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/CONTRIBUTING.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/LICENSE.txt +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/README.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/assets/favicon.png +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/assets/logo.png +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/Makefile +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/_static/.gitignore +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/authors.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/changelog.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/conf.py +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/contributing.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/index.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/license.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/readme.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/requirements.txt +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/docs/usage.rst +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/pyproject.toml +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/setup.cfg +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/setup.py +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk/__init__.py +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk/conditions.py +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk/expressions.py +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/SOURCES.txt +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/dependency_links.txt +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/not-zip-safe +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/requires.txt +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/top_level.txt +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/tests/conftest.py +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/tests/test_expressions.py +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/tests/test_models.py +0 -0
- {statikk-0.0.9 → statikk-0.0.10}/tox.ini +0 -0
@@ -6,7 +6,7 @@ import boto3
|
|
6
6
|
from botocore.config import Config
|
7
7
|
from pydantic.fields import FieldInfo
|
8
8
|
from boto3.dynamodb.conditions import ComparisonCondition, Key
|
9
|
-
from boto3.dynamodb.types import TypeDeserializer
|
9
|
+
from boto3.dynamodb.types import TypeDeserializer, Decimal
|
10
10
|
|
11
11
|
from statikk.conditions import Condition, Equals, BeginsWith
|
12
12
|
from statikk.expressions import UpdateExpressionBuilder
|
@@ -157,7 +157,13 @@ class Table:
|
|
157
157
|
key = {self.key_schema.hash_key: id}
|
158
158
|
self._get_dynamodb_table().delete_item(Key=key)
|
159
159
|
|
160
|
-
def get_item(
|
160
|
+
def get_item(
|
161
|
+
self,
|
162
|
+
id: str,
|
163
|
+
model_class: Type[DatabaseModel],
|
164
|
+
sort_key: Optional[Any] = None,
|
165
|
+
consistent_read: bool = False,
|
166
|
+
):
|
161
167
|
"""
|
162
168
|
Returns an item from the database by id, using the partition key of the table.
|
163
169
|
:param id: The id of the item to retrieve.
|
@@ -167,11 +173,10 @@ class Table:
|
|
167
173
|
key = {self.key_schema.hash_key: id}
|
168
174
|
if sort_key:
|
169
175
|
key[self.key_schema.sort_key] = self._serialize_value(sort_key)
|
170
|
-
raw_data = self._get_dynamodb_table().get_item(Key=key)
|
176
|
+
raw_data = self._get_dynamodb_table().get_item(Key=key, ConsistentRead=consistent_read)
|
171
177
|
if "Item" not in raw_data:
|
172
178
|
raise ItemNotFoundError(f"{model_class} with id '{id}' not found.")
|
173
179
|
data = raw_data["Item"]
|
174
|
-
del data["type"]
|
175
180
|
for key, value in data.items():
|
176
181
|
data[key] = self._deserialize_value(value, model_class.model_fields[key])
|
177
182
|
return model_class(**data)
|
@@ -207,7 +212,6 @@ class Table:
|
|
207
212
|
"""
|
208
213
|
data = self._serialize_item(model)
|
209
214
|
self._get_dynamodb_table().put_item(Item=data)
|
210
|
-
del data["type"]
|
211
215
|
for key, value in data.items():
|
212
216
|
data[key] = self._deserialize_value(value, model.model_fields[key])
|
213
217
|
return type(model)(**data)
|
@@ -231,6 +235,7 @@ class Table:
|
|
231
235
|
def _find_changed_indexes():
|
232
236
|
changed_index_values = set()
|
233
237
|
for prefixed_attribute, value in expression_attribute_values.items():
|
238
|
+
expression_attribute_values[prefixed_attribute] = self._serialize_value(value)
|
234
239
|
attribute = prefixed_attribute.replace(":", "")
|
235
240
|
if model.model_fields[attribute].annotation is IndexSecondaryKeyField:
|
236
241
|
idx_field = getattr(model, attribute)
|
@@ -313,13 +318,14 @@ class Table:
|
|
313
318
|
|
314
319
|
while last_evaluated_key:
|
315
320
|
items = self._get_dynamodb_table().query(**query_params)
|
316
|
-
yield from [
|
321
|
+
yield from [self._deserialize_item(item, model_class=model_class) for item in items["Items"]]
|
317
322
|
last_evaluated_key = items.get("LastEvaluatedKey", False)
|
318
323
|
|
319
324
|
def scan(
|
320
325
|
self,
|
321
326
|
model_class: Type[DatabaseModel],
|
322
327
|
filter_condition: Optional[ComparisonCondition] = None,
|
328
|
+
consistent_read: bool = False,
|
323
329
|
):
|
324
330
|
"""
|
325
331
|
Scans the database for items matching the provided filter condition. The method returns a list of items matching
|
@@ -328,7 +334,9 @@ class Table:
|
|
328
334
|
:param model_class: The model class to use to deserialize the items.
|
329
335
|
:param filter_condition: An optional filter condition to use for the query. See boto3.dynamodb.conditions.ComparisonCondition for more information.
|
330
336
|
"""
|
331
|
-
query_params = {
|
337
|
+
query_params = {
|
338
|
+
"ConsistentRead": consistent_read,
|
339
|
+
}
|
332
340
|
if filter_condition:
|
333
341
|
query_params["FilterExpression"] = filter_condition
|
334
342
|
last_evaluated_key = True
|
@@ -394,17 +402,29 @@ class Table:
|
|
394
402
|
data = self._prepare_model_data(item, self.indexes)
|
395
403
|
for key, value in data.items():
|
396
404
|
data[key] = self._serialize_value(value)
|
397
|
-
data["type"] = item.model_type()
|
398
405
|
return data
|
399
406
|
|
407
|
+
def _deserialize_item(self, item: Dict[str, Any], model_class: Type[DatabaseModel]):
|
408
|
+
for key, value in item.items():
|
409
|
+
item[key] = self._deserialize_value(value, model_class.model_fields[key])
|
410
|
+
return model_class(**item)
|
411
|
+
|
400
412
|
def _deserialize_value(self, value: Any, annotation: Any):
|
401
413
|
if annotation is datetime or "datetime" in str(annotation):
|
402
414
|
return datetime.fromtimestamp(int(value))
|
415
|
+
if annotation is float:
|
416
|
+
return float(value)
|
403
417
|
return value
|
404
418
|
|
405
419
|
def _serialize_value(self, value: Any):
|
406
420
|
if isinstance(value, datetime):
|
407
421
|
return int(value.timestamp())
|
422
|
+
if isinstance(value, float):
|
423
|
+
return Decimal(value)
|
424
|
+
if isinstance(value, list):
|
425
|
+
return [self._serialize_value(item) for item in value]
|
426
|
+
if isinstance(value, dict):
|
427
|
+
return {key: self._serialize_value(item) for key, item in value.items() if item is not None}
|
408
428
|
return value
|
409
429
|
|
410
430
|
def _set_index_fields(self, model: DatabaseModel | Type[DatabaseModel], idx: GSI):
|
@@ -422,6 +442,8 @@ class Table:
|
|
422
442
|
if field_info.annotation is IndexSecondaryKeyField and idx.name in getattr(model, field_name).index_names
|
423
443
|
]
|
424
444
|
|
445
|
+
if len(sort_key_fields_unordered) == idx.sort_key is not None:
|
446
|
+
raise IncorrectSortKeyError(f"Model {model.__class__} does not have a sort key defined.")
|
425
447
|
if sort_key_fields_unordered[0][1] is not None:
|
426
448
|
sort_key_fields_unordered.sort(key=lambda x: x[1])
|
427
449
|
|
@@ -111,15 +111,19 @@ class DatabaseModel(BaseModel):
|
|
111
111
|
)
|
112
112
|
|
113
113
|
@classmethod
|
114
|
-
def get(cls, id: str, sort_key: Optional[str] = None):
|
115
|
-
return cls._table.get_item(id=id, model_class=cls, sort_key=sort_key)
|
114
|
+
def get(cls, id: str, sort_key: Optional[str] = None, consistent_read: bool = False):
|
115
|
+
return cls._table.get_item(id=id, model_class=cls, sort_key=sort_key, consistent_read=consistent_read)
|
116
116
|
|
117
117
|
@classmethod
|
118
118
|
def batch_get(cls, ids: List[str], batch_size: int = 100):
|
119
119
|
return cls._table.batch_get_items(ids=ids, model_class=cls, batch_size=batch_size)
|
120
120
|
|
121
121
|
@classmethod
|
122
|
-
def scan(
|
122
|
+
def scan(
|
123
|
+
cls,
|
124
|
+
filter_condition: Optional[ComparisonCondition] = None,
|
125
|
+
consistent_read: bool = False,
|
126
|
+
):
|
123
127
|
return cls._table.scan(model_class=cls, filter_condition=filter_condition)
|
124
128
|
|
125
129
|
@staticmethod
|
@@ -1,6 +1,7 @@
|
|
1
|
+
from _decimal import Decimal
|
1
2
|
from datetime import datetime, timezone
|
2
3
|
from typing import List
|
3
|
-
|
4
|
+
from pydantic import BaseModel
|
4
5
|
import pytest
|
5
6
|
from boto3.dynamodb.conditions import Attr
|
6
7
|
from moto import mock_dynamodb
|
@@ -28,6 +29,7 @@ class MyAwesomeModel(DatabaseModel):
|
|
28
29
|
name: str = "Foo"
|
29
30
|
values: set = {1, 2, 3, 4}
|
30
31
|
cost: int = 4
|
32
|
+
probability: float = 0.5
|
31
33
|
|
32
34
|
|
33
35
|
class SimpleModel(DatabaseModel):
|
@@ -89,6 +91,7 @@ def test_create_my_awesome_model():
|
|
89
91
|
"values": {1, 2, 3, 4},
|
90
92
|
"cost": 4,
|
91
93
|
"type": "MyAwesomeModel",
|
94
|
+
"probability": 0.5,
|
92
95
|
}
|
93
96
|
model_2 = MyAwesomeModel(id="foo-2", player_id="123", tier="EPIC", name="FooFoo")
|
94
97
|
table.put_item(model_2)
|
@@ -102,6 +105,7 @@ def test_create_my_awesome_model():
|
|
102
105
|
"values": {1, 2, 3, 4},
|
103
106
|
"cost": 4,
|
104
107
|
"type": "MyAwesomeModel",
|
108
|
+
"probability": 0.5,
|
105
109
|
}
|
106
110
|
mock_dynamodb().stop()
|
107
111
|
|
@@ -744,3 +748,71 @@ def test_index_field_order_is_respected():
|
|
744
748
|
model.save()
|
745
749
|
item = table.get_item("123", ModelWithIndexOrdersDefined)
|
746
750
|
assert item.gsi_sk == "ModelWithIndexOrdersDefined|EPIC|Mage"
|
751
|
+
|
752
|
+
|
753
|
+
def test_nested_models():
|
754
|
+
class InnerInnerModel(BaseModel):
|
755
|
+
baz: str
|
756
|
+
|
757
|
+
class InnerModel(BaseModel):
|
758
|
+
foo: str
|
759
|
+
values: List[datetime] = [
|
760
|
+
datetime(2023, 9, 9, 12, 0, 0),
|
761
|
+
datetime(2023, 9, 9, 13, 0, 0),
|
762
|
+
]
|
763
|
+
cost: int = 5
|
764
|
+
inner_inner: InnerInnerModel
|
765
|
+
|
766
|
+
class NestedModel(DatabaseModel):
|
767
|
+
player_id: IndexPrimaryKeyField
|
768
|
+
unit_class: IndexSecondaryKeyField = IndexSecondaryKeyField(order=2)
|
769
|
+
tier: IndexSecondaryKeyField = IndexSecondaryKeyField(order=1)
|
770
|
+
name: str = "Foo"
|
771
|
+
values: set = {1, 2, 3, 4}
|
772
|
+
cost: int = 4
|
773
|
+
inner_model: InnerModel
|
774
|
+
|
775
|
+
mock_dynamodb().start()
|
776
|
+
table = Table(
|
777
|
+
name="my-dynamodb-table",
|
778
|
+
key_schema=KeySchema(hash_key="id"),
|
779
|
+
indexes=[
|
780
|
+
GSI(
|
781
|
+
name="main-index",
|
782
|
+
hash_key=Key(name="gsi_pk"),
|
783
|
+
sort_key=Key(name="gsi_sk"),
|
784
|
+
)
|
785
|
+
],
|
786
|
+
models=[NestedModel],
|
787
|
+
)
|
788
|
+
_create_dynamodb_table(table)
|
789
|
+
model = NestedModel(
|
790
|
+
id="123",
|
791
|
+
player_id="456",
|
792
|
+
unit_class="Mage",
|
793
|
+
tier="EPIC",
|
794
|
+
inner_model=InnerModel(foo="bar", inner_inner=InnerInnerModel(baz="baz")),
|
795
|
+
)
|
796
|
+
model.save()
|
797
|
+
item = table.get_item("123", NestedModel)
|
798
|
+
assert item.model_dump() == {
|
799
|
+
"id": "123",
|
800
|
+
"player_id": "456",
|
801
|
+
"unit_class": "Mage",
|
802
|
+
"tier": "EPIC",
|
803
|
+
"name": "Foo",
|
804
|
+
"values": {Decimal("1"), Decimal("2"), Decimal("3"), Decimal("4")},
|
805
|
+
"cost": 4,
|
806
|
+
"inner_model": {
|
807
|
+
"foo": "bar",
|
808
|
+
"values": [
|
809
|
+
datetime(2023, 9, 9, 10, 0, tzinfo=timezone.utc),
|
810
|
+
datetime(2023, 9, 9, 11, 0, tzinfo=timezone.utc),
|
811
|
+
],
|
812
|
+
"cost": 5,
|
813
|
+
"inner_inner": {"baz": "baz"},
|
814
|
+
},
|
815
|
+
"gsi_pk": "456",
|
816
|
+
"gsi_sk": "NestedModel|EPIC|Mage",
|
817
|
+
"type": "NestedModel",
|
818
|
+
}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|