cachify 0.1.0__py3-none-any.whl → 0.2.1__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.
@@ -1,219 +1,219 @@
1
- import asyncio
2
- import functools
3
- import inspect
4
- import threading
5
- import time
6
- from asyncio import AbstractEventLoop
7
- from concurrent.futures import Future as ConcurrentFuture
8
- from dataclasses import dataclass
9
- from typing import Any, Callable
10
-
11
- from cachify.config import logger
12
- from cachify.types import CacheConfig, CacheKeyFunction, Number
13
- from cachify.utils.arguments import create_cache_key
14
-
15
- _NEVER_DIE_THREAD: threading.Thread | None = None
16
- _NEVER_DIE_LOCK: threading.Lock = threading.Lock()
17
- _NEVER_DIE_REGISTRY: list["NeverDieCacheEntry"] = []
18
- _NEVER_DIE_CACHE_THREADS: dict[str, threading.Thread] = {}
19
- _NEVER_DIE_CACHE_FUTURES: dict[str, ConcurrentFuture] = {}
20
-
21
- _MAX_BACKOFF: int = 10
22
- _BACKOFF_MULTIPLIER: float = 1.25
23
- _REFRESH_INTERVAL_SECONDS: float = 0.1
24
-
25
-
26
- @dataclass
27
- class NeverDieCacheEntry:
28
- function: Callable[..., Any]
29
- ttl: Number
30
- args: tuple
31
- kwargs: dict
32
- cache_key_func: CacheKeyFunction | None
33
- ignore_fields: tuple[str, ...]
34
- loop: AbstractEventLoop | None
35
- config: CacheConfig
36
-
37
- def __post_init__(self):
38
- self._backoff: float = 1
39
- self._expires_at: float = time.monotonic() + self.ttl
40
-
41
- @functools.cached_property
42
- def cache_key(self) -> str:
43
- return create_cache_key(
44
- self.function,
45
- self.cache_key_func,
46
- self.ignore_fields,
47
- self.args,
48
- self.kwargs,
49
- )
50
-
51
- def __eq__(self, other: Any) -> bool:
52
- if not isinstance(other, NeverDieCacheEntry):
53
- return False
54
- return self.cache_key == other.cache_key
55
-
56
- def __hash__(self) -> int:
57
- return hash(self.cache_key)
58
-
59
- def is_expired(self) -> bool:
60
- return time.monotonic() > self._expires_at
61
-
62
- def reset(self):
63
- self._backoff = 1
64
- self._expires_at = time.monotonic() + self.ttl
65
-
66
- def revive(self):
67
- self._backoff = min(self._backoff * _BACKOFF_MULTIPLIER, _MAX_BACKOFF)
68
- self._expires_at = time.monotonic() + self.ttl * self._backoff
69
-
70
-
71
- def _run_sync_function_and_cache(entry: NeverDieCacheEntry):
72
- """Run a function and cache its result"""
73
- try:
74
- with entry.config.sync_lock(entry.cache_key):
75
- result = entry.function(*entry.args, **entry.kwargs)
76
- entry.config.storage.set(entry.cache_key, result, None)
77
- entry.reset()
78
- except BaseException:
79
- entry.revive()
80
- logger.debug(
81
- "Exception caching function with never_die",
82
- extra={"function": entry.function.__qualname__},
83
- exc_info=True,
84
- )
85
-
86
-
87
- async def _run_async_function_and_cache(entry: NeverDieCacheEntry):
88
- """Run a function and cache its result"""
89
- try:
90
- async with entry.config.async_lock(entry.cache_key):
91
- result = await entry.function(*entry.args, **entry.kwargs)
92
- await entry.config.storage.aset(entry.cache_key, result, None)
93
- entry.reset()
94
- except BaseException:
95
- entry.revive()
96
- logger.debug(
97
- "Exception caching function with never_die",
98
- extra={"function": entry.function.__qualname__},
99
- exc_info=True,
100
- )
101
-
102
-
103
- def _cache_is_being_set(entry: NeverDieCacheEntry) -> bool:
104
- if entry.loop:
105
- return entry.cache_key in _NEVER_DIE_CACHE_FUTURES and not _NEVER_DIE_CACHE_FUTURES[entry.cache_key].done()
106
- return entry.cache_key in _NEVER_DIE_CACHE_THREADS and _NEVER_DIE_CACHE_THREADS[entry.cache_key].is_alive()
107
-
108
-
109
- def _clear_dead_futures():
110
- """Clear dead futures from the cache future registry"""
111
- for cache_key, thread in list(_NEVER_DIE_CACHE_FUTURES.items()):
112
- if thread.done():
113
- del _NEVER_DIE_CACHE_FUTURES[cache_key]
114
-
115
-
116
- def _clear_dead_threads():
117
- """Clear dead threads from the cache thread registry"""
118
- for cache_key, thread in list(_NEVER_DIE_CACHE_THREADS.items()):
119
- if thread.is_alive():
120
- continue
121
- del _NEVER_DIE_CACHE_THREADS[cache_key]
122
-
123
-
124
- def _refresh_never_die_caches():
125
- """Background thread function that periodically refreshes never_die cache entries"""
126
- while True:
127
- try:
128
- for entry in list(_NEVER_DIE_REGISTRY):
129
- if not entry.is_expired():
130
- continue
131
-
132
- if _cache_is_being_set(entry):
133
- continue
134
-
135
- if not entry.loop: # sync
136
- thread = threading.Thread(target=_run_sync_function_and_cache, args=(entry,), daemon=True)
137
- thread.start()
138
- _NEVER_DIE_CACHE_THREADS[entry.cache_key] = thread
139
- continue
140
-
141
- if entry.loop.is_closed():
142
- logger.debug(
143
- f"Loop is closed, skipping future creation",
144
- extra={"function": entry.function.__qualname__},
145
- exc_info=True,
146
- )
147
- continue
148
-
149
- try:
150
- coroutine = _run_async_function_and_cache(entry)
151
- future = asyncio.run_coroutine_threadsafe(coroutine, entry.loop)
152
- except RuntimeError:
153
- coroutine.close()
154
- logger.debug(
155
- f"Loop is closed, skipping future creation",
156
- extra={"function": entry.function.__qualname__},
157
- exc_info=True,
158
- )
159
- continue
160
-
161
- _NEVER_DIE_CACHE_FUTURES[entry.cache_key] = future
162
- finally:
163
- time.sleep(_REFRESH_INTERVAL_SECONDS)
164
- _clear_dead_futures()
165
- _clear_dead_threads()
166
-
167
-
168
- def _start_never_die_thread():
169
- """Start the background thread if it's not already running"""
170
- global _NEVER_DIE_THREAD
171
- with _NEVER_DIE_LOCK:
172
- if _NEVER_DIE_THREAD and _NEVER_DIE_THREAD.is_alive():
173
- return
174
-
175
- _NEVER_DIE_THREAD = threading.Thread(target=_refresh_never_die_caches, daemon=True)
176
- _NEVER_DIE_THREAD.start()
177
-
178
-
179
- def register_never_die_function(
180
- function: Callable[..., Any],
181
- ttl: Number,
182
- args: tuple,
183
- kwargs: dict,
184
- cache_key_func: CacheKeyFunction | None,
185
- ignore_fields: tuple[str, ...],
186
- config: CacheConfig,
187
- ):
188
- """Register a function for never_die cache refreshing"""
189
- is_async = inspect.iscoroutinefunction(function)
190
-
191
- entry = NeverDieCacheEntry(
192
- function=function,
193
- ttl=ttl,
194
- args=args,
195
- kwargs=kwargs,
196
- cache_key_func=cache_key_func,
197
- ignore_fields=ignore_fields,
198
- loop=asyncio.get_running_loop() if is_async else None,
199
- config=config,
200
- )
201
-
202
- with _NEVER_DIE_LOCK:
203
- if entry not in _NEVER_DIE_REGISTRY:
204
- _NEVER_DIE_REGISTRY.append(entry)
205
-
206
- _start_never_die_thread()
207
-
208
-
209
- def clear_never_die_registry():
210
- """
211
- Clear all entries from the never_die registry.
212
-
213
- Useful for testing to prevent background threads from
214
- accessing resources that have been cleaned up.
215
- """
216
- with _NEVER_DIE_LOCK:
217
- _NEVER_DIE_REGISTRY.clear()
218
- _NEVER_DIE_CACHE_THREADS.clear()
219
- _NEVER_DIE_CACHE_FUTURES.clear()
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+ import threading
5
+ import time
6
+ from asyncio import AbstractEventLoop
7
+ from concurrent.futures import Future as ConcurrentFuture
8
+ from dataclasses import dataclass
9
+ from typing import Any, Callable
10
+
11
+ from cachify.config import logger
12
+ from cachify.types import CacheConfig, CacheKeyFunction, Number
13
+ from cachify.utils.arguments import create_cache_key
14
+
15
+ _NEVER_DIE_THREAD: threading.Thread | None = None
16
+ _NEVER_DIE_LOCK: threading.Lock = threading.Lock()
17
+ _NEVER_DIE_REGISTRY: list["NeverDieCacheEntry"] = []
18
+ _NEVER_DIE_CACHE_THREADS: dict[str, threading.Thread] = {}
19
+ _NEVER_DIE_CACHE_FUTURES: dict[str, ConcurrentFuture] = {}
20
+
21
+ _MAX_BACKOFF: int = 10
22
+ _BACKOFF_MULTIPLIER: float = 1.25
23
+ _REFRESH_INTERVAL_SECONDS: float = 0.1
24
+
25
+
26
+ @dataclass
27
+ class NeverDieCacheEntry:
28
+ function: Callable[..., Any]
29
+ ttl: Number
30
+ args: tuple
31
+ kwargs: dict
32
+ cache_key_func: CacheKeyFunction | None
33
+ ignore_fields: tuple[str, ...]
34
+ loop: AbstractEventLoop | None
35
+ config: CacheConfig
36
+
37
+ def __post_init__(self):
38
+ self._backoff: float = 1
39
+ self._expires_at: float = time.monotonic() + self.ttl
40
+
41
+ @functools.cached_property
42
+ def cache_key(self) -> str:
43
+ return create_cache_key(
44
+ self.function,
45
+ self.cache_key_func,
46
+ self.ignore_fields,
47
+ self.args,
48
+ self.kwargs,
49
+ )
50
+
51
+ def __eq__(self, other: Any) -> bool:
52
+ if not isinstance(other, NeverDieCacheEntry):
53
+ return False
54
+ return self.cache_key == other.cache_key
55
+
56
+ def __hash__(self) -> int:
57
+ return hash(self.cache_key)
58
+
59
+ def is_expired(self) -> bool:
60
+ return time.monotonic() > self._expires_at
61
+
62
+ def reset(self):
63
+ self._backoff = 1
64
+ self._expires_at = time.monotonic() + self.ttl
65
+
66
+ def revive(self):
67
+ self._backoff = min(self._backoff * _BACKOFF_MULTIPLIER, _MAX_BACKOFF)
68
+ self._expires_at = time.monotonic() + self.ttl * self._backoff
69
+
70
+
71
+ def _run_sync_function_and_cache(entry: NeverDieCacheEntry):
72
+ """Run a function and cache its result"""
73
+ try:
74
+ with entry.config.sync_lock(entry.cache_key):
75
+ result = entry.function(*entry.args, **entry.kwargs)
76
+ entry.config.storage.set(entry.cache_key, result, None)
77
+ entry.reset()
78
+ except BaseException:
79
+ entry.revive()
80
+ logger.debug(
81
+ "Exception caching function with never_die",
82
+ extra={"function": entry.function.__qualname__},
83
+ exc_info=True,
84
+ )
85
+
86
+
87
+ async def _run_async_function_and_cache(entry: NeverDieCacheEntry):
88
+ """Run a function and cache its result"""
89
+ try:
90
+ async with entry.config.async_lock(entry.cache_key):
91
+ result = await entry.function(*entry.args, **entry.kwargs)
92
+ await entry.config.storage.aset(entry.cache_key, result, None)
93
+ entry.reset()
94
+ except BaseException:
95
+ entry.revive()
96
+ logger.debug(
97
+ "Exception caching function with never_die",
98
+ extra={"function": entry.function.__qualname__},
99
+ exc_info=True,
100
+ )
101
+
102
+
103
+ def _cache_is_being_set(entry: NeverDieCacheEntry) -> bool:
104
+ if entry.loop:
105
+ return entry.cache_key in _NEVER_DIE_CACHE_FUTURES and not _NEVER_DIE_CACHE_FUTURES[entry.cache_key].done()
106
+ return entry.cache_key in _NEVER_DIE_CACHE_THREADS and _NEVER_DIE_CACHE_THREADS[entry.cache_key].is_alive()
107
+
108
+
109
+ def _clear_dead_futures():
110
+ """Clear dead futures from the cache future registry"""
111
+ for cache_key, thread in list(_NEVER_DIE_CACHE_FUTURES.items()):
112
+ if thread.done():
113
+ del _NEVER_DIE_CACHE_FUTURES[cache_key]
114
+
115
+
116
+ def _clear_dead_threads():
117
+ """Clear dead threads from the cache thread registry"""
118
+ for cache_key, thread in list(_NEVER_DIE_CACHE_THREADS.items()):
119
+ if thread.is_alive():
120
+ continue
121
+ del _NEVER_DIE_CACHE_THREADS[cache_key]
122
+
123
+
124
+ def _refresh_never_die_caches():
125
+ """Background thread function that periodically refreshes never_die cache entries"""
126
+ while True:
127
+ try:
128
+ for entry in list(_NEVER_DIE_REGISTRY):
129
+ if not entry.is_expired():
130
+ continue
131
+
132
+ if _cache_is_being_set(entry):
133
+ continue
134
+
135
+ if not entry.loop: # sync
136
+ thread = threading.Thread(target=_run_sync_function_and_cache, args=(entry,), daemon=True)
137
+ thread.start()
138
+ _NEVER_DIE_CACHE_THREADS[entry.cache_key] = thread
139
+ continue
140
+
141
+ if entry.loop.is_closed():
142
+ logger.debug(
143
+ f"Loop is closed, skipping future creation",
144
+ extra={"function": entry.function.__qualname__},
145
+ exc_info=True,
146
+ )
147
+ continue
148
+
149
+ try:
150
+ coroutine = _run_async_function_and_cache(entry)
151
+ future = asyncio.run_coroutine_threadsafe(coroutine, entry.loop)
152
+ except RuntimeError:
153
+ coroutine.close()
154
+ logger.debug(
155
+ f"Loop is closed, skipping future creation",
156
+ extra={"function": entry.function.__qualname__},
157
+ exc_info=True,
158
+ )
159
+ continue
160
+
161
+ _NEVER_DIE_CACHE_FUTURES[entry.cache_key] = future
162
+ finally:
163
+ time.sleep(_REFRESH_INTERVAL_SECONDS)
164
+ _clear_dead_futures()
165
+ _clear_dead_threads()
166
+
167
+
168
+ def _start_never_die_thread():
169
+ """Start the background thread if it's not already running"""
170
+ global _NEVER_DIE_THREAD
171
+ with _NEVER_DIE_LOCK:
172
+ if _NEVER_DIE_THREAD and _NEVER_DIE_THREAD.is_alive():
173
+ return
174
+
175
+ _NEVER_DIE_THREAD = threading.Thread(target=_refresh_never_die_caches, daemon=True)
176
+ _NEVER_DIE_THREAD.start()
177
+
178
+
179
+ def register_never_die_function(
180
+ function: Callable[..., Any],
181
+ ttl: Number,
182
+ args: tuple,
183
+ kwargs: dict,
184
+ cache_key_func: CacheKeyFunction | None,
185
+ ignore_fields: tuple[str, ...],
186
+ config: CacheConfig,
187
+ ):
188
+ """Register a function for never_die cache refreshing"""
189
+ is_async = inspect.iscoroutinefunction(function)
190
+
191
+ entry = NeverDieCacheEntry(
192
+ function=function,
193
+ ttl=ttl,
194
+ args=args,
195
+ kwargs=kwargs,
196
+ cache_key_func=cache_key_func,
197
+ ignore_fields=ignore_fields,
198
+ loop=asyncio.get_running_loop() if is_async else None,
199
+ config=config,
200
+ )
201
+
202
+ with _NEVER_DIE_LOCK:
203
+ if entry not in _NEVER_DIE_REGISTRY:
204
+ _NEVER_DIE_REGISTRY.append(entry)
205
+
206
+ _start_never_die_thread()
207
+
208
+
209
+ def clear_never_die_registry():
210
+ """
211
+ Clear all entries from the never_die registry.
212
+
213
+ Useful for testing to prevent background threads from
214
+ accessing resources that have been cleaned up.
215
+ """
216
+ with _NEVER_DIE_LOCK:
217
+ _NEVER_DIE_REGISTRY.clear()
218
+ _NEVER_DIE_CACHE_THREADS.clear()
219
+ _NEVER_DIE_CACHE_FUTURES.clear()
cachify/memory_cache.py CHANGED
@@ -1,37 +1,37 @@
1
- import threading
2
- from typing import Callable
3
-
4
- from cachify.cache import base_cache
5
- from cachify.storage.memory_storage import MemoryStorage
6
- from cachify.types import CacheConfig, CacheKeyFunction, F, Number
7
- from cachify.utils.locks import ASYNC_LOCKS, SYNC_LOCKS
8
-
9
- _CACHE_CLEAR_THREAD: threading.Thread | None = None
10
- _CACHE_CLEAR_LOCK: threading.Lock = threading.Lock()
11
-
12
- _MEMORY_CONFIG = CacheConfig(
13
- storage=MemoryStorage,
14
- sync_lock=lambda cache_key: SYNC_LOCKS[cache_key],
15
- async_lock=lambda cache_key: ASYNC_LOCKS[cache_key],
16
- )
17
-
18
-
19
- def _start_cache_clear_thread():
20
- """This is to avoid memory leaks by clearing expired cache items periodically."""
21
- global _CACHE_CLEAR_THREAD
22
- with _CACHE_CLEAR_LOCK:
23
- if _CACHE_CLEAR_THREAD and _CACHE_CLEAR_THREAD.is_alive():
24
- return
25
- _CACHE_CLEAR_THREAD = threading.Thread(target=MemoryStorage.clear_expired_cached_items, daemon=True)
26
- _CACHE_CLEAR_THREAD.start()
27
-
28
-
29
- def cache(
30
- ttl: Number = 300,
31
- never_die: bool = False,
32
- cache_key_func: CacheKeyFunction | None = None,
33
- ignore_fields: tuple[str, ...] = (),
34
- ) -> Callable[[F], F]:
35
- """In-memory cache decorator. See `base_cache` for full documentation."""
36
- _start_cache_clear_thread()
37
- return base_cache(ttl, never_die, cache_key_func, ignore_fields, _MEMORY_CONFIG)
1
+ import threading
2
+ from typing import Callable
3
+
4
+ from cachify.cache import base_cache
5
+ from cachify.storage.memory_storage import MemoryStorage
6
+ from cachify.types import CacheConfig, CacheKeyFunction, F, Number
7
+ from cachify.utils.locks import ASYNC_LOCKS, SYNC_LOCKS
8
+
9
+ _CACHE_CLEAR_THREAD: threading.Thread | None = None
10
+ _CACHE_CLEAR_LOCK: threading.Lock = threading.Lock()
11
+
12
+ _MEMORY_CONFIG = CacheConfig(
13
+ storage=MemoryStorage,
14
+ sync_lock=lambda cache_key: SYNC_LOCKS[cache_key],
15
+ async_lock=lambda cache_key: ASYNC_LOCKS[cache_key],
16
+ )
17
+
18
+
19
+ def _start_cache_clear_thread():
20
+ """This is to avoid memory leaks by clearing expired cache items periodically."""
21
+ global _CACHE_CLEAR_THREAD
22
+ with _CACHE_CLEAR_LOCK:
23
+ if _CACHE_CLEAR_THREAD and _CACHE_CLEAR_THREAD.is_alive():
24
+ return
25
+ _CACHE_CLEAR_THREAD = threading.Thread(target=MemoryStorage.clear_expired_cached_items, daemon=True)
26
+ _CACHE_CLEAR_THREAD.start()
27
+
28
+
29
+ def cache(
30
+ ttl: Number = 300,
31
+ never_die: bool = False,
32
+ cache_key_func: CacheKeyFunction | None = None,
33
+ ignore_fields: tuple[str, ...] = (),
34
+ ) -> Callable[[F], F]:
35
+ """In-memory cache decorator. See `base_cache` for full documentation."""
36
+ _start_cache_clear_thread()
37
+ return base_cache(ttl, never_die, cache_key_func, ignore_fields, _MEMORY_CONFIG)
cachify/redis/__init__.py CHANGED
@@ -1,19 +1,19 @@
1
- from cachify.redis.config import (
2
- DEFAULT_KEY_PREFIX,
3
- DEFAULT_LOCK_TIMEOUT,
4
- RedisConfig,
5
- get_redis_config,
6
- reset_redis_config,
7
- setup_redis_config,
8
- )
9
- from cachify.redis.lock import RedisLockManager
10
-
11
- __all__ = [
12
- "DEFAULT_KEY_PREFIX",
13
- "DEFAULT_LOCK_TIMEOUT",
14
- "RedisConfig",
15
- "RedisLockManager",
16
- "get_redis_config",
17
- "reset_redis_config",
18
- "setup_redis_config",
19
- ]
1
+ from cachify.redis.config import (
2
+ DEFAULT_KEY_PREFIX,
3
+ DEFAULT_LOCK_TIMEOUT,
4
+ RedisConfig,
5
+ get_redis_config,
6
+ reset_redis_config,
7
+ setup_redis_config,
8
+ )
9
+ from cachify.redis.lock import RedisLockManager
10
+
11
+ __all__ = [
12
+ "DEFAULT_KEY_PREFIX",
13
+ "DEFAULT_LOCK_TIMEOUT",
14
+ "RedisConfig",
15
+ "RedisLockManager",
16
+ "get_redis_config",
17
+ "reset_redis_config",
18
+ "setup_redis_config",
19
+ ]