veilrender 0.1.0__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.
- veilrender/__init__.py +3 -0
- veilrender/__main__.py +6 -0
- veilrender/_vendor/__init__.py +2 -0
- veilrender/_vendor/benchmark_compare.py +323 -0
- veilrender/_vendor/cache.py +1023 -0
- veilrender/_vendor/config.py +713 -0
- veilrender/_vendor/dotenv.py +514 -0
- veilrender/_vendor/httpserver.py +1007 -0
- veilrender/_vendor/jsonc.py +352 -0
- veilrender/_vendor/markdown.py +904 -0
- veilrender/_vendor/readability.py +1002 -0
- veilrender/_vendor/retry.py +503 -0
- veilrender/_vendor/soup.py +998 -0
- veilrender/_vendor/structlog.py +888 -0
- veilrender/_vendor/useragent.py +475 -0
- veilrender/_vendor/yaml.py +1124 -0
- veilrender/app.py +158 -0
- veilrender/auth.py +39 -0
- veilrender/browser.py +172 -0
- veilrender/cdp_proxy.py +314 -0
- veilrender/config.py +25 -0
- veilrender/models.py +109 -0
- veilrender/routes/__init__.py +1 -0
- veilrender/routes/health.py +17 -0
- veilrender/routes/render.py +122 -0
- veilrender/routes/screenshot.py +65 -0
- veilrender-0.1.0.dist-info/METADATA +129 -0
- veilrender-0.1.0.dist-info/RECORD +31 -0
- veilrender-0.1.0.dist-info/WHEEL +5 -0
- veilrender-0.1.0.dist-info/entry_points.txt +2 -0
- veilrender-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
# /// zerodep
|
|
2
|
+
# version = "0.2.4"
|
|
3
|
+
# deps = []
|
|
4
|
+
# tier = "subsystem"
|
|
5
|
+
# category = "storage"
|
|
6
|
+
# note = "Install/update via `zerodep add cache`"
|
|
7
|
+
# ///
|
|
8
|
+
"""Zero-dependency caching with TTL, eviction policies, and async support.
|
|
9
|
+
|
|
10
|
+
stdlib only, Python 3.10+.
|
|
11
|
+
|
|
12
|
+
Part of zerodep: https://github.com/Oaklight/zerodep
|
|
13
|
+
Copyright (c) 2026 Peng Ding. MIT License.
|
|
14
|
+
|
|
15
|
+
Provides: LRUCache, FIFOCache, LFUCache, TTLCache cache classes (MutableMapping),
|
|
16
|
+
``cached`` decorator with sync/async auto-detection, and convenience decorators
|
|
17
|
+
``lru_cache``, ``ttl_cache``, ``lfu_cache``, ``fifo_cache``.
|
|
18
|
+
|
|
19
|
+
Does NOT implement: TLRUCache (per-item TTL function), MRUCache, RRCache,
|
|
20
|
+
``cachedmethod``, ``condition`` parameter, multiple backends, serialization.
|
|
21
|
+
|
|
22
|
+
Example::
|
|
23
|
+
|
|
24
|
+
from cache import lru_cache, ttl_cache, LRUCache, cached
|
|
25
|
+
|
|
26
|
+
# Convenience decorator (like functools.lru_cache but with more policies)
|
|
27
|
+
@lru_cache(maxsize=256)
|
|
28
|
+
def fibonacci(n):
|
|
29
|
+
return n if n < 2 else fibonacci(n - 1) + fibonacci(n - 2)
|
|
30
|
+
|
|
31
|
+
# TTL decorator
|
|
32
|
+
@ttl_cache(maxsize=100, ttl=60)
|
|
33
|
+
def fetch_config(key):
|
|
34
|
+
return load_from_db(key)
|
|
35
|
+
|
|
36
|
+
# Async support (auto-detected)
|
|
37
|
+
@lru_cache(maxsize=128)
|
|
38
|
+
async def fetch_data(url):
|
|
39
|
+
return await async_get(url)
|
|
40
|
+
|
|
41
|
+
# Direct cache class usage
|
|
42
|
+
cache = LRUCache(maxsize=1024)
|
|
43
|
+
cache["key"] = "value"
|
|
44
|
+
|
|
45
|
+
# Advanced: cached() with explicit lock
|
|
46
|
+
import threading
|
|
47
|
+
@cached(LRUCache(128), lock=threading.Lock(), info=True)
|
|
48
|
+
def thread_safe_compute(x):
|
|
49
|
+
return expensive(x)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import collections
|
|
55
|
+
import collections.abc
|
|
56
|
+
import functools
|
|
57
|
+
import inspect
|
|
58
|
+
import time
|
|
59
|
+
from typing import Any, Callable
|
|
60
|
+
|
|
61
|
+
# ── Key Functions ──────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _HashedTuple(tuple):
|
|
65
|
+
"""Tuple subclass that caches its hash value for fast repeated lookups."""
|
|
66
|
+
|
|
67
|
+
__hashvalue: int | None = None
|
|
68
|
+
|
|
69
|
+
def __hash__(self, hash=tuple.__hash__): # type: ignore[override]
|
|
70
|
+
hashvalue = self.__hashvalue
|
|
71
|
+
if hashvalue is None:
|
|
72
|
+
self.__hashvalue = hashvalue = hash(self)
|
|
73
|
+
return hashvalue
|
|
74
|
+
|
|
75
|
+
def __add__(self, other, add=tuple.__add__):
|
|
76
|
+
return _HashedTuple(add(self, other))
|
|
77
|
+
|
|
78
|
+
def __radd__(self, other, add=tuple.__add__):
|
|
79
|
+
return _HashedTuple(add(other, self))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_KWMARK = (_HashedTuple,)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def hashkey(*args: Any, **kwargs: Any) -> _HashedTuple:
|
|
86
|
+
"""Return a cache key for the given positional and keyword arguments.
|
|
87
|
+
|
|
88
|
+
This is the default key function for all cache decorators.
|
|
89
|
+
"""
|
|
90
|
+
if kwargs:
|
|
91
|
+
return _HashedTuple(args + _KWMARK + tuple(sorted(kwargs.items())))
|
|
92
|
+
return _HashedTuple(args)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def methodkey(_self: Any, *args: Any, **kwargs: Any) -> _HashedTuple:
|
|
96
|
+
"""Like ``hashkey`` but ignores the first positional argument (``self``).
|
|
97
|
+
|
|
98
|
+
Use as the key function when caching instance methods.
|
|
99
|
+
"""
|
|
100
|
+
return hashkey(*args, **kwargs)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def typedkey(*args: Any, **kwargs: Any) -> _HashedTuple:
|
|
104
|
+
"""Like ``hashkey`` but includes type information, so ``f(3)`` and
|
|
105
|
+
``f(3.0)`` are cached separately.
|
|
106
|
+
"""
|
|
107
|
+
if kwargs:
|
|
108
|
+
sorted_items = tuple(sorted(kwargs.items()))
|
|
109
|
+
return _HashedTuple(
|
|
110
|
+
args
|
|
111
|
+
+ _KWMARK
|
|
112
|
+
+ sorted_items
|
|
113
|
+
+ tuple(type(v) for v in args)
|
|
114
|
+
+ tuple(type(v) for _, v in sorted_items)
|
|
115
|
+
)
|
|
116
|
+
return _HashedTuple(args + tuple(type(v) for v in args))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── Cache Info ─────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
CacheInfo = collections.namedtuple(
|
|
122
|
+
"CacheInfo", ["hits", "misses", "maxsize", "currsize"]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# ── Cache Base Class ───────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class _DefaultSize:
|
|
129
|
+
"""Lightweight size tracker that always returns 1 for any key."""
|
|
130
|
+
|
|
131
|
+
__slots__ = ()
|
|
132
|
+
|
|
133
|
+
def __getitem__(self, _: Any) -> int:
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
def __setitem__(self, _: Any, __: Any) -> None:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
def pop(self, _: Any, *args: Any) -> int:
|
|
140
|
+
return 1
|
|
141
|
+
|
|
142
|
+
def clear(self) -> None:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Cache(collections.abc.MutableMapping):
|
|
147
|
+
"""Mutable mapping to serve as a simple cache or cache base class.
|
|
148
|
+
|
|
149
|
+
Subclasses must override ``popitem`` to implement a specific eviction
|
|
150
|
+
policy. The base class evicts arbitrary items (dict order).
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
maxsize: Maximum capacity (item count or weighted size).
|
|
154
|
+
getsizeof: Optional callable returning the size of a value.
|
|
155
|
+
Defaults to 1 per item.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
__marker = object()
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
maxsize: int | float,
|
|
163
|
+
getsizeof: Callable[[Any], int] | None = None,
|
|
164
|
+
):
|
|
165
|
+
if maxsize < 0:
|
|
166
|
+
raise ValueError("maxsize must be non-negative")
|
|
167
|
+
self.__data: dict[Any, Any] = {}
|
|
168
|
+
self.__currsize: int | float = 0
|
|
169
|
+
self.__maxsize = maxsize
|
|
170
|
+
if getsizeof is not None:
|
|
171
|
+
self.__size: dict | _DefaultSize = {}
|
|
172
|
+
self.getsizeof = getsizeof # type: ignore[assignment]
|
|
173
|
+
else:
|
|
174
|
+
self.__size = _DefaultSize()
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
cls = self.__class__.__name__
|
|
178
|
+
return (
|
|
179
|
+
f"{cls}({self.__data!r}, "
|
|
180
|
+
f"maxsize={self.__maxsize}, currsize={self.__currsize})"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def __getitem__(self, key: Any) -> Any:
|
|
184
|
+
try:
|
|
185
|
+
return self.__data[key]
|
|
186
|
+
except KeyError:
|
|
187
|
+
return self.__missing__(key)
|
|
188
|
+
|
|
189
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
190
|
+
maxsize = self.__maxsize
|
|
191
|
+
size = self.getsizeof(value)
|
|
192
|
+
if size > maxsize:
|
|
193
|
+
raise ValueError("value too large")
|
|
194
|
+
if key in self.__data:
|
|
195
|
+
diffsize = size - self.__size[key]
|
|
196
|
+
else:
|
|
197
|
+
diffsize = size
|
|
198
|
+
while self.__currsize + diffsize > maxsize:
|
|
199
|
+
self.popitem()
|
|
200
|
+
if key in self.__data:
|
|
201
|
+
# Key might have been evicted during popitem loop
|
|
202
|
+
self.__currsize -= self.__size[key]
|
|
203
|
+
self.__data[key] = value
|
|
204
|
+
self.__size[key] = size
|
|
205
|
+
self.__currsize += size
|
|
206
|
+
|
|
207
|
+
def __delitem__(self, key: Any) -> None:
|
|
208
|
+
size = self.__size.pop(key)
|
|
209
|
+
del self.__data[key]
|
|
210
|
+
self.__currsize -= size
|
|
211
|
+
|
|
212
|
+
def __contains__(self, key: object) -> bool:
|
|
213
|
+
return key in self.__data
|
|
214
|
+
|
|
215
|
+
def __missing__(self, key: Any) -> Any:
|
|
216
|
+
raise KeyError(key)
|
|
217
|
+
|
|
218
|
+
def __iter__(self):
|
|
219
|
+
return iter(self.__data)
|
|
220
|
+
|
|
221
|
+
def __len__(self) -> int:
|
|
222
|
+
return len(self.__data)
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def getsizeof(value: Any) -> int:
|
|
226
|
+
"""Return the size of *value*. Defaults to 1."""
|
|
227
|
+
return 1
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def maxsize(self) -> int | float:
|
|
231
|
+
"""Maximum cache capacity."""
|
|
232
|
+
return self.__maxsize
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def currsize(self) -> int | float:
|
|
236
|
+
"""Current cache size (sum of item sizes)."""
|
|
237
|
+
return self.__currsize
|
|
238
|
+
|
|
239
|
+
def get(self, key: Any, default: Any = None) -> Any:
|
|
240
|
+
if key in self:
|
|
241
|
+
return self[key]
|
|
242
|
+
return default
|
|
243
|
+
|
|
244
|
+
def pop(self, key: Any, default: Any = __marker) -> Any:
|
|
245
|
+
if key in self:
|
|
246
|
+
value = self[key]
|
|
247
|
+
del self[key]
|
|
248
|
+
return value
|
|
249
|
+
if default is self.__marker:
|
|
250
|
+
raise KeyError(key)
|
|
251
|
+
return default
|
|
252
|
+
|
|
253
|
+
def setdefault(self, key: Any, default: Any = None) -> Any:
|
|
254
|
+
if key in self:
|
|
255
|
+
return self[key]
|
|
256
|
+
try:
|
|
257
|
+
self[key] = default
|
|
258
|
+
except ValueError:
|
|
259
|
+
pass
|
|
260
|
+
return default
|
|
261
|
+
|
|
262
|
+
def popitem(self) -> tuple[Any, Any]:
|
|
263
|
+
"""Remove and return an arbitrary ``(key, value)`` pair.
|
|
264
|
+
|
|
265
|
+
Subclasses override this to implement eviction policies.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
key = next(iter(self.__data))
|
|
269
|
+
except StopIteration:
|
|
270
|
+
raise KeyError(f"{type(self).__name__} is empty") from None
|
|
271
|
+
return key, self.pop(key)
|
|
272
|
+
|
|
273
|
+
def clear(self) -> None:
|
|
274
|
+
self.__data.clear()
|
|
275
|
+
self.__size.clear()
|
|
276
|
+
self.__currsize = 0
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ── FIFO Cache ─────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class FIFOCache(Cache):
|
|
283
|
+
"""First In First Out (FIFO) cache implementation.
|
|
284
|
+
|
|
285
|
+
Evicts the oldest inserted item when the cache is full. Accessing an
|
|
286
|
+
item does **not** change its eviction priority.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
maxsize: int | float,
|
|
292
|
+
getsizeof: Callable[[Any], int] | None = None,
|
|
293
|
+
):
|
|
294
|
+
super().__init__(maxsize, getsizeof)
|
|
295
|
+
self.__order: collections.OrderedDict[Any, None] = collections.OrderedDict()
|
|
296
|
+
|
|
297
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
298
|
+
if key in self:
|
|
299
|
+
del self.__order[key]
|
|
300
|
+
super().__setitem__(key, value)
|
|
301
|
+
self.__order[key] = None
|
|
302
|
+
|
|
303
|
+
def __delitem__(self, key: Any) -> None:
|
|
304
|
+
super().__delitem__(key)
|
|
305
|
+
del self.__order[key]
|
|
306
|
+
|
|
307
|
+
def popitem(self) -> tuple[Any, Any]:
|
|
308
|
+
"""Remove and return the oldest inserted ``(key, value)`` pair."""
|
|
309
|
+
try:
|
|
310
|
+
key = next(iter(self.__order))
|
|
311
|
+
except StopIteration:
|
|
312
|
+
raise KeyError(f"{type(self).__name__} is empty") from None
|
|
313
|
+
return key, self.pop(key)
|
|
314
|
+
|
|
315
|
+
def clear(self) -> None:
|
|
316
|
+
super().clear()
|
|
317
|
+
self.__order.clear()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ── LRU Cache ──────────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class LRUCache(Cache):
|
|
324
|
+
"""Least Recently Used (LRU) cache implementation.
|
|
325
|
+
|
|
326
|
+
Evicts the least recently accessed item when the cache is full.
|
|
327
|
+
Both reads and writes count as accesses.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def __init__(
|
|
331
|
+
self,
|
|
332
|
+
maxsize: int | float,
|
|
333
|
+
getsizeof: Callable[[Any], int] | None = None,
|
|
334
|
+
):
|
|
335
|
+
super().__init__(maxsize, getsizeof)
|
|
336
|
+
self.__order: collections.OrderedDict[Any, None] = collections.OrderedDict()
|
|
337
|
+
|
|
338
|
+
def __getitem__(self, key: Any) -> Any:
|
|
339
|
+
# Bypass super().__getitem__ to avoid MRO dispatch overhead;
|
|
340
|
+
# call Cache.__getitem__ directly + inline move_to_end.
|
|
341
|
+
value = Cache.__getitem__(self, key)
|
|
342
|
+
self.__order.move_to_end(key)
|
|
343
|
+
return value
|
|
344
|
+
|
|
345
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
346
|
+
Cache.__setitem__(self, key, value)
|
|
347
|
+
try:
|
|
348
|
+
self.__order.move_to_end(key)
|
|
349
|
+
except KeyError:
|
|
350
|
+
self.__order[key] = None
|
|
351
|
+
|
|
352
|
+
def __delitem__(self, key: Any) -> None:
|
|
353
|
+
Cache.__delitem__(self, key)
|
|
354
|
+
del self.__order[key]
|
|
355
|
+
|
|
356
|
+
def popitem(self) -> tuple[Any, Any]:
|
|
357
|
+
"""Remove and return the least recently used ``(key, value)`` pair."""
|
|
358
|
+
try:
|
|
359
|
+
key = next(iter(self.__order))
|
|
360
|
+
except StopIteration:
|
|
361
|
+
raise KeyError(f"{type(self).__name__} is empty") from None
|
|
362
|
+
return key, self.pop(key)
|
|
363
|
+
|
|
364
|
+
def clear(self) -> None:
|
|
365
|
+
Cache.clear(self)
|
|
366
|
+
self.__order.clear()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ── LFU Cache ──────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class _LFUNode:
|
|
373
|
+
"""Doubly-linked node for the LFU frequency list."""
|
|
374
|
+
|
|
375
|
+
__slots__ = ("count", "keys", "prev", "next")
|
|
376
|
+
|
|
377
|
+
def __init__(self, count: int = 0):
|
|
378
|
+
self.count = count
|
|
379
|
+
self.keys: set[Any] = set()
|
|
380
|
+
self.prev: _LFUNode = self
|
|
381
|
+
self.next: _LFUNode = self
|
|
382
|
+
|
|
383
|
+
def insert_after(self, node: _LFUNode) -> None:
|
|
384
|
+
"""Insert *node* after this node."""
|
|
385
|
+
node.prev = self
|
|
386
|
+
node.next = self.next
|
|
387
|
+
self.next.prev = node
|
|
388
|
+
self.next = node
|
|
389
|
+
|
|
390
|
+
def unlink(self) -> None:
|
|
391
|
+
"""Remove this node from the linked list."""
|
|
392
|
+
self.prev.next = self.next
|
|
393
|
+
self.next.prev = self.prev
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class LFUCache(Cache):
|
|
397
|
+
"""Least Frequently Used (LFU) cache implementation.
|
|
398
|
+
|
|
399
|
+
Evicts the least frequently accessed item when the cache is full.
|
|
400
|
+
All operations are O(1).
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
def __init__(
|
|
404
|
+
self,
|
|
405
|
+
maxsize: int | float,
|
|
406
|
+
getsizeof: Callable[[Any], int] | None = None,
|
|
407
|
+
):
|
|
408
|
+
super().__init__(maxsize, getsizeof)
|
|
409
|
+
self.__root = _LFUNode() # sentinel
|
|
410
|
+
self.__links: dict[Any, _LFUNode] = {}
|
|
411
|
+
|
|
412
|
+
def __getitem__(self, key: Any) -> Any:
|
|
413
|
+
value = super().__getitem__(key)
|
|
414
|
+
self.__touch(key)
|
|
415
|
+
return value
|
|
416
|
+
|
|
417
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
418
|
+
existed = key in self
|
|
419
|
+
super().__setitem__(key, value)
|
|
420
|
+
if existed:
|
|
421
|
+
self.__touch(key)
|
|
422
|
+
else:
|
|
423
|
+
# New key starts at count=1
|
|
424
|
+
root = self.__root
|
|
425
|
+
first = root.next
|
|
426
|
+
if first is root or first.count != 1:
|
|
427
|
+
node = _LFUNode(count=1)
|
|
428
|
+
root.insert_after(node)
|
|
429
|
+
else:
|
|
430
|
+
node = first
|
|
431
|
+
node.keys.add(key)
|
|
432
|
+
self.__links[key] = node
|
|
433
|
+
|
|
434
|
+
def __delitem__(self, key: Any) -> None:
|
|
435
|
+
super().__delitem__(key)
|
|
436
|
+
node = self.__links.pop(key)
|
|
437
|
+
node.keys.discard(key)
|
|
438
|
+
if not node.keys:
|
|
439
|
+
node.unlink()
|
|
440
|
+
|
|
441
|
+
def __touch(self, key: Any) -> None:
|
|
442
|
+
"""Increment frequency count for *key*."""
|
|
443
|
+
node = self.__links[key]
|
|
444
|
+
new_count = node.count + 1
|
|
445
|
+
nxt = node.next
|
|
446
|
+
if nxt is self.__root or nxt.count != new_count:
|
|
447
|
+
new_node = _LFUNode(count=new_count)
|
|
448
|
+
node.insert_after(new_node)
|
|
449
|
+
else:
|
|
450
|
+
new_node = nxt
|
|
451
|
+
new_node.keys.add(key)
|
|
452
|
+
node.keys.discard(key)
|
|
453
|
+
self.__links[key] = new_node
|
|
454
|
+
if not node.keys:
|
|
455
|
+
node.unlink()
|
|
456
|
+
|
|
457
|
+
def popitem(self) -> tuple[Any, Any]:
|
|
458
|
+
"""Remove and return the least frequently used ``(key, value)`` pair."""
|
|
459
|
+
root = self.__root
|
|
460
|
+
node = root.next
|
|
461
|
+
if node is root:
|
|
462
|
+
raise KeyError(f"{type(self).__name__} is empty")
|
|
463
|
+
key = next(iter(node.keys))
|
|
464
|
+
# Bypass self.pop → self[key] → __touch to avoid wasteful
|
|
465
|
+
# frequency increment on a key that is about to be deleted.
|
|
466
|
+
value = Cache.__getitem__(self, key)
|
|
467
|
+
del self[key]
|
|
468
|
+
return key, value
|
|
469
|
+
|
|
470
|
+
def clear(self) -> None:
|
|
471
|
+
super().clear()
|
|
472
|
+
self.__links.clear()
|
|
473
|
+
self.__root.prev = self.__root
|
|
474
|
+
self.__root.next = self.__root
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# ── TTL Cache ──────────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class _Timer:
|
|
481
|
+
"""Wraps a timer function with nestable time-freezing.
|
|
482
|
+
|
|
483
|
+
Used by TTLCache to ensure time consistency within a single operation.
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
__slots__ = ("_timer", "_nesting", "_time")
|
|
487
|
+
|
|
488
|
+
def __init__(self, timer: Callable[[], float] = time.monotonic):
|
|
489
|
+
self._timer = timer
|
|
490
|
+
self._nesting = 0
|
|
491
|
+
self._time: float = 0.0
|
|
492
|
+
|
|
493
|
+
def __call__(self) -> float:
|
|
494
|
+
if self._nesting == 0:
|
|
495
|
+
return self._timer()
|
|
496
|
+
return self._time
|
|
497
|
+
|
|
498
|
+
def __enter__(self) -> float:
|
|
499
|
+
if self._nesting == 0:
|
|
500
|
+
self._time = self._timer()
|
|
501
|
+
self._nesting += 1
|
|
502
|
+
return self._time
|
|
503
|
+
|
|
504
|
+
def __exit__(self, *exc: Any) -> None:
|
|
505
|
+
self._nesting -= 1
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class _TTLLink:
|
|
509
|
+
"""Doubly-linked node for TTLCache expiry ordering."""
|
|
510
|
+
|
|
511
|
+
__slots__ = ("key", "expires", "prev", "next")
|
|
512
|
+
|
|
513
|
+
def __init__(
|
|
514
|
+
self,
|
|
515
|
+
key: Any = None,
|
|
516
|
+
expires: float = 0.0,
|
|
517
|
+
):
|
|
518
|
+
self.key = key
|
|
519
|
+
self.expires = expires
|
|
520
|
+
self.prev: _TTLLink = self
|
|
521
|
+
self.next: _TTLLink = self
|
|
522
|
+
|
|
523
|
+
def unlink(self) -> None:
|
|
524
|
+
self.prev.next = self.next
|
|
525
|
+
self.next.prev = self.prev
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class TTLCache(LRUCache):
|
|
529
|
+
"""LRU cache with per-item time-to-live (TTL).
|
|
530
|
+
|
|
531
|
+
Items are evicted when they expire (checked lazily on access) or when
|
|
532
|
+
the cache is full (LRU order).
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
maxsize: Maximum cache capacity.
|
|
536
|
+
ttl: Time-to-live in seconds for each item.
|
|
537
|
+
timer: Callable returning the current time (default: ``time.monotonic``).
|
|
538
|
+
getsizeof: Optional callable returning the size of a value.
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
def __init__(
|
|
542
|
+
self,
|
|
543
|
+
maxsize: int | float,
|
|
544
|
+
ttl: float,
|
|
545
|
+
*,
|
|
546
|
+
timer: Callable[[], float] = time.monotonic,
|
|
547
|
+
getsizeof: Callable[[Any], int] | None = None,
|
|
548
|
+
):
|
|
549
|
+
LRUCache.__init__(self, maxsize, getsizeof)
|
|
550
|
+
self.__ttl = ttl
|
|
551
|
+
self.__timer = _Timer(timer)
|
|
552
|
+
self.__root = _TTLLink() # sentinel for expiry list
|
|
553
|
+
self.__links: dict[Any, _TTLLink] = {}
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def ttl(self) -> float:
|
|
557
|
+
"""Time-to-live in seconds."""
|
|
558
|
+
return self.__ttl
|
|
559
|
+
|
|
560
|
+
@property
|
|
561
|
+
def timer(self) -> _Timer:
|
|
562
|
+
"""The timer function."""
|
|
563
|
+
return self.__timer
|
|
564
|
+
|
|
565
|
+
def __contains__(self, key: object) -> bool:
|
|
566
|
+
link = self.__links.get(key)
|
|
567
|
+
if link is None:
|
|
568
|
+
return False
|
|
569
|
+
return self.__timer() < link.expires
|
|
570
|
+
|
|
571
|
+
def __getitem__(self, key: Any) -> Any:
|
|
572
|
+
link = self.__links.get(key)
|
|
573
|
+
if link is not None and self.__timer() >= link.expires:
|
|
574
|
+
del self[key]
|
|
575
|
+
return self.__missing__(key)
|
|
576
|
+
# Bypass LRUCache.__getitem__ → Cache.__getitem__ call chain;
|
|
577
|
+
# inline both to save one method-call and one dict lookup.
|
|
578
|
+
# Same direct-call pattern used by LFUCache.popitem.
|
|
579
|
+
value = Cache.__getitem__(self, key)
|
|
580
|
+
self._LRUCache__order.move_to_end(key) # type: ignore[attr-defined]
|
|
581
|
+
return value
|
|
582
|
+
|
|
583
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
584
|
+
with self.__timer as now:
|
|
585
|
+
self.expire(now)
|
|
586
|
+
LRUCache.__setitem__(self, key, value)
|
|
587
|
+
# Update or create expiry link
|
|
588
|
+
link = self.__links.get(key)
|
|
589
|
+
if link is not None:
|
|
590
|
+
link.unlink()
|
|
591
|
+
else:
|
|
592
|
+
link = _TTLLink()
|
|
593
|
+
self.__links[key] = link
|
|
594
|
+
link.key = key
|
|
595
|
+
link.expires = now + self.__ttl
|
|
596
|
+
# Append to end of expiry list (newest)
|
|
597
|
+
tail = self.__root.prev
|
|
598
|
+
link.prev = tail
|
|
599
|
+
link.next = self.__root
|
|
600
|
+
tail.next = link
|
|
601
|
+
self.__root.prev = link
|
|
602
|
+
|
|
603
|
+
def __delitem__(self, key: Any) -> None:
|
|
604
|
+
LRUCache.__delitem__(self, key)
|
|
605
|
+
link = self.__links.pop(key)
|
|
606
|
+
link.unlink()
|
|
607
|
+
|
|
608
|
+
def __iter__(self):
|
|
609
|
+
now = self.__timer()
|
|
610
|
+
for key in super().__iter__():
|
|
611
|
+
link = self.__links.get(key)
|
|
612
|
+
if link is not None and now < link.expires:
|
|
613
|
+
yield key
|
|
614
|
+
|
|
615
|
+
def __len__(self) -> int:
|
|
616
|
+
now = self.__timer()
|
|
617
|
+
# Single dict lookup via .get() instead of ``key in`` + ``[key]``.
|
|
618
|
+
links_get = self.__links.get
|
|
619
|
+
return sum(
|
|
620
|
+
1
|
|
621
|
+
for key in super().__iter__()
|
|
622
|
+
if (link := links_get(key)) is not None and now < link.expires
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
@property
|
|
626
|
+
def currsize(self) -> int | float:
|
|
627
|
+
"""Current size, excluding expired items."""
|
|
628
|
+
self.expire()
|
|
629
|
+
return super().currsize
|
|
630
|
+
|
|
631
|
+
def expire(self, time: float | None = None) -> list[tuple[Any, Any]]:
|
|
632
|
+
"""Remove expired items and return them as a list of ``(key, value)`` pairs."""
|
|
633
|
+
if time is None:
|
|
634
|
+
time = self.__timer()
|
|
635
|
+
root = self.__root
|
|
636
|
+
expired: list[tuple[Any, Any]] = []
|
|
637
|
+
link = root.next
|
|
638
|
+
while link is not root and link.expires <= time:
|
|
639
|
+
nxt = link.next
|
|
640
|
+
key = link.key
|
|
641
|
+
try:
|
|
642
|
+
value = Cache.__getitem__(self, key)
|
|
643
|
+
expired.append((key, value))
|
|
644
|
+
del self[key]
|
|
645
|
+
except KeyError:
|
|
646
|
+
pass
|
|
647
|
+
link = nxt
|
|
648
|
+
return expired
|
|
649
|
+
|
|
650
|
+
def clear(self) -> None:
|
|
651
|
+
LRUCache.clear(self)
|
|
652
|
+
self.__links.clear()
|
|
653
|
+
self.__root.prev = self.__root
|
|
654
|
+
self.__root.next = self.__root
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
# ── Decorators ─────────────────────────────────────────────────────────────
|
|
658
|
+
|
|
659
|
+
_MISS = object() # sentinel for cache lookup miss
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class _Stats:
|
|
663
|
+
"""Mutable hit/miss counters shared by cache wrapper closures."""
|
|
664
|
+
|
|
665
|
+
__slots__ = ("hits", "misses")
|
|
666
|
+
|
|
667
|
+
def __init__(self) -> None:
|
|
668
|
+
self.hits = 0
|
|
669
|
+
self.misses = 0
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _get_cache_maxsize(
|
|
673
|
+
cache: collections.abc.MutableMapping,
|
|
674
|
+
) -> int | float | None:
|
|
675
|
+
"""Return maxsize if *cache* is a ``Cache`` instance, else ``None``."""
|
|
676
|
+
return cache.maxsize if isinstance(cache, Cache) else None
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _get_cache_currsize(cache: collections.abc.MutableMapping) -> int | float:
|
|
680
|
+
"""Return current size of *cache*."""
|
|
681
|
+
return cache.currsize if isinstance(cache, Cache) else len(cache)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _attach_cache_info(
|
|
685
|
+
wrapper: Callable,
|
|
686
|
+
cache: collections.abc.MutableMapping,
|
|
687
|
+
lock: Any | None,
|
|
688
|
+
stats: _Stats,
|
|
689
|
+
maxsize: int | float | None,
|
|
690
|
+
) -> None:
|
|
691
|
+
"""Attach ``cache_info()`` and ``cache_clear()`` methods to *wrapper*."""
|
|
692
|
+
|
|
693
|
+
def cache_info() -> CacheInfo:
|
|
694
|
+
return CacheInfo(stats.hits, stats.misses, maxsize, _get_cache_currsize(cache))
|
|
695
|
+
|
|
696
|
+
def cache_clear() -> None:
|
|
697
|
+
with lock if lock else _no_lock():
|
|
698
|
+
cache.clear()
|
|
699
|
+
stats.hits = stats.misses = 0
|
|
700
|
+
|
|
701
|
+
wrapper.cache_info = cache_info # type: ignore[attr-defined]
|
|
702
|
+
wrapper.cache_clear = cache_clear # type: ignore[attr-defined]
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _attach_noop_info(
|
|
706
|
+
wrapper: Callable,
|
|
707
|
+
stats: _Stats,
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Attach ``cache_info()`` and ``cache_clear()`` for a no-op wrapper."""
|
|
710
|
+
|
|
711
|
+
def cache_info() -> CacheInfo:
|
|
712
|
+
return CacheInfo(stats.hits, stats.misses, None, 0)
|
|
713
|
+
|
|
714
|
+
def cache_clear() -> None:
|
|
715
|
+
stats.hits = stats.misses = 0
|
|
716
|
+
|
|
717
|
+
wrapper.cache_info = cache_info # type: ignore[attr-defined]
|
|
718
|
+
wrapper.cache_clear = cache_clear # type: ignore[attr-defined]
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _make_sync_wrapper(
|
|
722
|
+
fn: Callable,
|
|
723
|
+
cache: collections.abc.MutableMapping | None,
|
|
724
|
+
key: Callable,
|
|
725
|
+
lock: Any | None,
|
|
726
|
+
info: bool,
|
|
727
|
+
) -> Callable:
|
|
728
|
+
"""Build a synchronous caching wrapper."""
|
|
729
|
+
stats = _Stats()
|
|
730
|
+
|
|
731
|
+
if cache is None:
|
|
732
|
+
|
|
733
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
734
|
+
stats.misses += 1
|
|
735
|
+
return fn(*args, **kwargs)
|
|
736
|
+
|
|
737
|
+
if info:
|
|
738
|
+
_attach_noop_info(wrapper, stats)
|
|
739
|
+
return wrapper
|
|
740
|
+
|
|
741
|
+
# Pre-cache method references to avoid attribute lookup per call.
|
|
742
|
+
cache_getitem = cache.__getitem__
|
|
743
|
+
cache_setitem = cache.__setitem__
|
|
744
|
+
cache_setdefault = cache.setdefault
|
|
745
|
+
|
|
746
|
+
if lock is not None:
|
|
747
|
+
|
|
748
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
749
|
+
k = key(*args, **kwargs)
|
|
750
|
+
with lock:
|
|
751
|
+
try:
|
|
752
|
+
result = cache_getitem(k)
|
|
753
|
+
except KeyError:
|
|
754
|
+
result = _MISS
|
|
755
|
+
if result is not _MISS:
|
|
756
|
+
stats.hits += 1
|
|
757
|
+
return result
|
|
758
|
+
stats.misses += 1
|
|
759
|
+
v = fn(*args, **kwargs)
|
|
760
|
+
# Inline thread-safe store (was _cache_store_default).
|
|
761
|
+
with lock:
|
|
762
|
+
try:
|
|
763
|
+
return cache_setdefault(k, v)
|
|
764
|
+
except ValueError:
|
|
765
|
+
return v
|
|
766
|
+
|
|
767
|
+
else:
|
|
768
|
+
|
|
769
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
770
|
+
k = key(*args, **kwargs)
|
|
771
|
+
try:
|
|
772
|
+
result = cache_getitem(k)
|
|
773
|
+
except KeyError:
|
|
774
|
+
stats.misses += 1
|
|
775
|
+
v = fn(*args, **kwargs)
|
|
776
|
+
try:
|
|
777
|
+
cache_setitem(k, v)
|
|
778
|
+
except ValueError:
|
|
779
|
+
pass
|
|
780
|
+
return v
|
|
781
|
+
stats.hits += 1
|
|
782
|
+
return result
|
|
783
|
+
|
|
784
|
+
if info:
|
|
785
|
+
_attach_cache_info(wrapper, cache, lock, stats, _get_cache_maxsize(cache))
|
|
786
|
+
|
|
787
|
+
return wrapper
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _make_async_wrapper(
|
|
791
|
+
fn: Callable,
|
|
792
|
+
cache: collections.abc.MutableMapping | None,
|
|
793
|
+
key: Callable,
|
|
794
|
+
lock: Any | None,
|
|
795
|
+
info: bool,
|
|
796
|
+
) -> Callable:
|
|
797
|
+
"""Build an asynchronous caching wrapper."""
|
|
798
|
+
stats = _Stats()
|
|
799
|
+
|
|
800
|
+
if cache is None:
|
|
801
|
+
|
|
802
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
803
|
+
stats.misses += 1
|
|
804
|
+
return await fn(*args, **kwargs)
|
|
805
|
+
|
|
806
|
+
if info:
|
|
807
|
+
_attach_noop_info(wrapper, stats)
|
|
808
|
+
return wrapper
|
|
809
|
+
|
|
810
|
+
# Pre-cache method references to avoid attribute lookup per call.
|
|
811
|
+
cache_getitem = cache.__getitem__
|
|
812
|
+
cache_setitem = cache.__setitem__
|
|
813
|
+
cache_setdefault = cache.setdefault
|
|
814
|
+
|
|
815
|
+
if lock is not None:
|
|
816
|
+
|
|
817
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
818
|
+
k = key(*args, **kwargs)
|
|
819
|
+
async with lock:
|
|
820
|
+
try:
|
|
821
|
+
result = cache_getitem(k)
|
|
822
|
+
except KeyError:
|
|
823
|
+
result = _MISS
|
|
824
|
+
if result is not _MISS:
|
|
825
|
+
stats.hits += 1
|
|
826
|
+
return result
|
|
827
|
+
stats.misses += 1
|
|
828
|
+
v = await fn(*args, **kwargs)
|
|
829
|
+
# Inline thread-safe store (was _cache_store_default).
|
|
830
|
+
async with lock:
|
|
831
|
+
try:
|
|
832
|
+
return cache_setdefault(k, v)
|
|
833
|
+
except ValueError:
|
|
834
|
+
return v
|
|
835
|
+
|
|
836
|
+
else:
|
|
837
|
+
|
|
838
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
839
|
+
k = key(*args, **kwargs)
|
|
840
|
+
try:
|
|
841
|
+
result = cache_getitem(k)
|
|
842
|
+
except KeyError:
|
|
843
|
+
stats.misses += 1
|
|
844
|
+
v = await fn(*args, **kwargs)
|
|
845
|
+
try:
|
|
846
|
+
cache_setitem(k, v)
|
|
847
|
+
except ValueError:
|
|
848
|
+
pass
|
|
849
|
+
return v
|
|
850
|
+
stats.hits += 1
|
|
851
|
+
return result
|
|
852
|
+
|
|
853
|
+
if info:
|
|
854
|
+
_attach_cache_info(wrapper, cache, lock, stats, _get_cache_maxsize(cache))
|
|
855
|
+
|
|
856
|
+
return wrapper
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
class _no_lock:
|
|
860
|
+
"""Dummy context manager when no lock is provided."""
|
|
861
|
+
|
|
862
|
+
def __enter__(self) -> None:
|
|
863
|
+
pass
|
|
864
|
+
|
|
865
|
+
def __exit__(self, *exc: Any) -> None:
|
|
866
|
+
pass
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def cached(
|
|
870
|
+
cache: Cache | collections.abc.MutableMapping | None,
|
|
871
|
+
*,
|
|
872
|
+
key: Callable[..., Any] = hashkey,
|
|
873
|
+
lock: Any | None = None,
|
|
874
|
+
info: bool = False,
|
|
875
|
+
) -> Callable:
|
|
876
|
+
"""Decorator to wrap a function with a memoizing callable.
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
cache: A cache instance (any MutableMapping) or ``None`` to disable.
|
|
880
|
+
key: Key function (default ``hashkey``).
|
|
881
|
+
lock: A lock object (``threading.Lock`` for sync, ``asyncio.Lock``
|
|
882
|
+
for async). The lock is released while the wrapped function
|
|
883
|
+
executes.
|
|
884
|
+
info: If ``True``, attach ``cache_info()`` and ``cache_clear()``
|
|
885
|
+
methods to the wrapper.
|
|
886
|
+
"""
|
|
887
|
+
|
|
888
|
+
def decorator(fn: Callable) -> Callable:
|
|
889
|
+
if inspect.iscoroutinefunction(fn):
|
|
890
|
+
wrapper = _make_async_wrapper(fn, cache, key, lock, info)
|
|
891
|
+
else:
|
|
892
|
+
wrapper = _make_sync_wrapper(fn, cache, key, lock, info)
|
|
893
|
+
wrapper.cache = cache # type: ignore[attr-defined]
|
|
894
|
+
wrapper.cache_key = key # type: ignore[attr-defined]
|
|
895
|
+
wrapper.cache_lock = lock # type: ignore[attr-defined]
|
|
896
|
+
return functools.update_wrapper(wrapper, fn)
|
|
897
|
+
|
|
898
|
+
return decorator
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
# ── Convenience Decorators ─────────────────────────────────────────────────
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def lru_cache(
|
|
905
|
+
fn: Callable[..., Any] | None = None,
|
|
906
|
+
*,
|
|
907
|
+
maxsize: int = 128,
|
|
908
|
+
key: Callable[..., Any] = hashkey,
|
|
909
|
+
lock: Any | None = None,
|
|
910
|
+
info: bool = True,
|
|
911
|
+
) -> Callable[..., Any]:
|
|
912
|
+
"""LRU cache decorator. Auto-detects sync/async.
|
|
913
|
+
|
|
914
|
+
Unlike ``functools.lru_cache``, supports TTL (via ``ttl_cache``),
|
|
915
|
+
async functions, and explicit lock objects.
|
|
916
|
+
|
|
917
|
+
Args:
|
|
918
|
+
maxsize: Maximum number of cached results.
|
|
919
|
+
key: Key function (default ``hashkey``).
|
|
920
|
+
lock: Optional lock for thread/async safety.
|
|
921
|
+
info: Attach ``cache_info()``/``cache_clear()`` (default ``True``).
|
|
922
|
+
"""
|
|
923
|
+
|
|
924
|
+
def decorator(fn: Callable) -> Callable:
|
|
925
|
+
c = LRUCache(maxsize)
|
|
926
|
+
return cached(c, key=key, lock=lock, info=info)(fn)
|
|
927
|
+
|
|
928
|
+
if fn is not None:
|
|
929
|
+
return decorator(fn)
|
|
930
|
+
return decorator
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def fifo_cache(
|
|
934
|
+
fn: Callable[..., Any] | None = None,
|
|
935
|
+
*,
|
|
936
|
+
maxsize: int = 128,
|
|
937
|
+
key: Callable[..., Any] = hashkey,
|
|
938
|
+
lock: Any | None = None,
|
|
939
|
+
info: bool = True,
|
|
940
|
+
) -> Callable[..., Any]:
|
|
941
|
+
"""FIFO cache decorator. Auto-detects sync/async.
|
|
942
|
+
|
|
943
|
+
Evicts the oldest inserted item regardless of access pattern.
|
|
944
|
+
"""
|
|
945
|
+
|
|
946
|
+
def decorator(fn: Callable) -> Callable:
|
|
947
|
+
c = FIFOCache(maxsize)
|
|
948
|
+
return cached(c, key=key, lock=lock, info=info)(fn)
|
|
949
|
+
|
|
950
|
+
if fn is not None:
|
|
951
|
+
return decorator(fn)
|
|
952
|
+
return decorator
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def lfu_cache(
|
|
956
|
+
fn: Callable[..., Any] | None = None,
|
|
957
|
+
*,
|
|
958
|
+
maxsize: int = 128,
|
|
959
|
+
key: Callable[..., Any] = hashkey,
|
|
960
|
+
lock: Any | None = None,
|
|
961
|
+
info: bool = True,
|
|
962
|
+
) -> Callable[..., Any]:
|
|
963
|
+
"""LFU cache decorator. Auto-detects sync/async.
|
|
964
|
+
|
|
965
|
+
Evicts the least frequently accessed item.
|
|
966
|
+
"""
|
|
967
|
+
|
|
968
|
+
def decorator(fn: Callable) -> Callable:
|
|
969
|
+
c = LFUCache(maxsize)
|
|
970
|
+
return cached(c, key=key, lock=lock, info=info)(fn)
|
|
971
|
+
|
|
972
|
+
if fn is not None:
|
|
973
|
+
return decorator(fn)
|
|
974
|
+
return decorator
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def ttl_cache(
|
|
978
|
+
fn: Callable[..., Any] | None = None,
|
|
979
|
+
*,
|
|
980
|
+
maxsize: int = 128,
|
|
981
|
+
ttl: float = 600,
|
|
982
|
+
timer: Callable[[], float] = time.monotonic,
|
|
983
|
+
key: Callable[..., Any] = hashkey,
|
|
984
|
+
lock: Any | None = None,
|
|
985
|
+
info: bool = True,
|
|
986
|
+
) -> Callable[..., Any]:
|
|
987
|
+
"""TTL cache decorator. Auto-detects sync/async.
|
|
988
|
+
|
|
989
|
+
Items expire after *ttl* seconds (default 600 = 10 minutes).
|
|
990
|
+
Eviction follows LRU order when the cache is full.
|
|
991
|
+
"""
|
|
992
|
+
|
|
993
|
+
def decorator(fn: Callable) -> Callable:
|
|
994
|
+
c = TTLCache(maxsize, ttl, timer=timer)
|
|
995
|
+
return cached(c, key=key, lock=lock, info=info)(fn)
|
|
996
|
+
|
|
997
|
+
if fn is not None:
|
|
998
|
+
return decorator(fn)
|
|
999
|
+
return decorator
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
# ── Public API ─────────────────────────────────────────────────────────────
|
|
1003
|
+
|
|
1004
|
+
__all__ = [
|
|
1005
|
+
# Cache classes
|
|
1006
|
+
"Cache",
|
|
1007
|
+
"FIFOCache",
|
|
1008
|
+
"LFUCache",
|
|
1009
|
+
"LRUCache",
|
|
1010
|
+
"TTLCache",
|
|
1011
|
+
# Decorators
|
|
1012
|
+
"cached",
|
|
1013
|
+
"fifo_cache",
|
|
1014
|
+
"lfu_cache",
|
|
1015
|
+
"lru_cache",
|
|
1016
|
+
"ttl_cache",
|
|
1017
|
+
# Key functions
|
|
1018
|
+
"hashkey",
|
|
1019
|
+
"methodkey",
|
|
1020
|
+
"typedkey",
|
|
1021
|
+
# Stats
|
|
1022
|
+
"CacheInfo",
|
|
1023
|
+
]
|