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 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