onecache 0.9.0__cp313-cp313-win32.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.
- onecache/__init__.py +122 -0
- onecache/_ccore.cp313-win32.pyd +0 -0
- onecache/_core.py +310 -0
- onecache/cache_value.py +3 -0
- onecache/utils.py +11 -0
- onecache-0.9.0.dist-info/METADATA +132 -0
- onecache-0.9.0.dist-info/RECORD +9 -0
- onecache-0.9.0.dist-info/WHEEL +5 -0
- onecache-0.9.0.dist-info/top_level.txt +1 -0
onecache/__init__.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Onecache main module."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from functools import update_wrapper
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
if os.environ.get("ONECACHE_NO_EXTENSIONS"):
|
|
8
|
+
from onecache._core import CacheValue, ExpirableCache, LRUCache, monotonic
|
|
9
|
+
|
|
10
|
+
COMPILED = False
|
|
11
|
+
else:
|
|
12
|
+
try:
|
|
13
|
+
from onecache._ccore import CacheValue, ExpirableCache, LRUCache, monotonic
|
|
14
|
+
|
|
15
|
+
COMPILED = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
from onecache._core import CacheValue, ExpirableCache, LRUCache, monotonic
|
|
18
|
+
|
|
19
|
+
COMPILED = False
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AsyncCacheDecorator",
|
|
23
|
+
"CacheDecorator",
|
|
24
|
+
"CacheValue",
|
|
25
|
+
"COMPILED",
|
|
26
|
+
"ExpirableCache",
|
|
27
|
+
"LRUCache",
|
|
28
|
+
"monotonic",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_MISSING = object()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CacheDecorator:
|
|
35
|
+
"""Decorator for ExpirableCache"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
maxsize=512,
|
|
40
|
+
ttl: Optional[int] = None,
|
|
41
|
+
skip_args: bool = False,
|
|
42
|
+
cache_class=LRUCache,
|
|
43
|
+
refresh_ttl: Optional[bool] = False,
|
|
44
|
+
thread_safe: Optional[bool] = False,
|
|
45
|
+
max_mem_size: Optional[int] = None,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Args:
|
|
49
|
+
* **maxsize (int)**: Maximun number of items to be cached. default: 512
|
|
50
|
+
* **ttl (int)**: time to expire in milliseconds, if None, it does not expire. default: None
|
|
51
|
+
* **skip_args (bool)**: apply cache as the function doesn't have any arguments, default: False
|
|
52
|
+
* **cache_class (class)**: Class to use for cache instance. default: LRUCache
|
|
53
|
+
* **refresh_ttl (bool)**: if cache with ttl, This flag makes key expiration timestamp to be
|
|
54
|
+
refresh per access. default: False
|
|
55
|
+
* **thread_safe (bool)**: tell decorator to use thread safe lock. default=False
|
|
56
|
+
* **max_mem_size (int)**: max mem size inside cache structure. default=None which means no limit.
|
|
57
|
+
For pypy this value is ignored as the objects can change by the JIT compilation.
|
|
58
|
+
"""
|
|
59
|
+
self.cache = cache_class(
|
|
60
|
+
maxsize,
|
|
61
|
+
ttl,
|
|
62
|
+
refresh_ttl=refresh_ttl,
|
|
63
|
+
thread_safe=thread_safe,
|
|
64
|
+
max_mem_size=max_mem_size,
|
|
65
|
+
)
|
|
66
|
+
self.maxsize = maxsize
|
|
67
|
+
self.skip_args = skip_args
|
|
68
|
+
self._fast = type(self.cache) in (ExpirableCache, LRUCache)
|
|
69
|
+
|
|
70
|
+
def __call__(self, func):
|
|
71
|
+
cache = self.cache
|
|
72
|
+
skip_args = self.skip_args
|
|
73
|
+
|
|
74
|
+
if self._fast:
|
|
75
|
+
return update_wrapper(cache._wrap(func, skip_args), func, updated=())
|
|
76
|
+
|
|
77
|
+
def wrapper(*args, **kwargs):
|
|
78
|
+
key = "any" if skip_args else cache.get_key(*args, **kwargs)
|
|
79
|
+
if key not in cache:
|
|
80
|
+
resp = func(*args, **kwargs)
|
|
81
|
+
cache.set(key, resp)
|
|
82
|
+
return resp
|
|
83
|
+
return cache.get(key)
|
|
84
|
+
|
|
85
|
+
wrapper.cache = cache
|
|
86
|
+
|
|
87
|
+
return update_wrapper(wrapper, func, updated=())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AsyncCacheDecorator(CacheDecorator):
|
|
91
|
+
"""Async Decorator for ExpirableCache"""
|
|
92
|
+
|
|
93
|
+
def __call__(self, func):
|
|
94
|
+
cache = self.cache
|
|
95
|
+
skip_args = self.skip_args
|
|
96
|
+
|
|
97
|
+
if self._fast:
|
|
98
|
+
make_key = cache._make_key
|
|
99
|
+
lookup = cache._lookup
|
|
100
|
+
set_value = cache.set
|
|
101
|
+
|
|
102
|
+
async def async_wrapper(*args, **kwargs):
|
|
103
|
+
key = "any" if skip_args else make_key(args, kwargs)
|
|
104
|
+
value = lookup(key, _MISSING)
|
|
105
|
+
if value is _MISSING:
|
|
106
|
+
value = await func(*args, **kwargs)
|
|
107
|
+
set_value(key, value)
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
|
|
112
|
+
async def async_wrapper(*args, **kwargs):
|
|
113
|
+
key = "any" if skip_args else cache.get_key(*args, **kwargs)
|
|
114
|
+
if key not in cache:
|
|
115
|
+
resp = await func(*args, **kwargs)
|
|
116
|
+
cache.set(key, resp)
|
|
117
|
+
return resp
|
|
118
|
+
return cache.get(key)
|
|
119
|
+
|
|
120
|
+
async_wrapper.cache = cache
|
|
121
|
+
|
|
122
|
+
return update_wrapper(async_wrapper, func, updated=())
|
|
Binary file
|
onecache/_core.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Pure Python cache implementation, used on PyPy or when the extension is unavailable."""
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from contextlib import nullcontext
|
|
5
|
+
from sys import getsizeof
|
|
6
|
+
from threading import RLock
|
|
7
|
+
from time import monotonic
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from onecache.utils import IS_PYPY
|
|
12
|
+
|
|
13
|
+
INF = float("inf")
|
|
14
|
+
_MISSING = object()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CacheValue:
|
|
18
|
+
"""Cache entry holding the value, its expiration (monotonic seconds) and access count."""
|
|
19
|
+
|
|
20
|
+
__slots__ = ("value", "expire_at", "size", "access")
|
|
21
|
+
|
|
22
|
+
def __init__(self, value: Any, expire_at: Optional[float] = None):
|
|
23
|
+
self.value = value
|
|
24
|
+
self.expire_at = INF if expire_at is None else expire_at
|
|
25
|
+
self.size = 0
|
|
26
|
+
self.access = 0
|
|
27
|
+
|
|
28
|
+
def expired(self, now: Optional[float] = None) -> bool:
|
|
29
|
+
"""Check if value is expired."""
|
|
30
|
+
return (monotonic() if now is None else now) > self.expire_at
|
|
31
|
+
|
|
32
|
+
def __eq__(self, other):
|
|
33
|
+
if not isinstance(other, CacheValue):
|
|
34
|
+
return NotImplemented
|
|
35
|
+
return self.value == other.value
|
|
36
|
+
|
|
37
|
+
__hash__ = None
|
|
38
|
+
|
|
39
|
+
def __reduce__(self):
|
|
40
|
+
return (_restore_value, (self.value, self.expire_at, self.size, self.access))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _restore_value(value, expire_at, size, access):
|
|
44
|
+
item = CacheValue(value, expire_at)
|
|
45
|
+
item.size = size
|
|
46
|
+
item.access = access
|
|
47
|
+
return item
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _serialize(data: Any):
|
|
51
|
+
if type(data) is str:
|
|
52
|
+
return data
|
|
53
|
+
if isinstance(data, dict):
|
|
54
|
+
return ",".join([f"{key}={_serialize(val)}" for key, val in data.items()])
|
|
55
|
+
if isinstance(data, list):
|
|
56
|
+
return ",".join([_serialize(val) for val in data])
|
|
57
|
+
if isinstance(data, str):
|
|
58
|
+
return data
|
|
59
|
+
return str(data)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _make_key(args: tuple, kwargs: dict) -> str:
|
|
63
|
+
if not kwargs and len(args) == 1:
|
|
64
|
+
return _serialize(args[0])
|
|
65
|
+
parts = [_serialize(arg) for arg in args]
|
|
66
|
+
if kwargs:
|
|
67
|
+
parts.extend(f"{key}={_serialize(kwargs[key])}" for key in sorted(kwargs))
|
|
68
|
+
return "-".join(parts)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ExpirableCache:
|
|
72
|
+
"""Cache with optional ttl, evicting the oldest inserted entry when full.
|
|
73
|
+
|
|
74
|
+
Params:
|
|
75
|
+
* **size (int)**: max items in dict. default=512
|
|
76
|
+
* **timeout (int)**: Timeout in milliseconds, if it is None,
|
|
77
|
+
there is no timeout. default=None
|
|
78
|
+
* **refresh_ttl (int)**: Refresh ttl anytime key is accessed. default=False
|
|
79
|
+
* **thread_safe (bool)**: Tell cache decorator to be thread safe. default=False
|
|
80
|
+
* **max_mem_size (int)**: max mem size inside cache structure. default=None which means no limit.
|
|
81
|
+
For pypy this value is ignored as the objects can change by the JIT compilation.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
_lru = False
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
size: int = 512,
|
|
89
|
+
timeout: Optional[int] = None,
|
|
90
|
+
refresh_ttl=False,
|
|
91
|
+
thread_safe=False,
|
|
92
|
+
max_mem_size: Optional[int] = None,
|
|
93
|
+
):
|
|
94
|
+
self._data: Dict[Any, CacheValue] = OrderedDict()
|
|
95
|
+
self._expiry: Dict[Any, None] = OrderedDict()
|
|
96
|
+
self._timeout = timeout
|
|
97
|
+
self._ttl = timeout / 1000 if timeout and timeout > 0 else None
|
|
98
|
+
if size < 1:
|
|
99
|
+
raise ValueError("size must be at least 1")
|
|
100
|
+
self.size = size
|
|
101
|
+
self._max_mem_size = None if IS_PYPY else max_mem_size
|
|
102
|
+
self._current_size = 0
|
|
103
|
+
self.refresh_ttl = refresh_ttl
|
|
104
|
+
self._thread_safe = bool(thread_safe)
|
|
105
|
+
self._guard = RLock() if thread_safe else nullcontext()
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def cache(self):
|
|
109
|
+
"""Read-only view of the cached entries."""
|
|
110
|
+
return MappingProxyType(self._data)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def timeout(self):
|
|
114
|
+
"""Ttl in milliseconds, None when entries do not expire."""
|
|
115
|
+
return self._timeout
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def max_mem_size(self):
|
|
119
|
+
"""Max memory size of the cached entries, None when unlimited."""
|
|
120
|
+
return self._max_mem_size
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def current_size(self):
|
|
124
|
+
"""Memory size of the cached entries, tracked only with max_mem_size."""
|
|
125
|
+
return self._current_size
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def thread_safe(self):
|
|
129
|
+
"""Whether cache operations are guarded by a lock."""
|
|
130
|
+
return self._thread_safe
|
|
131
|
+
|
|
132
|
+
def get(self, key):
|
|
133
|
+
"""Get value of key, None if missing or expired."""
|
|
134
|
+
with self._guard:
|
|
135
|
+
return self._lookup_unlocked(key, None)
|
|
136
|
+
|
|
137
|
+
def _lookup(self, key, default=None):
|
|
138
|
+
with self._guard:
|
|
139
|
+
return self._lookup_unlocked(key, default)
|
|
140
|
+
|
|
141
|
+
def _lookup_unlocked(self, key, default):
|
|
142
|
+
if self._expiry:
|
|
143
|
+
self._purge_expired(monotonic())
|
|
144
|
+
item = self._data.get(key)
|
|
145
|
+
if item is None:
|
|
146
|
+
return default
|
|
147
|
+
if item.expire_at != INF:
|
|
148
|
+
now = monotonic()
|
|
149
|
+
if now > item.expire_at:
|
|
150
|
+
self._remove_key(key)
|
|
151
|
+
return default
|
|
152
|
+
if self.refresh_ttl and self._ttl:
|
|
153
|
+
item.expire_at = now + self._ttl
|
|
154
|
+
try:
|
|
155
|
+
self._expiry.move_to_end(key)
|
|
156
|
+
except KeyError:
|
|
157
|
+
pass
|
|
158
|
+
if self._lru:
|
|
159
|
+
try:
|
|
160
|
+
self._data.move_to_end(key)
|
|
161
|
+
except KeyError:
|
|
162
|
+
pass
|
|
163
|
+
item.access += 1
|
|
164
|
+
return item.value
|
|
165
|
+
|
|
166
|
+
def set(self, key, data):
|
|
167
|
+
"""Set value for key, evicting entries if needed.
|
|
168
|
+
|
|
169
|
+
With max_mem_size, a value bigger than the limit is not cached.
|
|
170
|
+
"""
|
|
171
|
+
with self._guard:
|
|
172
|
+
self._set_unlocked(key, data)
|
|
173
|
+
|
|
174
|
+
def _set_unlocked(self, key, data):
|
|
175
|
+
now = monotonic() if self._ttl or self._expiry else 0.0
|
|
176
|
+
if self._expiry:
|
|
177
|
+
self._purge_expired(now)
|
|
178
|
+
size = 0
|
|
179
|
+
if self._max_mem_size:
|
|
180
|
+
size = getsizeof(key) + getsizeof(data)
|
|
181
|
+
if size > self._max_mem_size:
|
|
182
|
+
if key in self._data:
|
|
183
|
+
self._remove_key(key)
|
|
184
|
+
return
|
|
185
|
+
if key in self._data:
|
|
186
|
+
self._remove_key(key)
|
|
187
|
+
elif self._data and len(self._data) >= self.size:
|
|
188
|
+
self._remove_key(next(iter(self._data)))
|
|
189
|
+
|
|
190
|
+
item = CacheValue(data)
|
|
191
|
+
if self._max_mem_size:
|
|
192
|
+
item.size = size
|
|
193
|
+
while self._data and self._current_size + size > self._max_mem_size:
|
|
194
|
+
self._remove_key(next(iter(self._data)))
|
|
195
|
+
self._current_size += size
|
|
196
|
+
self._data[key] = item
|
|
197
|
+
if self._ttl:
|
|
198
|
+
item.expire_at = now + self._ttl
|
|
199
|
+
self._expiry[key] = None
|
|
200
|
+
|
|
201
|
+
def _wrap(self, func, skip_args: bool = False):
|
|
202
|
+
lookup = self._lookup
|
|
203
|
+
set_value = self.set
|
|
204
|
+
|
|
205
|
+
def wrapper(*args, **kwargs):
|
|
206
|
+
key = "any" if skip_args else _make_key(args, kwargs)
|
|
207
|
+
value = lookup(key, _MISSING)
|
|
208
|
+
if value is _MISSING:
|
|
209
|
+
value = func(*args, **kwargs)
|
|
210
|
+
set_value(key, value)
|
|
211
|
+
return value
|
|
212
|
+
|
|
213
|
+
wrapper.cache = self
|
|
214
|
+
return wrapper
|
|
215
|
+
|
|
216
|
+
def _make_key(self, args: tuple, kwargs: dict) -> str:
|
|
217
|
+
return _make_key(args, kwargs)
|
|
218
|
+
|
|
219
|
+
def _purge_expired(self, now: float):
|
|
220
|
+
expiry = self._expiry
|
|
221
|
+
data = self._data
|
|
222
|
+
while expiry:
|
|
223
|
+
key = next(iter(expiry))
|
|
224
|
+
item = data.get(key)
|
|
225
|
+
if item is not None and now <= item.expire_at:
|
|
226
|
+
break
|
|
227
|
+
self._remove_key(key)
|
|
228
|
+
|
|
229
|
+
def _remove_key(self, key):
|
|
230
|
+
item = self._data.pop(key, None)
|
|
231
|
+
self._expiry.pop(key, None)
|
|
232
|
+
if item is not None and self._max_mem_size:
|
|
233
|
+
self._current_size -= item.size
|
|
234
|
+
|
|
235
|
+
def __contains__(self, key):
|
|
236
|
+
with self._guard:
|
|
237
|
+
item = self._data.get(key)
|
|
238
|
+
if item is None:
|
|
239
|
+
return False
|
|
240
|
+
if item.expire_at != INF and monotonic() > item.expire_at:
|
|
241
|
+
self._remove_key(key)
|
|
242
|
+
return False
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
def __len__(self):
|
|
246
|
+
return len(self._data)
|
|
247
|
+
|
|
248
|
+
def __reduce__(self):
|
|
249
|
+
raise TypeError(f"cannot pickle {type(self).__name__!r} object")
|
|
250
|
+
|
|
251
|
+
def clear(self):
|
|
252
|
+
"""Remove every entry."""
|
|
253
|
+
with self._guard:
|
|
254
|
+
entries = list(self._data.values())
|
|
255
|
+
self._data.clear()
|
|
256
|
+
self._expiry.clear()
|
|
257
|
+
self._current_size = 0
|
|
258
|
+
entries.clear()
|
|
259
|
+
|
|
260
|
+
def refresh_key_ttl(self, key: Any, milliseconds=None):
|
|
261
|
+
"""Do refresh of key ttl, if present."""
|
|
262
|
+
ms = milliseconds or (self._timeout if self._ttl else None)
|
|
263
|
+
if not ms:
|
|
264
|
+
return
|
|
265
|
+
with self._guard:
|
|
266
|
+
item = self._data.get(key)
|
|
267
|
+
if item is None:
|
|
268
|
+
return
|
|
269
|
+
item.expire_at = monotonic() + ms / 1000
|
|
270
|
+
self._expiry[key] = None
|
|
271
|
+
self._expiry.move_to_end(key)
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
def get_key(cls, *args, **kwargs):
|
|
275
|
+
"""Helper method to generate keys from *args, **kwargs."""
|
|
276
|
+
serialize = cls.serialize_key
|
|
277
|
+
kwargs_list = [f"{key}={serialize(kwargs[key])}" for key in sorted(kwargs)]
|
|
278
|
+
return "-".join([serialize(item) for item in list(args) + kwargs_list])
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def serialize_key(cls, data: Any):
|
|
282
|
+
"""Serialize a single argument into its key representation."""
|
|
283
|
+
if type(data) is str:
|
|
284
|
+
return data
|
|
285
|
+
if isinstance(data, dict):
|
|
286
|
+
return ",".join([f"{key}={cls.serialize_key(val)}" for key, val in data.items()])
|
|
287
|
+
if isinstance(data, list):
|
|
288
|
+
return ",".join([cls.serialize_key(val) for val in data])
|
|
289
|
+
if isinstance(data, str):
|
|
290
|
+
return data
|
|
291
|
+
return str(data)
|
|
292
|
+
|
|
293
|
+
def expire(self, *args, **kwargs):
|
|
294
|
+
"""Usefull to expire any combination of *args, **kwargs."""
|
|
295
|
+
key = self.get_key(*args, **kwargs)
|
|
296
|
+
with self._guard:
|
|
297
|
+
if key in self._data:
|
|
298
|
+
self._remove_key(key)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class LRUCache(ExpirableCache):
|
|
302
|
+
"""Cache with optional ttl, evicting the least recently used entry when full."""
|
|
303
|
+
|
|
304
|
+
_lru = True
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def access(self):
|
|
308
|
+
"""Get accesses as map."""
|
|
309
|
+
with self._guard:
|
|
310
|
+
return {key: val.access for key, val in self._data.items()}
|
onecache/cache_value.py
ADDED
onecache/utils.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
IS_PYPY = platform.python_implementation().lower().startswith("pypy")
|
|
6
|
+
|
|
7
|
+
def utcnow():
|
|
8
|
+
if sys.version_info >= (3, 11):
|
|
9
|
+
return datetime.datetime.now(datetime.UTC)
|
|
10
|
+
else:
|
|
11
|
+
return datetime.datetime.utcnow()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: onecache
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Python cache for sync and async code
|
|
5
|
+
Author-email: Johanderson Mogollon <johander1822@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sonic182/onecache
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Cython
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Dynamic: license
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
[](https://coveralls.io/github/sonic182/onecache?branch=master)
|
|
27
|
+

|
|
28
|
+
# OneCache
|
|
29
|
+
|
|
30
|
+
Python cache for sync and async code.
|
|
31
|
+
|
|
32
|
+
Cache uses an LRU algorithm and can optionally set TTLs per entry.
|
|
33
|
+
|
|
34
|
+
On CPython the cache core is a compiled Cython extension (prebuilt wheels for Linux, macOS and Windows on x86_64 and ARM64). On PyPy, or on platforms without a prebuilt wheel, it falls back to an equivalent pure Python implementation. `onecache.COMPILED` tells which one is in use.
|
|
35
|
+
|
|
36
|
+
Tested automatically on CPython 3.10 and 3.14 and PyPy 3.11 across Linux, macOS, and Windows on x86_64 and ARM64 (see the workflow badge). Python 3.8 and 3.9 are still installable and get prebuilt wheels, but are no longer part of the test matrix.
|
|
37
|
+
|
|
38
|
+
# Usage
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from onecache import CacheDecorator
|
|
42
|
+
from onecache import AsyncCacheDecorator
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Counter:
|
|
46
|
+
def __init__(self, count=0):
|
|
47
|
+
self.count = count
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_async_cache_counter():
|
|
52
|
+
"""Test async cache, counter case."""
|
|
53
|
+
counter = Counter()
|
|
54
|
+
|
|
55
|
+
@AsyncCacheDecorator()
|
|
56
|
+
async def mycoro(counter: Counter):
|
|
57
|
+
counter.count += 1
|
|
58
|
+
return counter.count
|
|
59
|
+
|
|
60
|
+
assert 1 == (await mycoro(counter))
|
|
61
|
+
assert 1 == (await mycoro(counter))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_cache_counter():
|
|
65
|
+
"""Test async cache, counter case."""
|
|
66
|
+
counter = Counter()
|
|
67
|
+
|
|
68
|
+
@CacheDecorator()
|
|
69
|
+
def sample(counter: Counter):
|
|
70
|
+
counter.count += 1
|
|
71
|
+
return counter.count
|
|
72
|
+
|
|
73
|
+
assert 1 == (sample(counter))
|
|
74
|
+
assert 1 == (sample(counter))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Decorator classes supports the following arguments
|
|
78
|
+
|
|
79
|
+
* **maxsize (int)**: Maximun number of items to be cached. default: 512
|
|
80
|
+
* **ttl (int)**: time to expire in milliseconds, if None, it does not expire. default: None
|
|
81
|
+
* **skip_args (bool)**: apply cache as the function doesn't have any arguments, default: False
|
|
82
|
+
* **cache_class (class)**: Class to use for cache instance. default: LRUCache
|
|
83
|
+
* **refresh_ttl (bool)**: if cache with ttl, This flag makes key expiration timestamp to be refresh per access. default: False
|
|
84
|
+
* **thread_safe (bool)**: tell decorator to use thread safe lock. default=False
|
|
85
|
+
* **max_mem_size (int)**: max mem size in bytes, as the sum of `sys.getsizeof(key) + sys.getsizeof(value)` of every entry (shallow sizes). default=None which means no limit. For pypy this value is ignored as the objects can change by the JIT compilation.
|
|
86
|
+
|
|
87
|
+
If num of records exceds maxsize, it drops the least recently used one. With `max_mem_size`, a value bigger than the limit is not cached.
|
|
88
|
+
|
|
89
|
+
Use `thread_safe=True` when the decorated function is called from several threads.
|
|
90
|
+
|
|
91
|
+
The decorated function exposes its cache as `.cache`: `.cache.expire(*args, **kwargs)` drops one entry, `.cache.clear()` drops all of them, and `.cache.cache` is a read-only view of the entries.
|
|
92
|
+
|
|
93
|
+
Set `ONECACHE_NO_EXTENSIONS` to any non-empty value to force the pure Python implementation. If the extension failed to build when installing from source (e.g. no compiler), pip may keep reusing that pure Python build from its cache; reinstall with `pip install --no-cache-dir --force-reinstall onecache` after installing a compiler.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Development
|
|
97
|
+
|
|
98
|
+
Install dependencies with Poetry (includes dev + test groups):
|
|
99
|
+
```bash
|
|
100
|
+
poetry install --with test,dev
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Build the Cython extension in place (needed after any change to `onecache/_ccore.pyx`):
|
|
104
|
+
```bash
|
|
105
|
+
make build_ext
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Run the test suite against the compiled and the pure Python implementations:
|
|
109
|
+
```bash
|
|
110
|
+
poetry run pytest
|
|
111
|
+
ONECACHE_NO_EXTENSIONS=1 poetry run pytest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`onecache/_ccore.pyx` and `onecache/_core.py` implement the same API and must be kept in sync.
|
|
115
|
+
|
|
116
|
+
# Releasing
|
|
117
|
+
|
|
118
|
+
Pull requests only build cp310 and cp314 wheels, so first run the `Wheels` workflow by hand on `master` (Actions → Wheels → Run workflow) to build and test every Python version. Then bump `version` in `pyproject.toml` and push a tag with the same value (e.g. `0.9.0`). The `Wheels` workflow builds the wheels with cibuildwheel, the sdist and a pure Python fallback wheel, and publishes them to PyPI through trusted publishing. That needs a `pypi` environment in the GitHub repository settings and a trusted publisher on PyPI for this repository, workflow `wheels.yml` and environment `pypi`.
|
|
119
|
+
|
|
120
|
+
Lint and format checks:
|
|
121
|
+
```bash
|
|
122
|
+
poetry run flake8
|
|
123
|
+
poetry run autopep8 --in-place --recursive onecache tests
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
# Contribute
|
|
127
|
+
|
|
128
|
+
1. Fork
|
|
129
|
+
2. create a branch `feature/your_feature`
|
|
130
|
+
3. commit - push - pull request
|
|
131
|
+
|
|
132
|
+
Thanks :)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
onecache/__init__.py,sha256=2U27qdtNKwZ0rXbjV2Lfzpx5LA4kKH0_GoUSPZQAaR4,4044
|
|
2
|
+
onecache/_ccore.cp313-win32.pyd,sha256=UYPwwahS9KJPULKVZ8xzNwxPuihVJ8FDHYJg02r_3lI,106496
|
|
3
|
+
onecache/_core.py,sha256=X_dMV_BlijwL7Nuvk9gtTpYRfSUSSW-ao_13HInK8PI,10451
|
|
4
|
+
onecache/cache_value.py,sha256=9cLTkNHjQxlaVydUbS1UlDfuoiU2y_FRtMQOdsVrnvU,61
|
|
5
|
+
onecache/utils.py,sha256=KyWmvw_lDVCvcFmD4qGzHOFa1yv7I02fem37jpsVzTo,279
|
|
6
|
+
onecache-0.9.0.dist-info/METADATA,sha256=1baoMYJhRY7EV81UYQv0_J3L8FntNog14IfDsfRLeSg,5743
|
|
7
|
+
onecache-0.9.0.dist-info/WHEEL,sha256=9z4P_Kq9LAX0eTloa6Af3pe3LKOxNzaw-XoJA4vn4Ao,97
|
|
8
|
+
onecache-0.9.0.dist-info/top_level.txt,sha256=e1pCmlJnfcMBz3KIqLPb37lC1a2bYAZgXhfCfvFHriw,9
|
|
9
|
+
onecache-0.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
onecache
|