Skip to content

Commit 101ad64

Browse files
committed
WIP
1 parent 8385110 commit 101ad64

File tree

6 files changed

+181
-2
lines changed

6 files changed

+181
-2
lines changed

aiomisc/cache/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

aiomisc/cache/base.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import asyncio
2+
import threading
3+
from abc import ABC, abstractmethod
4+
from typing import Any, Union, Hashable, Dict, Optional
5+
6+
from llist import dllist, dllistnode
7+
8+
9+
class CacheBase(ABC):
10+
def __init__(self, max_size: int = 0):
11+
self._max_size: int = max_size
12+
self._loop: Optional[asyncio.AbstractEventLoop] = None
13+
self.usages: dllist = dllist()
14+
self.cache: Dict[Hashable, Any] = dict()
15+
self.lock = threading.RLock()
16+
17+
@property
18+
def is_overflow(self) -> bool:
19+
if self._max_size == 0:
20+
return False
21+
22+
if self._max_size < len(self.usages):
23+
return True
24+
25+
return False
26+
27+
@property
28+
def loop(self) -> asyncio.AbstractEventLoop:
29+
if self._loop is None:
30+
self._loop = asyncio.get_event_loop()
31+
return self._loop
32+
33+
@abstractmethod
34+
def _on_set(self, node: dllistnode) -> None:
35+
pass
36+
37+
def _on_expires(self, node: dllistnode) -> None:
38+
pass
39+
40+
@abstractmethod
41+
def _on_get(self, node: dllistnode) -> Any:
42+
pass
43+
44+
def __contains__(self, item: Hashable) -> bool:
45+
return item in self.cache
46+
47+
def get(self, item: Hashable) -> Any:
48+
with self.lock:
49+
node: dllistnode = self.cache[item]
50+
self.loop.call_soon(self._on_get, node)
51+
return node.value[1]
52+
53+
def expire(self, node: dllistnode):
54+
with self.lock:
55+
item, value = node.value
56+
node: Optional[dllistnode] = self.cache.pop(item, None)
57+
58+
if node is None:
59+
return
60+
61+
self.loop.call_soon(self._on_expires, node)
62+
self.usages.remove(node)
63+
64+
def set(self, item: Hashable, value: Any,
65+
expiration: Union[int, float] = None) -> None:
66+
with self.lock:
67+
node: dllistnode = self.usages.append((item, value))
68+
self.cache[item] = node
69+
70+
self.loop.call_soon(self._on_set, node)
71+
72+
if expiration is not None:
73+
self.loop.call_later(expiration, self.expire, node)

aiomisc/cache/lfu.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from dataclasses import dataclass
2+
from threading import RLock
3+
from typing import Any, Hashable, Optional
4+
5+
from llist import dllistnode, dllist
6+
7+
8+
@dataclass(frozen=True)
9+
class FrequencyItem:
10+
node: dllistnode
11+
key: Hashable
12+
value: Any
13+
14+
15+
class LFUCache:
16+
"""
17+
LFU cache implementation
18+
19+
>>> lfu = LFUCache(3)
20+
>>> lfu.set("foo", "bar")
21+
>>> assert "foo" in lfu
22+
>>> lfu.get('foo')
23+
'bar'
24+
>>> lfu.set("bar", "foo")
25+
>>> lfu.set("spam", "egg")
26+
27+
"""
28+
29+
def __init__(self, max_size: int = 0):
30+
self.cache = dict()
31+
self.usages = dllist()
32+
self.lock = RLock()
33+
self.size = 0
34+
self.max_size = max_size
35+
36+
def _create_node(self) -> dllistnode:
37+
return self.usages.append(set([]))
38+
39+
def _update_usage(self, item: FrequencyItem):
40+
with self.lock:
41+
old_node = item.node
42+
new_node = item.node.next
43+
44+
if new_node is None:
45+
new_node = self._create_node()
46+
47+
old_node.value.remove(item)
48+
item = FrequencyItem(
49+
node=new_node,
50+
key=item.key,
51+
value=item.value,
52+
)
53+
new_node.value.add(item)
54+
self.cache[item.key] = item
55+
56+
if not old_node.value:
57+
self.usages.remove(old_node)
58+
59+
def get(self, key: Hashable):
60+
item: FrequencyItem = self.cache[key]
61+
self._update_usage(item)
62+
return item.value
63+
64+
def set(self, key: Hashable, value: Any):
65+
with self.lock:
66+
node: Optional[dllistnode] = self.usages.first
67+
68+
if node is None:
69+
node = self._create_node()
70+
71+
item = FrequencyItem(node=node, key=key, value=value)
72+
node.value.add(item)
73+
self.cache[key] = item
74+
75+
def __contains__(self, key) -> Any:
76+
if key in self.cache:
77+
self._update_usage(self.cache[key])
78+
return True
79+
return False

aiomisc/cache/lru.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Any
2+
3+
from llist import dllistnode
4+
5+
from .base import CacheBase
6+
7+
8+
class LRUCache(CacheBase):
9+
def _on_set(self, node: dllistnode) -> None:
10+
if not self.is_overflow:
11+
return
12+
self._on_overflow()
13+
14+
def _on_get(self, node: dllistnode) -> Any:
15+
with self.lock:
16+
self.usages.remove(node)
17+
self.usages.appendright(node)
18+
19+
def _on_overflow(self):
20+
with self.lock:
21+
while self.is_overflow:
22+
node: dllistnode = self.usages.popleft()
23+
item, value = node.value
24+
self.cache.pop(item)

requirements.dev.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
aiohttp<4
21
aiohttp-asgi
2+
aiohttp<4
33
async-generator
44
async-timeout
55
coverage==4.5.1
66
coveralls
77
croniter~=0.3.34
88
fastapi
99
freezegun<1.1
10+
llist==0.6
1011
mypy~=0.782
1112
pylava
1213
pytest
1314
pytest-cov~=2.5.1
1415
pytest-freezegun~=0.4.2
15-
sphinx>=3.5.1
1616
sphinx-autobuild
1717
sphinx-intl
18+
sphinx>=3.5.1
1819
timeout-decorator
1920
tox>=2.4

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def load_requirements(fname):
6565
'raven': ['raven-aiohttp'],
6666
'uvloop': ['uvloop>=0.14,<1'],
6767
'cron': ['croniter~=0.3.34'],
68+
'lfu': ['llist==0.6'],
6869
':python_version < "3.7"': 'async-generator',
6970
},
7071
entry_points={

0 commit comments

Comments
 (0)