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/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 _get_backend, get_cache_info
10
- from .keys import mangle_key
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 @cache')
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 = _get_backend(meta.package, meta.backend, meta.ttl)
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 = _get_backend(meta.package, meta.backend, meta.ttl)
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 = _get_backend(meta.package, meta.backend, meta.ttl)
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
- if tag:
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 = _get_backend(package, backend, ttl)
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(_backends.items()):
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) # Validate it's decorated
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
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 | Default | Description |
79
- | ------------------- | ---------------------------- | ------------------------------------------------- |
80
- | `backend` | `'memory'` | Default backend type |
81
- | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
82
- | `file_dir` | `'/tmp'` | Directory for file-based caches |
83
- | `redis_url` | `'redis://localhost:6379/0'` | Redis connection 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,,
@@ -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