statikk 0.1.13__py3-none-any.whl → 0.1.14__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
@@ -246,6 +246,8 @@ class Table:
246
246
  else:
247
247
  batch.put(item)
248
248
 
249
+ model._session.reset()
250
+
249
251
  def update_item(
250
252
  self,
251
253
  hash_key: str,
statikk/models.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from datetime import datetime
3
4
  import typing
4
5
  import logging
5
6
  from uuid import uuid4
@@ -53,6 +54,43 @@ class IndexFieldConfig(BaseModel):
53
54
  sk_fields: list[str] = []
54
55
 
55
56
 
57
+ class TreeStructureChange(BaseModel):
58
+ new_parent: Optional[Any]
59
+ new_parent_field_name: Optional[str]
60
+ subtree: Any
61
+ timestamp: datetime = Field(default_factory=datetime.now)
62
+
63
+
64
+ class Session(BaseModel):
65
+ _changes: list[TreeStructureChange] = []
66
+
67
+ def add_change(self, new_parent: Optional[T], new_parent_field_name: Optional[str], subtree: T):
68
+ self._changes.append(
69
+ TreeStructureChange(new_parent=new_parent, new_parent_field_name=new_parent_field_name, subtree=subtree)
70
+ )
71
+
72
+ def get_subtree_changes_by_parent_id(
73
+ self, parent_id: str, subtree_id: str, field_name: str
74
+ ) -> Optional[TreeStructureChange]:
75
+ sorted_changes = sorted(self._changes, key=lambda change: change.timestamp, reverse=True)
76
+ return next(
77
+ filter(
78
+ lambda change: change.new_parent.id == parent_id
79
+ and change.subtree.id == subtree_id
80
+ and change.new_parent_field_name == field_name,
81
+ sorted_changes,
82
+ ),
83
+ None,
84
+ )
85
+
86
+ def get_last_change_for(self, subtree_id):
87
+ sorted_changes = sorted(self._changes, key=lambda change: change.timestamp, reverse=True)
88
+ return next(filter(lambda change: change.subtree.id == subtree_id, sorted_changes), None)
89
+
90
+ def reset(self):
91
+ self._changes = []
92
+
93
+
56
94
  class TrackingMixin:
57
95
  _original_hash: int = Field(exclude=True)
58
96
 
@@ -164,6 +202,10 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
164
202
  _model_types_in_hierarchy: dict[str, Type[DatabaseModel]] = {}
165
203
  _should_delete: bool = False
166
204
  _parent_changed: bool = False
205
+ _session = Session()
206
+
207
+ def __eq__(self, other):
208
+ return self.id == other.id
167
209
 
168
210
  def is_parent_changed(self):
169
211
  """
@@ -182,6 +224,17 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
182
224
  def type(cls) -> str:
183
225
  return cls.__name__
184
226
 
227
+ @property
228
+ def should_track_session(self) -> bool:
229
+ """
230
+ If set to True, subtree movements across the database model will be tracked in a session.
231
+ Use this if you need to move a subtree across multiple parent-child relationships within a single session.
232
+ Session is reset after each save.
233
+ """
234
+ if self._parent is not None:
235
+ return self._parent.should_track_session
236
+ return False
237
+
185
238
  @classmethod
186
239
  def index_definitions(cls) -> dict[str, IndexFieldConfig]:
187
240
  return {"main_index": IndexFieldConfig(pk_fields=[], sk_fields=[])}
@@ -296,6 +349,23 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
296
349
  def change_parent_to(self, new_parent: DatabaseModel) -> T:
297
350
  return self._table.reparent_subtree(self, new_parent)
298
351
 
352
+ def _remove_from_parent(self, parent, field_name, subtree):
353
+ is_optional, inner_type = inspect_optional_field(parent.__class__, field_name)
354
+ field_type = inner_type if is_optional else parent.model_fields[field_name].annotation
355
+ field = getattr(self, field_name)
356
+ if hasattr(field_type, "__origin__") and field_type.__origin__ == list:
357
+ if not isinstance(field, list):
358
+ setattr(self, field_name, [])
359
+ field.remove(next(filter(lambda item: item.id == subtree.id, getattr(parent, field_name)), None))
360
+
361
+ elif hasattr(field_type, "__origin__") and field_type.__origin__ == set:
362
+ if not isinstance(field, set):
363
+ setattr(self, field_name, set())
364
+ field.remove(next(filter(lambda item: item.id == subtree.id, getattr(parent, field_name)), None))
365
+
366
+ elif issubclass(field_type, DatabaseModel):
367
+ setattr(parent, field_name, None)
368
+
299
369
  def add_child_node(self, field_name: str, child_node: DatabaseModel):
300
370
  if not child_node.is_nested():
301
371
  raise ValueError("Child node must be nested.")
@@ -303,26 +373,33 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
303
373
  if not hasattr(self, field_name):
304
374
  raise ValueError(f"Field {field_name} does not exist on {self.__class__.__name__}")
305
375
 
376
+ if self.should_track_session:
377
+ previous_change = self._session.get_subtree_changes_by_parent_id(self.id, child_node.id, field_name)
378
+ if previous_change:
379
+ self._remove_from_parent(previous_change.new_parent, previous_change.new_parent_field_name, child_node)
380
+
306
381
  is_optional, inner_type = inspect_optional_field(self.__class__, field_name)
307
382
  field_type = inner_type if is_optional else self.model_fields[field_name].annotation
308
-
383
+ reparented = None
309
384
  if hasattr(field_type, "__origin__") and field_type.__origin__ == list:
310
385
  if not isinstance(getattr(self, field_name), list):
311
386
  setattr(self, field_name, [])
312
387
  reparented = child_node.change_parent_to(self)
313
388
  getattr(self, field_name).append(reparented)
314
- return reparented
315
389
 
316
390
  elif hasattr(field_type, "__origin__") and field_type.__origin__ == set:
317
391
  if not isinstance(getattr(self, field_name), set):
318
392
  setattr(self, field_name, set())
319
393
  reparented = child_node.change_parent_to(self)
320
394
  getattr(self, field_name).add(reparented)
321
- return reparented
322
395
 
323
396
  elif issubclass(field_type, DatabaseModel):
324
397
  reparented = child_node.change_parent_to(self)
325
398
  setattr(self, field_name, reparented)
399
+
400
+ if reparented:
401
+ if self.should_track_session:
402
+ self._session.add_change(self, field_name, reparented)
326
403
  return reparented
327
404
 
328
405
  raise ValueError(f"Unsupported field type: {field_type}")
@@ -371,9 +448,7 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
371
448
  if self not in items:
372
449
  items.append(self)
373
450
 
374
- # Iterate through all fields of the model
375
451
  for field_name, field_value in self:
376
- # Skip fields that start with underscore (private fields)
377
452
  if field_name.startswith("_"):
378
453
  continue
379
454
 
@@ -383,7 +458,6 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
383
458
  items.append(field_value)
384
459
  field_value.split_to_simple_objects(items)
385
460
 
386
- # Handle lists containing DatabaseModel instances
387
461
  elif isinstance(field_value, list):
388
462
  for item in field_value:
389
463
  if hasattr(item, "__class__") and issubclass(item.__class__, DatabaseModel):
@@ -391,7 +465,6 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
391
465
  items.append(item)
392
466
  item.split_to_simple_objects(items)
393
467
 
394
- # Handle sets containing DatabaseModel instances
395
468
  elif isinstance(field_value, set):
396
469
  for item in field_value:
397
470
  if hasattr(item, "__class__") and issubclass(item.__class__, DatabaseModel):
@@ -399,15 +472,6 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
399
472
  items.append(item)
400
473
  item.split_to_simple_objects(items)
401
474
 
402
- # Handle dictionaries that may contain DatabaseModel instances
403
- elif isinstance(field_value, dict):
404
- # Check dictionary values
405
- for value in field_value.values():
406
- if hasattr(value, "__class__") and issubclass(value.__class__, DatabaseModel):
407
- if value not in items:
408
- items.append(value)
409
- value.split_to_simple_objects(items)
410
-
411
475
  return items
412
476
 
413
477
  def get_attribute(self, attribute_name: str):
@@ -438,11 +502,18 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
438
502
  return self._model_types_in_hierarchy.get(name)
439
503
 
440
504
  def _set_parent_to_field(
441
- self, field: DatabaseModel, parent: DatabaseModel, root: DatabaseModel, force_override: bool = False
505
+ self,
506
+ field: DatabaseModel,
507
+ field_name: str,
508
+ parent: DatabaseModel,
509
+ root: DatabaseModel,
510
+ force_override: bool = False,
442
511
  ):
443
512
  if field._parent and not force_override:
444
513
  return # Already set
445
514
  field._parent = parent
515
+ if field.should_track_session:
516
+ field._session.add_change(parent, field_name, field)
446
517
  root._model_types_in_hierarchy[field.type()] = type(field)
447
518
  field.set_parent_references(root, force_override)
448
519
  field.init_tracking()
@@ -452,7 +523,7 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
452
523
  Sets parent references for all DatabaseModel objects in the hierarchy.
453
524
  """
454
525
  for parent, field_name, model in self.traverse_hierarchy():
455
- self._set_parent_to_field(model, parent, root, force_override)
526
+ self._set_parent_to_field(model, field_name, parent, root, force_override)
456
527
 
457
528
  def traverse_hierarchy(self):
458
529
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statikk
3
- Version: 0.1.13
3
+ Version: 0.1.14
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=-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,,
@@ -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=xH8vbUcup3FmZ3h3RL6-j1hxVDSQwrRASp8ILhoKXn8,32604
4
- statikk/expressions.py,sha256=boAeGxZj2cDsXxoiX3IIEzfX9voSMQngi4-rE_jYeuE,12233
5
- statikk/fields.py,sha256=LkMP5NnX7WS0HSLxI3Q-dMOrfaJ0SD7SayZxJU5Acgg,86
6
- statikk/models.py,sha256=fU5SwxKP1Afsy91RhYiJSD58CCF92s4YxX-fwH6p7ZI,17705
7
- statikk/typing.py,sha256=laOlOpWOm9_sOj4hhdZnGTUZRiq8760_B9I9B3wBhz8,750
8
- statikk-0.1.13.dist-info/licenses/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
9
- statikk-0.1.13.dist-info/METADATA,sha256=pv7MrB9Svi22O1z--jDhW8DkEvdxNP4nErQLt7HqQpg,3183
10
- statikk-0.1.13.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
11
- statikk-0.1.13.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
12
- statikk-0.1.13.dist-info/RECORD,,