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.
Files changed (41) hide show
  1. {statikk-0.0.9 → statikk-0.0.10}/PKG-INFO +1 -1
  2. {statikk-0.0.9 → statikk-0.0.10}/src/statikk/engine.py +30 -8
  3. {statikk-0.0.9 → statikk-0.0.10}/src/statikk/models.py +7 -3
  4. {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/PKG-INFO +1 -1
  5. {statikk-0.0.9 → statikk-0.0.10}/tests/test_engine.py +73 -1
  6. {statikk-0.0.9 → statikk-0.0.10}/.coveragerc +0 -0
  7. {statikk-0.0.9 → statikk-0.0.10}/.gitignore +0 -0
  8. {statikk-0.0.9 → statikk-0.0.10}/.readthedocs.yml +0 -0
  9. {statikk-0.0.9 → statikk-0.0.10}/AUTHORS.rst +0 -0
  10. {statikk-0.0.9 → statikk-0.0.10}/CHANGELOG.rst +0 -0
  11. {statikk-0.0.9 → statikk-0.0.10}/CONTRIBUTING.rst +0 -0
  12. {statikk-0.0.9 → statikk-0.0.10}/LICENSE.txt +0 -0
  13. {statikk-0.0.9 → statikk-0.0.10}/README.rst +0 -0
  14. {statikk-0.0.9 → statikk-0.0.10}/assets/favicon.png +0 -0
  15. {statikk-0.0.9 → statikk-0.0.10}/assets/logo.png +0 -0
  16. {statikk-0.0.9 → statikk-0.0.10}/docs/Makefile +0 -0
  17. {statikk-0.0.9 → statikk-0.0.10}/docs/_static/.gitignore +0 -0
  18. {statikk-0.0.9 → statikk-0.0.10}/docs/authors.rst +0 -0
  19. {statikk-0.0.9 → statikk-0.0.10}/docs/changelog.rst +0 -0
  20. {statikk-0.0.9 → statikk-0.0.10}/docs/conf.py +0 -0
  21. {statikk-0.0.9 → statikk-0.0.10}/docs/contributing.rst +0 -0
  22. {statikk-0.0.9 → statikk-0.0.10}/docs/index.rst +0 -0
  23. {statikk-0.0.9 → statikk-0.0.10}/docs/license.rst +0 -0
  24. {statikk-0.0.9 → statikk-0.0.10}/docs/readme.rst +0 -0
  25. {statikk-0.0.9 → statikk-0.0.10}/docs/requirements.txt +0 -0
  26. {statikk-0.0.9 → statikk-0.0.10}/docs/usage.rst +0 -0
  27. {statikk-0.0.9 → statikk-0.0.10}/pyproject.toml +0 -0
  28. {statikk-0.0.9 → statikk-0.0.10}/setup.cfg +0 -0
  29. {statikk-0.0.9 → statikk-0.0.10}/setup.py +0 -0
  30. {statikk-0.0.9 → statikk-0.0.10}/src/statikk/__init__.py +0 -0
  31. {statikk-0.0.9 → statikk-0.0.10}/src/statikk/conditions.py +0 -0
  32. {statikk-0.0.9 → statikk-0.0.10}/src/statikk/expressions.py +0 -0
  33. {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/SOURCES.txt +0 -0
  34. {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/dependency_links.txt +0 -0
  35. {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/not-zip-safe +0 -0
  36. {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/requires.txt +0 -0
  37. {statikk-0.0.9 → statikk-0.0.10}/src/statikk.egg-info/top_level.txt +0 -0
  38. {statikk-0.0.9 → statikk-0.0.10}/tests/conftest.py +0 -0
  39. {statikk-0.0.9 → statikk-0.0.10}/tests/test_expressions.py +0 -0
  40. {statikk-0.0.9 → statikk-0.0.10}/tests/test_models.py +0 -0
  41. {statikk-0.0.9 → statikk-0.0.10}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: statikk
3
- Version: 0.0.9
3
+ Version: 0.0.10
4
4
  Summary: statikk is a single table application (STA) library for DynamoDb.
5
5
  Home-page: https://github.com/terinia/statikk
6
6
  Author: Balint Biro
@@ -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(self, id: str, model_class: Type[DatabaseModel], sort_key: Optional[Any] = None):
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 [model_class(**item) for item in items["Items"]]
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(cls, filter_condition: Optional[ComparisonCondition] = None):
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,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: statikk
3
- Version: 0.0.9
3
+ Version: 0.0.10
4
4
  Summary: statikk is a single table application (STA) library for DynamoDb.
5
5
  Home-page: https://github.com/terinia/statikk
6
6
  Author: Balint Biro
@@ -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