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 +2 -0
- statikk/models.py +89 -18
- {statikk-0.1.13.dist-info → statikk-0.1.14.dist-info}/METADATA +1 -1
- statikk-0.1.14.dist-info/RECORD +12 -0
- statikk-0.1.13.dist-info/RECORD +0 -12
- {statikk-0.1.13.dist-info → statikk-0.1.14.dist-info}/WHEEL +0 -0
- {statikk-0.1.13.dist-info → statikk-0.1.14.dist-info}/licenses/LICENSE.txt +0 -0
- {statikk-0.1.13.dist-info → statikk-0.1.14.dist-info}/top_level.txt +0 -0
statikk/engine.py
CHANGED
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,
|
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
|
"""
|
@@ -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,,
|
statikk-0.1.13.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|