97 lines
2.6 KiB
Python
97 lines
2.6 KiB
Python
import collections
|
|
import time
|
|
|
|
from voussoirkit import sentinel
|
|
|
|
NO_ITEM = sentinel.Sentinel('no item')
|
|
|
|
class Cache:
|
|
def __init__(self, maxlen, expiry=float('inf')):
|
|
self.maxlen = maxlen
|
|
self.expiry = expiry
|
|
self.cache = collections.OrderedDict()
|
|
|
|
# To prevent excessive purge loops during repeated setitem, only allow
|
|
# a purge once every this many seconds.
|
|
self.max_purge_frequency = 0.5
|
|
|
|
self._last_purge = 0
|
|
|
|
def __contains__(self, key):
|
|
return self.get(key, fallback=NO_ITEM) is not NO_ITEM
|
|
|
|
def __getitem__(self, key):
|
|
'''
|
|
Return the key's value, or raise KeyError.
|
|
'''
|
|
# Let KeyError raise to caller.
|
|
(value, timestamp) = self.cache.pop(key)
|
|
|
|
now = time.time()
|
|
if (now - timestamp) > self.expiry:
|
|
raise KeyError(key)
|
|
|
|
self.cache[key] = (value, timestamp)
|
|
return value
|
|
|
|
def __len__(self):
|
|
'''
|
|
Purge expired items, then count the length.
|
|
Due to the purge, this method is not O(1) as most len methods are.
|
|
'''
|
|
self._purge_expired()
|
|
return len(self.cache)
|
|
|
|
def __setitem__(self, key, value):
|
|
# If the key was already present, we don't need to worry about maxlen
|
|
# because the net change is zero. If it was not present (KeyError) we
|
|
# check the maxlen and pop the oldest item if needed.
|
|
# Either way we update the timestamp.
|
|
try:
|
|
self.cache.pop(key)
|
|
except KeyError:
|
|
if len(self) >= self.maxlen:
|
|
self.cache.popitem(last=False)
|
|
self.cache[key] = (value, time.time())
|
|
|
|
def _purge_expired(self):
|
|
now = time.time()
|
|
if now - self._last_purge < self.max_purge_frequency:
|
|
return
|
|
|
|
for (key, (value, timestamp)) in list(self.cache.items()):
|
|
if (now - timestamp) > self.expiry:
|
|
self.cache.pop(key)
|
|
|
|
self._last_purge = now
|
|
|
|
def clear(self):
|
|
'''
|
|
Remove everything from the cache.
|
|
'''
|
|
self.cache.clear()
|
|
|
|
def get(self, key, fallback=None):
|
|
'''
|
|
Return the key's value, or fallback in case of KeyError.
|
|
'''
|
|
try:
|
|
return self[key]
|
|
except KeyError:
|
|
return fallback
|
|
|
|
def pop(self, key):
|
|
'''
|
|
Remove the key and return its value, or raise KeyError.
|
|
'''
|
|
(value, timestamp) = self.cache.pop(key)
|
|
return value
|
|
|
|
def remove(self, key):
|
|
'''
|
|
Remove the item and ignore KeyError.
|
|
'''
|
|
try:
|
|
self.pop(key)
|
|
except KeyError:
|
|
pass
|