ic-python-db 0.7.4__tar.gz → 0.7.6__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.4/ic_python_db.egg-info → ic_python_db-0.7.6}/PKG-INFO +1 -1
  2. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/__init__.py +1 -1
  3. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/entity.py +78 -28
  4. {ic_python_db-0.7.4 → ic_python_db-0.7.6/ic_python_db.egg-info}/PKG-INFO +1 -1
  5. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/pyproject.toml +1 -1
  6. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/setup.py +1 -1
  7. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/LICENSE +0 -0
  8. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/MANIFEST.in +0 -0
  9. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/README.md +0 -0
  10. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/_cdk.py +0 -0
  11. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/constants.py +0 -0
  12. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/context.py +0 -0
  13. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/db_engine.py +0 -0
  14. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/hooks.py +0 -0
  15. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/mixins.py +0 -0
  16. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/properties.py +0 -0
  17. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/py.typed +0 -0
  18. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/storage.py +0 -0
  19. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db/system_time.py +0 -0
  20. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db.egg-info/SOURCES.txt +0 -0
  21. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db.egg-info/dependency_links.txt +0 -0
  22. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/ic_python_db.egg-info/top_level.txt +0 -0
  23. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/requirements-dev.txt +0 -0
  24. {ic_python_db-0.7.4 → ic_python_db-0.7.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic_python_db
3
- Version: 0.7.4
3
+ Version: 0.7.6
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.4"
22
+ __version__ = "0.7.6"
23
23
  __all__ = [
24
24
  "Database",
25
25
  "Entity",
@@ -180,7 +180,8 @@ class Entity:
180
180
  setattr(self, k, v)
181
181
  self._do_not_save = False
182
182
 
183
- self._save()
183
+ if not self._loaded:
184
+ self._save()
184
185
 
185
186
  @classmethod
186
187
  def new(cls, **kwargs):
@@ -263,8 +264,8 @@ class Entity:
263
264
  )
264
265
  self._update_timestamps(caller_id)
265
266
 
266
- # Save to database
267
- data = self.serialize()
267
+ # Save to database (full serialization preserves all relations)
268
+ data = self._serialize_full()
268
269
 
269
270
  if not self._do_not_save:
270
271
  logger.debug(f"Saving entity {self._type}@{self._id} to database")
@@ -386,6 +387,10 @@ class Entity:
386
387
  # Create instance first
387
388
  entity = cls(**data, _loaded=True)
388
389
 
390
+ # If migration was applied, persist the migrated data
391
+ if stored_version != current_version:
392
+ entity._save()
393
+
389
394
  # Restore legacy "relations" block if present in serialized data.
390
395
  # Otherwise keep the _relations that __init__ already populated
391
396
  # via relationship descriptors (OneToMany, ManyToOne, etc.).
@@ -506,9 +511,14 @@ class Entity:
506
511
 
507
512
  while len(ret) < count and from_id <= cls.max_id():
508
513
  logger.info(f"Loading entity {from_id}")
509
- entity = cls.load(str(from_id))
510
- if entity:
511
- ret.append(entity)
514
+ try:
515
+ entity = cls.load(str(from_id))
516
+ if entity:
517
+ ret.append(entity)
518
+ except (ValueError, AttributeError) as e:
519
+ # Skip entities with broken/dangling relation references
520
+ # (full fix: issue #4 — lazy relation resolution)
521
+ logger.warning(f"Skipping {cls.__name__}@{from_id}: {e}")
512
522
  from_id += 1
513
523
 
514
524
  return ret
@@ -554,19 +564,15 @@ class Entity:
554
564
  # Remove from context
555
565
  self.__class__._context.discard(self)
556
566
 
557
- def serialize(self) -> Dict[str, Any]:
558
- """Convert the entity to a serializable dictionary.
559
-
560
- Returns:
561
- Dict containing the entity's serializable data
562
- """
567
+ def _serialize_base(self) -> Dict[str, Any]:
568
+ """Shared serialization logic: core data, properties, and instance attributes."""
563
569
  # Get mixin data first if available
564
570
  data = super().serialize() if hasattr(super(), "serialize") else {}
565
571
 
566
572
  # Add core entity data
567
573
  data.update(
568
574
  {
569
- "_type": self._type, # Use the entity type
575
+ "_type": self._type,
570
576
  "_id": self._id,
571
577
  }
572
578
  )
@@ -584,30 +590,74 @@ class Entity:
584
590
  if not k.startswith("_"):
585
591
  data[k] = v
586
592
 
587
- # Add relations as references (prefer alias over _id if available)
588
- def get_entity_reference(entity):
589
- """Get the best reference for an entity: alias value if available, otherwise _id."""
590
- if hasattr(entity.__class__, "__alias__") and entity.__class__.__alias__:
591
- alias_field = entity.__class__.__alias__
592
- alias_value = getattr(entity, alias_field, None)
593
- if alias_value is not None:
594
- return alias_value
595
- return entity._id
593
+ return data
594
+
595
+ @staticmethod
596
+ def _get_entity_reference(entity):
597
+ """Get the best reference for an entity: alias value if available, otherwise _id."""
598
+ if hasattr(entity.__class__, "__alias__") and entity.__class__.__alias__:
599
+ alias_field = entity.__class__.__alias__
600
+ alias_value = getattr(entity, alias_field, None)
601
+ if alias_value is not None:
602
+ return alias_value
603
+ return entity._id
604
+
605
+ def _serialize_full(self) -> Dict[str, Any]:
606
+ """Full serialization including all relations. Used by _save() for persistence."""
607
+ data = self._serialize_base()
608
+
609
+ from ic_python_db.properties import ManyToMany, OneToMany
610
+
611
+ for rel_name, rel_entities in self._relations.items():
612
+ if rel_entities:
613
+ rel_prop = getattr(self.__class__, rel_name, None)
614
+ is_to_many = isinstance(rel_prop, (OneToMany, ManyToMany))
615
+
616
+ if len(rel_entities) == 1 and not is_to_many:
617
+ data[rel_name] = self._get_entity_reference(rel_entities[0])
618
+ else:
619
+ data[rel_name] = [
620
+ self._get_entity_reference(e) for e in rel_entities
621
+ ]
622
+
623
+ return data
624
+
625
+ def serialize(self) -> Dict[str, Any]:
626
+ """Convert the entity to a portable serializable dictionary.
627
+
628
+ OneToMany relations are skipped (reconstructed from reverse ManyToOne).
629
+ For bilateral OneToOne relations, only the alphabetically-earlier entity
630
+ type serializes the reference, avoiding circular dependencies.
631
+
632
+ Returns:
633
+ Dict containing the entity's serializable data
634
+ """
635
+ data = self._serialize_base()
636
+
637
+ from ic_python_db.properties import ManyToMany, OneToMany, OneToOne
596
638
 
597
639
  for rel_name, rel_entities in self._relations.items():
598
640
  if rel_entities:
599
- # Check if this is a *ToMany relation that should always be a list
600
641
  rel_prop = getattr(self.__class__, rel_name, None)
601
- from ic_python_db.properties import ManyToMany, OneToMany
642
+
643
+ # Skip OneToMany — always reconstructed from reverse ManyToOne
644
+ if isinstance(rel_prop, OneToMany):
645
+ continue
646
+ # For OneToOne bilateral, only serialize on one deterministic side:
647
+ # the entity whose type name is alphabetically <= the target type.
648
+ if isinstance(rel_prop, OneToOne):
649
+ target_type = rel_entities[0]._type if rel_entities else None
650
+ if target_type and self._type > target_type:
651
+ continue
602
652
 
603
653
  is_to_many = isinstance(rel_prop, (OneToMany, ManyToMany))
604
654
 
605
655
  if len(rel_entities) == 1 and not is_to_many:
606
- # Single relation for OneToOne/ManyToOne - store as single reference
607
- data[rel_name] = get_entity_reference(rel_entities[0])
656
+ data[rel_name] = self._get_entity_reference(rel_entities[0])
608
657
  else:
609
- # Multiple relations or *ToMany relations - store as list of references
610
- data[rel_name] = [get_entity_reference(e) for e in rel_entities]
658
+ data[rel_name] = [
659
+ self._get_entity_reference(e) for e in rel_entities
660
+ ]
611
661
 
612
662
  return data
613
663
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic_python_db
3
- Version: 0.7.4
3
+ Version: 0.7.6
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.4"
7
+ version = "0.7.6"
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.4",
8
+ version="0.7.6",
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