ic-python-db 0.11.0__tar.gz → 0.12.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.
Files changed (26) hide show
  1. {ic_python_db-0.11.0/ic_python_db.egg-info → ic_python_db-0.12.0}/PKG-INFO +2 -1
  2. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/README.md +1 -0
  3. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/__init__.py +1 -1
  4. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/db_engine.py +128 -1
  5. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/entity.py +50 -30
  6. ic_python_db-0.12.0/ic_python_db/storage.py +118 -0
  7. {ic_python_db-0.11.0 → ic_python_db-0.12.0/ic_python_db.egg-info}/PKG-INFO +2 -1
  8. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/pyproject.toml +1 -1
  9. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/setup.py +1 -1
  10. ic_python_db-0.11.0/ic_python_db/storage.py +0 -69
  11. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/LICENSE +0 -0
  12. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/MANIFEST.in +0 -0
  13. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/_cdk.py +0 -0
  14. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/constants.py +0 -0
  15. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/context.py +0 -0
  16. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/hooks.py +0 -0
  17. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/mixins.py +0 -0
  18. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/properties.py +0 -0
  19. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/py.typed +0 -0
  20. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/schema.py +0 -0
  21. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db/system_time.py +0 -0
  22. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db.egg-info/SOURCES.txt +0 -0
  23. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db.egg-info/dependency_links.txt +0 -0
  24. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/ic_python_db.egg-info/top_level.txt +0 -0
  25. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/requirements-dev.txt +0 -0
  26. {ic_python_db-0.11.0 → ic_python_db-0.12.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic-python-db
3
- Version: 0.11.0
3
+ Version: 0.12.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
@@ -60,6 +60,7 @@ A lightweight key-value database with entity relationships and audit logging cap
60
60
  ## Features
61
61
 
62
62
  - **Persistent Storage**: Works with StableBTreeMap stable structure for persistent storage on your canister's stable memory so your data persists automatically across canister upgrades.
63
+ - **Range-based paging**: `load_some`, `find`, `instances` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id.
63
64
  - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
64
65
  - **Schema Versioning & Upgrade Safety**: Automatic schema introspection, compatibility checking, and auto-migration for safe changes. Breaking changes without a `migrate()` method are rejected, preventing data corruption on canister upgrades.
65
66
  - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
@@ -11,6 +11,7 @@ A lightweight key-value database with entity relationships and audit logging cap
11
11
  ## Features
12
12
 
13
13
  - **Persistent Storage**: Works with StableBTreeMap stable structure for persistent storage on your canister's stable memory so your data persists automatically across canister upgrades.
14
+ - **Range-based paging**: `load_some`, `find`, `instances` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id.
14
15
  - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
15
16
  - **Schema Versioning & Upgrade Safety**: Automatic schema introspection, compatibility checking, and auto-migration for safe changes. Breaking changes without a `migrate()` method are rejected, preventing data corruption on canister upgrades.
16
17
  - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
@@ -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.11.0"
23
+ __version__ = "0.12.0"
24
24
  __all__ = [
25
25
  "Database",
26
26
  "Entity",
@@ -9,10 +9,41 @@ from typing import Any, Dict, List, Optional, Tuple
9
9
 
10
10
  from ic_python_logging import get_logger
11
11
 
12
- from .storage import MemoryStorage, Storage
12
+ from .storage import MemoryStorage, Storage, supports_range
13
13
 
14
14
  logger = get_logger(__name__)
15
15
 
16
+ _DIGITS = frozenset("0123456789")
17
+
18
+
19
+ def _is_sequential_id(id_str: str) -> bool:
20
+ """True for canonical sequential ids: ASCII digits, no leading zero."""
21
+ return bool(id_str) and id_str[0] != "0" and all(c in _DIGITS for c in id_str)
22
+
23
+
24
+ def _next_sequential_id(id_str: str, digits: int) -> Optional[int]:
25
+ """Smallest ``digits``-digit sequential id that sorts after ``id_str``.
26
+
27
+ ``id_str`` is a key suffix returned by a range over the ``digits``-length
28
+ bucket, so it starts with a digit but may be a custom id such as
29
+ ``"1x"`` or ``"1-a"``. Returns None when no such id exists in the bucket.
30
+ """
31
+ i = 0
32
+ while i < len(id_str) and id_str[i] in _DIGITS:
33
+ i += 1
34
+ numeric_prefix, rest = id_str[:i], id_str[i:]
35
+ if not rest:
36
+ candidate = str(int(numeric_prefix) + 1)
37
+ elif rest[0] > "9":
38
+ # "1x" > every "1<digit>": jump to the next prefix, zero-filled
39
+ candidate = str(int(numeric_prefix) + 1) + "0" * len(rest)
40
+ else:
41
+ # "1-a" < "10": the zero-filled prefix is the next candidate
42
+ candidate = numeric_prefix + "0" * len(rest)
43
+ if len(candidate) > digits:
44
+ return None
45
+ return max(int(candidate), 10 ** (digits - 1))
46
+
16
47
 
17
48
  class Database:
18
49
  """Main database class providing high-level operations"""
@@ -181,6 +212,102 @@ class Database:
181
212
  return json.loads(data)
182
213
  return None
183
214
 
215
+ def max_id(self, type_name: str) -> int:
216
+ """Highest sequential id ever issued for ``type_name`` (0 if none)."""
217
+ raw = self.load("_system", f"{type_name}_id")
218
+ return int(raw) if raw else 0
219
+
220
+ def load_page(
221
+ self,
222
+ type_name: str,
223
+ from_id: int,
224
+ count: int,
225
+ max_id: Optional[int] = None,
226
+ ) -> List[Tuple[str, dict]]:
227
+ """Persisted entities of ``type_name`` with sequential id >= ``from_id``.
228
+
229
+ Returns up to ``count`` ``(id, data)`` pairs in ascending numeric id
230
+ order, skipping ids that were deleted. Only canonical sequential ids
231
+ (``"1"``, ``"42"``; no leading zeros, no custom string ids) are
232
+ returned, matching what probing ``1..max_id`` would find.
233
+
234
+ When the storage backend supports ``range`` (Basilisk's
235
+ ``StableBTreeMap``, ``MemoryStorage``) a page costs one range call per
236
+ digit-count bucket touched (usually one), independent of how many ids
237
+ were deleted. Otherwise it falls back to one ``get`` per id.
238
+ """
239
+ if from_id < 1:
240
+ raise ValueError("from_id must be at least 1")
241
+ if count < 1:
242
+ raise ValueError("count must be at least 1")
243
+ if max_id is None:
244
+ max_id = self.max_id(type_name)
245
+ if max_id < from_id:
246
+ return []
247
+ if supports_range(self._db_storage):
248
+ return self._load_page_range(type_name, from_id, count, max_id)
249
+ return self._load_page_probe(type_name, from_id, count, max_id)
250
+
251
+ def _load_page_probe(
252
+ self, type_name: str, from_id: int, count: int, max_id: int
253
+ ) -> List[Tuple[str, dict]]:
254
+ results: List[Tuple[str, dict]] = []
255
+ entity_id = from_id
256
+ while len(results) < count and entity_id <= max_id:
257
+ raw = self._db_storage.get(f"{type_name}@{entity_id}")
258
+ if raw:
259
+ results.append((str(entity_id), json.loads(raw)))
260
+ entity_id += 1
261
+ return results
262
+
263
+ def _load_page_range(
264
+ self, type_name: str, from_id: int, count: int, max_id: int
265
+ ) -> List[Tuple[str, dict]]:
266
+ # Keys are "Type@<id>" with unpadded decimal ids. The backend orders
267
+ # str keys by byte length first, then bytewise (see storage_sort_key),
268
+ # so all ids with the same digit count form one contiguous, numerically
269
+ # ordered run. Walk one run ("bucket") per digit count, from the digit
270
+ # count of from_id up to that of max_id.
271
+ prefix = f"{type_name}@"
272
+ results: List[Tuple[str, dict]] = []
273
+ digits = len(str(from_id))
274
+ max_digits = len(str(max_id))
275
+ while digits <= max_digits and len(results) < count:
276
+ lo_id = max(from_id, 10 ** (digits - 1))
277
+ if digits == max_digits:
278
+ # Exclusive upper bound right after max_id (same digit count),
279
+ # or the whole bucket if max_id is the largest d-digit number.
280
+ hi_id = max_id + 1
281
+ end = (
282
+ f"{prefix}{hi_id}"
283
+ if len(str(hi_id)) == digits
284
+ else f"{prefix}{':' * digits}"
285
+ )
286
+ else:
287
+ # ':' is the character after '9': every same-length key of
288
+ # the form "Type@<digits>" sorts before "Type@:::".
289
+ end = f"{prefix}{':' * digits}"
290
+
291
+ start = f"{prefix}{lo_id}"
292
+ plen = len(prefix)
293
+ while len(results) < count:
294
+ need = count - len(results)
295
+ page = self._db_storage.range(start, end, need)
296
+ for key, raw in page:
297
+ id_str = key[plen:]
298
+ if _is_sequential_id(id_str):
299
+ results.append((id_str, json.loads(raw)))
300
+ if len(page) < need:
301
+ break # bucket exhausted
302
+ # Page was full but may have contained skipped custom ids:
303
+ # continue from the next possible sequential id in this bucket.
304
+ next_id = _next_sequential_id(page[-1][0][plen:], digits)
305
+ if next_id is None:
306
+ break
307
+ start = f"{prefix}{next_id}"
308
+ digits += 1
309
+ return results
310
+
184
311
  def delete(self, type_name: str, entity_id: str) -> None:
185
312
  """Delete the data associated with the key
186
313
 
@@ -176,8 +176,11 @@ class Entity:
176
176
  next_id = str(int(current_id) + 1)
177
177
  self._id = next_id
178
178
  db.save("_system", f"{type_name}_id", self._id)
179
- else:
180
- # Update max_id if custom ID is higher than current max
179
+ elif not self._loaded:
180
+ # Update max_id if custom ID is higher than current max.
181
+ # Skipped when hydrating persisted state: that id already went
182
+ # through this check when it was first created, so re-reading the
183
+ # counter would only add one storage read per loaded entity.
181
184
  db = self.db()
182
185
  type_name = self._type
183
186
  current_max_id = db.load("_system", f"{type_name}_id")
@@ -418,6 +421,16 @@ class Entity:
418
421
  if not data:
419
422
  return None
420
423
 
424
+ return cls._hydrate(entity_id, data)
425
+
426
+ @classmethod
427
+ def _hydrate(cls: Type[T], entity_id: str, data: dict) -> T:
428
+ """Build an instance from its persisted dict, migrating if needed.
429
+
430
+ Shared by ``load`` (single ``get``) and ``load_some`` (range page).
431
+ Callers must have checked the identity registry first.
432
+ """
433
+ type_name = cls.get_full_type_name()
421
434
  stored_version = data.get("__version__", 1)
422
435
  current_version = cls.__version__
423
436
 
@@ -582,19 +595,17 @@ class Entity:
582
595
 
583
596
  db = cls.db()
584
597
  type_name = cls.get_full_type_name()
585
- end = min(from_id + batch - 1, max_id)
586
- for entity_id in range(from_id, end + 1):
587
- try:
588
- entity = cls.load(str(entity_id))
589
- except (ValueError, AttributeError):
590
- continue
591
- if entity is None:
592
- continue
598
+ # One range read per batch of *live* entities (deleted ids cost nothing)
599
+ entities = cls.load_some(from_id, batch)
600
+ for entity in entities:
593
601
  value = getattr(entity, field, None)
594
602
  if value is not None:
595
603
  db.field_index_add(type_name, field, str(value), entity._id)
596
604
 
597
- return end + 1 if end < max_id else None
605
+ if len(entities) < batch:
606
+ return None
607
+ next_from_id = int(entities[-1]._id) + 1
608
+ return next_from_id if next_from_id <= max_id else None
598
609
 
599
610
  @classmethod
600
611
  def instances(cls: Type[T]) -> List[T]:
@@ -676,11 +687,7 @@ class Entity:
676
687
  Returns:
677
688
  int: Maximum entity ID
678
689
  """
679
- type_name = cls.get_full_type_name()
680
- db = cls.db()
681
- max_id_key = f"{type_name}_id"
682
- max_id = db.load("_system", max_id_key)
683
- return int(max_id) if max_id else 0
690
+ return cls.db().max_id(cls.get_full_type_name())
684
691
 
685
692
  @classmethod
686
693
  def load_some(
@@ -698,29 +705,42 @@ class Entity:
698
705
  List[T]: List of entities for the requested page
699
706
 
700
707
  Raises:
701
- ValueError: If page or page_size is less than 1
708
+ ValueError: If from_id or count is less than 1
709
+
710
+ Cost: with a range-capable storage backend (Basilisk's StableBTreeMap,
711
+ MemoryStorage) this is one ordered range read per page regardless of
712
+ how many ids in between were deleted; otherwise one read per id.
702
713
  """
703
- logger.info(f"Loading entities from {from_id} to {from_id + count}")
714
+ logger.debug(f"Loading up to {count} entities from id {from_id}")
704
715
 
705
716
  if from_id < 1:
706
717
  raise ValueError("from_id must be at least 1")
707
718
  if count < 1:
708
719
  raise ValueError("count must be at least 1")
709
720
 
710
- # Return the slice of entities for the requested page
711
- ret = []
721
+ db = cls.db()
722
+ type_name = cls.get_full_type_name()
712
723
 
713
- while len(ret) < count and from_id <= cls.max_id():
714
- logger.info(f"Loading entity {from_id}")
715
- try:
716
- entity = cls.load(str(from_id))
717
- if entity:
724
+ ret: List[T] = []
725
+ max_id = db.max_id(type_name)
726
+ while len(ret) < count:
727
+ need = count - len(ret)
728
+ page = db.load_page(type_name, from_id, need, max_id=max_id)
729
+ for entity_id, data in page:
730
+ try:
731
+ # Identity map first, so callers keep getting the same instance
732
+ entity = db.get_entity(type_name, entity_id)
733
+ if entity is None:
734
+ entity = cls._hydrate(entity_id, data)
718
735
  ret.append(entity)
719
- except (ValueError, AttributeError) as e:
720
- # Skip entities with broken/dangling relation references
721
- # (full fix: issue #4 — lazy relation resolution)
722
- logger.warning(f"Skipping {cls.__name__}@{from_id}: {e}")
723
- from_id += 1
736
+ except (ValueError, AttributeError) as e:
737
+ # Skip entities with broken/dangling relation references
738
+ # (full fix: issue #4 — lazy relation resolution)
739
+ logger.warning(f"Skipping {cls.__name__}@{entity_id}: {e}")
740
+ if len(page) < need:
741
+ break # no more persisted entities of this type
742
+ # Page was full but something was skipped: continue after it
743
+ from_id = int(page[-1][0]) + 1
724
744
 
725
745
  return ret
726
746
 
@@ -0,0 +1,118 @@
1
+ """
2
+ Storage backends for IC Python DB
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, Iterator, List, Optional, Tuple
7
+
8
+
9
+ def storage_sort_key(key: str) -> Tuple[int, bytes]:
10
+ """Sort key reproducing the on-chain order of ``str`` keys.
11
+
12
+ Basilisk's ``StableBTreeMap`` stores a ``str`` key as
13
+ ``tag + 4-byte big-endian length + utf-8``, so the B-tree orders keys by
14
+ byte length first and bytewise second: ``"T@9" < "T@10" < "T@2x"``.
15
+ ``MemoryStorage.range`` uses this so local tests see exactly the order a
16
+ canister will.
17
+ """
18
+ raw = key.encode("utf-8")
19
+ return (len(raw), raw)
20
+
21
+
22
+ def supports_range(storage) -> bool:
23
+ """True if ``storage`` offers ``range(start, end, limit)``.
24
+
25
+ Basilisk's ``StableBTreeMap`` (>= the version that added ``range``) and
26
+ ``MemoryStorage`` do; older CDK builds and simple dict-backed test
27
+ doubles don't, and callers fall back to key probing.
28
+ """
29
+ return callable(getattr(storage, "range", None))
30
+
31
+
32
+ class Storage(ABC):
33
+ """Abstract base class for storage backends
34
+
35
+ ``range`` is optional: backends that can walk keys in order should
36
+ implement it (see ``storage_sort_key`` for the order that is expected);
37
+ callers must check ``supports_range`` and fall back to ``get`` probing.
38
+ """
39
+
40
+ @abstractmethod
41
+ def insert(self, key: str, value: str) -> None:
42
+ """Insert a key-value pair into storage"""
43
+ raise NotImplementedError
44
+
45
+ @abstractmethod
46
+ def get(self, key: str) -> Optional[str]:
47
+ """Retrieve a value by key"""
48
+ raise NotImplementedError
49
+
50
+ @abstractmethod
51
+ def remove(self, key: str) -> None:
52
+ """Remove a key-value pair from storage"""
53
+ raise NotImplementedError
54
+
55
+ @abstractmethod
56
+ def items(self) -> Iterator[Tuple[str, str]]:
57
+ """Return all items in storage"""
58
+ raise NotImplementedError
59
+
60
+ @abstractmethod
61
+ def __contains__(self, key: str) -> bool:
62
+ """Check if key exists in storage"""
63
+ raise NotImplementedError
64
+
65
+ @abstractmethod
66
+ def keys(self) -> Iterator[str]:
67
+ """Return all keys in storage"""
68
+ raise NotImplementedError
69
+
70
+
71
+ class MemoryStorage(Storage):
72
+ """In-memory storage implementation using Python dictionary"""
73
+
74
+ def __init__(self):
75
+ self._data: Dict[str, str] = {}
76
+
77
+ def insert(self, key: str, value: str) -> None:
78
+ self._data[key] = value
79
+
80
+ def get(self, key: str) -> Optional[str]:
81
+ return self._data.get(key)
82
+
83
+ def remove(self, key: str) -> None:
84
+ if key in self._data:
85
+ del self._data[key]
86
+ else:
87
+ raise KeyError(f"Key '{key}' not found in storage")
88
+
89
+ def items(self) -> Iterator[Tuple[str, str]]:
90
+ return iter(self._data.items())
91
+
92
+ def __contains__(self, key: str) -> bool:
93
+ return key in self._data
94
+
95
+ def keys(self) -> Iterator[str]:
96
+ """Return all keys in storage"""
97
+ return iter(self._data.keys())
98
+
99
+ def range(
100
+ self, start: str, end: Optional[str] = None, limit: int = 1000
101
+ ) -> List[Tuple[str, str]]:
102
+ """Ordered page of ``(key, value)`` with ``start <= key < end``.
103
+
104
+ Same contract as Basilisk's ``StableBTreeMap.range``: half-open,
105
+ ``end=None`` means unbounded, keys ordered by ``storage_sort_key``.
106
+ O(n log n) here, which is fine for an in-memory test backend.
107
+ """
108
+ if limit < 0:
109
+ raise ValueError("limit must be >= 0")
110
+ lo = storage_sort_key(start)
111
+ hi = None if end is None else storage_sort_key(end)
112
+ selected = [
113
+ k
114
+ for k in self._data
115
+ if lo <= storage_sort_key(k) and (hi is None or storage_sort_key(k) < hi)
116
+ ]
117
+ selected.sort(key=storage_sort_key)
118
+ return [(k, self._data[k]) for k in selected[:limit]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic-python-db
3
- Version: 0.11.0
3
+ Version: 0.12.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
@@ -60,6 +60,7 @@ A lightweight key-value database with entity relationships and audit logging cap
60
60
  ## Features
61
61
 
62
62
  - **Persistent Storage**: Works with StableBTreeMap stable structure for persistent storage on your canister's stable memory so your data persists automatically across canister upgrades.
63
+ - **Range-based paging**: `load_some`, `find`, `instances` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id.
63
64
  - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
64
65
  - **Schema Versioning & Upgrade Safety**: Automatic schema introspection, compatibility checking, and auto-migration for safe changes. Breaking changes without a `migrate()` method are rejected, preventing data corruption on canister upgrades.
65
66
  - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ic-python-db"
7
- version = "0.11.0"
7
+ version = "0.12.0"
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.11.0",
8
+ version="0.12.0",
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",
@@ -1,69 +0,0 @@
1
- """
2
- Storage backends for IC Python DB
3
- """
4
-
5
- from abc import ABC, abstractmethod
6
- from typing import Dict, Iterator, Optional, Tuple
7
-
8
-
9
- class Storage(ABC):
10
- """Abstract base class for storage backends"""
11
-
12
- @abstractmethod
13
- def insert(self, key: str, value: str) -> None:
14
- """Insert a key-value pair into storage"""
15
- raise NotImplementedError
16
-
17
- @abstractmethod
18
- def get(self, key: str) -> Optional[str]:
19
- """Retrieve a value by key"""
20
- raise NotImplementedError
21
-
22
- @abstractmethod
23
- def remove(self, key: str) -> None:
24
- """Remove a key-value pair from storage"""
25
- raise NotImplementedError
26
-
27
- @abstractmethod
28
- def items(self) -> Iterator[Tuple[str, str]]:
29
- """Return all items in storage"""
30
- raise NotImplementedError
31
-
32
- @abstractmethod
33
- def __contains__(self, key: str) -> bool:
34
- """Check if key exists in storage"""
35
- raise NotImplementedError
36
-
37
- @abstractmethod
38
- def keys(self) -> Iterator[str]:
39
- """Return all keys in storage"""
40
- raise NotImplementedError
41
-
42
-
43
- class MemoryStorage(Storage):
44
- """In-memory storage implementation using Python dictionary"""
45
-
46
- def __init__(self):
47
- self._data: Dict[str, str] = {}
48
-
49
- def insert(self, key: str, value: str) -> None:
50
- self._data[key] = value
51
-
52
- def get(self, key: str) -> Optional[str]:
53
- return self._data.get(key)
54
-
55
- def remove(self, key: str) -> None:
56
- if key in self._data:
57
- del self._data[key]
58
- else:
59
- raise KeyError(f"Key '{key}' not found in storage")
60
-
61
- def items(self) -> Iterator[Tuple[str, str]]:
62
- return iter(self._data.items())
63
-
64
- def __contains__(self, key: str) -> bool:
65
- return key in self._data
66
-
67
- def keys(self) -> Iterator[str]:
68
- """Return all keys in storage"""
69
- return iter(self._data.keys())
File without changes
File without changes
File without changes