statikk 0.1.7__py3-none-any.whl → 0.1.9__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 +56 -9
- statikk/expressions.py +1 -1
- statikk/models.py +74 -11
- {statikk-0.1.7.dist-info → statikk-0.1.9.dist-info}/METADATA +1 -1
- statikk-0.1.9.dist-info/RECORD +12 -0
- statikk-0.1.7.dist-info/RECORD +0 -12
- {statikk-0.1.7.dist-info → statikk-0.1.9.dist-info}/LICENSE.txt +0 -0
- {statikk-0.1.7.dist-info → statikk-0.1.9.dist-info}/WHEEL +0 -0
- {statikk-0.1.7.dist-info → statikk-0.1.9.dist-info}/top_level.txt +0 -0
statikk/engine.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import os
|
2
2
|
from datetime import datetime
|
3
|
-
from typing import Any, Dict, Type, Optional, List, Union
|
3
|
+
from typing import Any, Dict, Type, Optional, List, Union, get_type_hints, get_origin, get_args
|
4
4
|
|
5
5
|
import boto3
|
6
6
|
from botocore.config import Config
|
@@ -17,7 +17,7 @@ from statikk.models import (
|
|
17
17
|
KeySchema,
|
18
18
|
)
|
19
19
|
from statikk.fields import FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID
|
20
|
-
|
20
|
+
from copy import deepcopy
|
21
21
|
from aws_xray_sdk.core import patch_all
|
22
22
|
|
23
23
|
patch_all()
|
@@ -58,6 +58,7 @@ class Table:
|
|
58
58
|
for idx in self.indexes:
|
59
59
|
for model in self.models:
|
60
60
|
self._set_index_fields(model, idx)
|
61
|
+
model.model_rebuild(force=True)
|
61
62
|
model.set_table_ref(self)
|
62
63
|
self._client = None
|
63
64
|
self._dynamodb_table = None
|
@@ -162,7 +163,12 @@ class Table:
|
|
162
163
|
)
|
163
164
|
|
164
165
|
def _get_model_type_by_statikk_type(self, statikk_type: str) -> Type[DatabaseModel]:
|
165
|
-
|
166
|
+
model_type_filter = [model_type for model_type in self.models if model_type.type() == statikk_type]
|
167
|
+
if not model_type_filter:
|
168
|
+
raise InvalidModelTypeError(
|
169
|
+
f"Model type '{statikk_type}' not found. Make sure to register it through the models list."
|
170
|
+
)
|
171
|
+
return model_type_filter[0]
|
166
172
|
|
167
173
|
def delete(self):
|
168
174
|
"""Deletes the DynamoDB table."""
|
@@ -235,7 +241,7 @@ class Table:
|
|
235
241
|
|
236
242
|
with self.batch_write() as batch:
|
237
243
|
for item in model.split_to_simple_objects():
|
238
|
-
if item.
|
244
|
+
if item.should_delete:
|
239
245
|
batch.delete(item)
|
240
246
|
else:
|
241
247
|
batch.put(item)
|
@@ -288,11 +294,28 @@ class Table:
|
|
288
294
|
response = self._get_dynamodb_table().update_item(**request)
|
289
295
|
data = response["Attributes"]
|
290
296
|
for key, value in data.items():
|
291
|
-
if key
|
297
|
+
if key in [FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID]:
|
292
298
|
continue
|
293
299
|
data[key] = self._deserialize_value(value, model.model_fields[key])
|
294
300
|
return type(model)(**data)
|
295
301
|
|
302
|
+
def reparent_subtree(self, subtree_root: T, new_parent: T) -> T:
|
303
|
+
subtree_copy = deepcopy(subtree_root)
|
304
|
+
subtree_root._parent_changed = True
|
305
|
+
|
306
|
+
subtree_copy.set_parent_references(subtree_copy, force_override=True)
|
307
|
+
subtree_copy._parent = new_parent
|
308
|
+
for node in subtree_copy.dfs_traverse_hierarchy():
|
309
|
+
for idx in self.indexes:
|
310
|
+
new_index_values = self._compose_index_values(node, idx)
|
311
|
+
new_sort_key_value = new_index_values[idx.sort_key.name]
|
312
|
+
setattr(node, idx.sort_key.name, new_sort_key_value)
|
313
|
+
new_hash_key_value = new_index_values[idx.hash_key.name]
|
314
|
+
setattr(node, idx.hash_key.name, new_hash_key_value)
|
315
|
+
setattr(node, FIELD_STATIKK_PARENT_ID, new_parent.id)
|
316
|
+
|
317
|
+
return subtree_copy
|
318
|
+
|
296
319
|
def batch_write(self):
|
297
320
|
"""
|
298
321
|
Returns a context manager for batch writing items to the database. This method handles all the buffering of the
|
@@ -462,12 +485,11 @@ class Table:
|
|
462
485
|
self,
|
463
486
|
item: DatabaseModel,
|
464
487
|
indexes: List[GSI],
|
465
|
-
force_override_index_fields: bool = False,
|
466
488
|
) -> DatabaseModel:
|
467
489
|
for idx in indexes:
|
468
490
|
index_fields = self._compose_index_values(item, idx)
|
469
491
|
for key, value in index_fields.items():
|
470
|
-
if hasattr(item, key) and (getattr(item, key) is not None and not
|
492
|
+
if hasattr(item, key) and (getattr(item, key) is not None and not item._parent_changed):
|
471
493
|
continue
|
472
494
|
if value is not None:
|
473
495
|
setattr(item, key, value)
|
@@ -558,8 +580,11 @@ class Table:
|
|
558
580
|
return self.delimiter.join(sort_key_values)
|
559
581
|
|
560
582
|
def _compose_index_values(self, model: DatabaseModel, idx: GSI) -> Dict[str, Any]:
|
561
|
-
hash_key_fields = model.index_definitions().get(idx.name,
|
583
|
+
hash_key_fields = model.index_definitions().get(idx.name, None)
|
584
|
+
if hash_key_fields is None:
|
585
|
+
raise IncorrectHashKeyError(f"Model {model.__class__} does not have a hash key defined.")
|
562
586
|
|
587
|
+
hash_key_fields = hash_key_fields.pk_fields
|
563
588
|
if len(hash_key_fields) == 0 and not model.is_nested():
|
564
589
|
raise IncorrectHashKeyError(f"Model {model.__class__} does not have a hash key defined.")
|
565
590
|
|
@@ -598,6 +623,26 @@ class Table:
|
|
598
623
|
data = self._serialize_item(enriched_item)
|
599
624
|
batch.delete_item(Key=data)
|
600
625
|
|
626
|
+
def inspect_optional_field(self, model_class, field_name):
|
627
|
+
field_type = model_class.model_fields[field_name].annotation
|
628
|
+
|
629
|
+
is_optional = False
|
630
|
+
inner_type = field_type
|
631
|
+
|
632
|
+
if get_origin(field_type) is Union:
|
633
|
+
args = get_args(field_type)
|
634
|
+
if len(args) == 2 and args[1] is type(None):
|
635
|
+
is_optional = True
|
636
|
+
inner_type = args[0]
|
637
|
+
|
638
|
+
elif hasattr(field_type, "__origin__") and field_type.__origin__ is Union:
|
639
|
+
args = getattr(field_type, "__args__", [])
|
640
|
+
if len(args) == 2 and args[1] is type(None):
|
641
|
+
is_optional = True
|
642
|
+
inner_type = args[0]
|
643
|
+
|
644
|
+
return (is_optional, inner_type)
|
645
|
+
|
601
646
|
def reconstruct_hierarchy(self, items: list[dict]) -> Optional[dict]:
|
602
647
|
"""
|
603
648
|
Reconstructs a hierarchical dictionary structure from a flat list of dictionaries
|
@@ -679,7 +724,9 @@ class Table:
|
|
679
724
|
if field_name.startswith("_"):
|
680
725
|
continue
|
681
726
|
|
682
|
-
|
727
|
+
is_optional, inner_type = self.inspect_optional_field(parent_model_class, field_name)
|
728
|
+
|
729
|
+
field_type = inner_type if is_optional else field_info.annotation
|
683
730
|
|
684
731
|
if field_type == child_model_class:
|
685
732
|
matching_fields.append((field_name, "single"))
|
statikk/expressions.py
CHANGED
@@ -597,7 +597,7 @@ class UpdateExpressionBuilder:
|
|
597
597
|
|
598
598
|
def _safe_name(self, key):
|
599
599
|
"""Replace reserved words with a safe placeholder."""
|
600
|
-
if key.upper() in RESERVED_WORDS:
|
600
|
+
if key.upper() in RESERVED_WORDS or key.startswith("_"):
|
601
601
|
safe_key = self.safe_name(key)
|
602
602
|
self.expression_attribute_names[safe_key] = key
|
603
603
|
return safe_key
|
statikk/models.py
CHANGED
@@ -163,6 +163,20 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
163
163
|
_parent: Optional[DatabaseModel] = None
|
164
164
|
_model_types_in_hierarchy: dict[str, Type[DatabaseModel]] = {}
|
165
165
|
_should_delete: bool = False
|
166
|
+
_parent_changed: bool = False
|
167
|
+
|
168
|
+
def is_parent_changed(self):
|
169
|
+
"""
|
170
|
+
Recursively check if this node or any of its ancestors has been marked as having changed parents.
|
171
|
+
Returns True if the node or any ancestor has _parent_changed=True, otherwise False.
|
172
|
+
"""
|
173
|
+
if self._parent_changed:
|
174
|
+
return True
|
175
|
+
|
176
|
+
if self._parent is not None:
|
177
|
+
return self._parent.is_parent_changed()
|
178
|
+
|
179
|
+
return False
|
166
180
|
|
167
181
|
@classmethod
|
168
182
|
def type(cls) -> str:
|
@@ -188,6 +202,21 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
188
202
|
def is_simple_object(self) -> bool:
|
189
203
|
return len(self._model_types_in_hierarchy) == 1
|
190
204
|
|
205
|
+
@property
|
206
|
+
def should_delete(self) -> bool:
|
207
|
+
if self._is_any_parent_marked_for_deletion():
|
208
|
+
return True
|
209
|
+
|
210
|
+
return self._should_delete or self.is_parent_changed()
|
211
|
+
|
212
|
+
def _is_any_parent_marked_for_deletion(self) -> bool:
|
213
|
+
current = self._parent
|
214
|
+
while current is not None:
|
215
|
+
if current._should_delete:
|
216
|
+
return True
|
217
|
+
current = current._parent
|
218
|
+
return False
|
219
|
+
|
191
220
|
@classmethod
|
192
221
|
def query(
|
193
222
|
cls: Type[T],
|
@@ -260,6 +289,9 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
260
289
|
def mark_for_delete(self):
|
261
290
|
self._should_delete = True
|
262
291
|
|
292
|
+
def change_parent_to(self, new_parent: DatabaseModel) -> T:
|
293
|
+
return self._table.reparent_subtree(self, new_parent)
|
294
|
+
|
263
295
|
@classmethod
|
264
296
|
def scan(
|
265
297
|
cls,
|
@@ -280,7 +312,7 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
280
312
|
def initialize_tracking(self):
|
281
313
|
self._model_types_in_hierarchy[self.type()] = type(self)
|
282
314
|
if not self.is_nested():
|
283
|
-
self.
|
315
|
+
self.set_parent_references(self)
|
284
316
|
self.init_tracking()
|
285
317
|
|
286
318
|
return self
|
@@ -370,27 +402,58 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
370
402
|
def get_type_from_hierarchy_by_name(self, name: str) -> Optional[Type[DatabaseModel]]:
|
371
403
|
return self._model_types_in_hierarchy.get(name)
|
372
404
|
|
373
|
-
def _set_parent_to_field(
|
374
|
-
|
405
|
+
def _set_parent_to_field(
|
406
|
+
self, field: DatabaseModel, parent: DatabaseModel, root: DatabaseModel, force_override: bool = False
|
407
|
+
):
|
408
|
+
if field._parent and not force_override:
|
375
409
|
return # Already set
|
376
410
|
field._parent = parent
|
377
411
|
root._model_types_in_hierarchy[field.type()] = type(field)
|
378
|
-
field.
|
412
|
+
field.set_parent_references(root, force_override)
|
379
413
|
field.init_tracking()
|
380
414
|
|
381
|
-
def
|
415
|
+
def set_parent_references(self, root: DatabaseModel, force_override: bool = False):
|
416
|
+
"""
|
417
|
+
Sets parent references for all DatabaseModel objects in the hierarchy.
|
418
|
+
"""
|
419
|
+
for parent, field_name, model in self.traverse_hierarchy():
|
420
|
+
self._set_parent_to_field(model, parent, root, force_override)
|
421
|
+
|
422
|
+
def traverse_hierarchy(self):
|
423
|
+
"""
|
424
|
+
Traverses the object and yields tuples of (parent, field_name, field_value) for each DatabaseModel found.
|
425
|
+
"""
|
382
426
|
for field_name, field_value in self:
|
383
427
|
if isinstance(field_value, DatabaseModel):
|
384
|
-
self
|
385
|
-
elif isinstance(field_value, list):
|
428
|
+
yield self, field_name, field_value
|
429
|
+
elif isinstance(field_value, (list, set)):
|
386
430
|
for item in field_value:
|
387
431
|
if isinstance(item, DatabaseModel):
|
388
|
-
self
|
389
|
-
elif isinstance(field_value,
|
432
|
+
yield self, field_name, item
|
433
|
+
elif isinstance(field_value, dict):
|
434
|
+
for key, value in field_value.items():
|
435
|
+
if isinstance(value, DatabaseModel):
|
436
|
+
yield self, key, value
|
437
|
+
|
438
|
+
def dfs_traverse_hierarchy(self):
|
439
|
+
"""
|
440
|
+
Performs a depth-first traversal of the entire object hierarchy,
|
441
|
+
yielding each DatabaseModel in order from root to leaves.
|
442
|
+
"""
|
443
|
+
yield self
|
444
|
+
|
445
|
+
fields = []
|
446
|
+
for field_name, field_value in self:
|
447
|
+
fields.append((field_name, field_value))
|
448
|
+
|
449
|
+
for field_name, field_value in fields:
|
450
|
+
if isinstance(field_value, DatabaseModel):
|
451
|
+
yield from field_value.dfs_traverse_hierarchy()
|
452
|
+
elif isinstance(field_value, (list, set)):
|
390
453
|
for item in field_value:
|
391
454
|
if isinstance(item, DatabaseModel):
|
392
|
-
|
455
|
+
yield from item.dfs_traverse_hierarchy()
|
393
456
|
elif isinstance(field_value, dict):
|
394
457
|
for key, value in field_value.items():
|
395
458
|
if isinstance(value, DatabaseModel):
|
396
|
-
|
459
|
+
yield from value.dfs_traverse_hierarchy()
|
@@ -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=0jF5pUZJtr_lBZMEZXd7PGDerr26CLZUh91gy15Leac,33252
|
4
|
+
statikk/expressions.py,sha256=boAeGxZj2cDsXxoiX3IIEzfX9voSMQngi4-rE_jYeuE,12233
|
5
|
+
statikk/fields.py,sha256=LkMP5NnX7WS0HSLxI3Q-dMOrfaJ0SD7SayZxJU5Acgg,86
|
6
|
+
statikk/models.py,sha256=gzy_MV3LVsVlorqHencXyS7HCVV2QQ698aWK5bQIdFk,16115
|
7
|
+
statikk/typing.py,sha256=qfpegORcdODuILK3gvuD4SdcZA1a7Myn0yvscOLPHOM,68
|
8
|
+
statikk-0.1.9.dist-info/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
|
9
|
+
statikk-0.1.9.dist-info/METADATA,sha256=ZvTT-xMgID_v5WgHYz13E_JLHtqflVz9naDu9Qh-9rA,3160
|
10
|
+
statikk-0.1.9.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
11
|
+
statikk-0.1.9.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
|
12
|
+
statikk-0.1.9.dist-info/RECORD,,
|
statikk-0.1.7.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=_iUJYyKNypqQeg6f4MC45XWhOrwsYRsI8FZe0Hs3fDA,31067
|
4
|
-
statikk/expressions.py,sha256=mF6Hmj3Kmj6KKXTymeTHSepVA7rhiSINpFgSAPeBTRY,12210
|
5
|
-
statikk/fields.py,sha256=LkMP5NnX7WS0HSLxI3Q-dMOrfaJ0SD7SayZxJU5Acgg,86
|
6
|
-
statikk/models.py,sha256=uGCfpmoagz98BlTWiyj8NFvd7PTcsj1XV2edTumxAqY,13862
|
7
|
-
statikk/typing.py,sha256=qfpegORcdODuILK3gvuD4SdcZA1a7Myn0yvscOLPHOM,68
|
8
|
-
statikk-0.1.7.dist-info/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
|
9
|
-
statikk-0.1.7.dist-info/METADATA,sha256=YiDH3MeMZuJypLtkjGb-prE-Aqe15kbKBDT9cAyBRmM,3160
|
10
|
-
statikk-0.1.7.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
11
|
-
statikk-0.1.7.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
|
12
|
-
statikk-0.1.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|