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/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)
@@ -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
- """
@@ -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
@@ -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