cachebox 6.2.6__cp313-cp313-win_arm64.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.
- cachebox/__init__.py +26 -0
- cachebox/_cachebox.py +372 -0
- cachebox/_core.cp313-win_arm64.pyd +0 -0
- cachebox/_core.pyi +1889 -0
- cachebox/_wrappers.py +400 -0
- cachebox/utils.py +597 -0
- cachebox-6.2.6.dist-info/METADATA +194 -0
- cachebox-6.2.6.dist-info/RECORD +12 -0
- cachebox-6.2.6.dist-info/WHEEL +4 -0
- cachebox-6.2.6.dist-info/licenses/LICENSE +21 -0
- cachebox-6.2.6.dist-info/licenses/LICENSE-THIRD-PARTY +70 -0
- cachebox-6.2.6.dist-info/sboms/cachebox.cyclonedx.json +973 -0
cachebox/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from ._cachebox import BaseCacheImpl as BaseCacheImpl
|
|
2
|
+
from ._cachebox import Cache as Cache
|
|
3
|
+
from ._cachebox import FIFOCache as FIFOCache
|
|
4
|
+
from ._cachebox import LFUCache as LFUCache
|
|
5
|
+
from ._cachebox import LRUCache as LRUCache
|
|
6
|
+
from ._cachebox import RRCache as RRCache
|
|
7
|
+
from ._cachebox import TTLCache as TTLCache
|
|
8
|
+
from ._cachebox import VTTLCache as VTTLCache
|
|
9
|
+
from ._core import __version__ as __version__
|
|
10
|
+
from ._core import _use_small_offset_feature as _use_small_offset_feature
|
|
11
|
+
from ._wrappers import EVENT_HIT as EVENT_HIT
|
|
12
|
+
from ._wrappers import EVENT_MISS as EVENT_MISS
|
|
13
|
+
from .utils import Frozen as Frozen
|
|
14
|
+
from .utils import cached as cached
|
|
15
|
+
from .utils import clear_cached_cache as clear_cached_cache
|
|
16
|
+
from .utils import get_cached_cache as get_cached_cache
|
|
17
|
+
from .utils import get_cached_cache_info as get_cached_cache_info
|
|
18
|
+
from .utils import get_cached_callback as get_cached_callback
|
|
19
|
+
from .utils import is_cached as is_cached
|
|
20
|
+
from .utils import make_hash_key as make_hash_key
|
|
21
|
+
from .utils import make_key as make_key
|
|
22
|
+
from .utils import make_typed_key as make_typed_key
|
|
23
|
+
from .utils import postprocess_copy as postprocess_copy
|
|
24
|
+
from .utils import postprocess_copy_mutables as postprocess_copy_mutables
|
|
25
|
+
from .utils import postprocess_deepcopy as postprocess_deepcopy
|
|
26
|
+
from .utils import postprocess_deepcopy_mutables as postprocess_deepcopy_mutables
|
cachebox/_cachebox.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import typing
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
from ._core import BaseCacheImpl as BaseCacheImpl
|
|
7
|
+
from ._core import Cache as Cache
|
|
8
|
+
from ._core import FIFOCache as FIFOCache
|
|
9
|
+
from ._core import LFUCache as LFUCache
|
|
10
|
+
from ._core import LRUCache as LRUCache
|
|
11
|
+
from ._core import RRCache as RRCache
|
|
12
|
+
|
|
13
|
+
# private import
|
|
14
|
+
from ._core import TTLCache as _CoreTTLCache
|
|
15
|
+
from ._core import VTTLCache as _CoreVTTLCache
|
|
16
|
+
|
|
17
|
+
if typing.TYPE_CHECKING:
|
|
18
|
+
from ._core import _IterableType
|
|
19
|
+
|
|
20
|
+
KT = typing.TypeVar("KT", bound=typing.Hashable)
|
|
21
|
+
VT = typing.TypeVar("VT")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TTLCache(_CoreTTLCache[KT, VT]):
|
|
25
|
+
"""
|
|
26
|
+
A cache with a Time-To-Live (TTL) eviction policy.
|
|
27
|
+
|
|
28
|
+
Each entry carries an expiration timestamp and is considered stale — and
|
|
29
|
+
eligible for eviction — once that deadline has passed, regardless of how
|
|
30
|
+
recently or frequently it was accessed.
|
|
31
|
+
|
|
32
|
+
Every entry is stamped with an absolute ``expires_at`` timestamp at
|
|
33
|
+
insertion time (computed as ``now + global_ttl``). Entries are stored in
|
|
34
|
+
insertion order and eviction proceeds from the front of that queue, but
|
|
35
|
+
only after confirming the candidate has actually expired. A live entry at
|
|
36
|
+
the front of the queue blocks eviction of everything behind it, so the
|
|
37
|
+
cache may temporarily exceed capacity if the oldest entries are still
|
|
38
|
+
fresh.
|
|
39
|
+
|
|
40
|
+
Like ``FIFOCache``, this implementation backs the queue with a
|
|
41
|
+
double-ended queue for O(1) front removal and a hash map for O(1) key
|
|
42
|
+
lookups. The same logical-index trick applies: the table stores
|
|
43
|
+
monotonically increasing counters rather than physical deque positions, and
|
|
44
|
+
a ``front_offset`` counter converts a logical index back to a physical one
|
|
45
|
+
at read time via ``entries[table[key] - front_offset]``. This keeps
|
|
46
|
+
eviction and lookup O(1) without rewriting the table on every eviction.
|
|
47
|
+
Every read also checks ``expires_at`` against the current wall-clock time
|
|
48
|
+
and treats any expired entry as a cache miss.
|
|
49
|
+
|
|
50
|
+
Without ``sweep_interval``, an expiry sweep is triggered automatically on
|
|
51
|
+
every call to ``insert``, ``update``, ``current_size``, ``remaining_size``,
|
|
52
|
+
``last``, ``first``, ``items``, ``keys``, ``values``, and ``__iter__``. A
|
|
53
|
+
completely idle cache will accumulate stale entries between these calls,
|
|
54
|
+
but any normal interaction is sufficient to reclaim them. When
|
|
55
|
+
``sweep_interval`` is set, a background thread performs the sweep on that
|
|
56
|
+
interval instead, reclaiming expired entries independent of method calls.
|
|
57
|
+
|
|
58
|
+
| | get | insert | delete | popitem |
|
|
59
|
+
| ------------ | ----- | ------- | ---------------- | ------- |
|
|
60
|
+
| Worse-case | O(1) | O(1) | O(min(i, n-i)) | O(n) - very rare |
|
|
61
|
+
|
|
62
|
+
Pros:
|
|
63
|
+
- Insert, lookup, and evict are all O(1) amortized: the
|
|
64
|
+
``front_offset`` trick eliminates the O(n) index-shifting that a
|
|
65
|
+
naive implementation would require on every eviction.
|
|
66
|
+
- Entries expire automatically without a background thread or explicit
|
|
67
|
+
invalidation call; stale data is never returned to the caller.
|
|
68
|
+
- TTL expiry and insertion-order eviction compose cleanly: the oldest
|
|
69
|
+
expired entry is always evicted first.
|
|
70
|
+
- A single ``global_ttl`` keeps configuration simple; every entry ages
|
|
71
|
+
at the same rate.
|
|
72
|
+
|
|
73
|
+
Cons:
|
|
74
|
+
- Wall-clock dependency: correctness relies on a monotonically
|
|
75
|
+
advancing system clock. Clock adjustments (NTP steps,
|
|
76
|
+
suspend/resume) can cause entries to expire earlier or later than
|
|
77
|
+
intended.
|
|
78
|
+
- When ``sweep_interval`` is set, a background thread wakes on that
|
|
79
|
+
interval to remove all expired entries, adding a small amount of
|
|
80
|
+
background CPU usage for the lifetime of the cache.
|
|
81
|
+
- No per-entry TTL override: all entries share ``global_ttl``; mixed
|
|
82
|
+
expiry requirements need a different policy or a wrapper layer.
|
|
83
|
+
- A rare O(n) index rebase (triggered when ``front_offset`` nears
|
|
84
|
+
``usize::MAX - isize::MAX``) introduces an occasional latency spike;
|
|
85
|
+
amortised cost is negligible but worst-case latency is unbounded in
|
|
86
|
+
principle.
|
|
87
|
+
|
|
88
|
+
Use ``TTLCache`` when cached data has a natural freshness window (API
|
|
89
|
+
responses, auth tokens, DNS records, rate-limit counters), when automatic
|
|
90
|
+
expiry without a background reaper is sufficient, or when access patterns
|
|
91
|
+
are unpredictable enough that recency- or frequency-based eviction would
|
|
92
|
+
offer no meaningful advantage.
|
|
93
|
+
|
|
94
|
+
Avoid it when strong temporal locality makes LRU a better fit, when
|
|
95
|
+
per-entry TTL granularity is required (consider ``VTTLCache`` instead), or
|
|
96
|
+
when the system clock is unreliable or subject to adjustment.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from cachebox import TTLCache
|
|
100
|
+
import time
|
|
101
|
+
|
|
102
|
+
cache = TTLCache(0, global_ttl=2)
|
|
103
|
+
cache.update({i:str(i) for i in range(10)})
|
|
104
|
+
|
|
105
|
+
print(cache.get_with_expire(2)) # ('2', 1.99)
|
|
106
|
+
|
|
107
|
+
# Returns the oldest key in cache; this is the one which will be removed by `popitem()`
|
|
108
|
+
print(cache.first()) # 0
|
|
109
|
+
|
|
110
|
+
cache["mykey"] = "value"
|
|
111
|
+
time.sleep(2)
|
|
112
|
+
cache["mykey"] # KeyError
|
|
113
|
+
```
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
maxsize: int,
|
|
119
|
+
global_ttl: float | timedelta,
|
|
120
|
+
iterable: typing.Optional["_IterableType[KT, VT]"] = None,
|
|
121
|
+
*,
|
|
122
|
+
capacity: int = 0,
|
|
123
|
+
getsizeof: typing.Callable[[KT, VT], int] | None = None,
|
|
124
|
+
sweep_interval: float | timedelta | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Initializes a new TTLCache instance.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
maxsize: Maximum number of elements the cache can hold. If zero,
|
|
131
|
+
the limit is set to ``sys.maxsize`` internally.
|
|
132
|
+
global_ttl: Time-to-live for every entry, as seconds (float) or a
|
|
133
|
+
``timedelta``. Applied at insertion time.
|
|
134
|
+
iterable: Initial data to populate the cache.
|
|
135
|
+
capacity: Pre-allocate cache capacity to minimize reallocations.
|
|
136
|
+
Defaults to 0.
|
|
137
|
+
getsizeof: A callable that computes the size of a key-value pair.
|
|
138
|
+
When ``None``, each entry is assumed to have a size of 1
|
|
139
|
+
(equivalent to ``lambda k, v: 1``). Use this to implement
|
|
140
|
+
weighted caching — for example, sizing entries by memory
|
|
141
|
+
footprint or byte length.
|
|
142
|
+
sweep_interval: If set, starts a background thread that sweeps and
|
|
143
|
+
removes all expired entries on this interval (in seconds or as
|
|
144
|
+
a ``timedelta``). When ``None``, expiry is lazy. Defaults to
|
|
145
|
+
``None``. Must be greater than or equal to 1.
|
|
146
|
+
|
|
147
|
+
Note:
|
|
148
|
+
The cache can be pre-sized via ``capacity`` to reduce
|
|
149
|
+
reallocations when the number of expected entries is known
|
|
150
|
+
ahead of time.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
ValueError: If ``sweep_interval`` is set to a value less than 1.
|
|
154
|
+
"""
|
|
155
|
+
super().__init__(
|
|
156
|
+
maxsize,
|
|
157
|
+
global_ttl,
|
|
158
|
+
iterable,
|
|
159
|
+
capacity=capacity,
|
|
160
|
+
getsizeof=getsizeof,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self._thread: threading.Thread | None = None
|
|
164
|
+
self._stop_event = threading.Event()
|
|
165
|
+
|
|
166
|
+
if sweep_interval is not None:
|
|
167
|
+
if isinstance(sweep_interval, timedelta):
|
|
168
|
+
sweep_interval = sweep_interval.total_seconds()
|
|
169
|
+
|
|
170
|
+
if sweep_interval < 1:
|
|
171
|
+
raise ValueError("sweep_interval must be at least 1 second.")
|
|
172
|
+
|
|
173
|
+
self._thread = threading.Thread(
|
|
174
|
+
target=self._sweeper_thread,
|
|
175
|
+
args=(sweep_interval,),
|
|
176
|
+
daemon=True,
|
|
177
|
+
)
|
|
178
|
+
self._thread.start()
|
|
179
|
+
|
|
180
|
+
self._sweep_interval = sweep_interval
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def sweep_interval(self) -> float | None:
|
|
184
|
+
"""The configured ``sweep_interval`` in seconds."""
|
|
185
|
+
return self._sweep_interval
|
|
186
|
+
|
|
187
|
+
def _sweeper_thread(self, interval: float) -> None:
|
|
188
|
+
while not self._stop_event.wait(interval):
|
|
189
|
+
self.expire()
|
|
190
|
+
|
|
191
|
+
def stop_sweeper(self) -> None:
|
|
192
|
+
"""Signals the background sweeper thread to stop, if one is active."""
|
|
193
|
+
self._stop_event.set()
|
|
194
|
+
|
|
195
|
+
def __del__(self) -> None:
|
|
196
|
+
# If super().__init__ raises for any reason (bad arguments, type error, ...),
|
|
197
|
+
# the assignment to self._stop_event never happens.
|
|
198
|
+
if hasattr(self, "_stop_event"):
|
|
199
|
+
self.stop_sweeper()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class VTTLCache(_CoreVTTLCache[KT, VT]):
|
|
203
|
+
"""
|
|
204
|
+
A cache with a Variable Time-To-Live (VTTL) eviction policy.
|
|
205
|
+
|
|
206
|
+
Each item can be inserted with its own individual TTL (time-to-live). When
|
|
207
|
+
an item's TTL expires, it is considered stale and will be evicted. Items
|
|
208
|
+
inserted without a TTL never expire and are only evicted when the cache
|
|
209
|
+
reaches capacity.
|
|
210
|
+
|
|
211
|
+
Expiration is managed lazily by default: stale entries are not removed
|
|
212
|
+
immediately when they expire, but are cleaned up on the next access or
|
|
213
|
+
when the cache needs to reclaim capacity. Optionally, a ``sweep_interval``
|
|
214
|
+
can be configured to spawn a background thread that proactively removes
|
|
215
|
+
expired items on a fixed schedule, bounding the window in which stale
|
|
216
|
+
data can be observed or memory held unnecessarily.
|
|
217
|
+
|
|
218
|
+
Internally, a lazy-evaluated min-heap tracks expiration deadlines. The
|
|
219
|
+
heap is only fully sorted when needed (e.g. during eviction), keeping
|
|
220
|
+
insert costs low on average. A hash table stores cursors into the heap for
|
|
221
|
+
O(1) key lookups. A running total enables O(1) capacity checks.
|
|
222
|
+
|
|
223
|
+
When the cache is full and eviction is needed, expired items are reclaimed
|
|
224
|
+
first (in expiration order, cheapest deadline first). If no expired items
|
|
225
|
+
exist, the item with the nearest upcoming expiration is evicted. Items with
|
|
226
|
+
no TTL are the last resort and are evicted only when all expiring items
|
|
227
|
+
have been exhausted.
|
|
228
|
+
|
|
229
|
+
| | get | insert | delete(i) | popitem |
|
|
230
|
+
| ------------ | ----- | ------- | -------------- | ------- |
|
|
231
|
+
| Worse-case | O(1)~ | O(1)~ | O(min(i, n-i)) | O(1)~ |
|
|
232
|
+
|
|
233
|
+
Pros:
|
|
234
|
+
- Per-item TTL control: each entry can have a different lifetime.
|
|
235
|
+
- Expired items are reclaimed before live items, maximising useful
|
|
236
|
+
capacity.
|
|
237
|
+
- Lazy expiry avoids background threads and timer overhead by default.
|
|
238
|
+
- Optional background sweeping bounds stale-data visibility and memory
|
|
239
|
+
retention when lazy eviction is insufficient.
|
|
240
|
+
- Insert, lookup, and evict are O(1) amortized (O(log n) worst-case
|
|
241
|
+
during heap rebalancing).
|
|
242
|
+
- TTL-free items coexist naturally alongside expiring ones.
|
|
243
|
+
|
|
244
|
+
Cons:
|
|
245
|
+
- Without sweeping, stale items may linger in memory until the next
|
|
246
|
+
access or eviction pressure forces a cleanup.
|
|
247
|
+
- With sweeping, a background thread is running for the lifetime of
|
|
248
|
+
the cache, adding concurrency overhead and requiring thread-safe
|
|
249
|
+
internal locking.
|
|
250
|
+
- Slightly higher per-insert cost compared to pure LRU/LFU.
|
|
251
|
+
- No guarantee on the exact eviction moment for expired items in lazy
|
|
252
|
+
mode; callers that require strict TTL enforcement should validate
|
|
253
|
+
timestamps on read, or configure a sufficiently short
|
|
254
|
+
``sweep_interval``.
|
|
255
|
+
|
|
256
|
+
Use ``VTTLCache`` when different items have different natural lifetimes
|
|
257
|
+
(e.g. session tokens, API responses with varying freshness requirements,
|
|
258
|
+
or multi-tier data with mixed staleness tolerances). Set
|
|
259
|
+
``sweep_interval`` when bounded staleness or proactive memory reclamation
|
|
260
|
+
is required.
|
|
261
|
+
|
|
262
|
+
Avoid it when all items share a uniform TTL (consider ``TTLCache`` instead),
|
|
263
|
+
when strict and immediate expiry is a hard requirement, or when memory pressure
|
|
264
|
+
from temporarily lingering stale entries is unacceptable and a background thread
|
|
265
|
+
is not an option.
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from cachebox import VTTLCache
|
|
269
|
+
import time
|
|
270
|
+
|
|
271
|
+
cache = VTTLCache(100, iterable={i:i for i in range(4)}, ttl=3)
|
|
272
|
+
print(len(cache)) # 4
|
|
273
|
+
time.sleep(3)
|
|
274
|
+
print(len(cache)) # 0
|
|
275
|
+
|
|
276
|
+
# The "key1" is exists for 5 seconds
|
|
277
|
+
cache.insert("key1", "value", ttl=5)
|
|
278
|
+
# The "key2" is exists for 2 seconds
|
|
279
|
+
cache.insert("key2", "value", ttl=2)
|
|
280
|
+
|
|
281
|
+
time.sleep(2)
|
|
282
|
+
# "key1" is exists for 3 seconds
|
|
283
|
+
print(cache.get("key1")) # value
|
|
284
|
+
|
|
285
|
+
# "key2" has expired
|
|
286
|
+
print(cache.get("key2")) # None
|
|
287
|
+
```
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
def __init__(
|
|
291
|
+
self,
|
|
292
|
+
maxsize: int,
|
|
293
|
+
iterable: typing.Optional["_IterableType[KT, VT]"] = None,
|
|
294
|
+
ttl: float | timedelta | datetime | None = None,
|
|
295
|
+
*,
|
|
296
|
+
capacity: int = 0,
|
|
297
|
+
getsizeof: typing.Callable[[KT, VT], int] | None = None,
|
|
298
|
+
sweep_interval: float | timedelta | None = None,
|
|
299
|
+
) -> None:
|
|
300
|
+
"""
|
|
301
|
+
Initializes a new TTLCache instance.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
maxsize: Maximum number of elements the cache can hold. If zero,
|
|
305
|
+
the limit is set to ``sys.maxsize`` internally.
|
|
306
|
+
iterable: Initial data to populate the cache.
|
|
307
|
+
ttl: Time-to-live duration for ``iterable`` items. This *is not* a global ttl.
|
|
308
|
+
capacity: Pre-allocate cache capacity to minimize reallocations.
|
|
309
|
+
Defaults to 0.
|
|
310
|
+
getsizeof: A callable that computes the size of a key-value pair.
|
|
311
|
+
When ``None``, each entry is assumed to have a size of 1
|
|
312
|
+
(equivalent to ``lambda k, v: 1``). Use this to implement
|
|
313
|
+
weighted caching — for example, sizing entries by memory
|
|
314
|
+
footprint or byte length.
|
|
315
|
+
sweep_interval: If set, starts a background thread that sweeps and
|
|
316
|
+
removes all expired entries on this interval (in seconds or as
|
|
317
|
+
a ``timedelta``). When ``None``, expiry is lazy. Defaults to
|
|
318
|
+
``None``. Must be greater than or equal to 1.
|
|
319
|
+
|
|
320
|
+
Note:
|
|
321
|
+
The cache can be pre-sized via ``capacity`` to reduce
|
|
322
|
+
reallocations when the number of expected entries is known
|
|
323
|
+
ahead of time.
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
ValueError: If ``sweep_interval`` is set to a value less than 1.
|
|
327
|
+
"""
|
|
328
|
+
super().__init__(
|
|
329
|
+
maxsize,
|
|
330
|
+
iterable,
|
|
331
|
+
ttl,
|
|
332
|
+
capacity=capacity,
|
|
333
|
+
getsizeof=getsizeof,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
self._thread: threading.Thread | None = None
|
|
337
|
+
self._stop_event = threading.Event()
|
|
338
|
+
|
|
339
|
+
if sweep_interval is not None:
|
|
340
|
+
if isinstance(sweep_interval, timedelta):
|
|
341
|
+
sweep_interval = sweep_interval.total_seconds()
|
|
342
|
+
|
|
343
|
+
if sweep_interval < 1:
|
|
344
|
+
raise ValueError("sweep_interval must be at least 1 second.")
|
|
345
|
+
|
|
346
|
+
self._thread = threading.Thread(
|
|
347
|
+
target=self._sweeper_thread,
|
|
348
|
+
args=(sweep_interval,),
|
|
349
|
+
daemon=True,
|
|
350
|
+
)
|
|
351
|
+
self._thread.start()
|
|
352
|
+
|
|
353
|
+
self._sweep_interval = sweep_interval
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def sweep_interval(self) -> float | None:
|
|
357
|
+
"""The configured ``sweep_interval`` in seconds."""
|
|
358
|
+
return self._sweep_interval
|
|
359
|
+
|
|
360
|
+
def _sweeper_thread(self, interval: float) -> None:
|
|
361
|
+
while not self._stop_event.wait(interval):
|
|
362
|
+
self.expire()
|
|
363
|
+
|
|
364
|
+
def stop_sweeper(self) -> None:
|
|
365
|
+
"""Signals the background sweeper thread to stop, if one is active."""
|
|
366
|
+
self._stop_event.set()
|
|
367
|
+
|
|
368
|
+
def __del__(self) -> None:
|
|
369
|
+
# If super().__init__ raises for any reason (bad arguments, type error, ...),
|
|
370
|
+
# the assignment to self._stop_event never happens.
|
|
371
|
+
if hasattr(self, "_stop_event"):
|
|
372
|
+
self.stop_sweeper()
|
|
Binary file
|