statikk 0.1.14__py3-none-any.whl → 0.1.16__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.
statikk/engine.py CHANGED
@@ -16,7 +16,7 @@ from statikk.models import (
16
16
  GSI,
17
17
  KeySchema,
18
18
  )
19
- from statikk.fields import FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID
19
+ from statikk.fields import FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID, FIELD_STATIKK_PARENT_FIELD_NAME
20
20
  from copy import deepcopy
21
21
  from aws_xray_sdk.core import patch_all
22
22
 
@@ -296,20 +296,23 @@ class Table:
296
296
  response = self._get_dynamodb_table().update_item(**request)
297
297
  data = response["Attributes"]
298
298
  for key, value in data.items():
299
- if key in [FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID]:
299
+ if key in [FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID, FIELD_STATIKK_PARENT_FIELD_NAME]:
300
300
  continue
301
301
  data[key] = self._deserialize_value(value, model.model_fields[key])
302
302
  return type(model)(**data)
303
303
 
304
- def reparent_subtree(self, subtree_root: T, new_parent: T) -> T:
304
+ def reparent_subtree(self, subtree_root: T, new_parent: T, field_name: str) -> T:
305
305
  subtree_copy = deepcopy(subtree_root)
306
306
  subtree_root._parent_changed = True
307
307
 
308
308
  subtree_copy.set_parent_references(subtree_copy, force_override=True)
309
309
  subtree_copy._parent = new_parent
310
+ subtree_copy._parent_field_name = field_name
311
+ parent = subtree_copy._parent
310
312
  for node in subtree_copy.dfs_traverse_hierarchy():
311
313
  self.build_model_indexes(node)
312
- setattr(node, FIELD_STATIKK_PARENT_ID, new_parent.id)
314
+ setattr(node, FIELD_STATIKK_PARENT_ID, parent.id)
315
+ parent = node
313
316
 
314
317
  return subtree_copy
315
318
 
@@ -427,7 +430,10 @@ class Table:
427
430
  model_class = self._get_model_type_by_statikk_type(model_type)
428
431
 
429
432
  reconstructed_dict.pop(FIELD_STATIKK_TYPE, None)
430
- return model_class(**reconstructed_dict)
433
+ model = model_class.model_validate(reconstructed_dict)
434
+ for node in model.dfs_traverse_hierarchy():
435
+ node._is_persisted = True
436
+ return model
431
437
 
432
438
  def scan(
433
439
  self,
@@ -626,6 +632,7 @@ class Table:
626
632
  continue
627
633
  if not enriched_item.should_write_to_database():
628
634
  continue
635
+ enriched_item._is_persisted = True
629
636
  data = self._serialize_item(enriched_item)
630
637
  batch.put_item(Item=data)
631
638
 
@@ -640,9 +647,6 @@ class Table:
640
647
  Returns:
641
648
  The top-level dictionary with its hierarchy fully reconstructed, or None if the list is empty
642
649
  """
643
- if not items:
644
- return None
645
-
646
650
  items_by_id = {item["id"]: item for item in items}
647
651
  children_by_parent_id = {}
648
652
  for item in items:
@@ -652,128 +656,74 @@ class Table:
652
656
  children_by_parent_id[parent_id] = []
653
657
  children_by_parent_id[parent_id].append(item)
654
658
 
655
- # Find the root item (the one with no parent ID)
656
- root_items = [item for item in items if FIELD_STATIKK_PARENT_ID not in item]
659
+ root_item = [item for item in items if FIELD_STATIKK_PARENT_ID not in item][0]
657
660
 
658
- if not root_items:
659
- return None
660
-
661
- if len(root_items) > 1:
662
- root_item = root_items[0]
663
- else:
664
- root_item = root_items[0]
661
+ processed_root = self._process_item(root_item, items_by_id, children_by_parent_id)
662
+ return processed_root
665
663
 
666
- processed_items = set()
667
- return self._reconstruct_item_with_children(root_item, items_by_id, children_by_parent_id, processed_items)
668
-
669
- def _reconstruct_item_with_children(
670
- self, item: dict, items_by_id: dict, children_by_parent_id: dict, processed_items: set
671
- ) -> dict:
664
+ def _process_item(self, item: dict, items_by_id: dict, children_by_parent_id: dict) -> dict:
672
665
  """
673
- Recursively reconstruct an item and its children using model class definitions.
666
+ Recursively processes an item and all its children to rebuild the hierarchical structure.
674
667
 
675
668
  Args:
676
- item: The item to reconstruct
677
- items_by_id: Map of all item IDs to items
678
- children_by_parent_id: Map of parent IDs to lists of child items
679
- processed_items: Set of already processed item IDs to avoid duplicates
669
+ item: The current item to process
670
+ items_by_id: Dictionary mapping item IDs to items
671
+ children_by_parent_id: Dictionary mapping parent IDs to lists of child items
680
672
 
681
673
  Returns:
682
- The reconstructed item with all child references integrated
674
+ The processed item with all its child relationships resolved
683
675
  """
684
- if item["id"] in processed_items:
685
- return item
686
- processed_items.add(item["id"])
687
- result = item.copy()
688
-
689
- if FIELD_STATIKK_PARENT_ID in result:
690
- result.pop(FIELD_STATIKK_PARENT_ID)
676
+ processed_item = deepcopy(item)
691
677
 
692
- children = children_by_parent_id.get(item["id"], [])
693
- if not children:
694
- return result
678
+ if FIELD_STATIKK_TYPE in processed_item:
679
+ model_class = self._get_model_type_by_statikk_type(processed_item[FIELD_STATIKK_TYPE])
680
+ model_fields = model_class.model_fields
681
+ else:
682
+ return processed_item
695
683
 
696
- parent_model_class = self._get_model_type_by_statikk_type(item[FIELD_STATIKK_TYPE])
684
+ # Get children of this item
685
+ children = children_by_parent_id.get(processed_item["id"], [])
697
686
 
698
- children_by_type = {}
687
+ # Group children by parent field name
688
+ children_by_field = {}
699
689
  for child in children:
700
- child_type = child[FIELD_STATIKK_TYPE]
701
- if child_type not in children_by_type:
702
- children_by_type[child_type] = []
703
- children_by_type[child_type].append(child)
704
-
705
- for child_type, child_items in children_by_type.items():
706
- child_model_class = self._get_model_type_by_statikk_type(child_type)
707
- matching_fields = []
708
-
709
- for field_name, field_info in parent_model_class.model_fields.items():
710
- if field_name.startswith("_"):
711
- continue
712
-
713
- is_optional, inner_type = inspect_optional_field(parent_model_class, field_name)
714
-
715
- field_type = inner_type if is_optional else field_info.annotation
716
-
717
- if field_type == child_model_class:
718
- matching_fields.append((field_name, "single"))
719
-
720
- elif hasattr(field_type, "__origin__") and field_type.__origin__ == list:
721
- args = getattr(field_type, "__args__", [])
722
- if args and args[0] == child_model_class:
723
- matching_fields.append((field_name, "list"))
724
-
725
- elif hasattr(field_type, "__origin__") and field_type.__origin__ == set:
726
- args = getattr(field_type, "__args__", [])
727
- if args and args[0] == child_model_class:
728
- matching_fields.append((field_name, "set"))
729
-
730
- if matching_fields:
731
- for field_name, container_type in matching_fields:
732
- if container_type == "list":
733
- if field_name not in result:
734
- result[field_name] = []
735
-
736
- existing_ids = {
737
- item.get("id") for item in result[field_name] if isinstance(item, dict) and "id" in item
738
- }
739
-
740
- for child in child_items:
741
- if child["id"] in existing_ids:
742
- continue
690
+ field_name = child.get(FIELD_STATIKK_PARENT_FIELD_NAME)
691
+ if field_name:
692
+ if field_name not in children_by_field:
693
+ children_by_field[field_name] = []
694
+ children_by_field[field_name].append(child)
695
+
696
+ for field_name, field_info in model_fields.items():
697
+ if field_name not in children_by_field:
698
+ continue
743
699
 
744
- reconstructed_child = self._reconstruct_item_with_children(
745
- child, items_by_id, children_by_parent_id, processed_items
746
- )
700
+ field_children = children_by_field[field_name]
747
701
 
748
- result[field_name].append(reconstructed_child)
749
- existing_ids.add(child["id"])
702
+ field_type = field_info.annotation
703
+ is_optional = False
704
+ inner_type = field_type
750
705
 
751
- elif container_type == "set":
752
- if field_name not in result:
753
- result[field_name] = []
706
+ if hasattr(field_type, "__origin__") and field_type.__origin__ is Union:
707
+ args = field_type.__args__
708
+ if type(None) in args:
709
+ is_optional = True
710
+ # Get the non-None type
711
+ inner_type = next(arg for arg in args if arg is not type(None))
754
712
 
755
- existing_ids = {
756
- item.get("id") for item in result[field_name] if isinstance(item, dict) and "id" in item
757
- }
713
+ if hasattr(inner_type, "__origin__") and inner_type.__origin__ == list:
714
+ child_list = []
758
715
 
759
- for child in child_items:
760
- if child["id"] in existing_ids:
761
- continue
762
- reconstructed_child = self._reconstruct_item_with_children(
763
- child, items_by_id, children_by_parent_id, processed_items
764
- )
716
+ for child in field_children:
717
+ processed_child = self._process_item(child, items_by_id, children_by_parent_id)
718
+ child_list.append(processed_child)
765
719
 
766
- result[field_name].append(reconstructed_child)
767
- existing_ids.add(child["id"])
720
+ processed_item[field_name] = child_list
768
721
 
769
- elif container_type == "single":
770
- if child_items:
771
- reconstructed_child = self._reconstruct_item_with_children(
772
- child_items[0], items_by_id, children_by_parent_id, processed_items
773
- )
774
- result[field_name] = reconstructed_child
722
+ elif len(field_children) == 1:
723
+ processed_child = self._process_item(field_children[0], items_by_id, children_by_parent_id)
724
+ processed_item[field_name] = processed_child
775
725
 
776
- return result
726
+ return processed_item
777
727
 
778
728
 
779
729
  class BatchWriteContext:
statikk/fields.py CHANGED
@@ -1,2 +1,3 @@
1
1
  FIELD_STATIKK_TYPE = "__statikk_type"
2
2
  FIELD_STATIKK_PARENT_ID = "__statikk_parent_id"
3
+ FIELD_STATIKK_PARENT_FIELD_NAME = "__statikk_parent_field_name"
statikk/models.py CHANGED
@@ -14,7 +14,7 @@ from pydantic_core._pydantic_core import PydanticUndefined
14
14
 
15
15
  from statikk.conditions import Condition
16
16
  from statikk.expressions import DatabaseModelUpdateExpressionBuilder
17
- from statikk.fields import FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID
17
+ from statikk.fields import FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID, FIELD_STATIKK_PARENT_FIELD_NAME
18
18
 
19
19
  if typing.TYPE_CHECKING:
20
20
  from statikk.engine import Table
@@ -196,14 +196,20 @@ class TrackingMixin:
196
196
  return {}
197
197
 
198
198
 
199
- class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
199
+ class DatabaseModel(BaseModel, TrackingMixin):
200
200
  id: str = Field(default_factory=lambda: str(uuid4()))
201
201
  _parent: Optional[DatabaseModel] = None
202
+ _parent_field_name: Optional[str] = None
202
203
  _model_types_in_hierarchy: dict[str, Type[DatabaseModel]] = {}
203
204
  _should_delete: bool = False
204
205
  _parent_changed: bool = False
206
+ _is_persisted: bool = False
205
207
  _session = Session()
206
208
 
209
+ class Config:
210
+ extra = Extra.allow
211
+ arbitrary_types_allowed = True
212
+
207
213
  def __eq__(self, other):
208
214
  return self.id == other.id
209
215
 
@@ -255,12 +261,16 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
255
261
  def is_simple_object(self) -> bool:
256
262
  return len(self._model_types_in_hierarchy) == 1
257
263
 
264
+ @property
265
+ def is_persisted(self) -> bool:
266
+ return self._is_persisted
267
+
258
268
  @property
259
269
  def should_delete(self) -> bool:
260
270
  if self._is_any_parent_marked_for_deletion():
261
271
  return True
262
272
 
263
- return self._should_delete or self.is_parent_changed()
273
+ return self.is_persisted and (self._should_delete or self.is_parent_changed())
264
274
 
265
275
  def _is_any_parent_marked_for_deletion(self) -> bool:
266
276
  current = self._parent
@@ -346,8 +356,8 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
346
356
  def mark_for_delete(self):
347
357
  self._should_delete = True
348
358
 
349
- def change_parent_to(self, new_parent: DatabaseModel) -> T:
350
- return self._table.reparent_subtree(self, new_parent)
359
+ def _change_parent_to(self, new_parent: DatabaseModel, field_name: str) -> T:
360
+ return self._table.reparent_subtree(self, new_parent, field_name)
351
361
 
352
362
  def _remove_from_parent(self, parent, field_name, subtree):
353
363
  is_optional, inner_type = inspect_optional_field(parent.__class__, field_name)
@@ -384,17 +394,17 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
384
394
  if hasattr(field_type, "__origin__") and field_type.__origin__ == list:
385
395
  if not isinstance(getattr(self, field_name), list):
386
396
  setattr(self, field_name, [])
387
- reparented = child_node.change_parent_to(self)
397
+ reparented = child_node._change_parent_to(self, field_name)
388
398
  getattr(self, field_name).append(reparented)
389
399
 
390
400
  elif hasattr(field_type, "__origin__") and field_type.__origin__ == set:
391
401
  if not isinstance(getattr(self, field_name), set):
392
402
  setattr(self, field_name, set())
393
- reparented = child_node.change_parent_to(self)
403
+ reparented = child_node._change_parent_to(self, field_name)
394
404
  getattr(self, field_name).add(reparented)
395
405
 
396
406
  elif issubclass(field_type, DatabaseModel):
397
- reparented = child_node.change_parent_to(self)
407
+ reparented = child_node._change_parent_to(self, field_name)
398
408
  setattr(self, field_name, reparented)
399
409
 
400
410
  if reparented:
@@ -418,6 +428,7 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
418
428
  data[FIELD_STATIKK_TYPE] = self.type()
419
429
  if self._parent:
420
430
  data[FIELD_STATIKK_PARENT_ID] = self._parent.id
431
+ data[FIELD_STATIKK_PARENT_FIELD_NAME] = self._parent_field_name
421
432
  return data
422
433
 
423
434
  @model_validator(mode="after")
@@ -465,13 +476,6 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
465
476
  items.append(item)
466
477
  item.split_to_simple_objects(items)
467
478
 
468
- elif isinstance(field_value, set):
469
- for item in field_value:
470
- if hasattr(item, "__class__") and issubclass(item.__class__, DatabaseModel):
471
- if item not in items:
472
- items.append(item)
473
- item.split_to_simple_objects(items)
474
-
475
479
  return items
476
480
 
477
481
  def get_attribute(self, attribute_name: str):
@@ -488,14 +492,6 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
488
492
  for item in field_value:
489
493
  if issubclass(item.__class__, DatabaseModel) and item.is_nested():
490
494
  nested_models.append(field_name)
491
- elif isinstance(field_value, set):
492
- for item in field_value:
493
- if issubclass(item.__class__, DatabaseModel) and item.is_nested():
494
- nested_models.append(field_name)
495
- elif isinstance(field_value, dict):
496
- for key, value in field_value.items():
497
- if issubclass(value.__class__, DatabaseModel) and value.is_nested():
498
- nested_models.append(field_name)
499
495
  return set(nested_models)
500
496
 
501
497
  def get_type_from_hierarchy_by_name(self, name: str) -> Optional[Type[DatabaseModel]]:
@@ -512,6 +508,7 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
512
508
  if field._parent and not force_override:
513
509
  return # Already set
514
510
  field._parent = parent
511
+ field._parent_field_name = field_name
515
512
  if field.should_track_session:
516
513
  field._session.add_change(parent, field_name, field)
517
514
  root._model_types_in_hierarchy[field.type()] = type(field)
@@ -532,14 +529,10 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
532
529
  for field_name, field_value in self:
533
530
  if isinstance(field_value, DatabaseModel):
534
531
  yield self, field_name, field_value
535
- elif isinstance(field_value, (list, set)):
532
+ elif isinstance(field_value, list):
536
533
  for item in field_value:
537
534
  if isinstance(item, DatabaseModel):
538
535
  yield self, field_name, item
539
- elif isinstance(field_value, dict):
540
- for key, value in field_value.items():
541
- if isinstance(value, DatabaseModel):
542
- yield self, key, value
543
536
 
544
537
  def dfs_traverse_hierarchy(self):
545
538
  """
@@ -555,11 +548,7 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
555
548
  for field_name, field_value in fields:
556
549
  if isinstance(field_value, DatabaseModel):
557
550
  yield from field_value.dfs_traverse_hierarchy()
558
- elif isinstance(field_value, (list, set)):
551
+ elif isinstance(field_value, list):
559
552
  for item in field_value:
560
553
  if isinstance(item, DatabaseModel):
561
554
  yield from item.dfs_traverse_hierarchy()
562
- elif isinstance(field_value, dict):
563
- for key, value in field_value.items():
564
- if isinstance(value, DatabaseModel):
565
- yield from value.dfs_traverse_hierarchy()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statikk
3
- Version: 0.1.14
3
+ Version: 0.1.16
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
@@ -0,0 +1,12 @@
1
+ statikk/__init__.py,sha256=pH5i4Fj1tbXLqLtTVIdoojiplZssQn0nnud8-HXodRE,577
2
+ statikk/conditions.py,sha256=63FYMR-UUaE-ZJEb_8CU721CQTwhajq39-BbokmKeMA,2166
3
+ statikk/engine.py,sha256=YkJW2wUxFAIcw0o1r7RF0gwQm-znjjJ-rzBSzXBtCB0,30557
4
+ statikk/expressions.py,sha256=boAeGxZj2cDsXxoiX3IIEzfX9voSMQngi4-rE_jYeuE,12233
5
+ statikk/fields.py,sha256=uLGD3xEnz2nqQY6EwG8Dd7pnRMMVP4PU2Z2I-uGNaTA,150
6
+ statikk/models.py,sha256=27s8Rvb8lcQFtaxHEZsKSHebqa1c3GqClUtoE1PJH2I,19657
7
+ statikk/typing.py,sha256=laOlOpWOm9_sOj4hhdZnGTUZRiq8760_B9I9B3wBhz8,750
8
+ statikk-0.1.16.dist-info/licenses/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
9
+ statikk-0.1.16.dist-info/METADATA,sha256=mrUj-DWF9nBmjzjlLL5ax4Kkx3UahYRhEr4g39WbRpg,3183
10
+ statikk-0.1.16.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
11
+ statikk-0.1.16.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
12
+ statikk-0.1.16.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- statikk/__init__.py,sha256=pH5i4Fj1tbXLqLtTVIdoojiplZssQn0nnud8-HXodRE,577
2
- statikk/conditions.py,sha256=63FYMR-UUaE-ZJEb_8CU721CQTwhajq39-BbokmKeMA,2166
3
- statikk/engine.py,sha256=-YmezdiYViz1sR60VL7ryDCvrhp-WWGVCtPAakKRzg8,32636
4
- statikk/expressions.py,sha256=boAeGxZj2cDsXxoiX3IIEzfX9voSMQngi4-rE_jYeuE,12233
5
- statikk/fields.py,sha256=LkMP5NnX7WS0HSLxI3Q-dMOrfaJ0SD7SayZxJU5Acgg,86
6
- statikk/models.py,sha256=zLImQUtUURbAQ3gYAiotkypxaDUqAIiCL1edNzuey3k,20447
7
- statikk/typing.py,sha256=laOlOpWOm9_sOj4hhdZnGTUZRiq8760_B9I9B3wBhz8,750
8
- statikk-0.1.14.dist-info/licenses/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
9
- statikk-0.1.14.dist-info/METADATA,sha256=8NHWnMMS3Qy53_I9V3EDWOY6x3wTzQ_juoYSPS9QeII,3183
10
- statikk-0.1.14.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
11
- statikk-0.1.14.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
12
- statikk-0.1.14.dist-info/RECORD,,