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 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."""
@@ -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
- batch.put(item)
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 == FIELD_STATIKK_TYPE:
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 force_override_index_fields):
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, []).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.")
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._set_parent_references(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(self, field: DatabaseModel, parent: DatabaseModel, root: DatabaseModel):
370
- 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:
371
398
  return # Already set
372
399
  field._parent = parent
373
400
  root._model_types_in_hierarchy[field.type()] = type(field)
374
- field._set_parent_references(root)
401
+ field.set_parent_references(root, force_override)
375
402
  field.init_tracking()
376
403
 
377
- 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
+ """
378
415
  for field_name, field_value in self:
379
416
  if isinstance(field_value, DatabaseModel):
380
- self._set_parent_to_field(field_value, self, root)
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._set_parent_to_field(item, self, root)
385
- 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)):
386
442
  for item in field_value:
387
443
  if isinstance(item, DatabaseModel):
388
- self._set_parent_to_field(item, self, root)
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
- 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.6
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=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,,