statikk 0.1.6__py3-none-any.whl → 0.1.8__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 +36 -7
- statikk/expressions.py +1 -1
- statikk/models.py +67 -11
- {statikk-0.1.6.dist-info → statikk-0.1.8.dist-info}/METADATA +1 -1
- statikk-0.1.8.dist-info/RECORD +12 -0
- statikk-0.1.6.dist-info/RECORD +0 -12
- {statikk-0.1.6.dist-info → statikk-0.1.8.dist-info}/LICENSE.txt +0 -0
- {statikk-0.1.6.dist-info → statikk-0.1.8.dist-info}/WHEEL +0 -0
- {statikk-0.1.6.dist-info → statikk-0.1.8.dist-info}/top_level.txt +0 -0
statikk/engine.py
CHANGED
@@ -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."""
|
@@ -232,9 +238,13 @@ class Table:
|
|
232
238
|
|
233
239
|
Returns the enriched database model instance.
|
234
240
|
"""
|
241
|
+
|
235
242
|
with self.batch_write() as batch:
|
236
243
|
for item in model.split_to_simple_objects():
|
237
|
-
|
244
|
+
if item.should_delete:
|
245
|
+
batch.delete(item)
|
246
|
+
else:
|
247
|
+
batch.put(item)
|
238
248
|
|
239
249
|
def update_item(
|
240
250
|
self,
|
@@ -284,11 +294,28 @@ class Table:
|
|
284
294
|
response = self._get_dynamodb_table().update_item(**request)
|
285
295
|
data = response["Attributes"]
|
286
296
|
for key, value in data.items():
|
287
|
-
if key
|
297
|
+
if key in [FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID]:
|
288
298
|
continue
|
289
299
|
data[key] = self._deserialize_value(value, model.model_fields[key])
|
290
300
|
return type(model)(**data)
|
291
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
|
+
|
292
319
|
def batch_write(self):
|
293
320
|
"""
|
294
321
|
Returns a context manager for batch writing items to the database. This method handles all the buffering of the
|
@@ -458,12 +485,11 @@ class Table:
|
|
458
485
|
self,
|
459
486
|
item: DatabaseModel,
|
460
487
|
indexes: List[GSI],
|
461
|
-
force_override_index_fields: bool = False,
|
462
488
|
) -> DatabaseModel:
|
463
489
|
for idx in indexes:
|
464
490
|
index_fields = self._compose_index_values(item, idx)
|
465
491
|
for key, value in index_fields.items():
|
466
|
-
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):
|
467
493
|
continue
|
468
494
|
if value is not None:
|
469
495
|
setattr(item, key, value)
|
@@ -554,8 +580,11 @@ class Table:
|
|
554
580
|
return self.delimiter.join(sort_key_values)
|
555
581
|
|
556
582
|
def _compose_index_values(self, model: DatabaseModel, idx: GSI) -> Dict[str, Any]:
|
557
|
-
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.")
|
558
586
|
|
587
|
+
hash_key_fields = hash_key_fields.pk_fields
|
559
588
|
if len(hash_key_fields) == 0 and not model.is_nested():
|
560
589
|
raise IncorrectHashKeyError(f"Model {model.__class__} does not have a hash key defined.")
|
561
590
|
|
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
@@ -162,6 +162,21 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
162
162
|
id: str = Field(default_factory=lambda: str(uuid4()))
|
163
163
|
_parent: Optional[DatabaseModel] = None
|
164
164
|
_model_types_in_hierarchy: dict[str, Type[DatabaseModel]] = {}
|
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
|
165
180
|
|
166
181
|
@classmethod
|
167
182
|
def type(cls) -> str:
|
@@ -187,6 +202,10 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
187
202
|
def is_simple_object(self) -> bool:
|
188
203
|
return len(self._model_types_in_hierarchy) == 1
|
189
204
|
|
205
|
+
@property
|
206
|
+
def should_delete(self) -> bool:
|
207
|
+
return self._should_delete or self.is_parent_changed()
|
208
|
+
|
190
209
|
@classmethod
|
191
210
|
def query(
|
192
211
|
cls: Type[T],
|
@@ -256,6 +275,12 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
256
275
|
return self._parent.should_write_to_database()
|
257
276
|
return True
|
258
277
|
|
278
|
+
def mark_for_delete(self):
|
279
|
+
self._should_delete = True
|
280
|
+
|
281
|
+
def change_parent_to(self, new_parent: DatabaseModel) -> T:
|
282
|
+
return self._table.reparent_subtree(self, new_parent)
|
283
|
+
|
259
284
|
@classmethod
|
260
285
|
def scan(
|
261
286
|
cls,
|
@@ -276,7 +301,7 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
276
301
|
def initialize_tracking(self):
|
277
302
|
self._model_types_in_hierarchy[self.type()] = type(self)
|
278
303
|
if not self.is_nested():
|
279
|
-
self.
|
304
|
+
self.set_parent_references(self)
|
280
305
|
self.init_tracking()
|
281
306
|
|
282
307
|
return self
|
@@ -366,27 +391,58 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
|
366
391
|
def get_type_from_hierarchy_by_name(self, name: str) -> Optional[Type[DatabaseModel]]:
|
367
392
|
return self._model_types_in_hierarchy.get(name)
|
368
393
|
|
369
|
-
def _set_parent_to_field(
|
370
|
-
|
394
|
+
def _set_parent_to_field(
|
395
|
+
self, field: DatabaseModel, parent: DatabaseModel, root: DatabaseModel, force_override: bool = False
|
396
|
+
):
|
397
|
+
if field._parent and not force_override:
|
371
398
|
return # Already set
|
372
399
|
field._parent = parent
|
373
400
|
root._model_types_in_hierarchy[field.type()] = type(field)
|
374
|
-
field.
|
401
|
+
field.set_parent_references(root, force_override)
|
375
402
|
field.init_tracking()
|
376
403
|
|
377
|
-
def
|
404
|
+
def set_parent_references(self, root: DatabaseModel, force_override: bool = False):
|
405
|
+
"""
|
406
|
+
Sets parent references for all DatabaseModel objects in the hierarchy.
|
407
|
+
"""
|
408
|
+
for parent, field_name, model in self.traverse_hierarchy():
|
409
|
+
self._set_parent_to_field(model, parent, root, force_override)
|
410
|
+
|
411
|
+
def traverse_hierarchy(self):
|
412
|
+
"""
|
413
|
+
Traverses the object and yields tuples of (parent, field_name, field_value) for each DatabaseModel found.
|
414
|
+
"""
|
378
415
|
for field_name, field_value in self:
|
379
416
|
if isinstance(field_value, DatabaseModel):
|
380
|
-
self
|
381
|
-
elif isinstance(field_value, list):
|
417
|
+
yield self, field_name, field_value
|
418
|
+
elif isinstance(field_value, (list, set)):
|
382
419
|
for item in field_value:
|
383
420
|
if isinstance(item, DatabaseModel):
|
384
|
-
self
|
385
|
-
elif isinstance(field_value,
|
421
|
+
yield self, field_name, item
|
422
|
+
elif isinstance(field_value, dict):
|
423
|
+
for key, value in field_value.items():
|
424
|
+
if isinstance(value, DatabaseModel):
|
425
|
+
yield self, key, value
|
426
|
+
|
427
|
+
def dfs_traverse_hierarchy(self):
|
428
|
+
"""
|
429
|
+
Performs a depth-first traversal of the entire object hierarchy,
|
430
|
+
yielding each DatabaseModel in order from root to leaves.
|
431
|
+
"""
|
432
|
+
yield self
|
433
|
+
|
434
|
+
fields = []
|
435
|
+
for field_name, field_value in self:
|
436
|
+
fields.append((field_name, field_value))
|
437
|
+
|
438
|
+
for field_name, field_value in fields:
|
439
|
+
if isinstance(field_value, DatabaseModel):
|
440
|
+
yield from field_value.dfs_traverse_hierarchy()
|
441
|
+
elif isinstance(field_value, (list, set)):
|
386
442
|
for item in field_value:
|
387
443
|
if isinstance(item, DatabaseModel):
|
388
|
-
|
444
|
+
yield from item.dfs_traverse_hierarchy()
|
389
445
|
elif isinstance(field_value, dict):
|
390
446
|
for key, value in field_value.items():
|
391
447
|
if isinstance(value, DatabaseModel):
|
392
|
-
|
448
|
+
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=x3TB4iMXSpLudwv0dRBGFapttbc8etg36SA_tSq0tGY,32362
|
4
|
+
statikk/expressions.py,sha256=boAeGxZj2cDsXxoiX3IIEzfX9voSMQngi4-rE_jYeuE,12233
|
5
|
+
statikk/fields.py,sha256=LkMP5NnX7WS0HSLxI3Q-dMOrfaJ0SD7SayZxJU5Acgg,86
|
6
|
+
statikk/models.py,sha256=X2MRFn3TAmKdxUQ9KDO9oNoekct4F3QBdcB1COtCpDU,15785
|
7
|
+
statikk/typing.py,sha256=qfpegORcdODuILK3gvuD4SdcZA1a7Myn0yvscOLPHOM,68
|
8
|
+
statikk-0.1.8.dist-info/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
|
9
|
+
statikk-0.1.8.dist-info/METADATA,sha256=9TYBs4MKqRMN2TJN6iRyl3g8nWSIG_zBlVnAAUtfClc,3160
|
10
|
+
statikk-0.1.8.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
11
|
+
statikk-0.1.8.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
|
12
|
+
statikk-0.1.8.dist-info/RECORD,,
|
statikk-0.1.6.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=U2STo-d8Rf6jiPtIdVgGdZ11G6xYjn-yehFdeCb7ZO8,30961
|
4
|
-
statikk/expressions.py,sha256=mF6Hmj3Kmj6KKXTymeTHSepVA7rhiSINpFgSAPeBTRY,12210
|
5
|
-
statikk/fields.py,sha256=LkMP5NnX7WS0HSLxI3Q-dMOrfaJ0SD7SayZxJU5Acgg,86
|
6
|
-
statikk/models.py,sha256=jc6Ta0-jSwE5mAMd2703EyKVKrG6VGqLStpYCCxYaKs,13762
|
7
|
-
statikk/typing.py,sha256=qfpegORcdODuILK3gvuD4SdcZA1a7Myn0yvscOLPHOM,68
|
8
|
-
statikk-0.1.6.dist-info/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
|
9
|
-
statikk-0.1.6.dist-info/METADATA,sha256=K99ceCXlXRr3soKTtKxLEXUQKspay_LyNhYcgFUdJc0,3160
|
10
|
-
statikk-0.1.6.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
11
|
-
statikk-0.1.6.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
|
12
|
-
statikk-0.1.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|