ic-python-db 0.8.2__tar.gz → 0.9.0__tar.gz
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.
- {ic_python_db-0.8.2/ic_python_db.egg-info → ic_python_db-0.9.0}/PKG-INFO +1 -1
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/__init__.py +1 -1
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/db_engine.py +60 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/entity.py +140 -140
- ic_python_db-0.9.0/ic_python_db/properties.py +606 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0/ic_python_db.egg-info}/PKG-INFO +1 -1
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/pyproject.toml +1 -1
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/setup.py +1 -1
- ic_python_db-0.8.2/ic_python_db/properties.py +0 -644
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/LICENSE +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/MANIFEST.in +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/README.md +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/_cdk.py +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/constants.py +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/context.py +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/hooks.py +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/mixins.py +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/py.typed +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/schema.py +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/storage.py +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db/system_time.py +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db.egg-info/SOURCES.txt +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db.egg-info/dependency_links.txt +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/ic_python_db.egg-info/top_level.txt +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/requirements-dev.txt +0 -0
- {ic_python_db-0.8.2 → ic_python_db-0.9.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ic-python-db
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A lightweight key-value database written in Python, intended for use on the Internet Computer (IC)
|
|
5
5
|
Home-page: https://github.com/smart-social-contracts/ic-python-db
|
|
6
6
|
Author: Smart Social Contracts
|
|
@@ -20,7 +20,7 @@ from .schema import SchemaIncompatibleError, build_schema, diff_schemas, schema_
|
|
|
20
20
|
from .storage import MemoryStorage, Storage
|
|
21
21
|
from .system_time import SystemTime
|
|
22
22
|
|
|
23
|
-
__version__ = "0.
|
|
23
|
+
__version__ = "0.9.0"
|
|
24
24
|
__all__ = [
|
|
25
25
|
"Database",
|
|
26
26
|
"Entity",
|
|
@@ -366,6 +366,66 @@ class Database:
|
|
|
366
366
|
|
|
367
367
|
return check_upgrade_compatibility(self, raise_on_error=raise_on_error)
|
|
368
368
|
|
|
369
|
+
# ── Reverse Index API ──────────────────────────────────────────────────────
|
|
370
|
+
#
|
|
371
|
+
# Reverse indexes allow OneToMany / ManyToMany / inverse-OneToOne relations
|
|
372
|
+
# to be resolved without scanning all child entities.
|
|
373
|
+
#
|
|
374
|
+
# Key format: _ri:{ParentType}:{parent_id}:{relation_name}
|
|
375
|
+
# Value: JSON array of child entity IDs, e.g. ["1", "2", "3"]
|
|
376
|
+
|
|
377
|
+
def _ri_key(self, parent_type: str, parent_id: str, relation_name: str) -> str:
|
|
378
|
+
return f"_ri:{parent_type}:{parent_id}:{relation_name}"
|
|
379
|
+
|
|
380
|
+
def reverse_index_get(
|
|
381
|
+
self, parent_type: str, parent_id: str, relation_name: str
|
|
382
|
+
) -> List[str]:
|
|
383
|
+
"""Read the reverse index for a parent entity's relation.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
List of child entity IDs, or empty list if no index exists.
|
|
387
|
+
"""
|
|
388
|
+
key = self._ri_key(parent_type, parent_id, relation_name)
|
|
389
|
+
raw = self._db_storage.get(key)
|
|
390
|
+
if raw:
|
|
391
|
+
return json.loads(raw)
|
|
392
|
+
return []
|
|
393
|
+
|
|
394
|
+
def reverse_index_add(
|
|
395
|
+
self, parent_type: str, parent_id: str, relation_name: str, child_id: str
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Add a child ID to a parent's reverse index."""
|
|
398
|
+
key = self._ri_key(parent_type, parent_id, relation_name)
|
|
399
|
+
raw = self._db_storage.get(key)
|
|
400
|
+
ids = json.loads(raw) if raw else []
|
|
401
|
+
if child_id not in ids:
|
|
402
|
+
ids.append(child_id)
|
|
403
|
+
self._db_storage.insert(key, json.dumps(ids))
|
|
404
|
+
|
|
405
|
+
def reverse_index_remove(
|
|
406
|
+
self, parent_type: str, parent_id: str, relation_name: str, child_id: str
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Remove a child ID from a parent's reverse index."""
|
|
409
|
+
key = self._ri_key(parent_type, parent_id, relation_name)
|
|
410
|
+
raw = self._db_storage.get(key)
|
|
411
|
+
if not raw:
|
|
412
|
+
return
|
|
413
|
+
ids = json.loads(raw)
|
|
414
|
+
if child_id in ids:
|
|
415
|
+
ids.remove(child_id)
|
|
416
|
+
if ids:
|
|
417
|
+
self._db_storage.insert(key, json.dumps(ids))
|
|
418
|
+
else:
|
|
419
|
+
self._db_storage.remove(key)
|
|
420
|
+
|
|
421
|
+
def reverse_index_delete(
|
|
422
|
+
self, parent_type: str, parent_id: str, relation_name: str
|
|
423
|
+
) -> None:
|
|
424
|
+
"""Delete an entire reverse index entry (used when parent is deleted)."""
|
|
425
|
+
key = self._ri_key(parent_type, parent_id, relation_name)
|
|
426
|
+
if self._db_storage.get(key) is not None:
|
|
427
|
+
self._db_storage.remove(key)
|
|
428
|
+
|
|
369
429
|
def get_audit(
|
|
370
430
|
self, id_from: Optional[int] = None, id_to: Optional[int] = None
|
|
371
431
|
) -> Dict[str, str]:
|
|
@@ -71,7 +71,6 @@ class Entity:
|
|
|
71
71
|
_id (str): Unique sequential identifier ("1", "2", "3", ...)
|
|
72
72
|
_loaded (bool): True if loaded from DB, False if newly created
|
|
73
73
|
_counted (bool): True if entity has been counted (prevents double-counting)
|
|
74
|
-
_relations (dict): Dictionary mapping relation names to related entities
|
|
75
74
|
_do_not_save (bool): Temporary flag to prevent saving during initialization
|
|
76
75
|
|
|
77
76
|
Class-level attributes:
|
|
@@ -161,8 +160,6 @@ class Entity:
|
|
|
161
160
|
self._loaded = False if kwargs.get("_loaded") is None else kwargs["_loaded"]
|
|
162
161
|
self._counted = False # Track if this entity has been counted
|
|
163
162
|
|
|
164
|
-
self._relations = {}
|
|
165
|
-
|
|
166
163
|
# Add to context
|
|
167
164
|
self.__class__._context.add(self)
|
|
168
165
|
|
|
@@ -439,62 +436,62 @@ class Entity:
|
|
|
439
436
|
if stored_version != current_version:
|
|
440
437
|
entity._save()
|
|
441
438
|
|
|
442
|
-
# Restore legacy "relations" block if present in serialized data.
|
|
443
|
-
# Otherwise keep the _relations that __init__ already populated
|
|
444
|
-
# via relationship descriptors (OneToMany, ManyToOne, etc.).
|
|
445
|
-
if "relations" in data:
|
|
446
|
-
relations_data = data.pop("relations")
|
|
447
|
-
relations = {}
|
|
448
|
-
for rel_name, rel_refs in relations_data.items():
|
|
449
|
-
relations[rel_name] = []
|
|
450
|
-
for ref in rel_refs:
|
|
451
|
-
related = (
|
|
452
|
-
Entity.db()
|
|
453
|
-
._entity_types[ref["_type"]]
|
|
454
|
-
.load(ref["_id"], level=level - 1)
|
|
455
|
-
)
|
|
456
|
-
if related:
|
|
457
|
-
relations[rel_name].append(related)
|
|
458
|
-
entity._relations = relations
|
|
459
|
-
|
|
460
439
|
return entity
|
|
461
440
|
|
|
462
441
|
@classmethod
|
|
463
442
|
def find(cls: Type[T], d) -> List[T]:
|
|
464
|
-
|
|
465
|
-
|
|
443
|
+
"""Find entities matching a dictionary filter.
|
|
444
|
+
|
|
445
|
+
Uses load_some over the full ID range. Prefer indexed lookups where possible.
|
|
446
|
+
"""
|
|
447
|
+
max_id = cls.max_id()
|
|
448
|
+
if max_id == 0:
|
|
449
|
+
return []
|
|
450
|
+
all_entities = cls.load_some(1, max_id)
|
|
466
451
|
return [
|
|
467
|
-
|
|
452
|
+
e
|
|
453
|
+
for e in all_entities
|
|
454
|
+
if all(getattr(e, k, None) == v for k, v in d.items())
|
|
468
455
|
]
|
|
469
456
|
|
|
470
457
|
@classmethod
|
|
471
458
|
def instances(cls: Type[T]) -> List[T]:
|
|
472
459
|
"""Get all instances of this entity type, including subclass instances.
|
|
473
460
|
|
|
474
|
-
|
|
461
|
+
.. deprecated::
|
|
462
|
+
This method loads ALL entities and is O(max_id). Prefer load_some()
|
|
463
|
+
with pagination or use relationship accessors.
|
|
475
464
|
|
|
476
465
|
Returns:
|
|
477
466
|
List of entities
|
|
478
467
|
"""
|
|
468
|
+
try:
|
|
469
|
+
import warnings
|
|
470
|
+
|
|
471
|
+
warnings.warn(
|
|
472
|
+
"Entity.instances() is deprecated and will be removed. "
|
|
473
|
+
"Use load_some() with pagination instead.",
|
|
474
|
+
DeprecationWarning,
|
|
475
|
+
stacklevel=2,
|
|
476
|
+
)
|
|
477
|
+
except (ImportError, AttributeError):
|
|
478
|
+
pass
|
|
479
479
|
db = Database.get_instance()
|
|
480
480
|
full_type_name = cls.get_full_type_name()
|
|
481
481
|
db.register_entity_type(cls, full_type_name)
|
|
482
482
|
|
|
483
|
-
# Use load_some for O(max_id) instead of O(total_keys)
|
|
484
483
|
max_id = cls.max_id()
|
|
485
484
|
if max_id == 0:
|
|
486
485
|
instances = []
|
|
487
486
|
else:
|
|
488
487
|
instances = cls.load_some(1, max_id)
|
|
489
488
|
|
|
490
|
-
# Also check for subclass instances
|
|
491
|
-
# Track processed classes to avoid duplicates when types are registered under multiple names
|
|
492
489
|
processed_classes = {cls}
|
|
493
490
|
for type_name, type_cls in db._entity_types.items():
|
|
494
491
|
if type_cls in processed_classes:
|
|
495
|
-
continue
|
|
492
|
+
continue
|
|
496
493
|
if type_name == full_type_name or type_name == cls.__name__:
|
|
497
|
-
continue
|
|
494
|
+
continue
|
|
498
495
|
if db.is_subclass(type_name, cls):
|
|
499
496
|
processed_classes.add(type_cls)
|
|
500
497
|
subclass_max_id = type_cls.max_id()
|
|
@@ -572,10 +569,11 @@ class Entity:
|
|
|
572
569
|
return ret
|
|
573
570
|
|
|
574
571
|
def delete(self) -> None:
|
|
572
|
+
"""Delete this entity from the database, cleaning up all reverse indexes."""
|
|
575
573
|
logger.debug(f"Deleting entity {self._type}@{self._id}")
|
|
576
|
-
"""Delete this entity from the database."""
|
|
577
574
|
from .constants import ACTION_DELETE
|
|
578
575
|
from .hooks import call_entity_hook
|
|
576
|
+
from .properties import ManyToMany, ManyToOne, OneToMany, OneToOne
|
|
579
577
|
|
|
580
578
|
# Call hook before deletion
|
|
581
579
|
allow, _ = call_entity_hook(self, None, self, None, ACTION_DELETE)
|
|
@@ -583,17 +581,74 @@ class Entity:
|
|
|
583
581
|
if not allow:
|
|
584
582
|
raise PermissionError("Hook rejected entity deletion")
|
|
585
583
|
|
|
586
|
-
self.db()
|
|
584
|
+
db = self.db()
|
|
585
|
+
|
|
586
|
+
# Clean up reverse indexes for all relation properties
|
|
587
|
+
for cls in reversed(self.__class__.__mro__):
|
|
588
|
+
for attr_name, attr_value in cls.__dict__.items():
|
|
589
|
+
if attr_name.startswith("_"):
|
|
590
|
+
continue
|
|
591
|
+
if isinstance(attr_value, ManyToOne):
|
|
592
|
+
# Remove self from parent's reverse index
|
|
593
|
+
parent_ref = self.__dict__.get(f"_rel_{attr_name}")
|
|
594
|
+
if parent_ref is not None:
|
|
595
|
+
for type_name in attr_value._get_allowed_types():
|
|
596
|
+
entity_class = db._entity_types.get(type_name)
|
|
597
|
+
if entity_class and db.load(type_name, parent_ref):
|
|
598
|
+
db.reverse_index_remove(
|
|
599
|
+
type_name,
|
|
600
|
+
parent_ref,
|
|
601
|
+
attr_value.reverse_name,
|
|
602
|
+
self._id,
|
|
603
|
+
)
|
|
604
|
+
break
|
|
605
|
+
elif isinstance(attr_value, OneToOne):
|
|
606
|
+
# Remove self from target's reverse index
|
|
607
|
+
target_ref = self.__dict__.get(f"_rel_{attr_name}")
|
|
608
|
+
if target_ref is not None:
|
|
609
|
+
for type_name in attr_value._get_allowed_types():
|
|
610
|
+
entity_class = db._entity_types.get(type_name)
|
|
611
|
+
if entity_class and db.load(type_name, target_ref):
|
|
612
|
+
db.reverse_index_remove(
|
|
613
|
+
type_name,
|
|
614
|
+
target_ref,
|
|
615
|
+
attr_value.reverse_name,
|
|
616
|
+
self._id,
|
|
617
|
+
)
|
|
618
|
+
break
|
|
619
|
+
# Also delete any reverse index where self is parent (inverse side)
|
|
620
|
+
db.reverse_index_delete(self._type, self._id, attr_name)
|
|
621
|
+
elif isinstance(attr_value, OneToMany):
|
|
622
|
+
# Delete the reverse index entry for this parent
|
|
623
|
+
db.reverse_index_delete(self._type, self._id, attr_name)
|
|
624
|
+
elif isinstance(attr_value, ManyToMany):
|
|
625
|
+
# Remove self from each related entity's reverse index
|
|
626
|
+
related_ids = db.reverse_index_get(self._type, self._id, attr_name)
|
|
627
|
+
for related_id in related_ids:
|
|
628
|
+
for type_name in attr_value._get_allowed_types():
|
|
629
|
+
entity_class = db._entity_types.get(type_name)
|
|
630
|
+
if entity_class and db.load(type_name, related_id):
|
|
631
|
+
db.reverse_index_remove(
|
|
632
|
+
type_name,
|
|
633
|
+
related_id,
|
|
634
|
+
attr_value.reverse_name,
|
|
635
|
+
self._id,
|
|
636
|
+
)
|
|
637
|
+
break
|
|
638
|
+
# Delete own index
|
|
639
|
+
db.reverse_index_delete(self._type, self._id, attr_name)
|
|
640
|
+
|
|
641
|
+
db.delete(self._type, self._id)
|
|
587
642
|
|
|
588
643
|
# Remove from entity registry
|
|
589
|
-
|
|
644
|
+
db.unregister_entity(self._type, self._id)
|
|
590
645
|
|
|
591
646
|
# Decrement the count when an entity is deleted
|
|
592
647
|
type_name = self.__class__.get_full_type_name()
|
|
593
648
|
count_key = f"{type_name}_count"
|
|
594
|
-
current_count = int(
|
|
649
|
+
current_count = int(db.load("_system", count_key) or 0)
|
|
595
650
|
if current_count > 0:
|
|
596
|
-
|
|
651
|
+
db.save("_system", count_key, str(current_count - 1))
|
|
597
652
|
else:
|
|
598
653
|
raise ValueError(
|
|
599
654
|
f"Entity count for {type_name} is already zero; cannot decrement further."
|
|
@@ -605,7 +660,7 @@ class Entity:
|
|
|
605
660
|
if hasattr(self, alias_field):
|
|
606
661
|
alias_value = getattr(self, alias_field)
|
|
607
662
|
if alias_value is not None:
|
|
608
|
-
|
|
663
|
+
db.delete(self._alias_key(), alias_value)
|
|
609
664
|
|
|
610
665
|
logger.debug(f"Deleted entity {self._type}@{self._id}")
|
|
611
666
|
|
|
@@ -651,61 +706,73 @@ class Entity:
|
|
|
651
706
|
return entity._id
|
|
652
707
|
|
|
653
708
|
def _serialize_full(self) -> Dict[str, Any]:
|
|
654
|
-
"""Full serialization including
|
|
709
|
+
"""Full serialization including relation FK references. Used by _save()."""
|
|
655
710
|
data = self._serialize_base()
|
|
656
711
|
|
|
657
|
-
from ic_python_db.properties import
|
|
658
|
-
|
|
659
|
-
for rel_name, rel_entities in self._relations.items():
|
|
660
|
-
if rel_entities:
|
|
661
|
-
rel_prop = getattr(self.__class__, rel_name, None)
|
|
662
|
-
is_to_many = isinstance(rel_prop, (OneToMany, ManyToMany))
|
|
712
|
+
from ic_python_db.properties import ManyToOne, OneToOne
|
|
663
713
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
714
|
+
for cls in reversed(self.__class__.__mro__):
|
|
715
|
+
for attr_name, attr_value in cls.__dict__.items():
|
|
716
|
+
if attr_name.startswith("_"):
|
|
717
|
+
continue
|
|
718
|
+
if isinstance(attr_value, (ManyToOne, OneToOne)):
|
|
719
|
+
ref = self.__dict__.get(f"_rel_{attr_name}")
|
|
720
|
+
if ref is not None:
|
|
721
|
+
data[attr_name] = ref
|
|
670
722
|
|
|
671
723
|
return data
|
|
672
724
|
|
|
725
|
+
def _resolve_ref_with_alias(self, entity_id: str, allowed_types: list) -> str:
|
|
726
|
+
"""Resolve an entity ID to its best reference (alias value or ID)."""
|
|
727
|
+
db = self.db()
|
|
728
|
+
for type_name in allowed_types:
|
|
729
|
+
entity_class = db._entity_types.get(type_name)
|
|
730
|
+
if entity_class:
|
|
731
|
+
entity = entity_class.load(entity_id)
|
|
732
|
+
if entity:
|
|
733
|
+
return self._get_entity_reference(entity)
|
|
734
|
+
return entity_id
|
|
735
|
+
|
|
673
736
|
def serialize(self) -> Dict[str, Any]:
|
|
674
737
|
"""Convert the entity to a portable serializable dictionary.
|
|
675
738
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
type serializes the reference, avoiding circular dependencies.
|
|
739
|
+
Includes: ManyToOne FK, owning-side OneToOne FK, ManyToMany entries.
|
|
740
|
+
Excludes: OneToMany (reconstructed from ManyToOne during import).
|
|
679
741
|
|
|
680
742
|
Returns:
|
|
681
743
|
Dict containing the entity's serializable data
|
|
682
744
|
"""
|
|
683
745
|
data = self._serialize_base()
|
|
684
746
|
|
|
685
|
-
from ic_python_db.properties import ManyToMany,
|
|
747
|
+
from ic_python_db.properties import ManyToMany, ManyToOne, OneToOne
|
|
686
748
|
|
|
687
|
-
|
|
688
|
-
if rel_entities:
|
|
689
|
-
rel_prop = getattr(self.__class__, rel_name, None)
|
|
749
|
+
db = self.db()
|
|
690
750
|
|
|
691
|
-
|
|
692
|
-
|
|
751
|
+
for cls in reversed(self.__class__.__mro__):
|
|
752
|
+
for attr_name, attr_value in cls.__dict__.items():
|
|
753
|
+
if attr_name.startswith("_"):
|
|
693
754
|
continue
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
755
|
+
if isinstance(attr_value, ManyToOne):
|
|
756
|
+
ref = self.__dict__.get(f"_rel_{attr_name}")
|
|
757
|
+
if ref is not None:
|
|
758
|
+
data[attr_name] = self._resolve_ref_with_alias(
|
|
759
|
+
ref, attr_value._get_allowed_types()
|
|
760
|
+
)
|
|
761
|
+
elif isinstance(attr_value, OneToOne):
|
|
762
|
+
ref = self.__dict__.get(f"_rel_{attr_name}")
|
|
763
|
+
if ref is not None:
|
|
764
|
+
data[attr_name] = self._resolve_ref_with_alias(
|
|
765
|
+
ref, attr_value._get_allowed_types()
|
|
766
|
+
)
|
|
767
|
+
elif isinstance(attr_value, ManyToMany):
|
|
768
|
+
related_ids = db.reverse_index_get(self._type, self._id, attr_name)
|
|
769
|
+
if related_ids:
|
|
770
|
+
data[attr_name] = [
|
|
771
|
+
self._resolve_ref_with_alias(
|
|
772
|
+
rid, attr_value._get_allowed_types()
|
|
773
|
+
)
|
|
774
|
+
for rid in related_ids
|
|
775
|
+
]
|
|
709
776
|
|
|
710
777
|
return data
|
|
711
778
|
|
|
@@ -970,70 +1037,3 @@ class Entity:
|
|
|
970
1037
|
Hash value
|
|
971
1038
|
"""
|
|
972
1039
|
return hash((self._type, self._id))
|
|
973
|
-
|
|
974
|
-
def add_relation(self, from_rel: str, to_rel: str, other: "Entity") -> None:
|
|
975
|
-
"""Add a bidirectional relationship with another entity.
|
|
976
|
-
|
|
977
|
-
Args:
|
|
978
|
-
from_rel: Name of relation from this entity to other
|
|
979
|
-
to_rel: Name of relation from other entity to this
|
|
980
|
-
other: Entity to create relationship with
|
|
981
|
-
"""
|
|
982
|
-
# Add forward relation
|
|
983
|
-
if from_rel not in self._relations:
|
|
984
|
-
self._relations[from_rel] = []
|
|
985
|
-
if other not in self._relations[from_rel]:
|
|
986
|
-
self._relations[from_rel].append(other)
|
|
987
|
-
|
|
988
|
-
# Add reverse relation
|
|
989
|
-
if to_rel not in other._relations:
|
|
990
|
-
other._relations[to_rel] = []
|
|
991
|
-
if self not in other._relations[to_rel]:
|
|
992
|
-
other._relations[to_rel].append(self)
|
|
993
|
-
|
|
994
|
-
# Save both entities
|
|
995
|
-
self._save()
|
|
996
|
-
other._save()
|
|
997
|
-
|
|
998
|
-
def get_relations(
|
|
999
|
-
self, relation_name: str, entity_type: str = None
|
|
1000
|
-
) -> List["Entity"]:
|
|
1001
|
-
"""Get all related entities for a relation, optionally filtered by type.
|
|
1002
|
-
|
|
1003
|
-
Args:
|
|
1004
|
-
relation_name: Name of the relation to follow
|
|
1005
|
-
entity_type: Optional type name to filter entities by
|
|
1006
|
-
|
|
1007
|
-
Returns:
|
|
1008
|
-
List of related entities
|
|
1009
|
-
"""
|
|
1010
|
-
if relation_name not in self._relations:
|
|
1011
|
-
return []
|
|
1012
|
-
|
|
1013
|
-
entities = self._relations[relation_name]
|
|
1014
|
-
if entity_type:
|
|
1015
|
-
entities = [e for e in entities if e._type == entity_type]
|
|
1016
|
-
|
|
1017
|
-
return entities
|
|
1018
|
-
|
|
1019
|
-
def remove_relation(self, from_rel: str, to_rel: str, other: "Entity") -> None:
|
|
1020
|
-
"""Remove a bidirectional relationship with another entity.
|
|
1021
|
-
|
|
1022
|
-
Args:
|
|
1023
|
-
from_rel: Name of relation from this entity to other
|
|
1024
|
-
to_rel: Name of relation from other entity to this
|
|
1025
|
-
other: Entity to remove relationship with
|
|
1026
|
-
"""
|
|
1027
|
-
# Remove forward relation
|
|
1028
|
-
if from_rel in self._relations:
|
|
1029
|
-
if other in self._relations[from_rel]:
|
|
1030
|
-
self._relations[from_rel].remove(other)
|
|
1031
|
-
|
|
1032
|
-
# Remove reverse relation
|
|
1033
|
-
if to_rel in other._relations:
|
|
1034
|
-
if self in other._relations[to_rel]:
|
|
1035
|
-
other._relations[to_rel].remove(self)
|
|
1036
|
-
|
|
1037
|
-
# Save both entities
|
|
1038
|
-
self._save()
|
|
1039
|
-
other._save()
|