cachu 0.2.3__py3-none-any.whl → 0.2.4__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 +6 -9
- cachu/backends/__init__.py +45 -2
- cachu/backends/memory.py +102 -2
- cachu/backends/redis.py +132 -21
- cachu/backends/sqlite.py +257 -12
- cachu/config.py +0 -6
- cachu/decorator.py +353 -68
- cachu/keys.py +8 -0
- cachu/operations.py +172 -23
- {cachu-0.2.3.dist-info → cachu-0.2.4.dist-info}/METADATA +7 -9
- cachu-0.2.4.dist-info/RECORD +21 -0
- cachu-0.2.3.dist-info/RECORD +0 -21
- {cachu-0.2.3.dist-info → cachu-0.2.4.dist-info}/WHEEL +0 -0
- {cachu-0.2.3.dist-info → cachu-0.2.4.dist-info}/top_level.txt +0 -0
cachu/operations.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Cache CRUD operations.
|
|
1
|
+
"""Cache CRUD operations for sync and async APIs.
|
|
2
2
|
"""
|
|
3
3
|
import logging
|
|
4
4
|
from collections.abc import Callable
|
|
@@ -6,8 +6,9 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from .backends import NO_VALUE
|
|
8
8
|
from .config import _get_caller_package, get_config
|
|
9
|
-
from .decorator import
|
|
10
|
-
from .
|
|
9
|
+
from .decorator import async_manager, get_async_cache_info, get_cache_info
|
|
10
|
+
from .decorator import manager
|
|
11
|
+
from .keys import _tag_to_pattern, mangle_key
|
|
11
12
|
from .types import CacheInfo, CacheMeta
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
@@ -15,12 +16,12 @@ logger = logging.getLogger(__name__)
|
|
|
15
16
|
_MISSING = object()
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
def _get_meta(fn: Callable[..., Any]) -> CacheMeta:
|
|
19
|
+
def _get_meta(fn: Callable[..., Any], decorator_name: str = '@cache') -> CacheMeta:
|
|
19
20
|
"""Get CacheMeta from a decorated function.
|
|
20
21
|
"""
|
|
21
22
|
meta = getattr(fn, '_cache_meta', None)
|
|
22
23
|
if meta is None:
|
|
23
|
-
raise ValueError(f'{fn.__name__} is not decorated with
|
|
24
|
+
raise ValueError(f'{fn.__name__} is not decorated with {decorator_name}')
|
|
24
25
|
return meta
|
|
25
26
|
|
|
26
27
|
|
|
@@ -46,7 +47,7 @@ def cache_get(fn: Callable[..., Any], default: Any = _MISSING, **kwargs: Any) ->
|
|
|
46
47
|
base_key = key_generator(**kwargs)
|
|
47
48
|
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
48
49
|
|
|
49
|
-
backend =
|
|
50
|
+
backend = manager.get_backend(meta.package, meta.backend, meta.ttl)
|
|
50
51
|
value = backend.get(cache_key)
|
|
51
52
|
|
|
52
53
|
if value is NO_VALUE:
|
|
@@ -75,7 +76,7 @@ def cache_set(fn: Callable[..., Any], value: Any, **kwargs: Any) -> None:
|
|
|
75
76
|
base_key = key_generator(**kwargs)
|
|
76
77
|
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
77
78
|
|
|
78
|
-
backend =
|
|
79
|
+
backend = manager.get_backend(meta.package, meta.backend, meta.ttl)
|
|
79
80
|
backend.set(cache_key, value, meta.ttl)
|
|
80
81
|
|
|
81
82
|
logger.debug(f'Set cache for {fn.__name__} with key {cache_key}')
|
|
@@ -98,7 +99,7 @@ def cache_delete(fn: Callable[..., Any], **kwargs: Any) -> None:
|
|
|
98
99
|
base_key = key_generator(**kwargs)
|
|
99
100
|
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
100
101
|
|
|
101
|
-
backend =
|
|
102
|
+
backend = manager.get_backend(meta.package, meta.backend, meta.ttl)
|
|
102
103
|
backend.delete(cache_key)
|
|
103
104
|
|
|
104
105
|
logger.debug(f'Deleted cache for {fn.__name__} with key {cache_key}')
|
|
@@ -129,28 +130,18 @@ def cache_clear(
|
|
|
129
130
|
else:
|
|
130
131
|
backends_to_clear = ['memory', 'file', 'redis']
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
from .keys import _normalize_tag
|
|
134
|
-
pattern = f'*|{_normalize_tag(tag)}|*'
|
|
135
|
-
else:
|
|
136
|
-
pattern = None
|
|
137
|
-
|
|
133
|
+
pattern = _tag_to_pattern(tag)
|
|
138
134
|
total_cleared = 0
|
|
139
135
|
|
|
140
|
-
from .decorator import _backends, _backends_lock
|
|
141
|
-
|
|
142
|
-
# When both backend and ttl are specified, directly get/create and clear that backend.
|
|
143
|
-
# This is essential for distributed caches (Redis) where cache_clear may be called
|
|
144
|
-
# from a different process than the one that populated the cache.
|
|
145
136
|
if backend is not None and ttl is not None:
|
|
146
|
-
backend_instance =
|
|
137
|
+
backend_instance = manager.get_backend(package, backend, ttl)
|
|
147
138
|
cleared = backend_instance.clear(pattern)
|
|
148
139
|
if cleared > 0:
|
|
149
140
|
total_cleared += cleared
|
|
150
141
|
logger.debug(f'Cleared {cleared} entries from {backend} backend (ttl={ttl})')
|
|
151
142
|
else:
|
|
152
|
-
with _backends_lock:
|
|
153
|
-
for (pkg, btype, bttl), backend_instance in list(
|
|
143
|
+
with manager._backends_lock:
|
|
144
|
+
for (pkg, btype, bttl), backend_instance in list(manager.backends.items()):
|
|
154
145
|
if pkg != package:
|
|
155
146
|
continue
|
|
156
147
|
if btype not in backends_to_clear:
|
|
@@ -163,6 +154,9 @@ def cache_clear(
|
|
|
163
154
|
total_cleared += cleared
|
|
164
155
|
logger.debug(f'Cleared {cleared} entries from {btype} backend (ttl={bttl})')
|
|
165
156
|
|
|
157
|
+
with manager._stats_lock:
|
|
158
|
+
manager.stats.clear()
|
|
159
|
+
|
|
166
160
|
return total_cleared
|
|
167
161
|
|
|
168
162
|
|
|
@@ -178,5 +172,160 @@ def cache_info(fn: Callable[..., Any]) -> CacheInfo:
|
|
|
178
172
|
Raises
|
|
179
173
|
ValueError: If function is not decorated with @cache
|
|
180
174
|
"""
|
|
181
|
-
_get_meta(fn)
|
|
175
|
+
_get_meta(fn)
|
|
182
176
|
return get_cache_info(fn)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def async_cache_get(
|
|
180
|
+
fn: Callable[..., Any],
|
|
181
|
+
default: Any = _MISSING,
|
|
182
|
+
**kwargs: Any,
|
|
183
|
+
) -> Any:
|
|
184
|
+
"""Get a cached value without calling the async function.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
fn: A function decorated with @async_cache
|
|
188
|
+
default: Value to return if not found (raises KeyError if not provided)
|
|
189
|
+
**kwargs: Function arguments to build the cache key
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
The cached value or default
|
|
193
|
+
|
|
194
|
+
Raises
|
|
195
|
+
KeyError: If not found and no default provided
|
|
196
|
+
ValueError: If function is not decorated with @async_cache
|
|
197
|
+
"""
|
|
198
|
+
meta = _get_meta(fn, '@async_cache')
|
|
199
|
+
cfg = get_config(meta.package)
|
|
200
|
+
|
|
201
|
+
key_generator = fn._cache_key_generator
|
|
202
|
+
base_key = key_generator(**kwargs)
|
|
203
|
+
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
204
|
+
|
|
205
|
+
backend = await async_manager.get_backend(meta.package, meta.backend, meta.ttl)
|
|
206
|
+
value = await backend.get(cache_key)
|
|
207
|
+
|
|
208
|
+
if value is NO_VALUE:
|
|
209
|
+
if default is _MISSING:
|
|
210
|
+
raise KeyError(f'No cached value for {fn.__name__} with {kwargs}')
|
|
211
|
+
return default
|
|
212
|
+
|
|
213
|
+
return value
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def async_cache_set(fn: Callable[..., Any], value: Any, **kwargs: Any) -> None:
|
|
217
|
+
"""Set a cached value directly without calling the async function.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
fn: A function decorated with @async_cache
|
|
221
|
+
value: The value to cache
|
|
222
|
+
**kwargs: Function arguments to build the cache key
|
|
223
|
+
|
|
224
|
+
Raises
|
|
225
|
+
ValueError: If function is not decorated with @async_cache
|
|
226
|
+
"""
|
|
227
|
+
meta = _get_meta(fn, '@async_cache')
|
|
228
|
+
cfg = get_config(meta.package)
|
|
229
|
+
|
|
230
|
+
key_generator = fn._cache_key_generator
|
|
231
|
+
base_key = key_generator(**kwargs)
|
|
232
|
+
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
233
|
+
|
|
234
|
+
backend = await async_manager.get_backend(meta.package, meta.backend, meta.ttl)
|
|
235
|
+
await backend.set(cache_key, value, meta.ttl)
|
|
236
|
+
|
|
237
|
+
logger.debug(f'Set cache for {fn.__name__} with key {cache_key}')
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def async_cache_delete(fn: Callable[..., Any], **kwargs: Any) -> None:
|
|
241
|
+
"""Delete a specific cached entry.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
fn: A function decorated with @async_cache
|
|
245
|
+
**kwargs: Function arguments to build the cache key
|
|
246
|
+
|
|
247
|
+
Raises
|
|
248
|
+
ValueError: If function is not decorated with @async_cache
|
|
249
|
+
"""
|
|
250
|
+
meta = _get_meta(fn, '@async_cache')
|
|
251
|
+
cfg = get_config(meta.package)
|
|
252
|
+
|
|
253
|
+
key_generator = fn._cache_key_generator
|
|
254
|
+
base_key = key_generator(**kwargs)
|
|
255
|
+
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
256
|
+
|
|
257
|
+
backend = await async_manager.get_backend(meta.package, meta.backend, meta.ttl)
|
|
258
|
+
await backend.delete(cache_key)
|
|
259
|
+
|
|
260
|
+
logger.debug(f'Deleted cache for {fn.__name__} with key {cache_key}')
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
async def async_cache_clear(
|
|
264
|
+
tag: str | None = None,
|
|
265
|
+
backend: str | None = None,
|
|
266
|
+
ttl: int | None = None,
|
|
267
|
+
package: str | None = None,
|
|
268
|
+
) -> int:
|
|
269
|
+
"""Clear async cache entries matching criteria.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
tag: Clear only entries with this tag
|
|
273
|
+
backend: Backend type to clear ('memory', 'file', 'redis'). Clears all if None.
|
|
274
|
+
ttl: Specific TTL region to clear. Clears all TTLs if None.
|
|
275
|
+
package: Package to clear for. Auto-detected if None.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
Number of entries cleared (may be approximate)
|
|
279
|
+
"""
|
|
280
|
+
if package is None:
|
|
281
|
+
package = _get_caller_package()
|
|
282
|
+
|
|
283
|
+
if backend is not None:
|
|
284
|
+
backends_to_clear = [backend]
|
|
285
|
+
else:
|
|
286
|
+
backends_to_clear = ['memory', 'file', 'redis']
|
|
287
|
+
|
|
288
|
+
pattern = _tag_to_pattern(tag)
|
|
289
|
+
total_cleared = 0
|
|
290
|
+
|
|
291
|
+
if backend is not None and ttl is not None:
|
|
292
|
+
backend_instance = await async_manager.get_backend(package, backend, ttl)
|
|
293
|
+
cleared = await backend_instance.clear(pattern)
|
|
294
|
+
if cleared > 0:
|
|
295
|
+
total_cleared += cleared
|
|
296
|
+
logger.debug(f'Cleared {cleared} entries from {backend} backend (ttl={ttl})')
|
|
297
|
+
else:
|
|
298
|
+
async with async_manager._backends_lock:
|
|
299
|
+
for (pkg, btype, bttl), backend_instance in list(async_manager.backends.items()):
|
|
300
|
+
if pkg != package:
|
|
301
|
+
continue
|
|
302
|
+
if btype not in backends_to_clear:
|
|
303
|
+
continue
|
|
304
|
+
if ttl is not None and bttl != ttl:
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
cleared = await backend_instance.clear(pattern)
|
|
308
|
+
if cleared > 0:
|
|
309
|
+
total_cleared += cleared
|
|
310
|
+
logger.debug(f'Cleared {cleared} entries from {btype} backend (ttl={bttl})')
|
|
311
|
+
|
|
312
|
+
async with async_manager._stats_lock:
|
|
313
|
+
async_manager.stats.clear()
|
|
314
|
+
|
|
315
|
+
return total_cleared
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def async_cache_info(fn: Callable[..., Any]) -> CacheInfo:
|
|
319
|
+
"""Get cache statistics for an async decorated function.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
fn: A function decorated with @async_cache
|
|
323
|
+
|
|
324
|
+
Returns
|
|
325
|
+
CacheInfo with hits, misses, and currsize
|
|
326
|
+
|
|
327
|
+
Raises
|
|
328
|
+
ValueError: If function is not decorated with @async_cache
|
|
329
|
+
"""
|
|
330
|
+
_get_meta(fn, '@async_cache')
|
|
331
|
+
return await get_async_cache_info(fn)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cachu
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Flexible caching library with sync and async support for memory, file (SQLite), and Redis backends
|
|
5
5
|
Author: bissli
|
|
6
6
|
License-Expression: 0BSD
|
|
@@ -69,19 +69,17 @@ cachu.configure(
|
|
|
69
69
|
key_prefix='v1:', # Prefix for all cache keys
|
|
70
70
|
file_dir='/var/cache/app', # Directory for file cache
|
|
71
71
|
redis_url='redis://localhost:6379/0', # Redis connection URL
|
|
72
|
-
redis_distributed=False, # Use distributed locks for Redis
|
|
73
72
|
)
|
|
74
73
|
```
|
|
75
74
|
|
|
76
75
|
### Configuration Options
|
|
77
76
|
|
|
78
|
-
| Option
|
|
79
|
-
|
|
|
80
|
-
| `backend`
|
|
81
|
-
| `key_prefix`
|
|
82
|
-
| `file_dir`
|
|
83
|
-
| `redis_url`
|
|
84
|
-
| `redis_distributed` | `False` | Enable distributed locks for Redis |
|
|
77
|
+
| Option | Default | Description |
|
|
78
|
+
| ------------ | ---------------------------- | ------------------------------------------------- |
|
|
79
|
+
| `backend` | `'memory'` | Default backend type |
|
|
80
|
+
| `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
|
|
81
|
+
| `file_dir` | `'/tmp'` | Directory for file-based caches |
|
|
82
|
+
| `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
|
|
85
83
|
|
|
86
84
|
### Package Isolation
|
|
87
85
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
cachu/__init__.py,sha256=JXeoRvouVGb02Vp8C97I9jEXQIuC86LoTGrXJ6lXyz4,1188
|
|
2
|
+
cachu/async_decorator.py,sha256=Jx2fHESLlld7NZiD2-6kcozukJtp5efnt4cMhntDDRA,8939
|
|
3
|
+
cachu/async_operations.py,sha256=eVqhZk3FVLNip_abjnCzG8AajzvJTtXbpL--dpMXBlc,5597
|
|
4
|
+
cachu/config.py,sha256=ky6brQ6_yjTXHEpjx43AIOr5bCloR72sbZWS-9LVuWs,5640
|
|
5
|
+
cachu/decorator.py,sha256=uEDP1FVzOj_yr57wppbLlHf-sdDQTJaOEvVtXL-JQz8,18315
|
|
6
|
+
cachu/keys.py,sha256=3em9_umQYFwwF2EwmIK8yWJq8aO2CI1DMRjq6Je3xC8,3747
|
|
7
|
+
cachu/operations.py,sha256=EieBJwHzTHlnXdkycKPT47ShhQ69L7ZUFVNdOyDq6SY,10371
|
|
8
|
+
cachu/types.py,sha256=FghBN5GhxnrpuT4WUL9iNnAfdoH__cw9_Ag4kHbIXq4,723
|
|
9
|
+
cachu/backends/__init__.py,sha256=gXzLDZ6uUEeH8pJJkOEd45-ZGRGeNnlB_bKxfREzRes,2546
|
|
10
|
+
cachu/backends/async_base.py,sha256=oZ3K3PhsYkbgZxFLFk3_NbxBxtNopqS90HZBizwg_q8,1394
|
|
11
|
+
cachu/backends/async_memory.py,sha256=SQvSHeWbySa52BnQLF75nhVXgsydubNu84a8hvSzQSc,3457
|
|
12
|
+
cachu/backends/async_redis.py,sha256=8kefPIoIJDAZ6C6HJCvHqKFMDS10sJYh8YcJMpXpQm8,4455
|
|
13
|
+
cachu/backends/async_sqlite.py,sha256=iS1YaVakzK7msKL3BVmnZc_n73-V5fUz1wCQJxEY0ak,8730
|
|
14
|
+
cachu/backends/file.py,sha256=Pu01VtgHDgK6ev5hqyZXuJRCSB2VbNKHQ4w4nNKNyeI,298
|
|
15
|
+
cachu/backends/memory.py,sha256=Sz1BAsttaMBqqEZfRqgYddbbwvHRNWbxm2KIzIDtZeA,6394
|
|
16
|
+
cachu/backends/redis.py,sha256=Gn96-Qn2EbNti0S50f3sFGJ3A9eCWsLlGgZgBKV8AJs,7197
|
|
17
|
+
cachu/backends/sqlite.py,sha256=NDhlH7Ex7kKNoS6wRvrv6qFETqUMuK8W1iL3w3yTuys,16315
|
|
18
|
+
cachu-0.2.4.dist-info/METADATA,sha256=jRuDdgJ7zPk9z52T8L4uCIcQafc9dFV2TVk5R1s_6h0,11777
|
|
19
|
+
cachu-0.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
+
cachu-0.2.4.dist-info/top_level.txt,sha256=g80nNoMvLMzhSwQWV-JotCBqtsLAHeFMBo_g8hCK8hQ,6
|
|
21
|
+
cachu-0.2.4.dist-info/RECORD,,
|
cachu-0.2.3.dist-info/RECORD
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
cachu/__init__.py,sha256=vMcuJYvSfRUsMwlFrpjS8FOuQiqPBP9e4dRlJbsvpZ0,1286
|
|
2
|
-
cachu/async_decorator.py,sha256=Jx2fHESLlld7NZiD2-6kcozukJtp5efnt4cMhntDDRA,8939
|
|
3
|
-
cachu/async_operations.py,sha256=eVqhZk3FVLNip_abjnCzG8AajzvJTtXbpL--dpMXBlc,5597
|
|
4
|
-
cachu/config.py,sha256=KtcDGpSTJmjRrcNLz9_Om3O814oJJ3p8gntB84Pd6Dk,5922
|
|
5
|
-
cachu/decorator.py,sha256=RHwDRZxZfOkBgEK1XgRyis22bxQ0ba0X4NtHBd9FTb4,8161
|
|
6
|
-
cachu/keys.py,sha256=fwwNOpnDJFCIWZoQ5UGJWhJa6xu36hsBsURI-n2NJKU,3557
|
|
7
|
-
cachu/operations.py,sha256=t42_Er-O59vrwFa5jdf4yq3Jr4li2l7php4yMVJnxPs,5588
|
|
8
|
-
cachu/types.py,sha256=FghBN5GhxnrpuT4WUL9iNnAfdoH__cw9_Ag4kHbIXq4,723
|
|
9
|
-
cachu/backends/__init__.py,sha256=Jn2yBAMmJ8d0J_NyjOtxRt7UTyMLf1rlY8QJ049hXE8,1318
|
|
10
|
-
cachu/backends/async_base.py,sha256=oZ3K3PhsYkbgZxFLFk3_NbxBxtNopqS90HZBizwg_q8,1394
|
|
11
|
-
cachu/backends/async_memory.py,sha256=SQvSHeWbySa52BnQLF75nhVXgsydubNu84a8hvSzQSc,3457
|
|
12
|
-
cachu/backends/async_redis.py,sha256=8kefPIoIJDAZ6C6HJCvHqKFMDS10sJYh8YcJMpXpQm8,4455
|
|
13
|
-
cachu/backends/async_sqlite.py,sha256=iS1YaVakzK7msKL3BVmnZc_n73-V5fUz1wCQJxEY0ak,8730
|
|
14
|
-
cachu/backends/file.py,sha256=Pu01VtgHDgK6ev5hqyZXuJRCSB2VbNKHQ4w4nNKNyeI,298
|
|
15
|
-
cachu/backends/memory.py,sha256=kIgrVU8k_3Aquyj2PDf8IPbTjCITM_0V5GU47m3fJmo,3138
|
|
16
|
-
cachu/backends/redis.py,sha256=yE5rEBgOij9QOeC1VhWdIbGCgi442q-aWfmbbG4aNSE,3858
|
|
17
|
-
cachu/backends/sqlite.py,sha256=whduN5G_bN6ZJNuCBwbraDcadv_sg0j-OEiFnP8EEsk,7803
|
|
18
|
-
cachu-0.2.3.dist-info/METADATA,sha256=UXittsVjHwFjAGq6Fl8LAS_2zTKs14BYc9KvOEJHX9I,11992
|
|
19
|
-
cachu-0.2.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
-
cachu-0.2.3.dist-info/top_level.txt,sha256=g80nNoMvLMzhSwQWV-JotCBqtsLAHeFMBo_g8hCK8hQ,6
|
|
21
|
-
cachu-0.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|