Skip to content

Commit 0f3483f

Browse files
committed
feat(wrappers): add expiration wrapper
1 parent 6127114 commit 0f3483f

File tree

2 files changed

+62
-4
lines changed

2 files changed

+62
-4
lines changed

class_cache/wrappers.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime as dt
12
import pickle # noqa: S403
23
from typing import Iterable, Iterator
34

@@ -21,7 +22,7 @@ def id(self) -> IdType:
2122
return f"{self.__class__.__name__}({self.wrapped.id})"
2223

2324
@property
24-
def wrapped(self) -> CacheInterface:
25+
def wrapped(self) -> CacheInterface[KeyType, ValueType]:
2526
return self._wrapped
2627

2728
@property
@@ -82,3 +83,53 @@ def __setitem__(self, key: KeyType, value: ValueType) -> None:
8283

8384
def set_many(self, items: Iterable[tuple[KeyType, ValueType]]) -> None:
8485
return super().set_many((key, self._encode(value)) for key, value in items) # type: ignore
86+
87+
88+
class ExpirationWrapper(BaseWrapper[KeyType, ValueType]):
89+
def __init__(
90+
self,
91+
wrapped: CacheInterface[KeyType, tuple[dt.datetime, ValueType]],
92+
lifespan=dt.timedelta(days=1),
93+
) -> None:
94+
super().__init__(wrapped) # type: ignore
95+
self._lifespan = lifespan
96+
97+
@property
98+
def wrapped(self) -> CacheInterface[KeyType, tuple[dt.datetime, ValueType]]:
99+
return self._wrapped # type: ignore
100+
101+
@property
102+
def lifespan(self) -> dt.timedelta:
103+
return self._lifespan
104+
105+
@property
106+
def _now(self) -> dt.datetime:
107+
return dt.datetime.now(dt.UTC)
108+
109+
def _check_item(self, key: KeyType) -> bool:
110+
expiration_time, _ = self.wrapped[key]
111+
if expiration_time < self._now:
112+
del self.wrapped[key]
113+
return False
114+
return True
115+
116+
def __len__(self) -> int:
117+
# TODO: This is very inefficient.
118+
# Need to store expiration_time in a separate cache, but will need a way to clone cache for that.
119+
return sum(int(self._check_item(key)) for key in self.wrapped) # type: ignore
120+
121+
def __iter__(self) -> Iterable[KeyType]:
122+
for key in self.wrapped: # type: ignore
123+
if self._check_item(key):
124+
yield key
125+
126+
def __setitem__(self, key: KeyType, value: ValueType) -> None:
127+
self.wrapped[key] = self._now + self.lifespan, value
128+
129+
def __getitem__(self, key: KeyType) -> ValueType:
130+
if self._check_item(key):
131+
return self.wrapped[key][1]
132+
raise KeyError(key)
133+
134+
def __contains__(self, key: KeyType) -> bool:
135+
return super().__contains__(key) and self._check_item(key)

tests/test_wrappers.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import pytest
2+
13
from class_cache import Cache
2-
from class_cache.wrappers import BrotliCompressWrapper
4+
from class_cache.wrappers import BrotliCompressWrapper, ExpirationWrapper
35

46

5-
def test_brotli_wrapper(test_id, test_key, test_value):
6-
cache = BrotliCompressWrapper(Cache(test_id))
7+
@pytest.mark.parametrize(("wrapper_cls"), [BrotliCompressWrapper, ExpirationWrapper])
8+
def test_wrapper(wrapper_cls, test_id, test_key, test_value):
9+
cache = wrapper_cls(Cache(test_id))
710
cache.clear()
811
assert test_key not in cache
912
cache[test_key] = test_value
@@ -13,3 +16,7 @@ def test_brotli_wrapper(test_id, test_key, test_value):
1316
assert list(cache) == [test_key]
1417
del cache[test_key]
1518
assert test_key not in cache
19+
20+
21+
# TODO: Add test that brotli actually reduces size on disk
22+
# TODO: Add test that items actually expire in ExpirationWrapper

0 commit comments

Comments
 (0)