ic-python-db 0.7.5__tar.gz → 0.7.7__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.
Files changed (24) hide show
  1. {ic_python_db-0.7.5/ic_python_db.egg-info → ic_python_db-0.7.7}/PKG-INFO +1 -1
  2. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/__init__.py +1 -1
  3. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/db_engine.py +6 -0
  4. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/entity.py +99 -27
  5. {ic_python_db-0.7.5 → ic_python_db-0.7.7/ic_python_db.egg-info}/PKG-INFO +1 -1
  6. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/pyproject.toml +1 -1
  7. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/setup.py +1 -1
  8. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/LICENSE +0 -0
  9. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/MANIFEST.in +0 -0
  10. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/README.md +0 -0
  11. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/_cdk.py +0 -0
  12. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/constants.py +0 -0
  13. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/context.py +0 -0
  14. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/hooks.py +0 -0
  15. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/mixins.py +0 -0
  16. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/properties.py +0 -0
  17. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/py.typed +0 -0
  18. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/storage.py +0 -0
  19. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db/system_time.py +0 -0
  20. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db.egg-info/SOURCES.txt +0 -0
  21. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db.egg-info/dependency_links.txt +0 -0
  22. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/ic_python_db.egg-info/top_level.txt +0 -0
  23. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/requirements-dev.txt +0 -0
  24. {ic_python_db-0.7.5 → ic_python_db-0.7.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic_python_db
3
- Version: 0.7.5
3
+ Version: 0.7.7
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
@@ -19,7 +19,7 @@ from .properties import (
19
19
  from .storage import MemoryStorage, Storage
20
20
  from .system_time import SystemTime
21
21
 
22
- __version__ = "0.7.5"
22
+ __version__ = "0.7.7"
23
23
  __all__ = [
24
24
  "Database",
25
25
  "Entity",
@@ -36,6 +36,12 @@ class Database:
36
36
  if cls._instance:
37
37
  raise RuntimeError("Database instance already exists")
38
38
  cls._instance = cls(audit_enabled, db_storage, db_audit)
39
+
40
+ # Flush any Entity subclasses that were defined before Database existed
41
+ from .entity import Entity
42
+
43
+ Entity._flush_deferred_types()
44
+
39
45
  return cls._instance
40
46
 
41
47
  def __init__(
@@ -91,10 +91,37 @@ class Entity:
91
91
 
92
92
  _entity_type = None # To be defined in subclasses
93
93
  _context: Set["Entity"] = set() # Set of entities in current context
94
+ _deferred_types: List[Type["Entity"]] = [] # Types defined before DB exists
94
95
  _do_not_save = False
95
96
  __version__ = 1 # Default schema version
96
97
  __namespace__: Optional[str] = None # Optional namespace for entity type
97
98
 
99
+ def __init_subclass__(cls, **kwargs):
100
+ """Auto-register Entity subclasses with the Database at class definition time."""
101
+ super().__init_subclass__(**kwargs)
102
+ db = Database._instance
103
+ if db is not None:
104
+ db.register_entity_type(cls, cls.get_full_type_name())
105
+ else:
106
+ # Database not initialized yet — defer registration
107
+ Entity._deferred_types.append(cls)
108
+
109
+ @classmethod
110
+ def _flush_deferred_types(cls):
111
+ """Register any Entity subclasses that were defined before Database existed."""
112
+ if not cls._deferred_types:
113
+ return
114
+ try:
115
+ db = Database.get_instance()
116
+ except Exception:
117
+ return
118
+ for deferred_cls in cls._deferred_types:
119
+ try:
120
+ db.register_entity_type(deferred_cls, deferred_cls.get_full_type_name())
121
+ except Exception:
122
+ pass
123
+ cls._deferred_types.clear()
124
+
98
125
  def __init__(self, **kwargs):
99
126
  """Initialize a new entity.
100
127
 
@@ -264,8 +291,8 @@ class Entity:
264
291
  )
265
292
  self._update_timestamps(caller_id)
266
293
 
267
- # Save to database
268
- data = self.serialize()
294
+ # Save to database (full serialization preserves all relations)
295
+ data = self._serialize_full()
269
296
 
270
297
  if not self._do_not_save:
271
298
  logger.debug(f"Saving entity {self._type}@{self._id} to database")
@@ -511,9 +538,14 @@ class Entity:
511
538
 
512
539
  while len(ret) < count and from_id <= cls.max_id():
513
540
  logger.info(f"Loading entity {from_id}")
514
- entity = cls.load(str(from_id))
515
- if entity:
516
- ret.append(entity)
541
+ try:
542
+ entity = cls.load(str(from_id))
543
+ if entity:
544
+ ret.append(entity)
545
+ except (ValueError, AttributeError) as e:
546
+ # Skip entities with broken/dangling relation references
547
+ # (full fix: issue #4 — lazy relation resolution)
548
+ logger.warning(f"Skipping {cls.__name__}@{from_id}: {e}")
517
549
  from_id += 1
518
550
 
519
551
  return ret
@@ -559,19 +591,15 @@ class Entity:
559
591
  # Remove from context
560
592
  self.__class__._context.discard(self)
561
593
 
562
- def serialize(self) -> Dict[str, Any]:
563
- """Convert the entity to a serializable dictionary.
564
-
565
- Returns:
566
- Dict containing the entity's serializable data
567
- """
594
+ def _serialize_base(self) -> Dict[str, Any]:
595
+ """Shared serialization logic: core data, properties, and instance attributes."""
568
596
  # Get mixin data first if available
569
597
  data = super().serialize() if hasattr(super(), "serialize") else {}
570
598
 
571
599
  # Add core entity data
572
600
  data.update(
573
601
  {
574
- "_type": self._type, # Use the entity type
602
+ "_type": self._type,
575
603
  "_id": self._id,
576
604
  }
577
605
  )
@@ -589,30 +617,74 @@ class Entity:
589
617
  if not k.startswith("_"):
590
618
  data[k] = v
591
619
 
592
- # Add relations as references (prefer alias over _id if available)
593
- def get_entity_reference(entity):
594
- """Get the best reference for an entity: alias value if available, otherwise _id."""
595
- if hasattr(entity.__class__, "__alias__") and entity.__class__.__alias__:
596
- alias_field = entity.__class__.__alias__
597
- alias_value = getattr(entity, alias_field, None)
598
- if alias_value is not None:
599
- return alias_value
600
- return entity._id
620
+ return data
621
+
622
+ @staticmethod
623
+ def _get_entity_reference(entity):
624
+ """Get the best reference for an entity: alias value if available, otherwise _id."""
625
+ if hasattr(entity.__class__, "__alias__") and entity.__class__.__alias__:
626
+ alias_field = entity.__class__.__alias__
627
+ alias_value = getattr(entity, alias_field, None)
628
+ if alias_value is not None:
629
+ return alias_value
630
+ return entity._id
631
+
632
+ def _serialize_full(self) -> Dict[str, Any]:
633
+ """Full serialization including all relations. Used by _save() for persistence."""
634
+ data = self._serialize_base()
635
+
636
+ from ic_python_db.properties import ManyToMany, OneToMany
601
637
 
602
638
  for rel_name, rel_entities in self._relations.items():
603
639
  if rel_entities:
604
- # Check if this is a *ToMany relation that should always be a list
605
640
  rel_prop = getattr(self.__class__, rel_name, None)
606
- from ic_python_db.properties import ManyToMany, OneToMany
641
+ is_to_many = isinstance(rel_prop, (OneToMany, ManyToMany))
642
+
643
+ if len(rel_entities) == 1 and not is_to_many:
644
+ data[rel_name] = self._get_entity_reference(rel_entities[0])
645
+ else:
646
+ data[rel_name] = [
647
+ self._get_entity_reference(e) for e in rel_entities
648
+ ]
649
+
650
+ return data
651
+
652
+ def serialize(self) -> Dict[str, Any]:
653
+ """Convert the entity to a portable serializable dictionary.
654
+
655
+ OneToMany relations are skipped (reconstructed from reverse ManyToOne).
656
+ For bilateral OneToOne relations, only the alphabetically-earlier entity
657
+ type serializes the reference, avoiding circular dependencies.
658
+
659
+ Returns:
660
+ Dict containing the entity's serializable data
661
+ """
662
+ data = self._serialize_base()
663
+
664
+ from ic_python_db.properties import ManyToMany, OneToMany, OneToOne
665
+
666
+ for rel_name, rel_entities in self._relations.items():
667
+ if rel_entities:
668
+ rel_prop = getattr(self.__class__, rel_name, None)
669
+
670
+ # Skip OneToMany — always reconstructed from reverse ManyToOne
671
+ if isinstance(rel_prop, OneToMany):
672
+ continue
673
+ # For OneToOne bilateral, only serialize on one deterministic side:
674
+ # the entity whose type name is alphabetically <= the target type.
675
+ if isinstance(rel_prop, OneToOne):
676
+ target_type = rel_entities[0]._type if rel_entities else None
677
+ if target_type and self._type > target_type:
678
+ continue
607
679
 
608
680
  is_to_many = isinstance(rel_prop, (OneToMany, ManyToMany))
609
681
 
610
682
  if len(rel_entities) == 1 and not is_to_many:
611
- # Single relation for OneToOne/ManyToOne - store as single reference
612
- data[rel_name] = get_entity_reference(rel_entities[0])
683
+ data[rel_name] = self._get_entity_reference(rel_entities[0])
613
684
  else:
614
- # Multiple relations or *ToMany relations - store as list of references
615
- data[rel_name] = [get_entity_reference(e) for e in rel_entities]
685
+ data[rel_name] = [
686
+ self._get_entity_reference(e) for e in rel_entities
687
+ ]
616
688
 
617
689
  return data
618
690
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic_python_db
3
- Version: 0.7.5
3
+ Version: 0.7.7
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ic_python_db"
7
- version = "0.7.5"
7
+ version = "0.7.7"
8
8
  description = "A lightweight key-value database written in Python, intended for use on the Internet Computer (IC)"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="ic_python_db",
8
- version="0.7.5",
8
+ version="0.7.7",
9
9
  author="Smart Social Contracts",
10
10
  author_email="smartsocialcontracts@gmail.com",
11
11
  description="A lightweight key-value database with entity relationships and audit logging",
File without changes
File without changes
File without changes
File without changes