tigrbl_engine_memkv 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.
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "0.1.0"
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import heapq
4
+ from dataclasses import dataclass
5
+ from threading import RLock
6
+ from time import monotonic
7
+ from typing import Any, Iterable
8
+
9
+
10
+ @dataclass(order=True)
11
+ class _Exp:
12
+ expires_at: float
13
+ key: str
14
+ version: int
15
+
16
+
17
+ @dataclass
18
+ class _Val:
19
+ version: int
20
+ value: Any
21
+ expires_at: float | None
22
+
23
+
24
+ class KVStore:
25
+ """Versioned in-memory KV with TTL and CAS.
26
+
27
+ Keyspace is namespaced at engine level.
28
+ TTL uses monotonic clock; cleanup is heap-based and amortized.
29
+ """
30
+
31
+ def __init__(self, *, namespace: str = "default", default_ttl_s: float | None = None, max_items: int = 1_000_000) -> None:
32
+ if max_items <= 0:
33
+ raise ValueError("max_items must be > 0")
34
+ if default_ttl_s is not None and default_ttl_s <= 0:
35
+ raise ValueError("default_ttl_s must be > 0 if set")
36
+ self.namespace = namespace
37
+ self.default_ttl_s = default_ttl_s
38
+ self.max_items = int(max_items)
39
+
40
+ self._lock = RLock()
41
+ self._m: dict[str, _Val] = {}
42
+ self._h: list[_Exp] = []
43
+
44
+ def _gc(self, now: float) -> None:
45
+ while self._h:
46
+ e = self._h[0]
47
+ if e.expires_at > now:
48
+ return
49
+ heapq.heappop(self._h)
50
+ cur = self._m.get(e.key)
51
+ if cur is None:
52
+ continue
53
+ # only evict if version matches and is actually expired
54
+ if cur.version == e.version and cur.expires_at is not None and cur.expires_at <= now:
55
+ self._m.pop(e.key, None)
56
+
57
+ def _set(self, key: str, value: Any, ttl_s: float | None) -> int:
58
+ now = monotonic()
59
+ with self._lock:
60
+ self._gc(now)
61
+ cur = self._m.get(key)
62
+ ver = 1 if cur is None else (cur.version + 1)
63
+ exp_at: float | None = None
64
+ if ttl_s is None:
65
+ ttl_s = self.default_ttl_s
66
+ if ttl_s is not None:
67
+ ttl = float(ttl_s)
68
+ if ttl <= 0:
69
+ raise ValueError("ttl_s must be > 0 if set")
70
+ exp_at = now + ttl
71
+ self._m[key] = _Val(version=ver, value=value, expires_at=exp_at)
72
+ if exp_at is not None:
73
+ heapq.heappush(self._h, _Exp(exp_at, key, ver))
74
+ self._enforce_max(now)
75
+ return ver
76
+
77
+ def get(self, key: str, default: Any = None) -> Any:
78
+ now = monotonic()
79
+ with self._lock:
80
+ self._gc(now)
81
+ v = self._m.get(key)
82
+ if v is None:
83
+ return default
84
+ if v.expires_at is not None and v.expires_at <= now:
85
+ self._m.pop(key, None)
86
+ return default
87
+ return v.value
88
+
89
+ def get_version(self, key: str) -> int | None:
90
+ now = monotonic()
91
+ with self._lock:
92
+ self._gc(now)
93
+ v = self._m.get(key)
94
+ if v is None:
95
+ return None
96
+ if v.expires_at is not None and v.expires_at <= now:
97
+ self._m.pop(key, None)
98
+ return None
99
+ return v.version
100
+
101
+ def set(self, key: str, value: Any, *, ttl_s: float | None = None) -> int:
102
+ return self._set(key, value, ttl_s)
103
+
104
+ def cas(self, key: str, expected_version: int, value: Any, *, ttl_s: float | None = None) -> int | None:
105
+ now = monotonic()
106
+ with self._lock:
107
+ self._gc(now)
108
+ cur = self._m.get(key)
109
+ if cur is None:
110
+ return None
111
+ if cur.expires_at is not None and cur.expires_at <= now:
112
+ self._m.pop(key, None)
113
+ return None
114
+ if cur.version != int(expected_version):
115
+ return None
116
+ # perform write outside of read lock? keep simple: do within lock
117
+ with self._lock:
118
+ self._gc(monotonic())
119
+ cur2 = self._m.get(key)
120
+ if cur2 is None or cur2.version != int(expected_version):
121
+ return None
122
+ return self._set(key, value, ttl_s)
123
+
124
+ def delete(self, key: str) -> bool:
125
+ now = monotonic()
126
+ with self._lock:
127
+ self._gc(now)
128
+ return self._m.pop(key, None) is not None
129
+
130
+ def keys(self, prefix: str = "") -> list[str]:
131
+ now = monotonic()
132
+ with self._lock:
133
+ self._gc(now)
134
+ if not prefix:
135
+ return sorted(self._m.keys())
136
+ return sorted(k for k in self._m.keys() if k.startswith(prefix))
137
+
138
+ def reset(self) -> None:
139
+ with self._lock:
140
+ self._m.clear()
141
+ self._h.clear()
142
+
143
+ def stats(self) -> dict[str, Any]:
144
+ now = monotonic()
145
+ with self._lock:
146
+ self._gc(now)
147
+ return {"size": len(self._m), "max_items": self.max_items, "default_ttl_s": self.default_ttl_s}
148
+
149
+ def _enforce_max(self, now: float) -> None:
150
+ # Evict arbitrary oldest-expiring entries to enforce cap
151
+ while len(self._m) > self.max_items and self._h:
152
+ e = heapq.heappop(self._h)
153
+ self._m.pop(e.key, None)
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from tigrbl.engine.registry import register_engine
4
+
5
+ from .kv import KVStore
6
+ from .session import KVSession, AsyncKVSession
7
+
8
+
9
+ def register() -> None:
10
+ register_engine(kind="memkv", build=build_memkv, capabilities=capabilities)
11
+
12
+
13
+ def capabilities() -> dict:
14
+ return {
15
+ "engine": "memkv",
16
+ "transactional": False,
17
+ "async_native": True,
18
+ "persistence": "process",
19
+ "features": {"kv", "ttl_optional", "cas", "versioned"},
20
+ }
21
+
22
+
23
+ def build_memkv(*, mapping=None, spec=None, dsn=None, **_) -> tuple[object, object]:
24
+ mapping = dict(mapping or {})
25
+ async_ = bool(getattr(spec, "async_", False))
26
+
27
+ namespace = str(mapping.get("namespace", "default"))
28
+ default_ttl_s = float(mapping.get("default_ttl_s", 0.0)) or None
29
+ max_items = int(mapping.get("max_items", 1_000_000))
30
+
31
+ engine = KVStore(namespace=namespace, default_ttl_s=default_ttl_s, max_items=max_items)
32
+
33
+ if async_:
34
+ def sessionmaker():
35
+ return AsyncKVSession(engine)
36
+ else:
37
+ def sessionmaker():
38
+ return KVSession(engine)
39
+
40
+ return engine, sessionmaker
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .kv import KVStore
6
+
7
+
8
+ class KVSession:
9
+ def __init__(self, engine: KVStore) -> 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 get_version(self, key: str) -> int | None:
21
+ self._require_open()
22
+ return self._engine.get_version(key)
23
+
24
+ def set(self, key: str, value: Any, *, ttl_s: float | None = None) -> int:
25
+ self._require_open()
26
+ return self._engine.set(key, value, ttl_s=ttl_s)
27
+
28
+ def cas(self, key: str, expected_version: int, value: Any, *, ttl_s: float | None = None) -> int | None:
29
+ self._require_open()
30
+ return self._engine.cas(key, expected_version, value, ttl_s=ttl_s)
31
+
32
+ def delete(self, key: str) -> bool:
33
+ self._require_open()
34
+ return self._engine.delete(key)
35
+
36
+ def keys(self, prefix: str = "") -> list[str]:
37
+ self._require_open()
38
+ return self._engine.keys(prefix)
39
+
40
+ def reset(self) -> None:
41
+ self._require_open()
42
+ self._engine.reset()
43
+
44
+ def stats(self) -> dict:
45
+ self._require_open()
46
+ return self._engine.stats()
47
+
48
+ def _require_open(self) -> None:
49
+ if self._closed:
50
+ raise RuntimeError("session is closed")
51
+
52
+
53
+ class AsyncKVSession(KVSession):
54
+ async def close(self) -> None: # type: ignore[override]
55
+ super().close()
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: tigrbl_engine_memkv
3
+ Version: 0.1.10.dev1
4
+ Requires-Python: <3.14,>=3.10
5
+ Requires-Dist: tigrbl
@@ -0,0 +1,8 @@
1
+ tigrbl_engine_memkv/__init__.py,sha256=tXbRXsO0NE_UV1kIHiZTTQQH0fj0U2KoxxNusu_gzrM,48
2
+ tigrbl_engine_memkv/kv.py,sha256=ZjkdH5x7ZribYG6aIuVAmI1joWEFmf6DZBzcaVCsIwc,5083
3
+ tigrbl_engine_memkv/plugin.py,sha256=4foQtxcFwsmKvFVTmDJITIS8rAc7w4Dd4akcbgYFjjk,1143
4
+ tigrbl_engine_memkv/session.py,sha256=4ztuZyHI2AA6CL3lKkCfvNOm8wc_qG6u7VDE87HK-Oc,1558
5
+ tigrbl_engine_memkv-0.1.10.dev1.dist-info/METADATA,sha256=xNKU14mI-1ji0izqt9dIh8Y3Jo1ipp5xUZhyq0beni0,121
6
+ tigrbl_engine_memkv-0.1.10.dev1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ tigrbl_engine_memkv-0.1.10.dev1.dist-info/entry_points.txt,sha256=gW5z9mC1ZuKPBD9OP0NNjf6tGz6tfO1YgRrRlykS1Ec,60
8
+ tigrbl_engine_memkv-0.1.10.dev1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [tigrbl.engine]
2
+ memkv = tigrbl_engine_memkv.plugin:register