statikk 0.1.7__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 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
- return [model_type for model_type in self.models if model_type.type() == statikk_type][0]
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._should_delete:
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 == FIELD_STATIKK_TYPE:
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 force_override_index_fields):
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, []).pk_fields
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
 
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,10 @@ 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
+ return self._should_delete or self.is_parent_changed()
208
+
191
209
  @classmethod
192
210
  def query(
193
211
  cls: Type[T],
@@ -260,6 +278,9 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
260
278
  def mark_for_delete(self):
261
279
  self._should_delete = True
262
280
 
281
+ def change_parent_to(self, new_parent: DatabaseModel) -> T:
282
+ return self._table.reparent_subtree(self, new_parent)
283
+
263
284
  @classmethod
264
285
  def scan(
265
286
  cls,
@@ -280,7 +301,7 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
280
301
  def initialize_tracking(self):
281
302
  self._model_types_in_hierarchy[self.type()] = type(self)
282
303
  if not self.is_nested():
283
- self._set_parent_references(self)
304
+ self.set_parent_references(self)
284
305
  self.init_tracking()
285
306
 
286
307
  return self
@@ -370,27 +391,58 @@ class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
370
391
  def get_type_from_hierarchy_by_name(self, name: str) -> Optional[Type[DatabaseModel]]:
371
392
  return self._model_types_in_hierarchy.get(name)
372
393
 
373
- def _set_parent_to_field(self, field: DatabaseModel, parent: DatabaseModel, root: DatabaseModel):
374
- if field._parent:
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:
375
398
  return # Already set
376
399
  field._parent = parent
377
400
  root._model_types_in_hierarchy[field.type()] = type(field)
378
- field._set_parent_references(root)
401
+ field.set_parent_references(root, force_override)
379
402
  field.init_tracking()
380
403
 
381
- def _set_parent_references(self, root: DatabaseModel):
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
+ """
382
415
  for field_name, field_value in self:
383
416
  if isinstance(field_value, DatabaseModel):
384
- self._set_parent_to_field(field_value, self, root)
385
- elif isinstance(field_value, list):
417
+ yield self, field_name, field_value
418
+ elif isinstance(field_value, (list, set)):
386
419
  for item in field_value:
387
420
  if isinstance(item, DatabaseModel):
388
- self._set_parent_to_field(item, self, root)
389
- elif isinstance(field_value, set):
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)):
390
442
  for item in field_value:
391
443
  if isinstance(item, DatabaseModel):
392
- self._set_parent_to_field(item, self, root)
444
+ yield from item.dfs_traverse_hierarchy()
393
445
  elif isinstance(field_value, dict):
394
446
  for key, value in field_value.items():
395
447
  if isinstance(value, DatabaseModel):
396
- self._set_parent_to_field(value, self, root)
448
+ yield from value.dfs_traverse_hierarchy()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: statikk
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: statikk is a single table application (STA) library for DynamoDb.
5
5
  Home-page: https://github.com/terinia/statikk
6
6
  Author: Balint Biro
@@ -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,,
@@ -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,,