statikk 0.1.17__py3-none-any.whl → 0.1.19__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 +47 -17
- statikk/models.py +13 -78
- {statikk-0.1.17.dist-info → statikk-0.1.19.dist-info}/METADATA +1 -1
- {statikk-0.1.17.dist-info → statikk-0.1.19.dist-info}/RECORD +7 -7
- {statikk-0.1.17.dist-info → statikk-0.1.19.dist-info}/WHEEL +0 -0
- {statikk-0.1.17.dist-info → statikk-0.1.19.dist-info}/licenses/LICENSE.txt +0 -0
- {statikk-0.1.17.dist-info → statikk-0.1.19.dist-info}/top_level.txt +0 -0
statikk/engine.py
CHANGED
@@ -240,13 +240,24 @@ class Table:
|
|
240
240
|
"""
|
241
241
|
|
242
242
|
with self.batch_write() as batch:
|
243
|
-
|
243
|
+
items = model.split_to_simple_objects()
|
244
|
+
new_keys = self._create_snapshot_representation(model)
|
245
|
+
keys_to_delete = model._db_snapshot_keys - new_keys
|
246
|
+
|
247
|
+
for key in keys_to_delete:
|
248
|
+
hash_key, sort_key = key.split("#", 1)
|
249
|
+
delete_params = {self.key_schema.hash_key: hash_key}
|
250
|
+
if sort_key:
|
251
|
+
delete_params[self.key_schema.sort_key] = sort_key
|
252
|
+
batch.delete_by_key(delete_params)
|
253
|
+
|
254
|
+
for item in items:
|
244
255
|
if item.should_delete:
|
245
256
|
batch.delete(item)
|
246
257
|
else:
|
247
258
|
batch.put(item)
|
248
259
|
|
249
|
-
model.
|
260
|
+
model._db_snapshot_keys = new_keys
|
250
261
|
|
251
262
|
def update_item(
|
252
263
|
self,
|
@@ -302,20 +313,17 @@ class Table:
|
|
302
313
|
return type(model)(**data)
|
303
314
|
|
304
315
|
def reparent_subtree(self, subtree_root: T, new_parent: T, field_name: str) -> T:
|
305
|
-
|
306
|
-
subtree_root.
|
307
|
-
|
308
|
-
subtree_copy.set_parent_references(subtree_copy, force_override=True)
|
309
|
-
subtree_copy._parent = new_parent
|
310
|
-
subtree_copy._parent_field_name = field_name
|
316
|
+
subtree_root._parent = new_parent
|
317
|
+
subtree_root._parent_field_name = field_name
|
318
|
+
subtree_root.set_parent_references(subtree_root, force_override=True)
|
311
319
|
parent = None
|
312
|
-
for node in
|
320
|
+
for node in subtree_root.dfs_traverse_hierarchy():
|
313
321
|
self.build_model_indexes(node)
|
314
322
|
if parent:
|
315
323
|
setattr(node, FIELD_STATIKK_PARENT_ID, parent.id)
|
316
324
|
parent = node
|
317
325
|
|
318
|
-
return
|
326
|
+
return subtree_root
|
319
327
|
|
320
328
|
def build_model_indexes(self, model: T) -> T:
|
321
329
|
for idx in self.indexes:
|
@@ -432,8 +440,7 @@ class Table:
|
|
432
440
|
|
433
441
|
reconstructed_dict.pop(FIELD_STATIKK_TYPE, None)
|
434
442
|
model = model_class.model_validate(reconstructed_dict)
|
435
|
-
|
436
|
-
node._is_persisted = True
|
443
|
+
model._db_snapshot_keys = self._create_snapshot_representation(model)
|
437
444
|
return model
|
438
445
|
|
439
446
|
def scan(
|
@@ -502,7 +509,7 @@ class Table:
|
|
502
509
|
for idx in indexes:
|
503
510
|
index_fields = self._compose_index_values(item, idx)
|
504
511
|
for key, value in index_fields.items():
|
505
|
-
if hasattr(item, key) and (getattr(item, key) is not None
|
512
|
+
if hasattr(item, key) and (getattr(item, key) is not None):
|
506
513
|
continue
|
507
514
|
if value is not None:
|
508
515
|
setattr(item, key, value)
|
@@ -612,7 +619,9 @@ class Table:
|
|
612
619
|
idx.sort_key.name: self._get_sort_key_value(model, idx),
|
613
620
|
}
|
614
621
|
|
615
|
-
def _perform_batch_write(
|
622
|
+
def _perform_batch_write(
|
623
|
+
self, put_items: List[DatabaseModel], delete_items: List[DatabaseModel], delete_keys: list[dict[str, Any]]
|
624
|
+
):
|
616
625
|
if len(put_items) == 0 and len(delete_items) == 0:
|
617
626
|
return
|
618
627
|
|
@@ -625,6 +634,11 @@ class Table:
|
|
625
634
|
data = self._serialize_item(enriched_item)
|
626
635
|
batch.delete_item(Key=data)
|
627
636
|
|
637
|
+
if len(delete_keys) > 0:
|
638
|
+
with dynamodb_table.batch_writer() as batch:
|
639
|
+
for key in delete_keys:
|
640
|
+
batch.delete_item(Key=key)
|
641
|
+
|
628
642
|
if len(put_items) > 0:
|
629
643
|
with dynamodb_table.batch_writer() as batch:
|
630
644
|
for item in put_items:
|
@@ -633,10 +647,19 @@ class Table:
|
|
633
647
|
continue
|
634
648
|
if not enriched_item.should_write_to_database():
|
635
649
|
continue
|
636
|
-
enriched_item._is_persisted = True
|
637
650
|
data = self._serialize_item(enriched_item)
|
638
651
|
batch.put_item(Item=data)
|
639
652
|
|
653
|
+
def _create_snapshot_representation(self, model: DatabaseModel) -> set:
|
654
|
+
db_snasphot = set()
|
655
|
+
for node in model.dfs_traverse_hierarchy():
|
656
|
+
model_key = {self.key_schema.hash_key: getattr(node, self.key_schema.hash_key)}
|
657
|
+
if self.key_schema.sort_key:
|
658
|
+
model_key[self.key_schema.sort_key] = getattr(node, self.key_schema.sort_key)
|
659
|
+
key_string = f"{model_key.get(self.key_schema.hash_key)}#{model_key.get(self.key_schema.sort_key, '')}"
|
660
|
+
db_snasphot.add(key_string)
|
661
|
+
return db_snasphot
|
662
|
+
|
640
663
|
def reconstruct_hierarchy(self, items: list[dict]) -> Optional[dict]:
|
641
664
|
"""
|
642
665
|
Reconstructs a hierarchical dictionary structure from a flat list of dictionaries
|
@@ -657,8 +680,11 @@ class Table:
|
|
657
680
|
children_by_parent_id[parent_id] = []
|
658
681
|
children_by_parent_id[parent_id].append(item)
|
659
682
|
|
660
|
-
|
683
|
+
root_items = [item for item in items if FIELD_STATIKK_PARENT_ID not in item]
|
684
|
+
if not root_items:
|
685
|
+
return None
|
661
686
|
|
687
|
+
root_item = root_items[0]
|
662
688
|
processed_root = self._process_item(root_item, items_by_id, children_by_parent_id)
|
663
689
|
return processed_root
|
664
690
|
|
@@ -732,6 +758,7 @@ class BatchWriteContext:
|
|
732
758
|
self._table = app
|
733
759
|
self._put_items: List[DatabaseModel] = []
|
734
760
|
self._delete_items: List[DatabaseModel] = []
|
761
|
+
self._delete_keys: List[dict[str, Any]] = []
|
735
762
|
|
736
763
|
def put(self, item: DatabaseModel):
|
737
764
|
self._put_items.append(item)
|
@@ -739,8 +766,11 @@ class BatchWriteContext:
|
|
739
766
|
def delete(self, item: DatabaseModel):
|
740
767
|
self._delete_items.append(item)
|
741
768
|
|
769
|
+
def delete_by_key(self, key: dict[str, Any]):
|
770
|
+
self._delete_keys.append(key)
|
771
|
+
|
742
772
|
def __enter__(self):
|
743
773
|
return self
|
744
774
|
|
745
775
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
746
|
-
self._table._perform_batch_write(self._put_items, self._delete_items)
|
776
|
+
self._table._perform_batch_write(self._put_items, self._delete_items, self._delete_keys)
|
statikk/models.py
CHANGED
@@ -61,36 +61,6 @@ class TreeStructureChange(BaseModel):
|
|
61
61
|
timestamp: datetime = Field(default_factory=datetime.now)
|
62
62
|
|
63
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
|
-
|
94
64
|
class TrackingMixin:
|
95
65
|
_original_hash: int = Field(exclude=True)
|
96
66
|
|
@@ -202,9 +172,7 @@ class DatabaseModel(BaseModel, TrackingMixin):
|
|
202
172
|
_parent_field_name: Optional[str] = None
|
203
173
|
_model_types_in_hierarchy: dict[str, Type[DatabaseModel]] = {}
|
204
174
|
_should_delete: bool = False
|
205
|
-
|
206
|
-
_is_persisted: bool = False
|
207
|
-
_session = Session()
|
175
|
+
_db_snapshot_keys = set()
|
208
176
|
|
209
177
|
class Config:
|
210
178
|
extra = Extra.allow
|
@@ -215,34 +183,10 @@ class DatabaseModel(BaseModel, TrackingMixin):
|
|
215
183
|
return False
|
216
184
|
return self.id == other.id
|
217
185
|
|
218
|
-
def is_parent_changed(self):
|
219
|
-
"""
|
220
|
-
Recursively check if this node or any of its ancestors has been marked as having changed parents.
|
221
|
-
Returns True if the node or any ancestor has _parent_changed=True, otherwise False.
|
222
|
-
"""
|
223
|
-
if self._parent_changed:
|
224
|
-
return True
|
225
|
-
|
226
|
-
if self._parent is not None:
|
227
|
-
return self._parent.is_parent_changed()
|
228
|
-
|
229
|
-
return False
|
230
|
-
|
231
186
|
@classmethod
|
232
187
|
def type(cls) -> str:
|
233
188
|
return cls.__name__
|
234
189
|
|
235
|
-
@property
|
236
|
-
def should_track_session(self) -> bool:
|
237
|
-
"""
|
238
|
-
If set to True, subtree movements across the database model will be tracked in a session.
|
239
|
-
Use this if you need to move a subtree across multiple parent-child relationships within a single session.
|
240
|
-
Session is reset after each save.
|
241
|
-
"""
|
242
|
-
if self._parent is not None:
|
243
|
-
return self._parent.should_track_session
|
244
|
-
return False
|
245
|
-
|
246
190
|
@classmethod
|
247
191
|
def index_definitions(cls) -> dict[str, IndexFieldConfig]:
|
248
192
|
return {"main_index": IndexFieldConfig(pk_fields=[], sk_fields=[])}
|
@@ -263,16 +207,11 @@ class DatabaseModel(BaseModel, TrackingMixin):
|
|
263
207
|
def is_simple_object(self) -> bool:
|
264
208
|
return len(self._model_types_in_hierarchy) == 1
|
265
209
|
|
266
|
-
@property
|
267
|
-
def is_persisted(self) -> bool:
|
268
|
-
return self._is_persisted
|
269
|
-
|
270
210
|
@property
|
271
211
|
def should_delete(self) -> bool:
|
272
212
|
if self._is_any_parent_marked_for_deletion():
|
273
213
|
return True
|
274
|
-
|
275
|
-
return self.is_persisted and (self._should_delete or self.is_parent_changed())
|
214
|
+
return self._should_delete
|
276
215
|
|
277
216
|
def _is_any_parent_marked_for_deletion(self) -> bool:
|
278
217
|
current = self._parent
|
@@ -361,22 +300,24 @@ class DatabaseModel(BaseModel, TrackingMixin):
|
|
361
300
|
def _change_parent_to(self, new_parent: DatabaseModel, field_name: str) -> T:
|
362
301
|
return self._table.reparent_subtree(self, new_parent, field_name)
|
363
302
|
|
364
|
-
def _remove_from_parent(self, parent, field_name
|
303
|
+
def _remove_from_parent(self, parent: DatabaseModel, field_name: str):
|
365
304
|
is_optional, inner_type = inspect_optional_field(parent.__class__, field_name)
|
366
305
|
field_type = inner_type if is_optional else parent.model_fields[field_name].annotation
|
367
|
-
field = getattr(
|
306
|
+
field = getattr(parent, field_name)
|
368
307
|
if hasattr(field_type, "__origin__") and field_type.__origin__ == list:
|
369
308
|
if not isinstance(field, list):
|
370
|
-
setattr(
|
371
|
-
field.remove(next(filter(lambda item: item.id ==
|
309
|
+
setattr(parent, field_name, [])
|
310
|
+
field.remove(next(filter(lambda item: item.id == self.id, getattr(parent, field_name)), None))
|
372
311
|
|
373
312
|
elif hasattr(field_type, "__origin__") and field_type.__origin__ == set:
|
374
313
|
if not isinstance(field, set):
|
375
|
-
setattr(
|
376
|
-
field.remove(next(filter(lambda item: item.id ==
|
314
|
+
setattr(parent, field_name, set())
|
315
|
+
field.remove(next(filter(lambda item: item.id == self.id, getattr(parent, field_name)), None))
|
377
316
|
|
378
317
|
elif issubclass(field_type, DatabaseModel):
|
379
|
-
|
318
|
+
current_value = getattr(parent, field_name)
|
319
|
+
if current_value and current_value.id == self.id:
|
320
|
+
setattr(parent, field_name, None)
|
380
321
|
|
381
322
|
def add_child_node(self, field_name: str, child_node: DatabaseModel):
|
382
323
|
if not child_node.is_nested():
|
@@ -385,10 +326,8 @@ class DatabaseModel(BaseModel, TrackingMixin):
|
|
385
326
|
if not hasattr(self, field_name):
|
386
327
|
raise ValueError(f"Field {field_name} does not exist on {self.__class__.__name__}")
|
387
328
|
|
388
|
-
if
|
389
|
-
|
390
|
-
if previous_change:
|
391
|
-
self._remove_from_parent(previous_change.new_parent, previous_change.new_parent_field_name, child_node)
|
329
|
+
if child_node._parent:
|
330
|
+
child_node._remove_from_parent(child_node._parent, child_node._parent_field_name)
|
392
331
|
|
393
332
|
is_optional, inner_type = inspect_optional_field(self.__class__, field_name)
|
394
333
|
field_type = inner_type if is_optional else self.model_fields[field_name].annotation
|
@@ -410,8 +349,6 @@ class DatabaseModel(BaseModel, TrackingMixin):
|
|
410
349
|
setattr(self, field_name, reparented)
|
411
350
|
|
412
351
|
if reparented:
|
413
|
-
if self.should_track_session:
|
414
|
-
self._session.add_change(self, field_name, reparented)
|
415
352
|
return reparented
|
416
353
|
|
417
354
|
raise ValueError(f"Unsupported field type: {field_type}")
|
@@ -511,8 +448,6 @@ class DatabaseModel(BaseModel, TrackingMixin):
|
|
511
448
|
return # Already set
|
512
449
|
field._parent = parent
|
513
450
|
field._parent_field_name = field_name
|
514
|
-
if field.should_track_session:
|
515
|
-
field._session.add_change(parent, field_name, field)
|
516
451
|
root._model_types_in_hierarchy[field.type()] = type(field)
|
517
452
|
field.set_parent_references(root, force_override)
|
518
453
|
field.init_tracking()
|
@@ -1,12 +1,12 @@
|
|
1
1
|
statikk/__init__.py,sha256=pH5i4Fj1tbXLqLtTVIdoojiplZssQn0nnud8-HXodRE,577
|
2
2
|
statikk/conditions.py,sha256=63FYMR-UUaE-ZJEb_8CU721CQTwhajq39-BbokmKeMA,2166
|
3
|
-
statikk/engine.py,sha256=
|
3
|
+
statikk/engine.py,sha256=hEI9xQMDMOd7F05-ePki1CZAUynY6RTEu1KstXANluc,31904
|
4
4
|
statikk/expressions.py,sha256=boAeGxZj2cDsXxoiX3IIEzfX9voSMQngi4-rE_jYeuE,12233
|
5
5
|
statikk/fields.py,sha256=uLGD3xEnz2nqQY6EwG8Dd7pnRMMVP4PU2Z2I-uGNaTA,150
|
6
|
-
statikk/models.py,sha256=
|
6
|
+
statikk/models.py,sha256=1qnj2VmJivo4vJtI2UnlK85d9imuh_IiIp3UC1K7Y3I,17207
|
7
7
|
statikk/typing.py,sha256=laOlOpWOm9_sOj4hhdZnGTUZRiq8760_B9I9B3wBhz8,750
|
8
|
-
statikk-0.1.
|
9
|
-
statikk-0.1.
|
10
|
-
statikk-0.1.
|
11
|
-
statikk-0.1.
|
12
|
-
statikk-0.1.
|
8
|
+
statikk-0.1.19.dist-info/licenses/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
|
9
|
+
statikk-0.1.19.dist-info/METADATA,sha256=6A80zHVaRA9Y08gS6Zd62juhMTdWKuhdR7Z97pfPazY,3183
|
10
|
+
statikk-0.1.19.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
11
|
+
statikk-0.1.19.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
|
12
|
+
statikk-0.1.19.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|