statikk 0.1.18__py3-none-any.whl → 0.1.20__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 +46 -16
- statikk/models.py +13 -78
- {statikk-0.1.18.dist-info → statikk-0.1.20.dist-info}/METADATA +1 -1
- {statikk-0.1.18.dist-info → statikk-0.1.20.dist-info}/RECORD +7 -7
- {statikk-0.1.18.dist-info → statikk-0.1.20.dist-info}/WHEEL +0 -0
- {statikk-0.1.18.dist-info → statikk-0.1.20.dist-info}/licenses/LICENSE.txt +0 -0
- {statikk-0.1.18.dist-info → statikk-0.1.20.dist-info}/top_level.txt +0 -0
statikk/engine.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import os
|
2
|
+
import decimal
|
2
3
|
from datetime import datetime
|
3
4
|
from typing import Any, Dict, Type, Optional, List, Union
|
4
5
|
|
@@ -240,13 +241,24 @@ class Table:
|
|
240
241
|
"""
|
241
242
|
|
242
243
|
with self.batch_write() as batch:
|
243
|
-
|
244
|
+
items = model.split_to_simple_objects()
|
245
|
+
new_keys = self._create_snapshot_representation(model)
|
246
|
+
keys_to_delete = model._db_snapshot_keys - new_keys
|
247
|
+
|
248
|
+
for key in keys_to_delete:
|
249
|
+
hash_key, sort_key = key.split("#", 1)
|
250
|
+
delete_params = {self.key_schema.hash_key: hash_key}
|
251
|
+
if sort_key:
|
252
|
+
delete_params[self.key_schema.sort_key] = sort_key
|
253
|
+
batch.delete_by_key(delete_params)
|
254
|
+
|
255
|
+
for item in items:
|
244
256
|
if item.should_delete:
|
245
257
|
batch.delete(item)
|
246
258
|
else:
|
247
259
|
batch.put(item)
|
248
260
|
|
249
|
-
model.
|
261
|
+
model._db_snapshot_keys = new_keys
|
250
262
|
|
251
263
|
def update_item(
|
252
264
|
self,
|
@@ -302,20 +314,17 @@ class Table:
|
|
302
314
|
return type(model)(**data)
|
303
315
|
|
304
316
|
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
|
317
|
+
subtree_root._parent = new_parent
|
318
|
+
subtree_root._parent_field_name = field_name
|
319
|
+
subtree_root.set_parent_references(subtree_root, force_override=True)
|
311
320
|
parent = None
|
312
|
-
for node in
|
321
|
+
for node in subtree_root.dfs_traverse_hierarchy():
|
313
322
|
self.build_model_indexes(node)
|
314
323
|
if parent:
|
315
324
|
setattr(node, FIELD_STATIKK_PARENT_ID, parent.id)
|
316
325
|
parent = node
|
317
326
|
|
318
|
-
return
|
327
|
+
return subtree_root
|
319
328
|
|
320
329
|
def build_model_indexes(self, model: T) -> T:
|
321
330
|
for idx in self.indexes:
|
@@ -432,8 +441,7 @@ class Table:
|
|
432
441
|
|
433
442
|
reconstructed_dict.pop(FIELD_STATIKK_TYPE, None)
|
434
443
|
model = model_class.model_validate(reconstructed_dict)
|
435
|
-
|
436
|
-
node._is_persisted = True
|
444
|
+
model._db_snapshot_keys = self._create_snapshot_representation(model)
|
437
445
|
return model
|
438
446
|
|
439
447
|
def scan(
|
@@ -502,7 +510,7 @@ class Table:
|
|
502
510
|
for idx in indexes:
|
503
511
|
index_fields = self._compose_index_values(item, idx)
|
504
512
|
for key, value in index_fields.items():
|
505
|
-
if hasattr(item, key) and (getattr(item, key) is not None
|
513
|
+
if hasattr(item, key) and (getattr(item, key) is not None):
|
506
514
|
continue
|
507
515
|
if value is not None:
|
508
516
|
setattr(item, key, value)
|
@@ -542,6 +550,8 @@ class Table:
|
|
542
550
|
return {self._deserialize_value(item, item_annotation) for item in value}
|
543
551
|
if isinstance(value, dict):
|
544
552
|
return {key: self._deserialize_value(item, annotation) for key, item in value.items() if item is not None}
|
553
|
+
if isinstance(value, decimal.Decimal):
|
554
|
+
return float(value)
|
545
555
|
return value
|
546
556
|
|
547
557
|
def _serialize_value(self, value: Any):
|
@@ -612,7 +622,9 @@ class Table:
|
|
612
622
|
idx.sort_key.name: self._get_sort_key_value(model, idx),
|
613
623
|
}
|
614
624
|
|
615
|
-
def _perform_batch_write(
|
625
|
+
def _perform_batch_write(
|
626
|
+
self, put_items: List[DatabaseModel], delete_items: List[DatabaseModel], delete_keys: list[dict[str, Any]]
|
627
|
+
):
|
616
628
|
if len(put_items) == 0 and len(delete_items) == 0:
|
617
629
|
return
|
618
630
|
|
@@ -625,6 +637,11 @@ class Table:
|
|
625
637
|
data = self._serialize_item(enriched_item)
|
626
638
|
batch.delete_item(Key=data)
|
627
639
|
|
640
|
+
if len(delete_keys) > 0:
|
641
|
+
with dynamodb_table.batch_writer() as batch:
|
642
|
+
for key in delete_keys:
|
643
|
+
batch.delete_item(Key=key)
|
644
|
+
|
628
645
|
if len(put_items) > 0:
|
629
646
|
with dynamodb_table.batch_writer() as batch:
|
630
647
|
for item in put_items:
|
@@ -633,10 +650,19 @@ class Table:
|
|
633
650
|
continue
|
634
651
|
if not enriched_item.should_write_to_database():
|
635
652
|
continue
|
636
|
-
enriched_item._is_persisted = True
|
637
653
|
data = self._serialize_item(enriched_item)
|
638
654
|
batch.put_item(Item=data)
|
639
655
|
|
656
|
+
def _create_snapshot_representation(self, model: DatabaseModel) -> set:
|
657
|
+
db_snasphot = set()
|
658
|
+
for node in model.dfs_traverse_hierarchy():
|
659
|
+
model_key = {self.key_schema.hash_key: getattr(node, self.key_schema.hash_key)}
|
660
|
+
if self.key_schema.sort_key:
|
661
|
+
model_key[self.key_schema.sort_key] = getattr(node, self.key_schema.sort_key)
|
662
|
+
key_string = f"{model_key.get(self.key_schema.hash_key)}#{model_key.get(self.key_schema.sort_key, '')}"
|
663
|
+
db_snasphot.add(key_string)
|
664
|
+
return db_snasphot
|
665
|
+
|
640
666
|
def reconstruct_hierarchy(self, items: list[dict]) -> Optional[dict]:
|
641
667
|
"""
|
642
668
|
Reconstructs a hierarchical dictionary structure from a flat list of dictionaries
|
@@ -735,6 +761,7 @@ class BatchWriteContext:
|
|
735
761
|
self._table = app
|
736
762
|
self._put_items: List[DatabaseModel] = []
|
737
763
|
self._delete_items: List[DatabaseModel] = []
|
764
|
+
self._delete_keys: List[dict[str, Any]] = []
|
738
765
|
|
739
766
|
def put(self, item: DatabaseModel):
|
740
767
|
self._put_items.append(item)
|
@@ -742,8 +769,11 @@ class BatchWriteContext:
|
|
742
769
|
def delete(self, item: DatabaseModel):
|
743
770
|
self._delete_items.append(item)
|
744
771
|
|
772
|
+
def delete_by_key(self, key: dict[str, Any]):
|
773
|
+
self._delete_keys.append(key)
|
774
|
+
|
745
775
|
def __enter__(self):
|
746
776
|
return self
|
747
777
|
|
748
778
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
749
|
-
self._table._perform_batch_write(self._put_items, self._delete_items)
|
779
|
+
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=bcUJfI9G1Nwe86NIrCwkB_Ni_fxdc_KBqa1ny2wJBzE,31998
|
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.20.dist-info/licenses/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
|
9
|
+
statikk-0.1.20.dist-info/METADATA,sha256=GOv6D1HUYYGNbs-5UgWLS7C1wahrMZ7L39r4Gzni9T4,3183
|
10
|
+
statikk-0.1.20.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
11
|
+
statikk-0.1.20.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
|
12
|
+
statikk-0.1.20.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|