cachu 0.2.3__tar.gz → 0.2.5__tar.gz

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.
Files changed (56) hide show
  1. {cachu-0.2.3 → cachu-0.2.5}/PKG-INFO +7 -10
  2. {cachu-0.2.3 → cachu-0.2.5}/README.md +6 -8
  3. {cachu-0.2.3 → cachu-0.2.5}/pyproject.toml +8 -9
  4. {cachu-0.2.3 → cachu-0.2.5}/setup.cfg +1 -1
  5. {cachu-0.2.3 → cachu-0.2.5}/src/cachu/__init__.py +8 -12
  6. cachu-0.2.5/src/cachu/backends/__init__.py +112 -0
  7. cachu-0.2.5/src/cachu/backends/memory.py +204 -0
  8. cachu-0.2.5/src/cachu/backends/redis.py +263 -0
  9. cachu-0.2.5/src/cachu/backends/sqlite.py +525 -0
  10. {cachu-0.2.3 → cachu-0.2.5}/src/cachu/config.py +6 -6
  11. cachu-0.2.5/src/cachu/decorator.py +519 -0
  12. {cachu-0.2.3 → cachu-0.2.5}/src/cachu/keys.py +8 -0
  13. cachu-0.2.5/src/cachu/mutex.py +247 -0
  14. cachu-0.2.5/src/cachu/operations.py +330 -0
  15. {cachu-0.2.3 → cachu-0.2.5}/src/cachu.egg-info/PKG-INFO +7 -10
  16. {cachu-0.2.3 → cachu-0.2.5}/src/cachu.egg-info/SOURCES.txt +5 -7
  17. {cachu-0.2.3 → cachu-0.2.5}/src/cachu.egg-info/requires.txt +0 -1
  18. {cachu-0.2.3 → cachu-0.2.5}/tests/test_async_memory.py +8 -8
  19. {cachu-0.2.3 → cachu-0.2.5}/tests/test_async_redis.py +32 -35
  20. {cachu-0.2.3 → cachu-0.2.5}/tests/test_async_sqlite.py +40 -45
  21. {cachu-0.2.3 → cachu-0.2.5}/tests/test_clearing.py +156 -25
  22. {cachu-0.2.3 → cachu-0.2.5}/tests/test_config.py +1 -5
  23. cachu-0.2.5/tests/test_dogpile.py +331 -0
  24. cachu-0.2.5/tests/test_error_handling.py +173 -0
  25. cachu-0.2.5/tests/test_helpers.py +299 -0
  26. {cachu-0.2.3 → cachu-0.2.5}/tests/test_memory_cache.py +26 -0
  27. cachu-0.2.5/tests/test_mutex.py +303 -0
  28. {cachu-0.2.3 → cachu-0.2.5}/tests/test_namespace_isolation.py +8 -3
  29. cachu-0.2.5/tests/test_ttl_isolation.py +233 -0
  30. cachu-0.2.3/src/cachu/async_decorator.py +0 -262
  31. cachu-0.2.3/src/cachu/async_operations.py +0 -178
  32. cachu-0.2.3/src/cachu/backends/__init__.py +0 -52
  33. cachu-0.2.3/src/cachu/backends/async_base.py +0 -50
  34. cachu-0.2.3/src/cachu/backends/async_memory.py +0 -111
  35. cachu-0.2.3/src/cachu/backends/async_redis.py +0 -141
  36. cachu-0.2.3/src/cachu/backends/async_sqlite.py +0 -256
  37. cachu-0.2.3/src/cachu/backends/file.py +0 -10
  38. cachu-0.2.3/src/cachu/backends/memory.py +0 -102
  39. cachu-0.2.3/src/cachu/backends/redis.py +0 -131
  40. cachu-0.2.3/src/cachu/backends/sqlite.py +0 -240
  41. cachu-0.2.3/src/cachu/decorator.py +0 -252
  42. cachu-0.2.3/src/cachu/operations.py +0 -182
  43. cachu-0.2.3/tests/test_ttl_isolation.py +0 -246
  44. {cachu-0.2.3 → cachu-0.2.5}/src/cachu/types.py +0 -0
  45. {cachu-0.2.3 → cachu-0.2.5}/src/cachu.egg-info/dependency_links.txt +0 -0
  46. {cachu-0.2.3 → cachu-0.2.5}/src/cachu.egg-info/top_level.txt +0 -0
  47. {cachu-0.2.3 → cachu-0.2.5}/tests/test_defaultcache.py +0 -0
  48. {cachu-0.2.3 → cachu-0.2.5}/tests/test_delete_keys.py +0 -0
  49. {cachu-0.2.3 → cachu-0.2.5}/tests/test_disable.py +0 -0
  50. {cachu-0.2.3 → cachu-0.2.5}/tests/test_exclude_params.py +0 -0
  51. {cachu-0.2.3 → cachu-0.2.5}/tests/test_file_cache.py +0 -0
  52. {cachu-0.2.3 → cachu-0.2.5}/tests/test_integration.py +0 -0
  53. {cachu-0.2.3 → cachu-0.2.5}/tests/test_namespace.py +0 -0
  54. {cachu-0.2.3 → cachu-0.2.5}/tests/test_redis_cache.py +0 -0
  55. {cachu-0.2.3 → cachu-0.2.5}/tests/test_set_keys.py +0 -0
  56. {cachu-0.2.3 → cachu-0.2.5}/tests/test_sqlite_backend.py +0 -0
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachu
3
- Version: 0.2.3
3
+ Version: 0.2.5
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
7
7
  Project-URL: Repository, https://github.com/bissli/cachu.git
8
8
  Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
- Requires-Dist: dogpile.cache
11
10
  Requires-Dist: func-timeout
12
11
  Provides-Extra: async
13
12
  Requires-Dist: aiosqlite; extra == "async"
@@ -69,19 +68,17 @@ cachu.configure(
69
68
  key_prefix='v1:', # Prefix for all cache keys
70
69
  file_dir='/var/cache/app', # Directory for file cache
71
70
  redis_url='redis://localhost:6379/0', # Redis connection URL
72
- redis_distributed=False, # Use distributed locks for Redis
73
71
  )
74
72
  ```
75
73
 
76
74
  ### Configuration Options
77
75
 
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 |
76
+ | Option | Default | Description |
77
+ | ------------ | ---------------------------- | ------------------------------------------------- |
78
+ | `backend` | `'memory'` | Default backend type |
79
+ | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
80
+ | `file_dir` | `'/tmp'` | Directory for file-based caches |
81
+ | `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
85
82
 
86
83
  ### Package Isolation
87
84
 
@@ -46,19 +46,17 @@ cachu.configure(
46
46
  key_prefix='v1:', # Prefix for all cache keys
47
47
  file_dir='/var/cache/app', # Directory for file cache
48
48
  redis_url='redis://localhost:6379/0', # Redis connection URL
49
- redis_distributed=False, # Use distributed locks for Redis
50
49
  )
51
50
  ```
52
51
 
53
52
  ### Configuration Options
54
53
 
55
- | Option | Default | Description |
56
- | ------------------- | ---------------------------- | ------------------------------------------------- |
57
- | `backend` | `'memory'` | Default backend type |
58
- | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
59
- | `file_dir` | `'/tmp'` | Directory for file-based caches |
60
- | `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
61
- | `redis_distributed` | `False` | Enable distributed locks for Redis |
54
+ | Option | Default | Description |
55
+ | ------------ | ---------------------------- | ------------------------------------------------- |
56
+ | `backend` | `'memory'` | Default backend type |
57
+ | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
58
+ | `file_dir` | `'/tmp'` | Directory for file-based caches |
59
+ | `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
62
60
 
63
61
  ### Package Isolation
64
62
 
@@ -1,26 +1,25 @@
1
1
  [project]
2
2
  name = "cachu"
3
- version = "0.2.3"
3
+ version = "0.2.5"
4
4
  description = "Flexible caching library with sync and async support for memory, file (SQLite), and Redis backends"
5
5
  readme = "README.md"
6
6
  license = "0BSD"
7
7
  authors = [{ name = "bissli" }]
8
8
  requires-python = ">=3.10"
9
9
  dependencies = [
10
- "dogpile.cache",
11
- "func-timeout",
10
+ "func-timeout",
12
11
  ]
13
12
 
14
13
  [project.optional-dependencies]
15
14
  async = ["aiosqlite"]
16
15
  redis = ["redis>=4.2.0"]
17
16
  test = [
18
- "pytest",
19
- "pytest-asyncio",
20
- "pytest-mock",
21
- "redis>=4.2.0",
22
- "testcontainers[redis]",
23
- "aiosqlite",
17
+ "pytest",
18
+ "pytest-asyncio",
19
+ "pytest-mock",
20
+ "redis>=4.2.0",
21
+ "testcontainers[redis]",
22
+ "aiosqlite",
24
23
  ]
25
24
 
26
25
  [project.urls]
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.2.3
2
+ current_version = 0.2.5
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -1,19 +1,16 @@
1
1
  """Flexible caching library with support for memory, file, and Redis backends.
2
2
  """
3
- __version__ = '0.2.3'
3
+ __version__ = '0.2.5'
4
4
 
5
- from .async_decorator import async_cache, clear_async_backends
6
- from .async_decorator import get_async_backend, get_async_cache_info
7
- from .async_operations import async_cache_clear, async_cache_delete
8
- from .async_operations import async_cache_get, async_cache_info
9
- from .async_operations import async_cache_set
10
- from .backends import AsyncBackend, Backend
5
+ from .backends import Backend
11
6
  from .backends.redis import get_redis_client
12
7
  from .config import configure, disable, enable, get_all_configs, get_config
13
8
  from .config import is_disabled
14
- from .decorator import cache, get_backend
15
- from .operations import cache_clear, cache_delete, cache_get, cache_info
16
- from .operations import cache_set
9
+ from .decorator import aget_backend, cache, clear_async_backends
10
+ from .decorator import get_async_backend, get_async_cache_info, get_backend
11
+ from .operations import async_cache_clear, async_cache_delete, async_cache_get
12
+ from .operations import async_cache_info, async_cache_set, cache_clear
13
+ from .operations import cache_delete, cache_get, cache_info, cache_set
17
14
 
18
15
  __all__ = [
19
16
  'configure',
@@ -29,10 +26,9 @@ __all__ = [
29
26
  'cache_clear',
30
27
  'cache_info',
31
28
  'get_backend',
29
+ 'aget_backend',
32
30
  'get_redis_client',
33
31
  'Backend',
34
- 'AsyncBackend',
35
- 'async_cache',
36
32
  'async_cache_get',
37
33
  'async_cache_set',
38
34
  'async_cache_delete',
@@ -0,0 +1,112 @@
1
+ """Cache backend implementations.
2
+ """
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncIterator, Iterator
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from ..mutex import AsyncCacheMutex, CacheMutex
9
+
10
+ NO_VALUE = object()
11
+
12
+
13
+ class Backend(ABC):
14
+ """Abstract base class for cache backends with both sync and async interfaces.
15
+ """
16
+
17
+ # ===== Sync interface =====
18
+
19
+ @abstractmethod
20
+ def get(self, key: str) -> Any:
21
+ """Get value by key. Returns NO_VALUE if not found.
22
+ """
23
+
24
+ @abstractmethod
25
+ def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
26
+ """Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
27
+ """
28
+
29
+ @abstractmethod
30
+ def set(self, key: str, value: Any, ttl: int) -> None:
31
+ """Set value with TTL in seconds.
32
+ """
33
+
34
+ @abstractmethod
35
+ def delete(self, key: str) -> None:
36
+ """Delete value by key.
37
+ """
38
+
39
+ @abstractmethod
40
+ def clear(self, pattern: str | None = None) -> int:
41
+ """Clear entries matching pattern. Returns count of cleared entries.
42
+ """
43
+
44
+ @abstractmethod
45
+ def keys(self, pattern: str | None = None) -> Iterator[str]:
46
+ """Iterate over keys matching pattern.
47
+ """
48
+
49
+ @abstractmethod
50
+ def count(self, pattern: str | None = None) -> int:
51
+ """Count keys matching pattern.
52
+ """
53
+
54
+ @abstractmethod
55
+ def get_mutex(self, key: str) -> 'CacheMutex':
56
+ """Get a mutex for dogpile prevention on the given key.
57
+ """
58
+
59
+ # ===== Async interface =====
60
+
61
+ @abstractmethod
62
+ async def aget(self, key: str) -> Any:
63
+ """Async get value by key. Returns NO_VALUE if not found.
64
+ """
65
+
66
+ @abstractmethod
67
+ async def aget_with_metadata(self, key: str) -> tuple[Any, float | None]:
68
+ """Async get value and creation timestamp. Returns (NO_VALUE, None) if not found.
69
+ """
70
+
71
+ @abstractmethod
72
+ async def aset(self, key: str, value: Any, ttl: int) -> None:
73
+ """Async set value with TTL in seconds.
74
+ """
75
+
76
+ @abstractmethod
77
+ async def adelete(self, key: str) -> None:
78
+ """Async delete value by key.
79
+ """
80
+
81
+ @abstractmethod
82
+ async def aclear(self, pattern: str | None = None) -> int:
83
+ """Async clear entries matching pattern. Returns count of cleared entries.
84
+ """
85
+
86
+ @abstractmethod
87
+ def akeys(self, pattern: str | None = None) -> AsyncIterator[str]:
88
+ """Async iterate over keys matching pattern.
89
+ """
90
+
91
+ @abstractmethod
92
+ async def acount(self, pattern: str | None = None) -> int:
93
+ """Async count keys matching pattern.
94
+ """
95
+
96
+ @abstractmethod
97
+ def get_async_mutex(self, key: str) -> 'AsyncCacheMutex':
98
+ """Get an async mutex for dogpile prevention on the given key.
99
+ """
100
+
101
+ # ===== Lifecycle =====
102
+
103
+ def close(self) -> None:
104
+ """Close the backend and release resources.
105
+ """
106
+
107
+ async def aclose(self) -> None:
108
+ """Async close the backend and release resources.
109
+ """
110
+
111
+
112
+ __all__ = ['Backend', 'NO_VALUE']
@@ -0,0 +1,204 @@
1
+ """Memory cache backend implementation.
2
+ """
3
+ import asyncio
4
+ import fnmatch
5
+ import pickle
6
+ import threading
7
+ import time
8
+ from collections.abc import AsyncIterator, Iterator
9
+ from typing import Any
10
+
11
+ from ..mutex import AsyncioMutex, CacheMutex, ThreadingMutex
12
+ from . import NO_VALUE, Backend
13
+
14
+
15
+ class MemoryBackend(Backend):
16
+ """Thread-safe in-memory cache backend with both sync and async interfaces.
17
+ """
18
+
19
+ def __init__(self) -> None:
20
+ self._cache: dict[str, tuple[bytes, float, float]] = {}
21
+ self._sync_lock = threading.RLock()
22
+ self._async_lock: asyncio.Lock | None = None
23
+
24
+ def _get_async_lock(self) -> asyncio.Lock:
25
+ """Lazy-create async lock (must be called from async context).
26
+ """
27
+ if self._async_lock is None:
28
+ self._async_lock = asyncio.Lock()
29
+ return self._async_lock
30
+
31
+ # ===== Core logic (no locking) =====
32
+
33
+ def _do_get(self, key: str) -> tuple[Any, float | None]:
34
+ """Get value and metadata without locking.
35
+ """
36
+ entry = self._cache.get(key)
37
+ if entry is None:
38
+ return NO_VALUE, None
39
+
40
+ pickled_value, created_at, expires_at = entry
41
+ if time.time() > expires_at:
42
+ del self._cache[key]
43
+ return NO_VALUE, None
44
+
45
+ return pickle.loads(pickled_value), created_at
46
+
47
+ def _do_set(self, key: str, value: Any, ttl: int) -> None:
48
+ """Set value without locking.
49
+ """
50
+ now = time.time()
51
+ pickled_value = pickle.dumps(value)
52
+ self._cache[key] = (pickled_value, now, now + ttl)
53
+
54
+ def _do_delete(self, key: str) -> None:
55
+ """Delete value without locking.
56
+ """
57
+ self._cache.pop(key, None)
58
+
59
+ def _do_clear(self, pattern: str | None = None) -> int:
60
+ """Clear entries matching pattern without locking.
61
+ """
62
+ if pattern is None:
63
+ count = len(self._cache)
64
+ self._cache.clear()
65
+ return count
66
+
67
+ keys_to_delete = [k for k in self._cache if fnmatch.fnmatch(k, pattern)]
68
+ for key in keys_to_delete:
69
+ del self._cache[key]
70
+ return len(keys_to_delete)
71
+
72
+ def _do_keys(self, pattern: str | None = None) -> list[str]:
73
+ """Get keys matching pattern without locking (returns snapshot).
74
+ """
75
+ now = time.time()
76
+ result = []
77
+ keys_to_delete = []
78
+
79
+ for key, entry in list(self._cache.items()):
80
+ _, _, expires_at = entry
81
+ if now > expires_at:
82
+ keys_to_delete.append(key)
83
+ continue
84
+ if pattern is None or fnmatch.fnmatch(key, pattern):
85
+ result.append(key)
86
+
87
+ for key in keys_to_delete:
88
+ self._cache.pop(key, None)
89
+
90
+ return result
91
+
92
+ # ===== Sync interface =====
93
+
94
+ def get(self, key: str) -> Any:
95
+ """Get value by key. Returns NO_VALUE if not found or expired.
96
+ """
97
+ with self._sync_lock:
98
+ value, _ = self._do_get(key)
99
+ return value
100
+
101
+ def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
102
+ """Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
103
+ """
104
+ with self._sync_lock:
105
+ return self._do_get(key)
106
+
107
+ def set(self, key: str, value: Any, ttl: int) -> None:
108
+ """Set value with TTL in seconds.
109
+ """
110
+ with self._sync_lock:
111
+ self._do_set(key, value, ttl)
112
+
113
+ def delete(self, key: str) -> None:
114
+ """Delete value by key.
115
+ """
116
+ with self._sync_lock:
117
+ self._do_delete(key)
118
+
119
+ def clear(self, pattern: str | None = None) -> int:
120
+ """Clear entries matching pattern. Returns count of cleared entries.
121
+ """
122
+ with self._sync_lock:
123
+ return self._do_clear(pattern)
124
+
125
+ def keys(self, pattern: str | None = None) -> Iterator[str]:
126
+ """Iterate over keys matching pattern.
127
+ """
128
+ with self._sync_lock:
129
+ all_keys = self._do_keys(pattern)
130
+ yield from all_keys
131
+
132
+ def count(self, pattern: str | None = None) -> int:
133
+ """Count keys matching pattern.
134
+ """
135
+ with self._sync_lock:
136
+ return len(self._do_keys(pattern))
137
+
138
+ def get_mutex(self, key: str) -> CacheMutex:
139
+ """Get a mutex for dogpile prevention on the given key.
140
+ """
141
+ return ThreadingMutex(f'memory:{key}')
142
+
143
+ # ===== Async interface =====
144
+
145
+ async def aget(self, key: str) -> Any:
146
+ """Async get value by key. Returns NO_VALUE if not found or expired.
147
+ """
148
+ async with self._get_async_lock():
149
+ value, _ = self._do_get(key)
150
+ return value
151
+
152
+ async def aget_with_metadata(self, key: str) -> tuple[Any, float | None]:
153
+ """Async get value and creation timestamp. Returns (NO_VALUE, None) if not found.
154
+ """
155
+ async with self._get_async_lock():
156
+ return self._do_get(key)
157
+
158
+ async def aset(self, key: str, value: Any, ttl: int) -> None:
159
+ """Async set value with TTL in seconds.
160
+ """
161
+ async with self._get_async_lock():
162
+ self._do_set(key, value, ttl)
163
+
164
+ async def adelete(self, key: str) -> None:
165
+ """Async delete value by key.
166
+ """
167
+ async with self._get_async_lock():
168
+ self._do_delete(key)
169
+
170
+ async def aclear(self, pattern: str | None = None) -> int:
171
+ """Async clear entries matching pattern. Returns count of cleared entries.
172
+ """
173
+ async with self._get_async_lock():
174
+ return self._do_clear(pattern)
175
+
176
+ async def akeys(self, pattern: str | None = None) -> AsyncIterator[str]:
177
+ """Async iterate over keys matching pattern.
178
+ """
179
+ async with self._get_async_lock():
180
+ all_keys = self._do_keys(pattern)
181
+
182
+ for key in all_keys:
183
+ yield key
184
+
185
+ async def acount(self, pattern: str | None = None) -> int:
186
+ """Async count keys matching pattern.
187
+ """
188
+ async with self._get_async_lock():
189
+ return len(self._do_keys(pattern))
190
+
191
+ def get_async_mutex(self, key: str) -> AsyncioMutex:
192
+ """Get an async mutex for dogpile prevention on the given key.
193
+ """
194
+ return AsyncioMutex(f'memory:{key}')
195
+
196
+ # ===== Lifecycle =====
197
+
198
+ def close(self) -> None:
199
+ """Close the backend (no-op for memory backend).
200
+ """
201
+
202
+ async def aclose(self) -> None:
203
+ """Async close the backend (no-op for memory backend).
204
+ """