tigrbl_engine_mempubsub 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,40 @@
1
+ from __future__ import annotations
2
+
3
+ from tigrbl.engine.registry import register_engine
4
+
5
+ from .pubsub import PubSubHub
6
+ from .session import PubSubSession, AsyncPubSubSession
7
+
8
+
9
+ def register() -> None:
10
+ register_engine(kind="mempubsub", build=build_mempubsub, capabilities=capabilities)
11
+
12
+
13
+ def capabilities() -> dict:
14
+ return {
15
+ "engine": "mempubsub",
16
+ "transactional": False,
17
+ "async_native": True,
18
+ "persistence": "process",
19
+ "features": {"topics", "fanout", "subscriptions"},
20
+ }
21
+
22
+
23
+ def build_mempubsub(*, mapping=None, spec=None, dsn=None, **_) -> tuple[object, object]:
24
+ mapping = dict(mapping or {})
25
+ async_ = bool(getattr(spec, "async_", False))
26
+
27
+ max_backlog = int(mapping.get("max_backlog_per_sub", 1024))
28
+ drop_policy = str(mapping.get("drop_policy", "drop_old")).lower()
29
+ namespace = str(mapping.get("namespace", "default"))
30
+
31
+ engine = PubSubHub(max_backlog_per_sub=max_backlog, drop_policy=drop_policy, namespace=namespace)
32
+
33
+ if async_:
34
+ def sessionmaker():
35
+ return AsyncPubSubSession(engine)
36
+ else:
37
+ def sessionmaker():
38
+ return PubSubSession(engine)
39
+
40
+ return engine, sessionmaker
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import dataclass
5
+ from threading import Condition, RLock
6
+ from time import monotonic
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class _Sub:
12
+ lock: RLock
13
+ cv: Condition
14
+ dq: deque[Any]
15
+ closed: bool = False
16
+
17
+
18
+ class Subscription:
19
+ def __init__(self, hub: "PubSubHub", topic: str, sub: _Sub) -> None:
20
+ self._hub = hub
21
+ self._topic = topic
22
+ self._sub = sub
23
+ self._closed = False
24
+
25
+ def recv(self, timeout: float | None = None) -> Any | None:
26
+ if self._closed:
27
+ return None
28
+ deadline = None if timeout is None else (monotonic() + max(0.0, float(timeout)))
29
+ s = self._sub
30
+ with s.lock:
31
+ while not s.closed and not s.dq:
32
+ if deadline is None:
33
+ s.cv.wait()
34
+ else:
35
+ remain = deadline - monotonic()
36
+ if remain <= 0:
37
+ return None
38
+ s.cv.wait(remain)
39
+ if not s.dq:
40
+ return None
41
+ return s.dq.popleft()
42
+
43
+ def close(self) -> None:
44
+ if self._closed:
45
+ return
46
+ self._closed = True
47
+ self._hub._unsubscribe(self._topic, self._sub)
48
+
49
+ @property
50
+ def topic(self) -> str:
51
+ return self._topic
52
+
53
+
54
+ class PubSubHub:
55
+ """In-process pub/sub with fanout to per-subscriber queues."""
56
+
57
+ def __init__(self, *, max_backlog_per_sub: int = 1024, drop_policy: str = "drop_old", namespace: str = "default") -> None:
58
+ if max_backlog_per_sub <= 0:
59
+ raise ValueError("max_backlog_per_sub must be > 0")
60
+ drop_policy = (drop_policy or "drop_old").lower()
61
+ if drop_policy not in {"drop_old", "drop_new", "block"}:
62
+ raise ValueError("drop_policy must be drop_old|drop_new|block")
63
+ self.max_backlog_per_sub = int(max_backlog_per_sub)
64
+ self.drop_policy = drop_policy
65
+ self.namespace = namespace
66
+
67
+ self._lock = RLock()
68
+ self._topics: dict[str, set[_Sub]] = {}
69
+
70
+ def subscribe(self, topic: str) -> Subscription:
71
+ t = topic or "default"
72
+ sub_lock = RLock()
73
+ sub = _Sub(lock=sub_lock, cv=Condition(sub_lock), dq=deque())
74
+ with self._lock:
75
+ self._topics.setdefault(t, set()).add(sub)
76
+ return Subscription(self, t, sub)
77
+
78
+ def _unsubscribe(self, topic: str, sub: _Sub) -> None:
79
+ with self._lock:
80
+ s = self._topics.get(topic)
81
+ if not s:
82
+ return
83
+ s.discard(sub)
84
+ if not s:
85
+ self._topics.pop(topic, None)
86
+ with sub.lock:
87
+ sub.closed = True
88
+ sub.cv.notify_all()
89
+
90
+ def publish(self, topic: str, msg: Any, *, timeout: float | None = None) -> int:
91
+ t = topic or "default"
92
+ deadline = None if timeout is None else (monotonic() + max(0.0, float(timeout)))
93
+ with self._lock:
94
+ subs = list(self._topics.get(t, ()))
95
+ delivered = 0
96
+ for sub in subs:
97
+ with sub.lock:
98
+ if sub.closed:
99
+ continue
100
+ if len(sub.dq) >= self.max_backlog_per_sub:
101
+ if self.drop_policy == "drop_new":
102
+ continue
103
+ if self.drop_policy == "drop_old":
104
+ sub.dq.popleft()
105
+ else: # block
106
+ while not sub.closed and len(sub.dq) >= self.max_backlog_per_sub:
107
+ if deadline is None:
108
+ sub.cv.wait()
109
+ else:
110
+ remain = deadline - monotonic()
111
+ if remain <= 0:
112
+ break
113
+ sub.cv.wait(remain)
114
+ if sub.closed or len(sub.dq) >= self.max_backlog_per_sub:
115
+ continue
116
+ sub.dq.append(msg)
117
+ sub.cv.notify()
118
+ delivered += 1
119
+ return delivered
120
+
121
+ def topics(self) -> list[str]:
122
+ with self._lock:
123
+ return sorted(self._topics.keys())
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .pubsub import PubSubHub, Subscription
6
+
7
+
8
+ class PubSubSession:
9
+ def __init__(self, engine: PubSubHub) -> None:
10
+ self._engine = engine
11
+ self._closed = False
12
+
13
+ def close(self) -> None:
14
+ self._closed = True
15
+
16
+ def publish(self, topic: str, msg: Any, *, timeout: float | None = None) -> int:
17
+ self._require_open()
18
+ return self._engine.publish(topic, msg, timeout=timeout)
19
+
20
+ def subscribe(self, topic: str) -> Subscription:
21
+ self._require_open()
22
+ return self._engine.subscribe(topic)
23
+
24
+ def topics(self) -> list[str]:
25
+ self._require_open()
26
+ return self._engine.topics()
27
+
28
+ def _require_open(self) -> None:
29
+ if self._closed:
30
+ raise RuntimeError("session is closed")
31
+
32
+
33
+ class AsyncPubSubSession(PubSubSession):
34
+ async def close(self) -> None: # type: ignore[override]
35
+ super().close()
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: tigrbl_engine_mempubsub
3
+ Version: 0.1.10.dev1
4
+ Requires-Python: <3.14,>=3.10
5
+ Requires-Dist: tigrbl
@@ -0,0 +1,8 @@
1
+ tigrbl_engine_mempubsub/__init__.py,sha256=tXbRXsO0NE_UV1kIHiZTTQQH0fj0U2KoxxNusu_gzrM,48
2
+ tigrbl_engine_mempubsub/plugin.py,sha256=0IVs0rAmTWzGneP2aBObjCSILoE5_uODocCWNK72J08,1194
3
+ tigrbl_engine_mempubsub/pubsub.py,sha256=w13HpE7iAJoez8snBYxaLBi3-Pa6dc2I01Iknu_0tsI,4198
4
+ tigrbl_engine_mempubsub/session.py,sha256=Au_CaDrUcX28rlXPMjgjWrU1ta9u9Mku03Jtu8PTaR4,948
5
+ tigrbl_engine_mempubsub-0.1.10.dev1.dist-info/METADATA,sha256=URYgujQbLfuaeweupdcIUIoyr0VFoUsdbZ7eRHIav7s,125
6
+ tigrbl_engine_mempubsub-0.1.10.dev1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ tigrbl_engine_mempubsub-0.1.10.dev1.dist-info/entry_points.txt,sha256=nHmKqFdhMk9NA9xQh6FlP-U5iw_kRoniEeidTTaTi74,68
8
+ tigrbl_engine_mempubsub-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
+ mempubsub = tigrbl_engine_mempubsub.plugin:register