cachu 0.2.4__py3-none-any.whl → 0.2.5__py3-none-any.whl
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.
- cachu/__init__.py +4 -5
- cachu/backends/__init__.py +38 -21
- cachu/backends/memory.py +137 -135
- cachu/backends/redis.py +86 -65
- cachu/backends/sqlite.py +163 -123
- cachu/config.py +6 -0
- cachu/decorator.py +257 -275
- cachu/mutex.py +247 -0
- cachu/operations.py +27 -28
- {cachu-0.2.4.dist-info → cachu-0.2.5.dist-info}/METADATA +1 -2
- cachu-0.2.5.dist-info/RECORD +15 -0
- cachu/async_decorator.py +0 -262
- cachu/async_operations.py +0 -178
- cachu/backends/async_base.py +0 -50
- cachu/backends/async_memory.py +0 -111
- cachu/backends/async_redis.py +0 -141
- cachu/backends/async_sqlite.py +0 -256
- cachu/backends/file.py +0 -10
- cachu-0.2.4.dist-info/RECORD +0 -21
- {cachu-0.2.4.dist-info → cachu-0.2.5.dist-info}/WHEEL +0 -0
- {cachu-0.2.4.dist-info → cachu-0.2.5.dist-info}/top_level.txt +0 -0
cachu/backends/redis.py
CHANGED
|
@@ -6,7 +6,8 @@ import time
|
|
|
6
6
|
from collections.abc import AsyncIterator, Iterator
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from ..mutex import AsyncCacheMutex, AsyncRedisMutex, CacheMutex, RedisMutex
|
|
10
|
+
from . import NO_VALUE, Backend
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
import redis
|
|
@@ -25,7 +26,7 @@ def _get_redis_module() -> Any:
|
|
|
25
26
|
return redis
|
|
26
27
|
except ImportError as e:
|
|
27
28
|
raise RuntimeError(
|
|
28
|
-
"Redis support requires the 'redis' package. Install with: pip install
|
|
29
|
+
"Redis support requires the 'redis' package. Install with: pip install cachu[redis]"
|
|
29
30
|
) from e
|
|
30
31
|
|
|
31
32
|
|
|
@@ -42,16 +43,6 @@ def _get_async_redis_module() -> Any:
|
|
|
42
43
|
) from e
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
async def get_async_redis_client(url: str) -> 'aioredis.Redis':
|
|
46
|
-
"""Create an async Redis client from URL.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
url: Redis URL (e.g., 'redis://localhost:6379/0')
|
|
50
|
-
"""
|
|
51
|
-
aioredis = _get_async_redis_module()
|
|
52
|
-
return aioredis.from_url(url)
|
|
53
|
-
|
|
54
|
-
|
|
55
46
|
def get_redis_client(url: str) -> 'redis.Redis':
|
|
56
47
|
"""Create a Redis client from URL.
|
|
57
48
|
|
|
@@ -79,20 +70,32 @@ def _unpack_value(data: bytes) -> tuple[Any, float]:
|
|
|
79
70
|
|
|
80
71
|
|
|
81
72
|
class RedisBackend(Backend):
|
|
82
|
-
"""Redis cache backend.
|
|
73
|
+
"""Unified Redis cache backend with both sync and async interfaces.
|
|
83
74
|
"""
|
|
84
75
|
|
|
85
|
-
def __init__(self, url: str) -> None:
|
|
76
|
+
def __init__(self, url: str, lock_timeout: float = 10.0) -> None:
|
|
86
77
|
self._url = url
|
|
87
|
-
self.
|
|
78
|
+
self._lock_timeout = lock_timeout
|
|
79
|
+
self._sync_client: redis.Redis | None = None
|
|
80
|
+
self._async_client: aioredis.Redis | None = None
|
|
88
81
|
|
|
89
82
|
@property
|
|
90
83
|
def client(self) -> 'redis.Redis':
|
|
91
|
-
"""Lazy-load Redis client.
|
|
84
|
+
"""Lazy-load sync Redis client.
|
|
85
|
+
"""
|
|
86
|
+
if self._sync_client is None:
|
|
87
|
+
self._sync_client = get_redis_client(self._url)
|
|
88
|
+
return self._sync_client
|
|
89
|
+
|
|
90
|
+
def _get_async_client(self) -> 'aioredis.Redis':
|
|
91
|
+
"""Lazy-load async Redis client (from_url is NOT async).
|
|
92
92
|
"""
|
|
93
|
-
if self.
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
if self._async_client is None:
|
|
94
|
+
aioredis = _get_async_redis_module()
|
|
95
|
+
self._async_client = aioredis.from_url(self._url)
|
|
96
|
+
return self._async_client
|
|
97
|
+
|
|
98
|
+
# ===== Sync interface =====
|
|
96
99
|
|
|
97
100
|
def get(self, key: str) -> Any:
|
|
98
101
|
"""Get value by key. Returns NO_VALUE if not found.
|
|
@@ -148,67 +151,51 @@ class RedisBackend(Backend):
|
|
|
148
151
|
"""
|
|
149
152
|
return sum(1 for _ in self.keys(pattern))
|
|
150
153
|
|
|
151
|
-
def
|
|
152
|
-
"""
|
|
154
|
+
def get_mutex(self, key: str) -> CacheMutex:
|
|
155
|
+
"""Get a mutex for dogpile prevention on the given key.
|
|
153
156
|
"""
|
|
154
|
-
|
|
155
|
-
self._client.close()
|
|
156
|
-
self._client = None
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
class AsyncRedisBackend(AsyncBackend):
|
|
160
|
-
"""Async Redis cache backend using redis.asyncio.
|
|
161
|
-
"""
|
|
157
|
+
return RedisMutex(self.client, f'lock:{key}', self._lock_timeout)
|
|
162
158
|
|
|
163
|
-
|
|
164
|
-
self._url = url
|
|
165
|
-
self._client: aioredis.Redis | None = None
|
|
166
|
-
|
|
167
|
-
async def _get_client(self) -> 'aioredis.Redis':
|
|
168
|
-
"""Lazy-load async Redis client.
|
|
169
|
-
"""
|
|
170
|
-
if self._client is None:
|
|
171
|
-
self._client = await get_async_redis_client(self._url)
|
|
172
|
-
return self._client
|
|
159
|
+
# ===== Async interface =====
|
|
173
160
|
|
|
174
|
-
async def
|
|
175
|
-
"""
|
|
161
|
+
async def aget(self, key: str) -> Any:
|
|
162
|
+
"""Async get value by key. Returns NO_VALUE if not found.
|
|
176
163
|
"""
|
|
177
|
-
client =
|
|
164
|
+
client = self._get_async_client()
|
|
178
165
|
data = await client.get(key)
|
|
179
166
|
if data is None:
|
|
180
167
|
return NO_VALUE
|
|
181
168
|
value, _ = _unpack_value(data)
|
|
182
169
|
return value
|
|
183
170
|
|
|
184
|
-
async def
|
|
185
|
-
"""
|
|
171
|
+
async def aget_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
172
|
+
"""Async get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
186
173
|
"""
|
|
187
|
-
client =
|
|
174
|
+
client = self._get_async_client()
|
|
188
175
|
data = await client.get(key)
|
|
189
176
|
if data is None:
|
|
190
177
|
return NO_VALUE, None
|
|
191
178
|
value, created_at = _unpack_value(data)
|
|
192
179
|
return value, created_at
|
|
193
180
|
|
|
194
|
-
async def
|
|
195
|
-
"""
|
|
181
|
+
async def aset(self, key: str, value: Any, ttl: int) -> None:
|
|
182
|
+
"""Async set value with TTL in seconds.
|
|
196
183
|
"""
|
|
197
|
-
client =
|
|
184
|
+
client = self._get_async_client()
|
|
198
185
|
now = time.time()
|
|
199
186
|
packed = _pack_value(value, now)
|
|
200
187
|
await client.setex(key, ttl, packed)
|
|
201
188
|
|
|
202
|
-
async def
|
|
203
|
-
"""
|
|
189
|
+
async def adelete(self, key: str) -> None:
|
|
190
|
+
"""Async delete value by key.
|
|
204
191
|
"""
|
|
205
|
-
client =
|
|
192
|
+
client = self._get_async_client()
|
|
206
193
|
await client.delete(key)
|
|
207
194
|
|
|
208
|
-
async def
|
|
209
|
-
"""
|
|
195
|
+
async def aclear(self, pattern: str | None = None) -> int:
|
|
196
|
+
"""Async clear entries matching pattern. Returns count of cleared entries.
|
|
210
197
|
"""
|
|
211
|
-
client =
|
|
198
|
+
client = self._get_async_client()
|
|
212
199
|
if pattern is None:
|
|
213
200
|
pattern = '*'
|
|
214
201
|
|
|
@@ -218,25 +205,59 @@ class AsyncRedisBackend(AsyncBackend):
|
|
|
218
205
|
count += 1
|
|
219
206
|
return count
|
|
220
207
|
|
|
221
|
-
async def
|
|
222
|
-
"""
|
|
208
|
+
async def akeys(self, pattern: str | None = None) -> AsyncIterator[str]:
|
|
209
|
+
"""Async iterate over keys matching pattern.
|
|
223
210
|
"""
|
|
224
|
-
client =
|
|
211
|
+
client = self._get_async_client()
|
|
225
212
|
redis_pattern = pattern or '*'
|
|
226
213
|
async for key in client.scan_iter(match=redis_pattern):
|
|
227
214
|
yield key.decode() if isinstance(key, bytes) else key
|
|
228
215
|
|
|
229
|
-
async def
|
|
230
|
-
"""
|
|
216
|
+
async def acount(self, pattern: str | None = None) -> int:
|
|
217
|
+
"""Async count keys matching pattern.
|
|
231
218
|
"""
|
|
232
219
|
count = 0
|
|
233
|
-
async for _ in self.
|
|
220
|
+
async for _ in self.akeys(pattern):
|
|
234
221
|
count += 1
|
|
235
222
|
return count
|
|
236
223
|
|
|
237
|
-
|
|
238
|
-
"""
|
|
224
|
+
def get_async_mutex(self, key: str) -> AsyncCacheMutex:
|
|
225
|
+
"""Get an async mutex for dogpile prevention on the given key.
|
|
226
|
+
"""
|
|
227
|
+
return AsyncRedisMutex(self._get_async_client(), f'lock:{key}', self._lock_timeout)
|
|
228
|
+
|
|
229
|
+
# ===== Lifecycle =====
|
|
230
|
+
|
|
231
|
+
def _close_sync_client(self) -> None:
|
|
232
|
+
"""Close sync client if open.
|
|
233
|
+
"""
|
|
234
|
+
if self._sync_client is not None:
|
|
235
|
+
client = self._sync_client
|
|
236
|
+
self._sync_client = None
|
|
237
|
+
client.close()
|
|
238
|
+
|
|
239
|
+
def _close_async_client_sync(self) -> None:
|
|
240
|
+
"""Forcefully close async client from sync context.
|
|
241
|
+
"""
|
|
242
|
+
if self._async_client is not None:
|
|
243
|
+
client = self._async_client
|
|
244
|
+
self._async_client = None
|
|
245
|
+
try:
|
|
246
|
+
client.close()
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
def close(self) -> None:
|
|
251
|
+
"""Close all backend resources from sync context.
|
|
252
|
+
"""
|
|
253
|
+
self._close_sync_client()
|
|
254
|
+
self._close_async_client_sync()
|
|
255
|
+
|
|
256
|
+
async def aclose(self) -> None:
|
|
257
|
+
"""Close all backend resources from async context.
|
|
239
258
|
"""
|
|
240
|
-
if self.
|
|
241
|
-
|
|
242
|
-
self.
|
|
259
|
+
if self._async_client is not None:
|
|
260
|
+
client = self._async_client
|
|
261
|
+
self._async_client = None
|
|
262
|
+
await client.aclose()
|
|
263
|
+
self._close_sync_client()
|