statikk 0.1.12__tar.gz → 0.1.13__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 (44) hide show
  1. {statikk-0.1.12 → statikk-0.1.13}/PKG-INFO +1 -1
  2. {statikk-0.1.12 → statikk-0.1.13}/src/statikk/engine.py +4 -24
  3. {statikk-0.1.12 → statikk-0.1.13}/src/statikk/models.py +32 -1
  4. statikk-0.1.13/src/statikk/typing.py +24 -0
  5. {statikk-0.1.12 → statikk-0.1.13}/src/statikk.egg-info/PKG-INFO +1 -1
  6. {statikk-0.1.12 → statikk-0.1.13}/tests/test_engine.py +71 -1
  7. {statikk-0.1.12 → statikk-0.1.13}/tests/test_models.py +2 -0
  8. statikk-0.1.12/src/statikk/typing.py +0 -3
  9. {statikk-0.1.12 → statikk-0.1.13}/.coveragerc +0 -0
  10. {statikk-0.1.12 → statikk-0.1.13}/.gitignore +0 -0
  11. {statikk-0.1.12 → statikk-0.1.13}/.readthedocs.yml +0 -0
  12. {statikk-0.1.12 → statikk-0.1.13}/AUTHORS.rst +0 -0
  13. {statikk-0.1.12 → statikk-0.1.13}/CHANGELOG.rst +0 -0
  14. {statikk-0.1.12 → statikk-0.1.13}/CONTRIBUTING.rst +0 -0
  15. {statikk-0.1.12 → statikk-0.1.13}/LICENSE.txt +0 -0
  16. {statikk-0.1.12 → statikk-0.1.13}/README.rst +0 -0
  17. {statikk-0.1.12 → statikk-0.1.13}/assets/favicon.png +0 -0
  18. {statikk-0.1.12 → statikk-0.1.13}/assets/logo.png +0 -0
  19. {statikk-0.1.12 → statikk-0.1.13}/docs/Makefile +0 -0
  20. {statikk-0.1.12 → statikk-0.1.13}/docs/_static/.gitignore +0 -0
  21. {statikk-0.1.12 → statikk-0.1.13}/docs/authors.rst +0 -0
  22. {statikk-0.1.12 → statikk-0.1.13}/docs/changelog.rst +0 -0
  23. {statikk-0.1.12 → statikk-0.1.13}/docs/conf.py +0 -0
  24. {statikk-0.1.12 → statikk-0.1.13}/docs/contributing.rst +0 -0
  25. {statikk-0.1.12 → statikk-0.1.13}/docs/index.rst +0 -0
  26. {statikk-0.1.12 → statikk-0.1.13}/docs/license.rst +0 -0
  27. {statikk-0.1.12 → statikk-0.1.13}/docs/readme.rst +0 -0
  28. {statikk-0.1.12 → statikk-0.1.13}/docs/requirements.txt +0 -0
  29. {statikk-0.1.12 → statikk-0.1.13}/docs/usage.rst +0 -0
  30. {statikk-0.1.12 → statikk-0.1.13}/pyproject.toml +0 -0
  31. {statikk-0.1.12 → statikk-0.1.13}/setup.cfg +0 -0
  32. {statikk-0.1.12 → statikk-0.1.13}/setup.py +0 -0
  33. {statikk-0.1.12 → statikk-0.1.13}/src/statikk/__init__.py +0 -0
  34. {statikk-0.1.12 → statikk-0.1.13}/src/statikk/conditions.py +0 -0
  35. {statikk-0.1.12 → statikk-0.1.13}/src/statikk/expressions.py +0 -0
  36. {statikk-0.1.12 → statikk-0.1.13}/src/statikk/fields.py +0 -0
  37. {statikk-0.1.12 → statikk-0.1.13}/src/statikk.egg-info/SOURCES.txt +0 -0
  38. {statikk-0.1.12 → statikk-0.1.13}/src/statikk.egg-info/dependency_links.txt +0 -0
  39. {statikk-0.1.12 → statikk-0.1.13}/src/statikk.egg-info/not-zip-safe +0 -0
  40. {statikk-0.1.12 → statikk-0.1.13}/src/statikk.egg-info/requires.txt +0 -0
  41. {statikk-0.1.12 → statikk-0.1.13}/src/statikk.egg-info/top_level.txt +0 -0
  42. {statikk-0.1.12 → statikk-0.1.13}/tests/conftest.py +0 -0
  43. {statikk-0.1.12 → statikk-0.1.13}/tests/test_expressions.py +0 -0
  44. {statikk-0.1.12 → statikk-0.1.13}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statikk
3
- Version: 0.1.12
3
+ Version: 0.1.13
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,14 +1,14 @@
1
1
  import os
2
2
  from datetime import datetime
3
- from typing import Any, Dict, Type, Optional, List, Union, get_type_hints, get_origin, get_args
3
+ from typing import Any, Dict, Type, Optional, List, Union
4
4
 
5
5
  import boto3
6
6
  from botocore.config import Config
7
7
  from pydantic.fields import FieldInfo
8
- from boto3.dynamodb.conditions import ComparisonCondition, Key
8
+ from boto3.dynamodb.conditions import ComparisonCondition
9
9
  from boto3.dynamodb.types import TypeDeserializer, Decimal
10
10
 
11
- from statikk.typing import T
11
+ from statikk.typing import T, inspect_optional_field
12
12
  from statikk.conditions import Condition, Equals, BeginsWith
13
13
  from statikk.expressions import UpdateExpressionBuilder
14
14
  from statikk.models import (
@@ -627,26 +627,6 @@ class Table:
627
627
  data = self._serialize_item(enriched_item)
628
628
  batch.put_item(Item=data)
629
629
 
630
- def inspect_optional_field(self, model_class, field_name):
631
- field_type = model_class.model_fields[field_name].annotation
632
-
633
- is_optional = False
634
- inner_type = field_type
635
-
636
- if get_origin(field_type) is Union:
637
- args = get_args(field_type)
638
- if len(args) == 2 and args[1] is type(None):
639
- is_optional = True
640
- inner_type = args[0]
641
-
642
- elif hasattr(field_type, "__origin__") and field_type.__origin__ is Union:
643
- args = getattr(field_type, "__args__", [])
644
- if len(args) == 2 and args[1] is type(None):
645
- is_optional = True
646
- inner_type = args[0]
647
-
648
- return (is_optional, inner_type)
649
-
650
630
  def reconstruct_hierarchy(self, items: list[dict]) -> Optional[dict]:
651
631
  """
652
632
  Reconstructs a hierarchical dictionary structure from a flat list of dictionaries
@@ -728,7 +708,7 @@ class Table:
728
708
  if field_name.startswith("_"):
729
709
  continue
730
710
 
731
- is_optional, inner_type = self.inspect_optional_field(parent_model_class, field_name)
711
+ is_optional, inner_type = inspect_optional_field(parent_model_class, field_name)
732
712
 
733
713
  field_type = inner_type if is_optional else field_info.annotation
734
714
 
@@ -4,7 +4,7 @@ import typing
4
4
  import logging
5
5
  from uuid import uuid4
6
6
  from typing import Optional, List, Any, Set, Type
7
- from statikk.typing import T
7
+ from statikk.typing import T, inspect_optional_field
8
8
 
9
9
  from boto3.dynamodb.conditions import ComparisonCondition
10
10
  from pydantic import BaseModel, model_serializer, model_validator, Field, Extra
@@ -296,6 +296,37 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
296
296
  def change_parent_to(self, new_parent: DatabaseModel) -> T:
297
297
  return self._table.reparent_subtree(self, new_parent)
298
298
 
299
+ def add_child_node(self, field_name: str, child_node: DatabaseModel):
300
+ if not child_node.is_nested():
301
+ raise ValueError("Child node must be nested.")
302
+
303
+ if not hasattr(self, field_name):
304
+ raise ValueError(f"Field {field_name} does not exist on {self.__class__.__name__}")
305
+
306
+ is_optional, inner_type = inspect_optional_field(self.__class__, field_name)
307
+ field_type = inner_type if is_optional else self.model_fields[field_name].annotation
308
+
309
+ if hasattr(field_type, "__origin__") and field_type.__origin__ == list:
310
+ if not isinstance(getattr(self, field_name), list):
311
+ setattr(self, field_name, [])
312
+ reparented = child_node.change_parent_to(self)
313
+ getattr(self, field_name).append(reparented)
314
+ return reparented
315
+
316
+ elif hasattr(field_type, "__origin__") and field_type.__origin__ == set:
317
+ if not isinstance(getattr(self, field_name), set):
318
+ setattr(self, field_name, set())
319
+ reparented = child_node.change_parent_to(self)
320
+ getattr(self, field_name).add(reparented)
321
+ return reparented
322
+
323
+ elif issubclass(field_type, DatabaseModel):
324
+ reparented = child_node.change_parent_to(self)
325
+ setattr(self, field_name, reparented)
326
+ return reparented
327
+
328
+ raise ValueError(f"Unsupported field type: {field_type}")
329
+
299
330
  @classmethod
300
331
  def scan(
301
332
  cls,
@@ -0,0 +1,24 @@
1
+ from typing import TypeVar, get_origin, Union, get_args
2
+
3
+ T = TypeVar("T", bound="DatabaseModel")
4
+
5
+
6
+ def inspect_optional_field(model_class, field_name):
7
+ field_type = model_class.model_fields[field_name].annotation
8
+
9
+ is_optional = False
10
+ inner_type = field_type
11
+
12
+ if get_origin(field_type) is Union:
13
+ args = get_args(field_type)
14
+ if len(args) == 2 and args[1] is type(None):
15
+ is_optional = True
16
+ inner_type = args[0]
17
+
18
+ elif hasattr(field_type, "__origin__") and field_type.__origin__ is Union:
19
+ args = getattr(field_type, "__args__", [])
20
+ if len(args) == 2 and args[1] is type(None):
21
+ is_optional = True
22
+ inner_type = args[0]
23
+
24
+ return (is_optional, inner_type)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statikk
3
- Version: 0.1.12
3
+ Version: 0.1.13
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,6 @@
1
1
  from _decimal import Decimal
2
2
  from datetime import datetime, timezone
3
- from typing import List, Optional
3
+ from typing import List, Optional, Type
4
4
 
5
5
  import pytest
6
6
  from boto3.dynamodb.conditions import Attr
@@ -24,6 +24,23 @@ from statikk.models import (
24
24
  )
25
25
 
26
26
 
27
+ def _create_default_dynamodb_table(models: list[Type[DatabaseModel]]):
28
+ mock_dynamodb().start()
29
+ table = Table(
30
+ name="my-dynamodb-table",
31
+ key_schema=KeySchema(hash_key="id"),
32
+ indexes=[
33
+ GSI(
34
+ name="main-index",
35
+ hash_key=Key(name="gsi_pk"),
36
+ sort_key=Key(name="gsi_sk"),
37
+ )
38
+ ],
39
+ models=models,
40
+ )
41
+ _create_dynamodb_table(table)
42
+
43
+
27
44
  class MyAwesomeModel(DatabaseModel):
28
45
  player_id: str
29
46
  tier: str
@@ -980,3 +997,56 @@ def test_rebuild_model_indexes():
980
997
  my_database_model.build_model_indexes()
981
998
  assert my_database_model.gsi_pk == "foo"
982
999
  assert my_database_model.gsi_sk == "MyDatabaseModel|bar"
1000
+
1001
+
1002
+ def test_add_child_node():
1003
+ class MyOtherNestedDatabaseModel(DatabaseModel):
1004
+ baz: str
1005
+
1006
+ @classmethod
1007
+ def is_nested(cls) -> bool:
1008
+ return True
1009
+
1010
+ @classmethod
1011
+ def index_definitions(cls) -> dict[str, IndexFieldConfig]:
1012
+ return {"main-index": IndexFieldConfig(sk_fields=["baz"])}
1013
+
1014
+ __hash__ = object.__hash__
1015
+
1016
+ class MyNestedDatabaseModel(DatabaseModel):
1017
+ bar: str
1018
+ other_nested: Optional[MyOtherNestedDatabaseModel] = None
1019
+ list_nested: list[MyOtherNestedDatabaseModel] = []
1020
+ set_nested: set[MyOtherNestedDatabaseModel] = {}
1021
+
1022
+ @classmethod
1023
+ def is_nested(cls) -> bool:
1024
+ return True
1025
+
1026
+ @classmethod
1027
+ def index_definitions(cls) -> dict[str, IndexFieldConfig]:
1028
+ return {"main-index": IndexFieldConfig(sk_fields=["bar"])}
1029
+
1030
+ class MyDatabaseModel(DatabaseModel):
1031
+ foo: str
1032
+ nested: MyNestedDatabaseModel
1033
+
1034
+ @classmethod
1035
+ def index_definitions(cls) -> dict[str, IndexFieldConfig]:
1036
+ return {"main-index": IndexFieldConfig(pk_fields=["foo"], sk_fields=[FIELD_STATIKK_TYPE])}
1037
+
1038
+ _create_default_dynamodb_table([MyDatabaseModel, MyNestedDatabaseModel, MyOtherNestedDatabaseModel])
1039
+ my_database_model = MyDatabaseModel(foo="foo", nested=MyNestedDatabaseModel(bar="bar"))
1040
+ my_database_model.build_model_indexes()
1041
+ my_database_model.nested.add_child_node("other_nested", MyOtherNestedDatabaseModel(baz="baz"))
1042
+ my_database_model.nested.add_child_node("list_nested", MyOtherNestedDatabaseModel(baz="bazz"))
1043
+ my_database_model.nested.add_child_node("set_nested", MyOtherNestedDatabaseModel(baz="bazzz"))
1044
+ assert my_database_model.nested.other_nested.baz == "baz"
1045
+ assert my_database_model.nested.list_nested[0].baz == "bazz"
1046
+ set_nested_item = my_database_model.nested.set_nested.pop()
1047
+ assert set_nested_item.baz == "bazzz"
1048
+ assert set_nested_item._parent == my_database_model.nested
1049
+ assert set_nested_item.gsi_pk == set_nested_item._parent.gsi_pk
1050
+ assert set_nested_item.gsi_sk == "MyDatabaseModel|MyNestedDatabaseModel|bar|MyOtherNestedDatabaseModel|bazzz"
1051
+ assert my_database_model.nested.other_nested._parent == my_database_model.nested
1052
+ assert my_database_model.nested.list_nested[0]._parent == my_database_model.nested
@@ -1,3 +1,5 @@
1
+ from typing import Optional
2
+
1
3
  from pydantic import BaseModel
2
4
  from statikk.models import DatabaseModel, IndexFieldConfig
3
5
 
@@ -1,3 +0,0 @@
1
- from typing import TypeVar
2
-
3
- T = TypeVar("T", bound="DatabaseModel")
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