forked from bton/matekasse
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
|
import typing as _t
|
||
|
from time import time
|
||
|
|
||
|
from cachelib.base import BaseCache
|
||
|
from cachelib.serializers import SimpleSerializer
|
||
|
|
||
|
|
||
|
class SimpleCache(BaseCache):
|
||
|
|
||
|
"""Simple memory cache for single process environments. This class exists
|
||
|
mainly for the development server and is not 100% thread safe. It tries
|
||
|
to use as many atomic operations as possible and no locks for simplicity
|
||
|
but it could happen under heavy load that keys are added multiple times.
|
||
|
|
||
|
:param threshold: the maximum number of items the cache stores before
|
||
|
it starts deleting some.
|
||
|
:param default_timeout: the default timeout that is used if no timeout is
|
||
|
specified on :meth:`~BaseCache.set`. A timeout of
|
||
|
0 indicates that the cache never expires.
|
||
|
"""
|
||
|
|
||
|
serializer = SimpleSerializer()
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
threshold: int = 500,
|
||
|
default_timeout: int = 300,
|
||
|
):
|
||
|
BaseCache.__init__(self, default_timeout)
|
||
|
self._cache: _t.Dict[str, _t.Any] = {}
|
||
|
self._threshold = threshold or 500 # threshold = 0
|
||
|
|
||
|
def _over_threshold(self) -> bool:
|
||
|
return len(self._cache) > self._threshold
|
||
|
|
||
|
def _remove_expired(self, now: float) -> None:
|
||
|
toremove = [k for k, (expires, _) in self._cache.items() if expires < now]
|
||
|
for k in toremove:
|
||
|
self._cache.pop(k, None)
|
||
|
|
||
|
def _remove_older(self) -> None:
|
||
|
k_ordered = (
|
||
|
k
|
||
|
for k, v in sorted(
|
||
|
self._cache.items(), key=lambda item: item[1][0] # type: ignore
|
||
|
)
|
||
|
)
|
||
|
for k in k_ordered:
|
||
|
self._cache.pop(k, None)
|
||
|
if not self._over_threshold():
|
||
|
break
|
||
|
|
||
|
def _prune(self) -> None:
|
||
|
if self._over_threshold():
|
||
|
now = time()
|
||
|
self._remove_expired(now)
|
||
|
# remove older items if still over threshold
|
||
|
if self._over_threshold():
|
||
|
self._remove_older()
|
||
|
|
||
|
def _normalize_timeout(self, timeout: _t.Optional[int]) -> int:
|
||
|
timeout = BaseCache._normalize_timeout(self, timeout)
|
||
|
if timeout > 0:
|
||
|
timeout = int(time()) + timeout
|
||
|
return timeout
|
||
|
|
||
|
def get(self, key: str) -> _t.Any:
|
||
|
try:
|
||
|
expires, value = self._cache[key]
|
||
|
if expires == 0 or expires > time():
|
||
|
return self.serializer.loads(value)
|
||
|
except KeyError:
|
||
|
return None
|
||
|
|
||
|
def set(
|
||
|
self, key: str, value: _t.Any, timeout: _t.Optional[int] = None
|
||
|
) -> _t.Optional[bool]:
|
||
|
expires = self._normalize_timeout(timeout)
|
||
|
self._prune()
|
||
|
self._cache[key] = (expires, self.serializer.dumps(value))
|
||
|
return True
|
||
|
|
||
|
def add(self, key: str, value: _t.Any, timeout: _t.Optional[int] = None) -> bool:
|
||
|
expires = self._normalize_timeout(timeout)
|
||
|
self._prune()
|
||
|
item = (expires, self.serializer.dumps(value))
|
||
|
if key in self._cache:
|
||
|
return False
|
||
|
self._cache.setdefault(key, item)
|
||
|
return True
|
||
|
|
||
|
def delete(self, key: str) -> bool:
|
||
|
return self._cache.pop(key, None) is not None
|
||
|
|
||
|
def has(self, key: str) -> bool:
|
||
|
try:
|
||
|
expires, value = self._cache[key]
|
||
|
return bool(expires == 0 or expires > time())
|
||
|
except KeyError:
|
||
|
return False
|
||
|
|
||
|
def clear(self) -> bool:
|
||
|
self._cache.clear()
|
||
|
return not bool(self._cache)
|