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/async_operations.py
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
"""Async cache CRUD operations.
|
|
2
|
-
"""
|
|
3
|
-
import logging
|
|
4
|
-
from collections.abc import Callable
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from .backends import NO_VALUE
|
|
8
|
-
from .config import _get_caller_package, get_config
|
|
9
|
-
from .async_decorator import _get_async_backend, get_async_cache_info
|
|
10
|
-
from .async_decorator import _async_backends, _async_backends_lock
|
|
11
|
-
from .keys import mangle_key
|
|
12
|
-
from .types import CacheInfo, CacheMeta
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
_MISSING = object()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _get_meta(fn: Callable[..., Any]) -> CacheMeta:
|
|
20
|
-
"""Get CacheMeta from a decorated function.
|
|
21
|
-
"""
|
|
22
|
-
meta = getattr(fn, '_cache_meta', None)
|
|
23
|
-
if meta is None:
|
|
24
|
-
raise ValueError(f'{fn.__name__} is not decorated with @async_cache')
|
|
25
|
-
return meta
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async def async_cache_get(fn: Callable[..., Any], default: Any = _MISSING, **kwargs: Any) -> Any:
|
|
29
|
-
"""Get a cached value without calling the async function.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
fn: A function decorated with @async_cache
|
|
33
|
-
default: Value to return if not found (raises KeyError if not provided)
|
|
34
|
-
**kwargs: Function arguments to build the cache key
|
|
35
|
-
|
|
36
|
-
Returns
|
|
37
|
-
The cached value or default
|
|
38
|
-
|
|
39
|
-
Raises
|
|
40
|
-
KeyError: If not found and no default provided
|
|
41
|
-
ValueError: If function is not decorated with @async_cache
|
|
42
|
-
"""
|
|
43
|
-
meta = _get_meta(fn)
|
|
44
|
-
cfg = get_config(meta.package)
|
|
45
|
-
|
|
46
|
-
key_generator = fn._cache_key_generator
|
|
47
|
-
base_key = key_generator(**kwargs)
|
|
48
|
-
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
49
|
-
|
|
50
|
-
backend = await _get_async_backend(meta.package, meta.backend, meta.ttl)
|
|
51
|
-
value = await backend.get(cache_key)
|
|
52
|
-
|
|
53
|
-
if value is NO_VALUE:
|
|
54
|
-
if default is _MISSING:
|
|
55
|
-
raise KeyError(f'No cached value for {fn.__name__} with {kwargs}')
|
|
56
|
-
return default
|
|
57
|
-
|
|
58
|
-
return value
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
async def async_cache_set(fn: Callable[..., Any], value: Any, **kwargs: Any) -> None:
|
|
62
|
-
"""Set a cached value directly without calling the async function.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
fn: A function decorated with @async_cache
|
|
66
|
-
value: The value to cache
|
|
67
|
-
**kwargs: Function arguments to build the cache key
|
|
68
|
-
|
|
69
|
-
Raises
|
|
70
|
-
ValueError: If function is not decorated with @async_cache
|
|
71
|
-
"""
|
|
72
|
-
meta = _get_meta(fn)
|
|
73
|
-
cfg = get_config(meta.package)
|
|
74
|
-
|
|
75
|
-
key_generator = fn._cache_key_generator
|
|
76
|
-
base_key = key_generator(**kwargs)
|
|
77
|
-
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
78
|
-
|
|
79
|
-
backend = await _get_async_backend(meta.package, meta.backend, meta.ttl)
|
|
80
|
-
await backend.set(cache_key, value, meta.ttl)
|
|
81
|
-
|
|
82
|
-
logger.debug(f'Set cache for {fn.__name__} with key {cache_key}')
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
async def async_cache_delete(fn: Callable[..., Any], **kwargs: Any) -> None:
|
|
86
|
-
"""Delete a specific cached entry.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
fn: A function decorated with @async_cache
|
|
90
|
-
**kwargs: Function arguments to build the cache key
|
|
91
|
-
|
|
92
|
-
Raises
|
|
93
|
-
ValueError: If function is not decorated with @async_cache
|
|
94
|
-
"""
|
|
95
|
-
meta = _get_meta(fn)
|
|
96
|
-
cfg = get_config(meta.package)
|
|
97
|
-
|
|
98
|
-
key_generator = fn._cache_key_generator
|
|
99
|
-
base_key = key_generator(**kwargs)
|
|
100
|
-
cache_key = mangle_key(base_key, cfg.key_prefix, meta.ttl)
|
|
101
|
-
|
|
102
|
-
backend = await _get_async_backend(meta.package, meta.backend, meta.ttl)
|
|
103
|
-
await backend.delete(cache_key)
|
|
104
|
-
|
|
105
|
-
logger.debug(f'Deleted cache for {fn.__name__} with key {cache_key}')
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
async def async_cache_clear(
|
|
109
|
-
tag: str | None = None,
|
|
110
|
-
backend: str | None = None,
|
|
111
|
-
ttl: int | None = None,
|
|
112
|
-
package: str | None = None,
|
|
113
|
-
) -> int:
|
|
114
|
-
"""Clear async cache entries matching criteria.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
tag: Clear only entries with this tag
|
|
118
|
-
backend: Backend type to clear ('memory', 'file', 'redis'). Clears all if None.
|
|
119
|
-
ttl: Specific TTL region to clear. Clears all TTLs if None.
|
|
120
|
-
package: Package to clear for. Auto-detected if None.
|
|
121
|
-
|
|
122
|
-
Returns
|
|
123
|
-
Number of entries cleared (may be approximate)
|
|
124
|
-
"""
|
|
125
|
-
if package is None:
|
|
126
|
-
package = _get_caller_package()
|
|
127
|
-
|
|
128
|
-
if backend is not None:
|
|
129
|
-
backends_to_clear = [backend]
|
|
130
|
-
else:
|
|
131
|
-
backends_to_clear = ['memory', 'file', 'redis']
|
|
132
|
-
|
|
133
|
-
if tag:
|
|
134
|
-
from .keys import _normalize_tag
|
|
135
|
-
pattern = f'*|{_normalize_tag(tag)}|*'
|
|
136
|
-
else:
|
|
137
|
-
pattern = None
|
|
138
|
-
|
|
139
|
-
total_cleared = 0
|
|
140
|
-
|
|
141
|
-
if backend is not None and ttl is not None:
|
|
142
|
-
backend_instance = await _get_async_backend(package, backend, ttl)
|
|
143
|
-
cleared = await backend_instance.clear(pattern)
|
|
144
|
-
if cleared > 0:
|
|
145
|
-
total_cleared += cleared
|
|
146
|
-
logger.debug(f'Cleared {cleared} entries from {backend} backend (ttl={ttl})')
|
|
147
|
-
else:
|
|
148
|
-
async with _async_backends_lock:
|
|
149
|
-
for (pkg, btype, bttl), backend_instance in list(_async_backends.items()):
|
|
150
|
-
if pkg != package:
|
|
151
|
-
continue
|
|
152
|
-
if btype not in backends_to_clear:
|
|
153
|
-
continue
|
|
154
|
-
if ttl is not None and bttl != ttl:
|
|
155
|
-
continue
|
|
156
|
-
|
|
157
|
-
cleared = await backend_instance.clear(pattern)
|
|
158
|
-
if cleared > 0:
|
|
159
|
-
total_cleared += cleared
|
|
160
|
-
logger.debug(f'Cleared {cleared} entries from {btype} backend (ttl={bttl})')
|
|
161
|
-
|
|
162
|
-
return total_cleared
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
async def async_cache_info(fn: Callable[..., Any]) -> CacheInfo:
|
|
166
|
-
"""Get cache statistics for an async decorated function.
|
|
167
|
-
|
|
168
|
-
Args:
|
|
169
|
-
fn: A function decorated with @async_cache
|
|
170
|
-
|
|
171
|
-
Returns
|
|
172
|
-
CacheInfo with hits, misses, and currsize
|
|
173
|
-
|
|
174
|
-
Raises
|
|
175
|
-
ValueError: If function is not decorated with @async_cache
|
|
176
|
-
"""
|
|
177
|
-
_get_meta(fn)
|
|
178
|
-
return await get_async_cache_info(fn)
|
cachu/backends/async_base.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Async cache backend abstract base class.
|
|
2
|
-
"""
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from collections.abc import AsyncIterator
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AsyncBackend(ABC):
|
|
9
|
-
"""Abstract base class for async cache backends.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
@abstractmethod
|
|
13
|
-
async def get(self, key: str) -> Any:
|
|
14
|
-
"""Get value by key. Returns NO_VALUE if not found.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
@abstractmethod
|
|
18
|
-
async def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
19
|
-
"""Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
@abstractmethod
|
|
23
|
-
async def set(self, key: str, value: Any, ttl: int) -> None:
|
|
24
|
-
"""Set value with TTL in seconds.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
@abstractmethod
|
|
28
|
-
async def delete(self, key: str) -> None:
|
|
29
|
-
"""Delete value by key.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
@abstractmethod
|
|
33
|
-
async def clear(self, pattern: str | None = None) -> int:
|
|
34
|
-
"""Clear entries matching pattern. Returns count of cleared entries.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
@abstractmethod
|
|
38
|
-
def keys(self, pattern: str | None = None) -> AsyncIterator[str]:
|
|
39
|
-
"""Iterate over keys matching pattern.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
@abstractmethod
|
|
43
|
-
async def count(self, pattern: str | None = None) -> int:
|
|
44
|
-
"""Count keys matching pattern.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
@abstractmethod
|
|
48
|
-
async def close(self) -> None:
|
|
49
|
-
"""Close the backend and release resources.
|
|
50
|
-
"""
|
cachu/backends/async_memory.py
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
"""Async memory cache backend implementation.
|
|
2
|
-
"""
|
|
3
|
-
import asyncio
|
|
4
|
-
import fnmatch
|
|
5
|
-
import pickle
|
|
6
|
-
import time
|
|
7
|
-
from collections.abc import AsyncIterator
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from . import NO_VALUE
|
|
11
|
-
from .async_base import AsyncBackend
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class AsyncMemoryBackend(AsyncBackend):
|
|
15
|
-
"""Async in-memory cache backend using asyncio.Lock.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
def __init__(self) -> None:
|
|
19
|
-
self._cache: dict[str, tuple[bytes, float, float]] = {}
|
|
20
|
-
self._lock = asyncio.Lock()
|
|
21
|
-
|
|
22
|
-
async def get(self, key: str) -> Any:
|
|
23
|
-
"""Get value by key. Returns NO_VALUE if not found or expired.
|
|
24
|
-
"""
|
|
25
|
-
async with self._lock:
|
|
26
|
-
entry = self._cache.get(key)
|
|
27
|
-
if entry is None:
|
|
28
|
-
return NO_VALUE
|
|
29
|
-
|
|
30
|
-
pickled_value, created_at, expires_at = entry
|
|
31
|
-
if time.time() > expires_at:
|
|
32
|
-
del self._cache[key]
|
|
33
|
-
return NO_VALUE
|
|
34
|
-
|
|
35
|
-
return pickle.loads(pickled_value)
|
|
36
|
-
|
|
37
|
-
async def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
38
|
-
"""Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
39
|
-
"""
|
|
40
|
-
async with self._lock:
|
|
41
|
-
entry = self._cache.get(key)
|
|
42
|
-
if entry is None:
|
|
43
|
-
return NO_VALUE, None
|
|
44
|
-
|
|
45
|
-
pickled_value, created_at, expires_at = entry
|
|
46
|
-
if time.time() > expires_at:
|
|
47
|
-
del self._cache[key]
|
|
48
|
-
return NO_VALUE, None
|
|
49
|
-
|
|
50
|
-
return pickle.loads(pickled_value), created_at
|
|
51
|
-
|
|
52
|
-
async def set(self, key: str, value: Any, ttl: int) -> None:
|
|
53
|
-
"""Set value with TTL in seconds.
|
|
54
|
-
"""
|
|
55
|
-
now = time.time()
|
|
56
|
-
pickled_value = pickle.dumps(value)
|
|
57
|
-
async with self._lock:
|
|
58
|
-
self._cache[key] = (pickled_value, now, now + ttl)
|
|
59
|
-
|
|
60
|
-
async def delete(self, key: str) -> None:
|
|
61
|
-
"""Delete value by key.
|
|
62
|
-
"""
|
|
63
|
-
async with self._lock:
|
|
64
|
-
self._cache.pop(key, None)
|
|
65
|
-
|
|
66
|
-
async def clear(self, pattern: str | None = None) -> int:
|
|
67
|
-
"""Clear entries matching pattern. Returns count of cleared entries.
|
|
68
|
-
"""
|
|
69
|
-
async with self._lock:
|
|
70
|
-
if pattern is None:
|
|
71
|
-
count = len(self._cache)
|
|
72
|
-
self._cache.clear()
|
|
73
|
-
return count
|
|
74
|
-
|
|
75
|
-
keys_to_delete = [k for k in self._cache if fnmatch.fnmatch(k, pattern)]
|
|
76
|
-
for key in keys_to_delete:
|
|
77
|
-
del self._cache[key]
|
|
78
|
-
return len(keys_to_delete)
|
|
79
|
-
|
|
80
|
-
async def keys(self, pattern: str | None = None) -> AsyncIterator[str]:
|
|
81
|
-
"""Iterate over keys matching pattern.
|
|
82
|
-
"""
|
|
83
|
-
now = time.time()
|
|
84
|
-
async with self._lock:
|
|
85
|
-
all_keys = list(self._cache.keys())
|
|
86
|
-
|
|
87
|
-
for key in all_keys:
|
|
88
|
-
async with self._lock:
|
|
89
|
-
entry = self._cache.get(key)
|
|
90
|
-
if entry is None:
|
|
91
|
-
continue
|
|
92
|
-
_, _, expires_at = entry
|
|
93
|
-
if now > expires_at:
|
|
94
|
-
del self._cache[key]
|
|
95
|
-
continue
|
|
96
|
-
|
|
97
|
-
if pattern is None or fnmatch.fnmatch(key, pattern):
|
|
98
|
-
yield key
|
|
99
|
-
|
|
100
|
-
async def count(self, pattern: str | None = None) -> int:
|
|
101
|
-
"""Count keys matching pattern.
|
|
102
|
-
"""
|
|
103
|
-
count = 0
|
|
104
|
-
async for _ in self.keys(pattern):
|
|
105
|
-
count += 1
|
|
106
|
-
return count
|
|
107
|
-
|
|
108
|
-
async def close(self) -> None:
|
|
109
|
-
"""Close the backend (no-op for memory backend).
|
|
110
|
-
"""
|
|
111
|
-
pass
|
cachu/backends/async_redis.py
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
"""Async Redis cache backend implementation using redis.asyncio.
|
|
2
|
-
"""
|
|
3
|
-
import pickle
|
|
4
|
-
import struct
|
|
5
|
-
import time
|
|
6
|
-
from collections.abc import AsyncIterator
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
-
|
|
9
|
-
from . import NO_VALUE
|
|
10
|
-
from .async_base import AsyncBackend
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
import redis.asyncio as aioredis
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
_METADATA_FORMAT = 'd'
|
|
17
|
-
_METADATA_SIZE = struct.calcsize(_METADATA_FORMAT)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _get_async_redis_module() -> Any:
|
|
21
|
-
"""Import redis.asyncio module, raising helpful error if not installed.
|
|
22
|
-
"""
|
|
23
|
-
try:
|
|
24
|
-
import redis.asyncio as aioredis
|
|
25
|
-
return aioredis
|
|
26
|
-
except ImportError as e:
|
|
27
|
-
raise RuntimeError(
|
|
28
|
-
"Async Redis support requires the 'redis' package (>=4.2.0). "
|
|
29
|
-
"Install with: pip install cachu[redis]"
|
|
30
|
-
) from e
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
async def get_async_redis_client(url: str) -> 'aioredis.Redis':
|
|
34
|
-
"""Create an async Redis client from URL.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
url: Redis URL (e.g., 'redis://localhost:6379/0')
|
|
38
|
-
"""
|
|
39
|
-
aioredis = _get_async_redis_module()
|
|
40
|
-
return aioredis.from_url(url)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class AsyncRedisBackend(AsyncBackend):
|
|
44
|
-
"""Async Redis cache backend using redis.asyncio.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
def __init__(self, url: str, distributed_lock: bool = False) -> None:
|
|
48
|
-
self._url = url
|
|
49
|
-
self._distributed_lock = distributed_lock
|
|
50
|
-
self._client: aioredis.Redis | None = None
|
|
51
|
-
|
|
52
|
-
async def _get_client(self) -> 'aioredis.Redis':
|
|
53
|
-
"""Lazy-load async Redis client.
|
|
54
|
-
"""
|
|
55
|
-
if self._client is None:
|
|
56
|
-
self._client = await get_async_redis_client(self._url)
|
|
57
|
-
return self._client
|
|
58
|
-
|
|
59
|
-
def _pack_value(self, value: Any, created_at: float) -> bytes:
|
|
60
|
-
"""Pack value with creation timestamp.
|
|
61
|
-
"""
|
|
62
|
-
metadata = struct.pack(_METADATA_FORMAT, created_at)
|
|
63
|
-
pickled = pickle.dumps(value)
|
|
64
|
-
return metadata + pickled
|
|
65
|
-
|
|
66
|
-
def _unpack_value(self, data: bytes) -> tuple[Any, float]:
|
|
67
|
-
"""Unpack value and creation timestamp.
|
|
68
|
-
"""
|
|
69
|
-
created_at = struct.unpack(_METADATA_FORMAT, data[:_METADATA_SIZE])[0]
|
|
70
|
-
value = pickle.loads(data[_METADATA_SIZE:])
|
|
71
|
-
return value, created_at
|
|
72
|
-
|
|
73
|
-
async def get(self, key: str) -> Any:
|
|
74
|
-
"""Get value by key. Returns NO_VALUE if not found.
|
|
75
|
-
"""
|
|
76
|
-
client = await self._get_client()
|
|
77
|
-
data = await client.get(key)
|
|
78
|
-
if data is None:
|
|
79
|
-
return NO_VALUE
|
|
80
|
-
value, _ = self._unpack_value(data)
|
|
81
|
-
return value
|
|
82
|
-
|
|
83
|
-
async def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
84
|
-
"""Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
85
|
-
"""
|
|
86
|
-
client = await self._get_client()
|
|
87
|
-
data = await client.get(key)
|
|
88
|
-
if data is None:
|
|
89
|
-
return NO_VALUE, None
|
|
90
|
-
value, created_at = self._unpack_value(data)
|
|
91
|
-
return value, created_at
|
|
92
|
-
|
|
93
|
-
async def set(self, key: str, value: Any, ttl: int) -> None:
|
|
94
|
-
"""Set value with TTL in seconds.
|
|
95
|
-
"""
|
|
96
|
-
client = await self._get_client()
|
|
97
|
-
now = time.time()
|
|
98
|
-
packed = self._pack_value(value, now)
|
|
99
|
-
await client.setex(key, ttl, packed)
|
|
100
|
-
|
|
101
|
-
async def delete(self, key: str) -> None:
|
|
102
|
-
"""Delete value by key.
|
|
103
|
-
"""
|
|
104
|
-
client = await self._get_client()
|
|
105
|
-
await client.delete(key)
|
|
106
|
-
|
|
107
|
-
async def clear(self, pattern: str | None = None) -> int:
|
|
108
|
-
"""Clear entries matching pattern. Returns count of cleared entries.
|
|
109
|
-
"""
|
|
110
|
-
client = await self._get_client()
|
|
111
|
-
if pattern is None:
|
|
112
|
-
pattern = '*'
|
|
113
|
-
|
|
114
|
-
count = 0
|
|
115
|
-
async for key in client.scan_iter(match=pattern):
|
|
116
|
-
await client.delete(key)
|
|
117
|
-
count += 1
|
|
118
|
-
return count
|
|
119
|
-
|
|
120
|
-
async def keys(self, pattern: str | None = None) -> AsyncIterator[str]:
|
|
121
|
-
"""Iterate over keys matching pattern.
|
|
122
|
-
"""
|
|
123
|
-
client = await self._get_client()
|
|
124
|
-
redis_pattern = pattern or '*'
|
|
125
|
-
async for key in client.scan_iter(match=redis_pattern):
|
|
126
|
-
yield key.decode() if isinstance(key, bytes) else key
|
|
127
|
-
|
|
128
|
-
async def count(self, pattern: str | None = None) -> int:
|
|
129
|
-
"""Count keys matching pattern.
|
|
130
|
-
"""
|
|
131
|
-
count = 0
|
|
132
|
-
async for _ in self.keys(pattern):
|
|
133
|
-
count += 1
|
|
134
|
-
return count
|
|
135
|
-
|
|
136
|
-
async def close(self) -> None:
|
|
137
|
-
"""Close the Redis connection.
|
|
138
|
-
"""
|
|
139
|
-
if self._client is not None:
|
|
140
|
-
await self._client.close()
|
|
141
|
-
self._client = None
|