beaver-db 0.24.5__tar.gz → 0.25.2__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.

Potentially problematic release.


This version of beaver-db might be problematic. Click here for more details.

Files changed (108) hide show
  1. {beaver_db-0.24.5 → beaver_db-0.25.2}/PKG-INFO +1 -1
  2. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/__init__.py +1 -1
  3. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/blobs.py +42 -45
  4. beaver_db-0.25.2/beaver/cache.py +275 -0
  5. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/core.py +87 -99
  6. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/dicts.py +31 -28
  7. beaver_db-0.25.2/beaver/lists.py +342 -0
  8. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/logs.py +13 -13
  9. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/manager.py +7 -0
  10. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/queues.py +57 -47
  11. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/types.py +1 -1
  12. beaver_db-0.25.2/dockerfile +58 -0
  13. {beaver_db-0.24.5/issues → beaver_db-0.25.2/issues/closed}/16-add-clear-method-for-all-data-structures.md +1 -1
  14. {beaver_db-0.24.5/issues → beaver_db-0.25.2/issues/closed}/17-replace-atomic-operations-with-custom-beaver-lock.md +1 -1
  15. {beaver_db-0.24.5/issues → beaver_db-0.25.2/issues/closed}/21-implement-dbcache-property-with-wal-invalidation-dummy-fallback-and-performance-stats.md +1 -1
  16. {beaver_db-0.24.5 → beaver_db-0.25.2}/makefile +9 -2
  17. {beaver_db-0.24.5 → beaver_db-0.25.2}/pyproject.toml +1 -1
  18. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/conftest.py +17 -0
  19. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/integration/test_cache.py +4 -2
  20. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/integration/test_realtime.py +29 -36
  21. beaver_db-0.24.5/beaver/cache.py +0 -146
  22. beaver_db-0.24.5/beaver/lists.py +0 -345
  23. beaver_db-0.24.5/dockerfile +0 -28
  24. {beaver_db-0.24.5 → beaver_db-0.25.2}/.dockerignore +0 -0
  25. {beaver_db-0.24.5 → beaver_db-0.25.2}/.github/workflows/release.yaml +0 -0
  26. {beaver_db-0.24.5 → beaver_db-0.25.2}/.github/workflows/tests.yaml +0 -0
  27. {beaver_db-0.24.5 → beaver_db-0.25.2}/.gitignore +0 -0
  28. {beaver_db-0.24.5 → beaver_db-0.25.2}/.python-version +0 -0
  29. {beaver_db-0.24.5 → beaver_db-0.25.2}/.vscode/settings.json +0 -0
  30. {beaver_db-0.24.5 → beaver_db-0.25.2}/LICENSE +0 -0
  31. {beaver_db-0.24.5 → beaver_db-0.25.2}/README.md +0 -0
  32. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/channels.py +0 -0
  33. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/__init__.py +0 -0
  34. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/blobs.py +0 -0
  35. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/channels.py +0 -0
  36. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/collections.py +0 -0
  37. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/dicts.py +0 -0
  38. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/lists.py +0 -0
  39. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/locks.py +0 -0
  40. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/logs.py +0 -0
  41. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/queues.py +0 -0
  42. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/client.py +0 -0
  43. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/collections.py +0 -0
  44. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/locks.py +0 -0
  45. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/server.py +0 -0
  46. {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/vectors.py +0 -0
  47. {beaver_db-0.24.5 → beaver_db-0.25.2}/design.md +0 -0
  48. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/.gitignore +0 -0
  49. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/_quarto.yml +0 -0
  50. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/cover.png +0 -0
  51. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/dev-architecture.qmd +0 -0
  52. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/dev-concurrency.qmd +0 -0
  53. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/dev-contributing.qmd +0 -0
  54. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/dev-search.qmd +0 -0
  55. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-collections.qmd +0 -0
  56. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-concurrency.qmd +0 -0
  57. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-deployment.qmd +0 -0
  58. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-dicts-blobs.qmd +0 -0
  59. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-lists-queues.qmd +0 -0
  60. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-realtime.qmd +0 -0
  61. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/index.qmd +0 -0
  62. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/logo.png +0 -0
  63. {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/quickstart.qmd +0 -0
  64. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/async_pubsub.py +0 -0
  65. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/blobs.py +0 -0
  66. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/cache.py +0 -0
  67. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/fts.py +0 -0
  68. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/fuzzy.py +0 -0
  69. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/general_test.py +0 -0
  70. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/graph.py +0 -0
  71. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/kvstore.py +0 -0
  72. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/list.py +0 -0
  73. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/locks.py +0 -0
  74. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/logs.py +0 -0
  75. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/pqueue.py +0 -0
  76. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/producer_consumer.py +0 -0
  77. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/publisher.py +0 -0
  78. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/pubsub.py +0 -0
  79. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/rerank.py +0 -0
  80. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/stress_vectors.py +0 -0
  81. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/subscriber.py +0 -0
  82. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/textual_chat.css +0 -0
  83. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/textual_chat.py +0 -0
  84. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/type_hints.py +0 -0
  85. {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/vector.py +0 -0
  86. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/13-adopt-pydantic-deprecate-beavermodel-and-refactor-document-to-be-generic.md +0 -0
  87. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/15-enhance-cli-with-admin-commands-shell-piping-and-interactivity.md +0 -0
  88. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/18-enhanced-dump-and-load-for-etl.md +0 -0
  89. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/19-add-comprehensive-unit-integration-and-concurrency-test-suite.md +0 -0
  90. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/2-comprehensive-async-wrappers.md +0 -0
  91. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/6-drop-in-replacement-for-beaver-rest-server-client.md +0 -0
  92. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/1-refactor-vector-store-to-use-faiss.md +0 -0
  93. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/10-expose-dblock-functionality-on-all-high-level-data-managers.md +0 -0
  94. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/12-add-dump-method-for-json-export-to-all-data-managers.md +0 -0
  95. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/14-deprecate-fire-based-cli-and-build-a-feature-rich-typer-and-rich-cli.md +0 -0
  96. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/5-add-dblock-for-inter-process-synchronization.md +0 -0
  97. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/7-replace-faiss-with-simpler-linear-numpy-vectorial-search.md +0 -0
  98. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/8-first-class-synchronization-primitive.md +0 -0
  99. {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/9-type-safe-wrappers-based-on-pydantic-compatible-models.md +0 -0
  100. {beaver_db-0.24.5 → beaver_db-0.25.2}/logo.png +0 -0
  101. {beaver_db-0.24.5 → beaver_db-0.25.2}/pytest.ini +0 -0
  102. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_blob_manager.py +0 -0
  103. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_collection_manager.py +0 -0
  104. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_dict_manager.py +0 -0
  105. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_list_manager.py +0 -0
  106. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_log_manager.py +0 -0
  107. {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_queue_manager.py +0 -0
  108. {beaver_db-0.24.5 → beaver_db-0.25.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beaver-db
3
- Version: 0.24.5
3
+ Version: 0.25.2
4
4
  Summary: Fast, embedded, and multi-modal DB based on SQLite for AI-powered applications.
5
5
  License-File: LICENSE
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -2,7 +2,7 @@ from .core import BeaverDB
2
2
  from .types import Model
3
3
  from .collections import Document, WalkDirection
4
4
 
5
- __version__ = "0.24.5"
5
+ __version__ = "0.25.2"
6
6
 
7
7
  __all__ = [
8
8
  "BeaverDB",
@@ -17,36 +17,15 @@ class Blob[M](NamedTuple):
17
17
  class BlobManager[M: JsonSerializable](ManagerBase[M]):
18
18
  """A wrapper providing a Pythonic interface to a blob store in the database."""
19
19
 
20
- def put(self, key: str, data: bytes, metadata: Optional[M] = None):
21
- """
22
- Stores or replaces a blob in the store.
23
-
24
- Args:
25
- key: The unique string identifier for the blob.
26
- data: The binary data to store.
27
- metadata: Optional JSON-serializable dictionary for metadata.
28
- """
29
- if not isinstance(data, bytes):
30
- raise TypeError("Blob data must be of type bytes.")
31
-
32
- metadata_json = self._serialize(metadata) if metadata else None
33
-
34
- with self.connection:
35
- self.connection.execute(
36
- "INSERT OR REPLACE INTO beaver_blobs (store_name, key, data, metadata) VALUES (?, ?, ?, ?)",
37
- (self._name, key, data, metadata_json),
38
- )
39
-
20
+ @synced
40
21
  def get(self, key: str) -> Optional[Blob[M]]:
41
- """
42
- Retrieves a blob from the store.
22
+ """Retrieves a blob from the store."""
23
+ # --- 1. Check cache first ---
24
+ cached_blob = self.cache.get(key)
25
+ if cached_blob is not None:
26
+ return cached_blob # Cache HIT
43
27
 
44
- Args:
45
- key: The unique string identifier for the blob.
46
-
47
- Returns:
48
- A Blob object containing the data and metadata, or None if the key is not found.
49
- """
28
+ # --- 2. Cache MISS ---
50
29
  cursor = self.connection.cursor()
51
30
  cursor.execute(
52
31
  "SELECT data, metadata FROM beaver_blobs WHERE store_name = ? AND key = ?",
@@ -61,23 +40,43 @@ class BlobManager[M: JsonSerializable](ManagerBase[M]):
61
40
  data, metadata_json = result
62
41
  metadata = self._deserialize(metadata_json) if metadata_json else None
63
42
 
64
- return Blob(key=key, data=data, metadata=metadata)
43
+ # --- 3. Create object and populate cache ---
44
+ blob_obj = Blob(key=key, data=data, metadata=metadata)
45
+ self.cache.set(key, blob_obj)
65
46
 
66
- def delete(self, key: str):
67
- """
68
- Deletes a blob from the store.
47
+ return blob_obj
48
+
49
+ @synced
50
+ def put(self, key: str, data: bytes, metadata: Optional[M] = None):
51
+ """Stores or replaces a blob in the store."""
52
+ if not isinstance(data, bytes):
53
+ raise TypeError("Blob data must be of type bytes.")
54
+
55
+ metadata_json = self._serialize(metadata) if metadata else None
69
56
 
70
- Raises:
71
- KeyError: If the key does not exist in the store.
72
- """
73
57
  with self.connection:
74
- cursor = self.connection.cursor()
75
- cursor.execute(
76
- "DELETE FROM beaver_blobs WHERE store_name = ? AND key = ?",
77
- (self._name, key),
58
+ self.connection.execute(
59
+ "INSERT OR REPLACE INTO beaver_blobs (store_name, key, data, metadata) VALUES (?, ?, ?, ?)",
60
+ (self._name, key, data, metadata_json),
78
61
  )
79
- if cursor.rowcount == 0:
80
- raise KeyError(f"Key '{key}' not found in blob store '{self._name}'")
62
+
63
+ # Write-through to cache
64
+ blob_obj = Blob(key=key, data=data, metadata=metadata)
65
+ self.cache.set(key, blob_obj)
66
+
67
+ @synced
68
+ def delete(self, key: str):
69
+ """Deletes a blob from the store."""
70
+ cursor = self.connection.cursor()
71
+ cursor.execute(
72
+ "DELETE FROM beaver_blobs WHERE store_name = ? AND key = ?",
73
+ (self._name, key),
74
+ )
75
+ if cursor.rowcount == 0:
76
+ raise KeyError(f"Key '{key}' not found in blob store '{self._name}'")
77
+
78
+ # evict from cache
79
+ self.cache.pop(key)
81
80
 
82
81
  def __contains__(self, key: str) -> bool:
83
82
  """
@@ -183,10 +182,8 @@ class BlobManager[M: JsonSerializable](ManagerBase[M]):
183
182
 
184
183
  @synced
185
184
  def clear(self):
186
- """
187
- Atomically removes all blobs from this store.
188
- """
185
+ """Atomically removes all blobs from this store."""
189
186
  self.connection.execute(
190
187
  "DELETE FROM beaver_blobs WHERE store_name = ?",
191
188
  (self._name,),
192
- )
189
+ )
@@ -0,0 +1,275 @@
1
+ import os
2
+ import functools
3
+ import threading
4
+ import time
5
+ from typing import Optional, Any, Protocol, NamedTuple
6
+
7
+
8
+ class CacheStats(NamedTuple):
9
+ """Holds performance metrics for a cache instance."""
10
+
11
+ hits: int
12
+ misses: int
13
+ invalidations: int
14
+ sets: int
15
+ pops: int
16
+
17
+ @property
18
+ def reads(self) -> int:
19
+ return self.hits + self.misses
20
+
21
+ @property
22
+ def operations(self) -> int:
23
+ return self.hits + self.misses + self.sets + self.pops
24
+
25
+ @property
26
+ def hit_rate(self) -> float:
27
+ """Returns the cache hit rate (0.0 to 1.0)."""
28
+ if self.reads == 0:
29
+ return 0.0
30
+
31
+ return self.hits / self.reads
32
+
33
+ @property
34
+ def invalidation_rate(self) -> float:
35
+ """Returns the rate of invalidations per operation (0.0 to 1.0)."""
36
+ if self.reads == 0:
37
+ return 0.0
38
+
39
+ return self.invalidations / self.operations
40
+
41
+
42
+ class ICache(Protocol):
43
+ """Defines the public interface for all cache objects."""
44
+
45
+ def get(self, key: str) -> Optional[Any]: ...
46
+ def set(self, key: Any, value: Any): ...
47
+ def pop(self, key: str): ...
48
+ def invalidate(self): ...
49
+ def stats(self) -> CacheStats: ...
50
+ def touch(self): ...
51
+
52
+
53
+ class DummyCache:
54
+ """A cache object that does nothing. Used when caching is disabled."""
55
+
56
+ _stats = CacheStats(hits=0, misses=0, invalidations=0, sets=0, pops=0)
57
+
58
+ def get(self, key: str) -> Optional[Any]:
59
+ return None
60
+
61
+ def set(self, key: str, value: Any):
62
+ pass
63
+
64
+ def pop(self, key: str):
65
+ pass
66
+
67
+ def invalidate(self):
68
+ pass
69
+
70
+ def stats(self) -> CacheStats:
71
+ return self._stats
72
+
73
+ @classmethod
74
+ def singleton(cls) -> ICache:
75
+ if not hasattr(cls, "__instance"):
76
+ cls.__instance = cls()
77
+
78
+ return cls.__instance
79
+
80
+ def touch(self):
81
+ pass
82
+
83
+ class LocalCache:
84
+ """
85
+ A thread-local cache that invalidates based on a central,
86
+ database-backed version number, checking only once per interval.
87
+ """
88
+ def __init__(
89
+ self,
90
+ db,
91
+ cache_namespace: str,
92
+ check_interval: float
93
+ ):
94
+ from .types import IDatabase
95
+
96
+ self._db: IDatabase = db
97
+ self._data: dict[str, Any] = {}
98
+ self._lock = threading.Lock()
99
+
100
+ self._version_key: str = cache_namespace # e.g., "list:tasks"
101
+ self._local_version: int = -1
102
+ self._last_check_time: float = 0.0
103
+ self._min_check_interval: float = check_interval
104
+
105
+ # Statistics
106
+ self._hits = 0
107
+ self._misses = 0
108
+ self._invalidations = 0
109
+ self._sets = 0
110
+ self._pops = 0
111
+ self._clears = 0
112
+
113
+ def _get_global_version(self) -> int:
114
+ """Reads the 'source of truth' version from the DB."""
115
+ # This is a raw, direct DB call to avoid circular dependencies
116
+ cursor = self._db.connection.cursor()
117
+ cursor.execute(
118
+ "SELECT version FROM beaver_manager_versions WHERE namespace = ?",
119
+ (self._version_key,)
120
+ )
121
+ result = cursor.fetchone()
122
+ return int(result[0]) if result else 0
123
+
124
+ def _check_and_invalidate(self):
125
+ """
126
+ Checks if the cache is stale, but only hits the DB
127
+ once per check_interval.
128
+ """
129
+ now = time.time()
130
+
131
+ # --- 1. The Hot Path (Pure In-Memory Check) ---
132
+ if (now - self._last_check_time) < self._min_check_interval:
133
+ return
134
+
135
+ # --- 2. The "Coalesced" DB Check ---
136
+ with self._lock:
137
+ # Double-check inside lock in case another thread just ran this
138
+ if (time.time() - self._last_check_time) < self._min_check_interval:
139
+ return
140
+
141
+ global_version = self._get_global_version()
142
+ self._last_check_time = time.time() # Reset timer
143
+
144
+ if global_version != self._local_version:
145
+ self._data.clear()
146
+ self._local_version = global_version
147
+ self._invalidations += 1
148
+
149
+ def get(self, key: str) -> Optional[Any]:
150
+ # This check is now extremely fast
151
+ self._check_and_invalidate()
152
+
153
+ with self._lock:
154
+ value = self._data.get(key)
155
+
156
+ if value is not None:
157
+ self._hits += 1
158
+ return value
159
+
160
+ self._misses += 1
161
+
162
+ return None
163
+
164
+ def set(self, key: str, value: Any):
165
+ with self._lock:
166
+ self._data[key] = value
167
+ self._sets += 1
168
+
169
+ def pop(self, key: str):
170
+ with self._lock:
171
+ self._data.pop(key, None)
172
+ self._pops += 1
173
+
174
+ def invalidate(self):
175
+ with self._lock:
176
+ self._data.clear()
177
+ self._local_version = 0 # Must force re-check
178
+ self._invalidations += 1
179
+ self._last_check_time = 0.0
180
+
181
+ def touch(self):
182
+ """
183
+ Atomically increments the cache version in the native SQL table
184
+ and syncs the cache's local version to avoid self-invalidation.
185
+
186
+ Only call this when you make a change that should invalidate
187
+ other caches of the same namespace in other processes,
188
+ but keep this cache valid.
189
+ """
190
+ with self._lock:
191
+ new_version = 0
192
+
193
+ with self._db.connection:
194
+ # This is a single, atomic, native SQL operation.
195
+ cursor = self._db.connection.execute(
196
+ """
197
+ INSERT INTO beaver_manager_versions (namespace, version)
198
+ VALUES (?, 1)
199
+ ON CONFLICT(namespace) DO UPDATE SET
200
+ version = version + 1
201
+ RETURNING version;
202
+ """,
203
+ (self._version_key,)
204
+ )
205
+ new_version = cursor.fetchone()[0]
206
+
207
+ # Keep the cache in sync to avoid self-invalidation
208
+ self._last_check_time = time.time()
209
+ self._local_version = new_version
210
+
211
+ def stats(self) -> CacheStats:
212
+ return CacheStats(
213
+ hits=self._hits,
214
+ misses=self._misses,
215
+ invalidations=self._invalidations,
216
+ sets=self._sets,
217
+ pops=self._pops,
218
+ )
219
+
220
+ def __repr__(self) -> str:
221
+ return f"<LocalCache namespace='{self._version_key}', version={self._local_version}>"
222
+
223
+
224
+ def cached(key):
225
+ """
226
+ Decorator for read methods.
227
+ - Generates a cache key using key on the arguments.
228
+ - If key is None, bypasses cache.
229
+ - If key is in cache, returns cached value.
230
+ - If key is not in cache, runs the decorated function,
231
+ stores the result, and returns it.
232
+ """
233
+ from .manager import ManagerBase
234
+
235
+ def decorator(func):
236
+ @functools.wraps(func)
237
+ def wrapper(self: ManagerBase, *args, **kwargs):
238
+ cache = self.cache
239
+ cache_key = key(*args, **kwargs)
240
+
241
+ if cache_key is None:
242
+ return func(self, *args, **kwargs)
243
+
244
+ if not self.locked:
245
+ cached_value = cache.get(cache_key)
246
+
247
+ if cached_value is not None:
248
+ return cached_value # HIT
249
+
250
+ result = func(self, *args, **kwargs)
251
+ cache.set(cache_key, result)
252
+
253
+ return result
254
+ return wrapper
255
+ return decorator
256
+
257
+
258
+ def invalidates_cache(func):
259
+ """
260
+ Decorator for write methods that need to invalidate cache.
261
+ - Runs the decorated function.
262
+ - Clears the cache even if there is any exception.
263
+ """
264
+ from .manager import ManagerBase
265
+
266
+ @functools.wraps(func)
267
+ def wrapper(self: "ManagerBase", *args, **kwargs):
268
+ try:
269
+ result = func(self, *args, **kwargs)
270
+ finally:
271
+ self.cache.invalidate()
272
+
273
+ return result
274
+
275
+ return wrapper