statikk 0.0.13__py3-none-any.whl → 0.1.1__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/conditions.py +3 -2
- statikk/engine.py +294 -85
- statikk/fields.py +2 -0
- statikk/models.py +249 -77
- {statikk-0.0.13.dist-info → statikk-0.1.1.dist-info}/METADATA +10 -8
- statikk-0.1.1.dist-info/RECORD +11 -0
- {statikk-0.0.13.dist-info → statikk-0.1.1.dist-info}/WHEEL +1 -1
- statikk-0.0.13.dist-info/RECORD +0 -10
- {statikk-0.0.13.dist-info → statikk-0.1.1.dist-info}/LICENSE.txt +0 -0
- {statikk-0.0.13.dist-info → statikk-0.1.1.dist-info}/top_level.txt +0 -0
statikk/conditions.py
CHANGED
@@ -10,6 +10,7 @@ Example usage of this module:
|
|
10
10
|
from statikk.conditions import Equals, BeginsWith
|
11
11
|
app.query(range_key=Equals("123"), hash_key=BeginsWith("abc"))
|
12
12
|
"""
|
13
|
+
|
13
14
|
from abc import ABC, abstractmethod
|
14
15
|
from boto3.dynamodb.conditions import Key, ComparisonCondition
|
15
16
|
from typing import Any
|
@@ -37,8 +38,8 @@ class BeginsWith(Condition):
|
|
37
38
|
return Key(key).begins_with(self.value)
|
38
39
|
|
39
40
|
def enrich(self, model_class, **kwargs):
|
40
|
-
if not self.value.startswith(model_class.
|
41
|
-
self.value = f"{model_class.
|
41
|
+
if not self.value.startswith(model_class.type()):
|
42
|
+
self.value = f"{model_class.type()}|{self.value}"
|
42
43
|
|
43
44
|
|
44
45
|
class LessThan(Condition):
|
statikk/engine.py
CHANGED
@@ -13,17 +13,15 @@ from statikk.expressions import UpdateExpressionBuilder
|
|
13
13
|
from statikk.models import (
|
14
14
|
DatabaseModel,
|
15
15
|
GSI,
|
16
|
-
Index,
|
17
|
-
IndexPrimaryKeyField,
|
18
|
-
IndexSecondaryKeyField,
|
19
16
|
KeySchema,
|
20
17
|
)
|
18
|
+
from statikk.fields import FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID
|
21
19
|
|
22
|
-
from aws_xray_sdk.core import xray_recorder
|
23
20
|
from aws_xray_sdk.core import patch_all
|
24
21
|
|
25
22
|
patch_all()
|
26
23
|
|
24
|
+
|
27
25
|
class InvalidIndexNameError(Exception):
|
28
26
|
pass
|
29
27
|
|
@@ -32,6 +30,10 @@ class IncorrectSortKeyError(Exception):
|
|
32
30
|
pass
|
33
31
|
|
34
32
|
|
33
|
+
class IncorrectHashKeyError(Exception):
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
35
37
|
class ItemNotFoundError(Exception):
|
36
38
|
pass
|
37
39
|
|
@@ -56,8 +58,6 @@ class Table:
|
|
56
58
|
for model in self.models:
|
57
59
|
self._set_index_fields(model, idx)
|
58
60
|
model.set_table_ref(self)
|
59
|
-
if "type" not in model.model_fields:
|
60
|
-
model.model_fields["type"] = FieldInfo(annotation=str, default=model.model_type(), required=False)
|
61
61
|
self._client = None
|
62
62
|
self._dynamodb_table = None
|
63
63
|
|
@@ -160,6 +160,9 @@ class Table:
|
|
160
160
|
BillingMode=self.billing_mode,
|
161
161
|
)
|
162
162
|
|
163
|
+
def _get_model_type_by_statikk_type(self, statikk_type: str) -> Type[DatabaseModel]:
|
164
|
+
return [model_type for model_type in self.models if model_type.type() == statikk_type][0]
|
165
|
+
|
163
166
|
def delete(self):
|
164
167
|
"""Deletes the DynamoDB table."""
|
165
168
|
self._dynamodb_client().delete_table(TableName=self.name)
|
@@ -185,18 +188,21 @@ class Table:
|
|
185
188
|
raise ItemNotFoundError(f"{model_class} with id '{id}' not found.")
|
186
189
|
data = raw_data["Item"]
|
187
190
|
for key, value in data.items():
|
191
|
+
if key == FIELD_STATIKK_TYPE:
|
192
|
+
continue
|
188
193
|
data[key] = self._deserialize_value(value, model_class.model_fields[key])
|
189
194
|
return model_class(**data)
|
190
195
|
|
191
|
-
def delete_item(self,
|
196
|
+
def delete_item(self, model: DatabaseModel):
|
192
197
|
"""
|
193
198
|
Deletes an item from the database by id, using the partition key of the table.
|
194
199
|
:param id: The id of the item to delete.
|
195
200
|
"""
|
196
|
-
|
197
|
-
|
201
|
+
with self.batch_write() as batch:
|
202
|
+
for item in model.split_to_simple_objects():
|
203
|
+
batch.delete(item)
|
198
204
|
|
199
|
-
def put_item(self, model: DatabaseModel)
|
205
|
+
def put_item(self, model: DatabaseModel):
|
200
206
|
"""
|
201
207
|
Puts an item into the database.
|
202
208
|
|
@@ -207,7 +213,7 @@ class Table:
|
|
207
213
|
always prefixed with the type of the model to avoid collisions between different model types.
|
208
214
|
|
209
215
|
Example:
|
210
|
-
class Card(
|
216
|
+
class Card(DatabaseModelV2):
|
211
217
|
id: str
|
212
218
|
player_id: IndexPrimaryKeyField
|
213
219
|
type: IndexSecondaryKeyField
|
@@ -225,11 +231,9 @@ class Table:
|
|
225
231
|
|
226
232
|
Returns the enriched database model instance.
|
227
233
|
"""
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
data[key] = self._deserialize_value(value, model.model_fields[key])
|
232
|
-
return type(model)(**data)
|
234
|
+
with self.batch_write() as batch:
|
235
|
+
for item in model.split_to_simple_objects():
|
236
|
+
batch.put(item)
|
233
237
|
|
234
238
|
def update_item(
|
235
239
|
self,
|
@@ -252,11 +256,11 @@ class Table:
|
|
252
256
|
for prefixed_attribute, value in expression_attribute_values.items():
|
253
257
|
expression_attribute_values[prefixed_attribute] = self._serialize_value(value)
|
254
258
|
attribute = prefixed_attribute.replace(":", "")
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
259
|
+
for index_name, index_fields in model.index_definitions().items():
|
260
|
+
if attribute in index_fields.sk_fields:
|
261
|
+
setattr(model, attribute, value)
|
262
|
+
changed_index_values.add(index_name)
|
263
|
+
|
260
264
|
return changed_index_values
|
261
265
|
|
262
266
|
changed_index_values = _find_changed_indexes()
|
@@ -279,6 +283,8 @@ class Table:
|
|
279
283
|
response = self._get_dynamodb_table().update_item(**request)
|
280
284
|
data = response["Attributes"]
|
281
285
|
for key, value in data.items():
|
286
|
+
if key == FIELD_STATIKK_TYPE:
|
287
|
+
continue
|
282
288
|
data[key] = self._deserialize_value(value, model.model_fields[key])
|
283
289
|
return type(model)(**data)
|
284
290
|
|
@@ -289,7 +295,7 @@ class Table:
|
|
289
295
|
"""
|
290
296
|
return BatchWriteContext(self)
|
291
297
|
|
292
|
-
def
|
298
|
+
def _prepare_index_query_params(
|
293
299
|
self,
|
294
300
|
hash_key: Union[Condition | str],
|
295
301
|
model_class: Type[DatabaseModel],
|
@@ -297,17 +303,6 @@ class Table:
|
|
297
303
|
filter_condition: Optional[ComparisonCondition] = None,
|
298
304
|
index_name: Optional[str] = None,
|
299
305
|
):
|
300
|
-
"""
|
301
|
-
Queries the database using the provided hash key and range key conditions. A filter condition can also be provided
|
302
|
-
using the filter_condition parameter. The method returns a list of items matching the query, deserialized into the
|
303
|
-
provided model_class parameter.
|
304
|
-
|
305
|
-
:param hash_key: The hash key condition to use for the query. See statikk.conditions.Condition for more information.
|
306
|
-
:param range_key: The range key condition to use for the query. See statikk.conditions.Condition for more information.
|
307
|
-
:param model_class: The model class to use to deserialize the items.
|
308
|
-
:param filter_condition: An optional filter condition to use for the query. See boto3.dynamodb.conditions.ComparisonCondition for more information.
|
309
|
-
:param index_name: The name of the index to use for the query. If not provided, the first index configured on the table is used.
|
310
|
-
"""
|
311
306
|
if isinstance(hash_key, str):
|
312
307
|
hash_key = Equals(hash_key)
|
313
308
|
if not index_name:
|
@@ -317,28 +312,89 @@ class Table:
|
|
317
312
|
raise InvalidIndexNameError(f"The provided index name '{index_name}' is not configured on the table.")
|
318
313
|
index = index_filter[0]
|
319
314
|
key_condition = hash_key.evaluate(index.hash_key.name)
|
320
|
-
if range_key is None and model_class.
|
321
|
-
range_key = BeginsWith(model_class.
|
315
|
+
if range_key is None and FIELD_STATIKK_TYPE not in model_class.index_definitions()[index_name].pk_fields:
|
316
|
+
range_key = BeginsWith(model_class.type())
|
322
317
|
if range_key:
|
323
|
-
|
318
|
+
if not model_class.is_nested():
|
319
|
+
range_key.enrich(model_class=model_class)
|
324
320
|
key_condition = key_condition & range_key.evaluate(index.sort_key.name)
|
325
321
|
|
326
322
|
query_params = {
|
327
323
|
"IndexName": index_name,
|
328
324
|
"KeyConditionExpression": key_condition,
|
329
325
|
}
|
326
|
+
|
330
327
|
if filter_condition:
|
331
328
|
query_params["FilterExpression"] = filter_condition
|
329
|
+
return query_params
|
330
|
+
|
331
|
+
def query_index(
|
332
|
+
self,
|
333
|
+
hash_key: Union[Condition | str],
|
334
|
+
model_class: Type[DatabaseModel],
|
335
|
+
range_key: Optional[Condition] = None,
|
336
|
+
filter_condition: Optional[ComparisonCondition] = None,
|
337
|
+
index_name: Optional[str] = None,
|
338
|
+
):
|
339
|
+
"""
|
340
|
+
Queries the database using the provided hash key and range key conditions. A filter condition can also be provided
|
341
|
+
using the filter_condition parameter. The method returns a list of items matching the query, deserialized into the
|
342
|
+
provided model_class parameter.
|
343
|
+
|
344
|
+
:param hash_key: The hash key condition to use for the query. See statikk.conditions.Condition for more information.
|
345
|
+
:param range_key: The range key condition to use for the query. See statikk.conditions.Condition for more information.
|
346
|
+
:param model_class: The model class to use to deserialize the items.
|
347
|
+
:param filter_condition: An optional filter condition to use for the query. See boto3.dynamodb.conditions.ComparisonCondition for more information.
|
348
|
+
:param index_name: The name of the index to use for the query. If not provided, the first index configured on the table is used.
|
349
|
+
"""
|
350
|
+
query_params = self._prepare_index_query_params(
|
351
|
+
hash_key=hash_key,
|
352
|
+
model_class=model_class,
|
353
|
+
range_key=range_key,
|
354
|
+
filter_condition=filter_condition,
|
355
|
+
index_name=index_name,
|
356
|
+
)
|
332
357
|
last_evaluated_key = True
|
358
|
+
while last_evaluated_key:
|
359
|
+
items = self._get_dynamodb_table().query(**query_params)
|
360
|
+
yield from [model_class(**item) for item in items["Items"]]
|
361
|
+
last_evaluated_key = items.get("LastEvaluatedKey", False)
|
333
362
|
|
363
|
+
def query_hierarchy(
|
364
|
+
self,
|
365
|
+
hash_key: Union[Condition | str],
|
366
|
+
model_class: Type[DatabaseModel],
|
367
|
+
range_key: Optional[Condition] = None,
|
368
|
+
filter_condition: Optional[ComparisonCondition] = None,
|
369
|
+
index_name: Optional[str] = None,
|
370
|
+
) -> Optional[DatabaseModel]:
|
371
|
+
query_params = self._prepare_index_query_params(
|
372
|
+
hash_key=hash_key,
|
373
|
+
model_class=model_class,
|
374
|
+
range_key=range_key,
|
375
|
+
filter_condition=filter_condition,
|
376
|
+
index_name=index_name,
|
377
|
+
)
|
378
|
+
hierarchy_items = []
|
379
|
+
last_evaluated_key = True
|
334
380
|
while last_evaluated_key:
|
335
381
|
items = self._get_dynamodb_table().query(**query_params)
|
336
|
-
|
382
|
+
hierarchy_items.extend([item for item in items["Items"]])
|
337
383
|
last_evaluated_key = items.get("LastEvaluatedKey", False)
|
338
384
|
|
385
|
+
reconstructed_dict = self.reconstruct_hierarchy(hierarchy_items)
|
386
|
+
|
387
|
+
if not reconstructed_dict:
|
388
|
+
return None
|
389
|
+
|
390
|
+
model_type = reconstructed_dict.get(FIELD_STATIKK_TYPE)
|
391
|
+
model_class = self._get_model_type_by_statikk_type(model_type)
|
392
|
+
|
393
|
+
reconstructed_dict.pop(FIELD_STATIKK_TYPE, None)
|
394
|
+
return model_class(**reconstructed_dict)
|
395
|
+
|
339
396
|
def scan(
|
340
397
|
self,
|
341
|
-
model_class: Type[DatabaseModel],
|
342
398
|
filter_condition: Optional[ComparisonCondition] = None,
|
343
399
|
consistent_read: bool = False,
|
344
400
|
):
|
@@ -358,7 +414,7 @@ class Table:
|
|
358
414
|
|
359
415
|
while last_evaluated_key:
|
360
416
|
items = self._get_dynamodb_table().scan(**query_params)
|
361
|
-
yield from [
|
417
|
+
yield from [self._get_model_type_by_statikk_type(item["__statikk_type"])(**item) for item in items["Items"]]
|
362
418
|
last_evaluated_key = items.get("LastEvaluatedKey", False)
|
363
419
|
|
364
420
|
def _convert_dynamodb_to_python(self, item) -> Dict[str, Any]:
|
@@ -400,9 +456,9 @@ class Table:
|
|
400
456
|
def _prepare_model_data(
|
401
457
|
self,
|
402
458
|
item: DatabaseModel,
|
403
|
-
indexes: List[
|
459
|
+
indexes: List[GSI],
|
404
460
|
force_override_index_fields: bool = False,
|
405
|
-
) ->
|
461
|
+
) -> DatabaseModel:
|
406
462
|
for idx in indexes:
|
407
463
|
index_fields = self._compose_index_values(item, idx)
|
408
464
|
for key, value in index_fields.items():
|
@@ -411,24 +467,41 @@ class Table:
|
|
411
467
|
if value is not None:
|
412
468
|
setattr(item, key, value)
|
413
469
|
item.model_rebuild(force=True)
|
414
|
-
return item
|
470
|
+
return item
|
415
471
|
|
416
472
|
def _serialize_item(self, item: DatabaseModel):
|
417
|
-
data =
|
473
|
+
data = item.model_dump(exclude=item.get_nested_model_fields())
|
474
|
+
serialized_data = {}
|
418
475
|
for key, value in data.items():
|
419
476
|
data[key] = self._serialize_value(value)
|
420
477
|
return data
|
421
478
|
|
422
479
|
def _deserialize_item(self, item: Dict[str, Any], model_class: Type[DatabaseModel]):
|
423
480
|
for key, value in item.items():
|
481
|
+
if key == FIELD_STATIKK_TYPE:
|
482
|
+
continue
|
424
483
|
item[key] = self._deserialize_value(value, model_class.model_fields[key])
|
425
484
|
return model_class(**item)
|
426
485
|
|
427
486
|
def _deserialize_value(self, value: Any, annotation: Any):
|
428
|
-
|
487
|
+
actual_annotation = annotation.annotation if hasattr(annotation, "annotation") else annotation
|
488
|
+
|
489
|
+
if actual_annotation is datetime or "datetime" in str(annotation) and value is not None:
|
429
490
|
return datetime.fromtimestamp(int(value))
|
430
|
-
if
|
491
|
+
if actual_annotation is float:
|
431
492
|
return float(value)
|
493
|
+
if actual_annotation is list:
|
494
|
+
origin = getattr(actual_annotation, "__origin__", None)
|
495
|
+
args = getattr(actual_annotation, "__args__", None)
|
496
|
+
item_annotation = args[0] if args else Any
|
497
|
+
return [self._deserialize_value(item, item_annotation) for item in value]
|
498
|
+
if actual_annotation is set:
|
499
|
+
origin = getattr(actual_annotation, "__origin__", None)
|
500
|
+
args = getattr(actual_annotation, "__args__", None)
|
501
|
+
item_annotation = args[0] if args else Any
|
502
|
+
return {self._deserialize_value(item, item_annotation) for item in value}
|
503
|
+
if isinstance(value, dict):
|
504
|
+
return {key: self._deserialize_value(item, annotation) for key, item in value.items() if item is not None}
|
432
505
|
return value
|
433
506
|
|
434
507
|
def _serialize_value(self, value: Any):
|
@@ -438,36 +511,26 @@ class Table:
|
|
438
511
|
return Decimal(value)
|
439
512
|
if isinstance(value, list):
|
440
513
|
return [self._serialize_value(item) for item in value]
|
514
|
+
if isinstance(value, set):
|
515
|
+
return {self._serialize_value(item) for item in value}
|
441
516
|
if isinstance(value, dict):
|
442
517
|
return {key: self._serialize_value(item) for key, item in value.items() if item is not None}
|
443
518
|
return value
|
444
519
|
|
445
|
-
def _set_index_fields(self, model:
|
520
|
+
def _set_index_fields(self, model: Type[DatabaseModel], idx: GSI):
|
446
521
|
model_fields = model.model_fields
|
447
|
-
if idx.hash_key.name not in model_fields:
|
522
|
+
if idx.hash_key.name not in model_fields.keys():
|
448
523
|
model_fields[idx.hash_key.name] = FieldInfo(annotation=idx.hash_key.type, default=None, required=False)
|
449
|
-
if idx.sort_key.name not in model_fields:
|
524
|
+
if idx.sort_key.name not in model_fields.keys():
|
450
525
|
model_fields[idx.sort_key.name] = FieldInfo(annotation=idx.sort_key.type, default=None, required=False)
|
451
526
|
|
452
527
|
def _get_sort_key_value(self, model: DatabaseModel, idx: GSI) -> str:
|
453
|
-
|
454
|
-
|
455
|
-
for field_name, field_info in model.model_fields.items()
|
456
|
-
if field_info.annotation is not None
|
457
|
-
if field_info.annotation is IndexSecondaryKeyField and idx.name in getattr(model, field_name).index_names
|
458
|
-
]
|
459
|
-
|
460
|
-
if len(sort_key_fields_unordered) == idx.sort_key is not None:
|
528
|
+
sort_key_fields = model.index_definitions().get(idx.name, []).sk_fields
|
529
|
+
if len(sort_key_fields) == 0:
|
461
530
|
raise IncorrectSortKeyError(f"Model {model.__class__} does not have a sort key defined.")
|
462
|
-
if sort_key_fields_unordered[0][1] is not None:
|
463
|
-
sort_key_fields_unordered.sort(key=lambda x: x[1])
|
464
531
|
|
465
|
-
sort_key_fields = [field[0] for field in sort_key_fields_unordered]
|
466
|
-
|
467
|
-
if len(sort_key_fields) == 0 and model.include_type_in_sort_key():
|
468
|
-
return model.model_type()
|
469
532
|
if idx.sort_key.type is not str:
|
470
|
-
value = getattr(model, sort_key_fields[0])
|
533
|
+
value = getattr(model, sort_key_fields[0])
|
471
534
|
if type(value) is not idx.sort_key.type:
|
472
535
|
raise IncorrectSortKeyError(
|
473
536
|
f"Incorrect sort key type. Sort key type for sort key '{idx.sort_key.name}' should be: "
|
@@ -478,32 +541,28 @@ class Table:
|
|
478
541
|
value = value or idx.sort_key.default
|
479
542
|
return self._serialize_value(value)
|
480
543
|
|
481
|
-
sort_key_values
|
482
|
-
if model.
|
483
|
-
sort_key_values.append(model.
|
544
|
+
sort_key_values = []
|
545
|
+
if model._parent:
|
546
|
+
sort_key_values.append(getattr(model._parent, idx.sort_key.name))
|
547
|
+
if FIELD_STATIKK_TYPE not in model.index_definitions()[idx.name].pk_fields:
|
548
|
+
sort_key_values.append(model.type())
|
549
|
+
for sort_key_field in sort_key_fields:
|
550
|
+
if sort_key_field in model.model_fields.keys():
|
551
|
+
sort_key_values.append(self._serialize_value(getattr(model, sort_key_field)))
|
484
552
|
|
485
|
-
for field in sort_key_fields:
|
486
|
-
value = getattr(model, field).value
|
487
|
-
sort_key_values.append(value)
|
488
553
|
return self.delimiter.join(sort_key_values)
|
489
554
|
|
490
555
|
def _compose_index_values(self, model: DatabaseModel, idx: GSI) -> Dict[str, Any]:
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
if field_info.annotation is not None
|
496
|
-
if field_info.annotation is IndexPrimaryKeyField and idx.name in getattr(model, field_name).index_names
|
497
|
-
]
|
498
|
-
if len(hash_key_field) == 0 and model.type_is_primary_key():
|
499
|
-
hash_key_field.append(model.model_type())
|
500
|
-
hash_key_field = hash_key_field[0]
|
556
|
+
hash_key_fields = model.index_definitions().get(idx.name, []).pk_fields
|
557
|
+
|
558
|
+
if len(hash_key_fields) == 0 and not model.is_nested():
|
559
|
+
raise IncorrectHashKeyError(f"Model {model.__class__} does not have a hash key defined.")
|
501
560
|
|
502
561
|
def _get_hash_key_value():
|
503
|
-
if
|
504
|
-
return model.
|
505
|
-
|
506
|
-
|
562
|
+
if model._parent:
|
563
|
+
return getattr(model._parent, idx.hash_key.name)
|
564
|
+
|
565
|
+
return self.delimiter.join([self._serialize_value(model.get_attribute(field)) for field in hash_key_fields])
|
507
566
|
|
508
567
|
return {
|
509
568
|
idx.hash_key.name: _get_hash_key_value(),
|
@@ -519,15 +578,165 @@ class Table:
|
|
519
578
|
if len(put_items) > 0:
|
520
579
|
with dynamodb_table.batch_writer() as batch:
|
521
580
|
for item in put_items:
|
522
|
-
|
581
|
+
enriched_item = self._prepare_model_data(item, self.indexes)
|
582
|
+
if not enriched_item.was_modified:
|
583
|
+
continue
|
584
|
+
if not enriched_item.should_write_to_database():
|
585
|
+
continue
|
586
|
+
data = self._serialize_item(enriched_item)
|
523
587
|
batch.put_item(Item=data)
|
524
588
|
|
525
589
|
if len(delete_items) > 0:
|
526
590
|
with dynamodb_table.batch_writer() as batch:
|
527
591
|
for item in delete_items:
|
528
|
-
|
592
|
+
enriched_item = self._prepare_model_data(item, self.indexes)
|
593
|
+
data = self._serialize_item(enriched_item)
|
529
594
|
batch.delete_item(Key=data)
|
530
595
|
|
596
|
+
def reconstruct_hierarchy(self, items: list[dict]) -> Optional[dict]:
|
597
|
+
"""
|
598
|
+
Reconstructs a hierarchical dictionary structure from a flat list of dictionaries
|
599
|
+
using explicit parent-child relationships and model class definitions.
|
600
|
+
|
601
|
+
Args:
|
602
|
+
items: A flat list of dictionaries representing models with FIELD_STATIKK_PARENT_ID
|
603
|
+
|
604
|
+
Returns:
|
605
|
+
The top-level dictionary with its hierarchy fully reconstructed, or None if the list is empty
|
606
|
+
"""
|
607
|
+
if not items:
|
608
|
+
return None
|
609
|
+
|
610
|
+
items_by_id = {item["id"]: item for item in items}
|
611
|
+
children_by_parent_id = {}
|
612
|
+
for item in items:
|
613
|
+
parent_id = item.get(FIELD_STATIKK_PARENT_ID)
|
614
|
+
if parent_id:
|
615
|
+
if parent_id not in children_by_parent_id:
|
616
|
+
children_by_parent_id[parent_id] = []
|
617
|
+
children_by_parent_id[parent_id].append(item)
|
618
|
+
|
619
|
+
# Find the root item (the one with no parent ID)
|
620
|
+
root_items = [item for item in items if FIELD_STATIKK_PARENT_ID not in item]
|
621
|
+
|
622
|
+
if not root_items:
|
623
|
+
return None
|
624
|
+
|
625
|
+
if len(root_items) > 1:
|
626
|
+
root_item = root_items[0]
|
627
|
+
else:
|
628
|
+
root_item = root_items[0]
|
629
|
+
|
630
|
+
processed_items = set()
|
631
|
+
return self._reconstruct_item_with_children(root_item, items_by_id, children_by_parent_id, processed_items)
|
632
|
+
|
633
|
+
def _reconstruct_item_with_children(
|
634
|
+
self, item: dict, items_by_id: dict, children_by_parent_id: dict, processed_items: set
|
635
|
+
) -> dict:
|
636
|
+
"""
|
637
|
+
Recursively reconstruct an item and its children using model class definitions.
|
638
|
+
|
639
|
+
Args:
|
640
|
+
item: The item to reconstruct
|
641
|
+
items_by_id: Map of all item IDs to items
|
642
|
+
children_by_parent_id: Map of parent IDs to lists of child items
|
643
|
+
processed_items: Set of already processed item IDs to avoid duplicates
|
644
|
+
|
645
|
+
Returns:
|
646
|
+
The reconstructed item with all child references integrated
|
647
|
+
"""
|
648
|
+
if item["id"] in processed_items:
|
649
|
+
return item
|
650
|
+
processed_items.add(item["id"])
|
651
|
+
result = item.copy()
|
652
|
+
|
653
|
+
if FIELD_STATIKK_PARENT_ID in result:
|
654
|
+
result.pop(FIELD_STATIKK_PARENT_ID)
|
655
|
+
|
656
|
+
children = children_by_parent_id.get(item["id"], [])
|
657
|
+
if not children:
|
658
|
+
return result
|
659
|
+
|
660
|
+
parent_model_class = self._get_model_type_by_statikk_type(item[FIELD_STATIKK_TYPE])
|
661
|
+
|
662
|
+
children_by_type = {}
|
663
|
+
for child in children:
|
664
|
+
child_type = child[FIELD_STATIKK_TYPE]
|
665
|
+
if child_type not in children_by_type:
|
666
|
+
children_by_type[child_type] = []
|
667
|
+
children_by_type[child_type].append(child)
|
668
|
+
|
669
|
+
for child_type, child_items in children_by_type.items():
|
670
|
+
child_model_class = self._get_model_type_by_statikk_type(child_type)
|
671
|
+
matching_fields = []
|
672
|
+
|
673
|
+
for field_name, field_info in parent_model_class.model_fields.items():
|
674
|
+
if field_name.startswith("_"):
|
675
|
+
continue
|
676
|
+
|
677
|
+
field_type = field_info.annotation
|
678
|
+
|
679
|
+
if field_type == child_model_class:
|
680
|
+
matching_fields.append((field_name, "single"))
|
681
|
+
|
682
|
+
elif hasattr(field_type, "__origin__") and field_type.__origin__ == list:
|
683
|
+
args = getattr(field_type, "__args__", [])
|
684
|
+
if args and args[0] == child_model_class:
|
685
|
+
matching_fields.append((field_name, "list"))
|
686
|
+
|
687
|
+
elif hasattr(field_type, "__origin__") and field_type.__origin__ == set:
|
688
|
+
args = getattr(field_type, "__args__", [])
|
689
|
+
if args and args[0] == child_model_class:
|
690
|
+
matching_fields.append((field_name, "set"))
|
691
|
+
|
692
|
+
if matching_fields:
|
693
|
+
for field_name, container_type in matching_fields:
|
694
|
+
if container_type == "list":
|
695
|
+
if field_name not in result:
|
696
|
+
result[field_name] = []
|
697
|
+
|
698
|
+
existing_ids = {
|
699
|
+
item.get("id") for item in result[field_name] if isinstance(item, dict) and "id" in item
|
700
|
+
}
|
701
|
+
|
702
|
+
for child in child_items:
|
703
|
+
if child["id"] in existing_ids:
|
704
|
+
continue
|
705
|
+
|
706
|
+
reconstructed_child = self._reconstruct_item_with_children(
|
707
|
+
child, items_by_id, children_by_parent_id, processed_items
|
708
|
+
)
|
709
|
+
|
710
|
+
result[field_name].append(reconstructed_child)
|
711
|
+
existing_ids.add(child["id"])
|
712
|
+
|
713
|
+
elif container_type == "set":
|
714
|
+
if field_name not in result:
|
715
|
+
result[field_name] = []
|
716
|
+
|
717
|
+
existing_ids = {
|
718
|
+
item.get("id") for item in result[field_name] if isinstance(item, dict) and "id" in item
|
719
|
+
}
|
720
|
+
|
721
|
+
for child in child_items:
|
722
|
+
if child["id"] in existing_ids:
|
723
|
+
continue
|
724
|
+
reconstructed_child = self._reconstruct_item_with_children(
|
725
|
+
child, items_by_id, children_by_parent_id, processed_items
|
726
|
+
)
|
727
|
+
|
728
|
+
result[field_name].append(reconstructed_child)
|
729
|
+
existing_ids.add(child["id"])
|
730
|
+
|
731
|
+
elif container_type == "single":
|
732
|
+
if child_items:
|
733
|
+
reconstructed_child = self._reconstruct_item_with_children(
|
734
|
+
child_items[0], items_by_id, children_by_parent_id, processed_items
|
735
|
+
)
|
736
|
+
result[field_name] = reconstructed_child
|
737
|
+
|
738
|
+
return result
|
739
|
+
|
531
740
|
|
532
741
|
class BatchWriteContext:
|
533
742
|
def __init__(self, app: Table):
|
statikk/fields.py
ADDED
statikk/models.py
CHANGED
@@ -1,15 +1,23 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
3
|
+
import typing
|
4
|
+
import logging
|
5
|
+
from uuid import uuid4
|
4
6
|
from typing import Optional, List, Any, Set, Type
|
5
7
|
|
6
8
|
from boto3.dynamodb.conditions import ComparisonCondition
|
7
|
-
from pydantic import BaseModel, model_serializer, model_validator
|
8
|
-
from pydantic.fields import FieldInfo
|
9
|
+
from pydantic import BaseModel, model_serializer, model_validator, Field, Extra
|
10
|
+
from pydantic.fields import FieldInfo, Field
|
9
11
|
from pydantic_core._pydantic_core import PydanticUndefined
|
10
12
|
|
11
13
|
from statikk.conditions import Condition
|
12
14
|
from statikk.expressions import DatabaseModelUpdateExpressionBuilder
|
15
|
+
from statikk.fields import FIELD_STATIKK_TYPE, FIELD_STATIKK_PARENT_ID
|
16
|
+
|
17
|
+
if typing.TYPE_CHECKING:
|
18
|
+
from statikk.engine import Table
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
13
21
|
|
14
22
|
|
15
23
|
class Key(BaseModel):
|
@@ -39,37 +47,117 @@ class Index(BaseModel):
|
|
39
47
|
return self.value
|
40
48
|
|
41
49
|
|
42
|
-
class
|
43
|
-
|
50
|
+
class IndexFieldConfig(BaseModel):
|
51
|
+
pk_fields: list[str] = []
|
52
|
+
sk_fields: list[str] = []
|
44
53
|
|
45
54
|
|
46
|
-
class
|
47
|
-
|
55
|
+
class TrackingMixin:
|
56
|
+
_original_hash: int = Field(exclude=True)
|
48
57
|
|
58
|
+
def __init__(self):
|
59
|
+
self._original_hash = self._recursive_hash()
|
49
60
|
|
50
|
-
|
51
|
-
|
61
|
+
def _recursive_hash(self) -> int:
|
62
|
+
"""
|
63
|
+
Compute a hash value for the model, ignoring nested DatabaseModel instances.
|
52
64
|
|
53
|
-
|
54
|
-
|
55
|
-
|
65
|
+
This ensures that changes to child models don't affect the parent's hash.
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
A hash value representing the model's non-model fields.
|
69
|
+
"""
|
70
|
+
values = []
|
71
|
+
for field_name in self.model_fields:
|
72
|
+
if not hasattr(self, field_name):
|
73
|
+
continue
|
74
|
+
|
75
|
+
if field_name.startswith("_"):
|
76
|
+
continue
|
77
|
+
|
78
|
+
value = getattr(self, field_name)
|
79
|
+
|
80
|
+
if hasattr(value, "__class__") and issubclass(value.__class__, DatabaseModel):
|
81
|
+
continue
|
82
|
+
|
83
|
+
if isinstance(value, list) or isinstance(value, set):
|
84
|
+
contains_model = False
|
85
|
+
for item in value:
|
86
|
+
if hasattr(item, "__class__") and issubclass(item.__class__, DatabaseModel):
|
87
|
+
contains_model = True
|
88
|
+
break
|
89
|
+
if contains_model:
|
90
|
+
continue
|
91
|
+
|
92
|
+
if isinstance(value, dict):
|
93
|
+
contains_model = False
|
94
|
+
if not contains_model:
|
95
|
+
for val in value.values():
|
96
|
+
if hasattr(val, "__class__") and issubclass(val.__class__, DatabaseModel):
|
97
|
+
contains_model = True
|
98
|
+
break
|
99
|
+
if contains_model:
|
100
|
+
continue
|
101
|
+
|
102
|
+
hashed_value = self._make_hashable(value)
|
103
|
+
if hashed_value is not None:
|
104
|
+
values.append(hashed_value)
|
105
|
+
|
106
|
+
return hash(tuple(values))
|
107
|
+
|
108
|
+
def _make_hashable(self, value: Any) -> Any:
|
109
|
+
if isinstance(value, (str, int, float, bool, type(None))):
|
110
|
+
return value
|
111
|
+
elif isinstance(value, list) or isinstance(value, set):
|
112
|
+
return tuple(self._make_hashable(item) for item in value)
|
113
|
+
elif isinstance(value, dict):
|
114
|
+
return tuple((self._make_hashable(k), self._make_hashable(v)) for k, v in sorted(value.items()))
|
115
|
+
elif isinstance(value, BaseModel) and hasattr(value, "_recursive_hash"):
|
116
|
+
return value._recursive_hash()
|
117
|
+
else:
|
118
|
+
try:
|
119
|
+
return hash(value)
|
120
|
+
except TypeError:
|
121
|
+
logger.warning(
|
122
|
+
f"{type(value)} is unhashable, tracking will not work. Consider implementing the TrackingMixin for this type."
|
123
|
+
)
|
124
|
+
return None
|
125
|
+
return value
|
126
|
+
|
127
|
+
@property
|
128
|
+
def was_modified(self) -> bool:
|
129
|
+
return self._recursive_hash() != self._original_hash
|
130
|
+
|
131
|
+
|
132
|
+
class DatabaseModel(BaseModel, TrackingMixin, extra=Extra.allow):
|
133
|
+
id: str = Field(default_factory=lambda: str(uuid4()))
|
134
|
+
_parent: Optional[DatabaseModel] = None
|
135
|
+
_model_types_in_hierarchy: dict[str, Type[DatabaseModel]] = {}
|
56
136
|
|
57
137
|
@classmethod
|
58
|
-
def
|
59
|
-
return
|
138
|
+
def type(cls) -> str:
|
139
|
+
return cls.__name__
|
60
140
|
|
61
141
|
@classmethod
|
62
|
-
def
|
63
|
-
return
|
142
|
+
def index_definitions(cls) -> dict[str, IndexFieldConfig]:
|
143
|
+
return {"main_index": IndexFieldConfig(pk_fields=[], sk_fields=[])}
|
64
144
|
|
65
145
|
@classmethod
|
66
|
-
def set_table_ref(cls, table):
|
146
|
+
def set_table_ref(cls, table: "Table"):
|
67
147
|
cls._table = table
|
68
148
|
|
69
149
|
@classmethod
|
70
150
|
def batch_write(cls):
|
71
151
|
return cls._table.batch_write()
|
72
152
|
|
153
|
+
@classmethod
|
154
|
+
def is_nested(cls) -> bool:
|
155
|
+
return False
|
156
|
+
|
157
|
+
@property
|
158
|
+
def is_simple_object(self) -> bool:
|
159
|
+
return len(self._model_types_in_hierarchy) == 1
|
160
|
+
|
73
161
|
@classmethod
|
74
162
|
def query(
|
75
163
|
cls,
|
@@ -86,11 +174,27 @@ class DatabaseModel(BaseModel):
|
|
86
174
|
index_name=index_name,
|
87
175
|
)
|
88
176
|
|
177
|
+
@classmethod
|
178
|
+
def query_hierarchy(
|
179
|
+
cls,
|
180
|
+
hash_key: Union[Condition | str],
|
181
|
+
range_key: Optional[Condition] = None,
|
182
|
+
filter_condition: Optional[ComparisonCondition] = None,
|
183
|
+
index_name: Optional[str] = None,
|
184
|
+
) -> DatabaseModel:
|
185
|
+
return cls._table.query_hierarchy(
|
186
|
+
hash_key=hash_key,
|
187
|
+
model_class=cls,
|
188
|
+
range_key=range_key,
|
189
|
+
filter_condition=filter_condition,
|
190
|
+
index_name=index_name,
|
191
|
+
)
|
192
|
+
|
89
193
|
def save(self):
|
90
194
|
return self._table.put_item(self)
|
91
195
|
|
92
196
|
def delete(self):
|
93
|
-
return self._table.delete_item(self
|
197
|
+
return self._table.delete_item(self)
|
94
198
|
|
95
199
|
def update(self, sort_key: Optional[str] = None) -> DatabaseModelUpdateExpressionBuilder:
|
96
200
|
return DatabaseModelUpdateExpressionBuilder(self, sort_key)
|
@@ -118,73 +222,141 @@ class DatabaseModel(BaseModel):
|
|
118
222
|
def batch_get(cls, ids: List[str], batch_size: int = 100):
|
119
223
|
return cls._table.batch_get_items(ids=ids, model_class=cls, batch_size=batch_size)
|
120
224
|
|
225
|
+
def should_write_to_database(self) -> bool:
|
226
|
+
if self.is_nested():
|
227
|
+
return self._parent.should_write_to_database()
|
228
|
+
return True
|
229
|
+
|
121
230
|
@classmethod
|
122
231
|
def scan(
|
123
232
|
cls,
|
124
233
|
filter_condition: Optional[ComparisonCondition] = None,
|
125
234
|
consistent_read: bool = False,
|
126
235
|
):
|
127
|
-
return cls._table.scan(
|
236
|
+
return cls._table.scan(filter_condition=filter_condition)
|
237
|
+
|
238
|
+
@model_serializer(mode="wrap")
|
239
|
+
def serialize_model(self, handler):
|
240
|
+
data = handler(self)
|
241
|
+
data[FIELD_STATIKK_TYPE] = self.type()
|
242
|
+
if self._parent:
|
243
|
+
data[FIELD_STATIKK_PARENT_ID] = self._parent.id
|
244
|
+
return data
|
128
245
|
|
129
|
-
@
|
130
|
-
def
|
131
|
-
|
246
|
+
@model_validator(mode="after")
|
247
|
+
def initialize_tracking(self):
|
248
|
+
self._original_hash = self._recursive_hash()
|
249
|
+
self._model_types_in_hierarchy[self.type()] = type(self)
|
250
|
+
if not self.is_nested():
|
251
|
+
self._set_parent_references(self)
|
132
252
|
|
133
|
-
|
134
|
-
def _is_index_field(field: FieldInfo) -> bool:
|
135
|
-
index_types = {Index, IndexPrimaryKeyField, IndexSecondaryKeyField}
|
136
|
-
return field.annotation in index_types
|
253
|
+
return self
|
137
254
|
|
138
|
-
|
139
|
-
def _create_index_field_from_shorthand(cls, field: FieldInfo, value: str) -> FieldInfo:
|
140
|
-
annotation = field.annotation
|
141
|
-
extra_fields = dict()
|
142
|
-
if field.default is not PydanticUndefined:
|
143
|
-
extra_fields["index_names"] = field.default.index_names
|
144
|
-
if field.annotation is IndexSecondaryKeyField:
|
145
|
-
extra_fields["order"] = field.default.order
|
146
|
-
return annotation(value=value, **extra_fields)
|
147
|
-
|
148
|
-
@model_validator(mode="before")
|
149
|
-
@classmethod
|
150
|
-
def check_boxed_indexes(cls, data: Any) -> Any:
|
255
|
+
def split_to_simple_objects(self, items: Optional[list[DatabaseModel]] = None) -> list[DatabaseModel]:
|
151
256
|
"""
|
152
|
-
|
153
|
-
Instead of having to manually box the index value, this method will do it for you.
|
154
|
-
For example:
|
155
|
-
my_model = MyModel(id="123", my_index="abc")
|
156
|
-
Is equivalent to:
|
157
|
-
my_model = MyModel(id="123", my_index=IndexPrimaryKeyField(value="abc"))
|
158
|
-
"""
|
159
|
-
if isinstance(data, dict):
|
160
|
-
if "id" not in data:
|
161
|
-
data["id"] = str(uuid.uuid4())
|
162
|
-
|
163
|
-
for key, value in data.items():
|
164
|
-
field = cls.model_fields[key]
|
165
|
-
if cls._is_index_field(field) and not isinstance(value, dict):
|
166
|
-
if isinstance(value, tuple(cls._index_types())):
|
167
|
-
continue
|
168
|
-
else:
|
169
|
-
data[key] = cls._create_index_field_from_shorthand(field, value)
|
170
|
-
return data
|
257
|
+
Split a complex nested DatabaseModel into a list of individual DatabaseModel instances.
|
171
258
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
)
|
259
|
+
This method recursively traverses the model and all its nested DatabaseModel instances,
|
260
|
+
collecting them into a flat list for simpler processing or storage.
|
261
|
+
|
262
|
+
Args:
|
263
|
+
items: An optional existing list to add items to. If None, a new list is created.
|
264
|
+
|
265
|
+
Returns:
|
266
|
+
A list containing this model and all nested DatabaseModel instances.
|
267
|
+
"""
|
268
|
+
if items is None:
|
269
|
+
items = [self]
|
270
|
+
else:
|
271
|
+
if self not in items:
|
272
|
+
items.append(self)
|
273
|
+
|
274
|
+
# Iterate through all fields of the model
|
275
|
+
for field_name, field_value in self:
|
276
|
+
# Skip fields that start with underscore (private fields)
|
277
|
+
if field_name.startswith("_"):
|
278
|
+
continue
|
279
|
+
|
280
|
+
# Handle direct DatabaseModel instances
|
281
|
+
if hasattr(field_value, "__class__") and issubclass(field_value.__class__, DatabaseModel):
|
282
|
+
if field_value not in items:
|
283
|
+
items.append(field_value)
|
284
|
+
field_value.split_to_simple_objects(items)
|
285
|
+
|
286
|
+
# Handle lists containing DatabaseModel instances
|
287
|
+
elif isinstance(field_value, list):
|
288
|
+
for item in field_value:
|
289
|
+
if hasattr(item, "__class__") and issubclass(item.__class__, DatabaseModel):
|
290
|
+
if item not in items:
|
291
|
+
items.append(item)
|
292
|
+
item.split_to_simple_objects(items)
|
293
|
+
|
294
|
+
# Handle sets containing DatabaseModel instances
|
295
|
+
elif isinstance(field_value, set):
|
296
|
+
for item in field_value:
|
297
|
+
if hasattr(item, "__class__") and issubclass(item.__class__, DatabaseModel):
|
298
|
+
if item not in items:
|
299
|
+
items.append(item)
|
300
|
+
item.split_to_simple_objects(items)
|
301
|
+
|
302
|
+
# Handle dictionaries that may contain DatabaseModel instances
|
303
|
+
elif isinstance(field_value, dict):
|
304
|
+
# Check dictionary values
|
305
|
+
for value in field_value.values():
|
306
|
+
if hasattr(value, "__class__") and issubclass(value.__class__, DatabaseModel):
|
307
|
+
if value not in items:
|
308
|
+
items.append(value)
|
309
|
+
value.split_to_simple_objects(items)
|
310
|
+
|
311
|
+
return items
|
312
|
+
|
313
|
+
def get_attribute(self, attribute_name: str):
|
314
|
+
if attribute_name == FIELD_STATIKK_TYPE:
|
315
|
+
return self.type()
|
316
|
+
return getattr(self, attribute_name)
|
317
|
+
|
318
|
+
def get_nested_model_fields(self) -> set[DatabaseModel]:
|
319
|
+
nested_models = []
|
320
|
+
for field_name, field_value in self:
|
321
|
+
if issubclass(field_value.__class__, DatabaseModel) and field_value.is_nested():
|
322
|
+
nested_models.append(field_name)
|
323
|
+
elif isinstance(field_value, list):
|
324
|
+
for item in field_value:
|
325
|
+
if issubclass(item.__class__, DatabaseModel) and item.is_nested():
|
326
|
+
nested_models.append(field_name)
|
327
|
+
elif isinstance(field_value, set):
|
328
|
+
for item in field_value:
|
329
|
+
if issubclass(item.__class__, DatabaseModel) and item.is_nested():
|
330
|
+
nested_models.append(field_name)
|
331
|
+
elif isinstance(field_value, dict):
|
332
|
+
for key, value in field_value.items():
|
333
|
+
if issubclass(value.__class__, DatabaseModel) and value.is_nested():
|
334
|
+
nested_models.append(field_name)
|
335
|
+
return set(nested_models)
|
336
|
+
|
337
|
+
def get_type_from_hierarchy_by_name(self, name: str) -> Optional[Type[DatabaseModel]]:
|
338
|
+
return self._model_types_in_hierarchy.get(name)
|
339
|
+
|
340
|
+
def _set_parent_to_field(self, field: DatabaseModel, parent: DatabaseModel, root: DatabaseModel):
|
341
|
+
if field._parent:
|
342
|
+
return # Already set
|
343
|
+
field._parent = parent
|
344
|
+
root._model_types_in_hierarchy[field.type()] = type(field)
|
345
|
+
field._set_parent_references(root)
|
346
|
+
|
347
|
+
def _set_parent_references(self, root: DatabaseModel):
|
348
|
+
for field_name, field_value in self:
|
349
|
+
if isinstance(field_value, DatabaseModel):
|
350
|
+
self._set_parent_to_field(field_value, self, root)
|
351
|
+
elif isinstance(field_value, list):
|
352
|
+
for item in field_value:
|
353
|
+
if isinstance(item, DatabaseModel):
|
354
|
+
self._set_parent_to_field(item, self, root)
|
355
|
+
elif isinstance(field_value, set):
|
356
|
+
for item in field_value:
|
357
|
+
if isinstance(item, DatabaseModel):
|
358
|
+
self._set_parent_to_field(item, self, root)
|
359
|
+
elif isinstance(field_value, dict):
|
360
|
+
for key, value in field_value.items():
|
361
|
+
if isinstance(value, DatabaseModel):
|
362
|
+
self._set_parent_to_field(value, self, root)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: statikk
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.1.1
|
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
|
@@ -27,11 +27,9 @@ Requires-Dist: moto[dynamodb]==4.2.14; extra == "testing"
|
|
27
27
|
:alt: Statikk
|
28
28
|
:align: center
|
29
29
|
|
30
|
-
|
31
|
-
We were originally using `PynamoDB <https://github.com/pynamodb/PynamoDB>`_ , but we found it to be too verbose for our liking.
|
30
|
+
An ORM-like Single Table Application (STA) architecture library for Python and DynamoDb. Makes it really eason to set up and work with STAs.
|
32
31
|
|
33
|
-
|
34
|
-
for our upcoming game, Conquests of Ethoas. Drastic API changes can still happen.
|
32
|
+
The library is in alpha. Constant work is being done on it as I'm developing my game.
|
35
33
|
|
36
34
|
=================
|
37
35
|
Requirements
|
@@ -55,15 +53,19 @@ Basic Usage
|
|
55
53
|
|
56
54
|
.. code-block:: python
|
57
55
|
|
58
|
-
from statikk.models import DatabaseModel, Table, GlobalSecondaryIndex, KeySchema
|
56
|
+
from statikk.models import DatabaseModel, Table, GlobalSecondaryIndex, KeySchema
|
59
57
|
|
60
58
|
class MyAwesomeModel(DatabaseModel):
|
61
|
-
player_id:
|
62
|
-
tier:
|
59
|
+
player_id: str
|
60
|
+
tier: str
|
63
61
|
name: str = "Foo"
|
64
62
|
values: set = {1, 2, 3, 4}
|
65
63
|
cost: int = 4
|
66
64
|
|
65
|
+
@classmethod
|
66
|
+
def index_definitions(cls) -> dict[str, IndexFieldConfig]:
|
67
|
+
return {"main-index": IndexFieldConfig(pk_fields=["player_id"], sk_fields=["tier"])}
|
68
|
+
|
67
69
|
table = Table(
|
68
70
|
name="my-dynamodb-table",
|
69
71
|
key_schema=KeySchema(hash_key="id"),
|
@@ -0,0 +1,11 @@
|
|
1
|
+
statikk/__init__.py,sha256=pH5i4Fj1tbXLqLtTVIdoojiplZssQn0nnud8-HXodRE,577
|
2
|
+
statikk/conditions.py,sha256=63FYMR-UUaE-ZJEb_8CU721CQTwhajq39-BbokmKeMA,2166
|
3
|
+
statikk/engine.py,sha256=ny15q1yLEJYTszxCaHxqdm7GRC3xsCZBtjPfNGHn0Dk,30878
|
4
|
+
statikk/expressions.py,sha256=mF6Hmj3Kmj6KKXTymeTHSepVA7rhiSINpFgSAPeBTRY,12210
|
5
|
+
statikk/fields.py,sha256=LkMP5NnX7WS0HSLxI3Q-dMOrfaJ0SD7SayZxJU5Acgg,86
|
6
|
+
statikk/models.py,sha256=6ssCECKFCHZwN_-LA-epHhqtVNJK0pt6PlIVj9UxKUI,12928
|
7
|
+
statikk-0.1.1.dist-info/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
|
8
|
+
statikk-0.1.1.dist-info/METADATA,sha256=bfSuQ9QD8mDp_9CdmlgApWp0h-rlAhCsgO-ChPETOkQ,3160
|
9
|
+
statikk-0.1.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
10
|
+
statikk-0.1.1.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
|
11
|
+
statikk-0.1.1.dist-info/RECORD,,
|
statikk-0.0.13.dist-info/RECORD
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
statikk/__init__.py,sha256=pH5i4Fj1tbXLqLtTVIdoojiplZssQn0nnud8-HXodRE,577
|
2
|
-
statikk/conditions.py,sha256=66kJ-2YWqlV-dzEkiAe6c985dMQ-lpaoZ8aaoWhSs_M,2220
|
3
|
-
statikk/engine.py,sha256=aYIVTwUxlSLnoB8DOUpX4udf0PcIoiTzgSfJ3YCtPaU,22193
|
4
|
-
statikk/expressions.py,sha256=mF6Hmj3Kmj6KKXTymeTHSepVA7rhiSINpFgSAPeBTRY,12210
|
5
|
-
statikk/models.py,sha256=q-wY2bQRgaH0NTnR4NGnQ22eO3rUshK-aAykt7NVFO4,6172
|
6
|
-
statikk-0.0.13.dist-info/LICENSE.txt,sha256=uSH_2Hpb2Bigy5_HhBliN2fZbBU64G3ERM5zzhKPUEE,1078
|
7
|
-
statikk-0.0.13.dist-info/METADATA,sha256=oJ8DfNGW9YKONAJKqq48r02v5WFlOQi75lV63HBJavI,3345
|
8
|
-
statikk-0.0.13.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
9
|
-
statikk-0.0.13.dist-info/top_level.txt,sha256=etKmBbjzIlLpSefXoiOfhWGEgvqUEALaFwCjFDBD9YI,8
|
10
|
-
statikk-0.0.13.dist-info/RECORD,,
|
File without changes
|
File without changes
|