tigrbl_engine_memlru 0.1.10.dev1__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.
- tigrbl_engine_memlru/__init__.py +2 -0
- tigrbl_engine_memlru/lru.py +102 -0
- tigrbl_engine_memlru/plugin.py +41 -0
- tigrbl_engine_memlru/session.py +43 -0
- tigrbl_engine_memlru-0.1.10.dev1.dist-info/METADATA +5 -0
- tigrbl_engine_memlru-0.1.10.dev1.dist-info/RECORD +8 -0
- tigrbl_engine_memlru-0.1.10.dev1.dist-info/WHEEL +4 -0
- tigrbl_engine_memlru-0.1.10.dev1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from threading import RLock
|
|
6
|
+
from time import monotonic
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class _Entry:
|
|
12
|
+
value: Any
|
|
13
|
+
cost: float
|
|
14
|
+
t: float
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LRUCache:
|
|
18
|
+
"""LRU cache with optional cost budget.
|
|
19
|
+
|
|
20
|
+
Eviction:
|
|
21
|
+
- Always enforces max_items.
|
|
22
|
+
- If max_cost set, evicts LRU until total_cost <= max_cost.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
max_items: int = 100_000,
|
|
29
|
+
max_cost: float | None = None,
|
|
30
|
+
default_cost: float = 1.0,
|
|
31
|
+
namespace: str = "default",
|
|
32
|
+
) -> None:
|
|
33
|
+
if max_items <= 0:
|
|
34
|
+
raise ValueError("max_items must be > 0")
|
|
35
|
+
if max_cost is not None and max_cost <= 0:
|
|
36
|
+
raise ValueError("max_cost must be > 0 if set")
|
|
37
|
+
if default_cost <= 0:
|
|
38
|
+
raise ValueError("default_cost must be > 0")
|
|
39
|
+
self.max_items = int(max_items)
|
|
40
|
+
self.max_cost = float(max_cost) if max_cost is not None else None
|
|
41
|
+
self.default_cost = float(default_cost)
|
|
42
|
+
self.namespace = namespace
|
|
43
|
+
|
|
44
|
+
self._lock = RLock()
|
|
45
|
+
self._od: OrderedDict[str, _Entry] = OrderedDict()
|
|
46
|
+
self._total_cost = 0.0
|
|
47
|
+
|
|
48
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
49
|
+
with self._lock:
|
|
50
|
+
e = self._od.get(key)
|
|
51
|
+
if e is None:
|
|
52
|
+
return default
|
|
53
|
+
self._od.move_to_end(key, last=True)
|
|
54
|
+
return e.value
|
|
55
|
+
|
|
56
|
+
def set(self, key: str, value: Any, *, cost: float | None = None) -> None:
|
|
57
|
+
if cost is None:
|
|
58
|
+
cost = self.default_cost
|
|
59
|
+
c = float(cost)
|
|
60
|
+
if c <= 0:
|
|
61
|
+
raise ValueError("cost must be > 0")
|
|
62
|
+
now = monotonic()
|
|
63
|
+
with self._lock:
|
|
64
|
+
old = self._od.pop(key, None)
|
|
65
|
+
if old is not None:
|
|
66
|
+
self._total_cost -= old.cost
|
|
67
|
+
self._od[key] = _Entry(value=value, cost=c, t=now)
|
|
68
|
+
self._od.move_to_end(key, last=True)
|
|
69
|
+
self._total_cost += c
|
|
70
|
+
self._evict()
|
|
71
|
+
|
|
72
|
+
def delete(self, key: str) -> bool:
|
|
73
|
+
with self._lock:
|
|
74
|
+
e = self._od.pop(key, None)
|
|
75
|
+
if e is None:
|
|
76
|
+
return False
|
|
77
|
+
self._total_cost -= e.cost
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
def clear(self) -> None:
|
|
81
|
+
with self._lock:
|
|
82
|
+
self._od.clear()
|
|
83
|
+
self._total_cost = 0.0
|
|
84
|
+
|
|
85
|
+
def stats(self) -> dict[str, Any]:
|
|
86
|
+
with self._lock:
|
|
87
|
+
return {
|
|
88
|
+
"size": len(self._od),
|
|
89
|
+
"max_items": self.max_items,
|
|
90
|
+
"total_cost": self._total_cost,
|
|
91
|
+
"max_cost": self.max_cost,
|
|
92
|
+
"default_cost": self.default_cost,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def _evict(self) -> None:
|
|
96
|
+
while len(self._od) > self.max_items:
|
|
97
|
+
k, e = self._od.popitem(last=False)
|
|
98
|
+
self._total_cost -= e.cost
|
|
99
|
+
if self.max_cost is not None:
|
|
100
|
+
while self._total_cost > self.max_cost and self._od:
|
|
101
|
+
k, e = self._od.popitem(last=False)
|
|
102
|
+
self._total_cost -= e.cost
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tigrbl.engine.registry import register_engine
|
|
4
|
+
|
|
5
|
+
from .lru import LRUCache
|
|
6
|
+
from .session import LRUSession, AsyncLRUSession
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register() -> None:
|
|
10
|
+
register_engine(kind="memlru", build=build_memlru, capabilities=capabilities)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def capabilities() -> dict:
|
|
14
|
+
return {
|
|
15
|
+
"engine": "memlru",
|
|
16
|
+
"transactional": False,
|
|
17
|
+
"async_native": True,
|
|
18
|
+
"persistence": "process",
|
|
19
|
+
"features": {"lru", "cost_based_optional"},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_memlru(*, mapping=None, spec=None, dsn=None, **_) -> tuple[object, object]:
|
|
24
|
+
mapping = dict(mapping or {})
|
|
25
|
+
async_ = bool(getattr(spec, "async_", False))
|
|
26
|
+
|
|
27
|
+
max_items = int(mapping.get("max_items", 100_000))
|
|
28
|
+
max_cost = float(mapping.get("max_cost", 0.0)) or None
|
|
29
|
+
default_cost = float(mapping.get("default_cost", 1.0))
|
|
30
|
+
namespace = str(mapping.get("namespace", "default"))
|
|
31
|
+
|
|
32
|
+
engine = LRUCache(max_items=max_items, max_cost=max_cost, default_cost=default_cost, namespace=namespace)
|
|
33
|
+
|
|
34
|
+
if async_:
|
|
35
|
+
def sessionmaker():
|
|
36
|
+
return AsyncLRUSession(engine)
|
|
37
|
+
else:
|
|
38
|
+
def sessionmaker():
|
|
39
|
+
return LRUSession(engine)
|
|
40
|
+
|
|
41
|
+
return engine, sessionmaker
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .lru import LRUCache
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LRUSession:
|
|
9
|
+
def __init__(self, engine: LRUCache) -> None:
|
|
10
|
+
self._engine = engine
|
|
11
|
+
self._closed = False
|
|
12
|
+
|
|
13
|
+
def close(self) -> None:
|
|
14
|
+
self._closed = True
|
|
15
|
+
|
|
16
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
17
|
+
self._require_open()
|
|
18
|
+
return self._engine.get(key, default)
|
|
19
|
+
|
|
20
|
+
def set(self, key: str, value: Any, *, cost: float | None = None) -> None:
|
|
21
|
+
self._require_open()
|
|
22
|
+
self._engine.set(key, value, cost=cost)
|
|
23
|
+
|
|
24
|
+
def delete(self, key: str) -> bool:
|
|
25
|
+
self._require_open()
|
|
26
|
+
return self._engine.delete(key)
|
|
27
|
+
|
|
28
|
+
def clear(self) -> None:
|
|
29
|
+
self._require_open()
|
|
30
|
+
self._engine.clear()
|
|
31
|
+
|
|
32
|
+
def stats(self) -> dict:
|
|
33
|
+
self._require_open()
|
|
34
|
+
return self._engine.stats()
|
|
35
|
+
|
|
36
|
+
def _require_open(self) -> None:
|
|
37
|
+
if self._closed:
|
|
38
|
+
raise RuntimeError("session is closed")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AsyncLRUSession(LRUSession):
|
|
42
|
+
async def close(self) -> None: # type: ignore[override]
|
|
43
|
+
super().close()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
tigrbl_engine_memlru/__init__.py,sha256=tXbRXsO0NE_UV1kIHiZTTQQH0fj0U2KoxxNusu_gzrM,48
|
|
2
|
+
tigrbl_engine_memlru/lru.py,sha256=CXVhMR9Aj2TrY3HIU2l0Y6mvIgSCSvycCM2oPBTmX2k,3097
|
|
3
|
+
tigrbl_engine_memlru/plugin.py,sha256=Fj_iH3GPiqYKmrgKWsWNv7TZ9485_J3j7kWCLtVhrxM,1206
|
|
4
|
+
tigrbl_engine_memlru/session.py,sha256=rOjMIiwcHsDx0Gpd3BoXIuPaSjL8jFEf0kGQWEFCVkk,1093
|
|
5
|
+
tigrbl_engine_memlru-0.1.10.dev1.dist-info/METADATA,sha256=OsvJfWbX6IBbh-NjXdLNZhHGv7lZogs4MXPjC0tD2ts,122
|
|
6
|
+
tigrbl_engine_memlru-0.1.10.dev1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
tigrbl_engine_memlru-0.1.10.dev1.dist-info/entry_points.txt,sha256=QhuLp8RxZvrSi9HoLKbMmy2mzwiQBpGyyArDf60LPsQ,62
|
|
8
|
+
tigrbl_engine_memlru-0.1.10.dev1.dist-info/RECORD,,
|