asyncactor 0.24.0__tar.gz → 0.25.1__tar.gz

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.
Files changed (56) hide show
  1. {asyncactor-0.24.0 → asyncactor-0.25.1}/PKG-INFO +30 -9
  2. asyncactor-0.25.1/asyncactor/__init__.py +15 -0
  3. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/abc.py +30 -16
  4. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/actor.py +77 -80
  5. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/backend/__init__.py +8 -0
  6. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/backend/moat_kv.py +30 -20
  7. asyncactor-0.25.1/asyncactor/backend/moat_link.py +77 -0
  8. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/backend/mqtt.py +30 -20
  9. asyncactor-0.25.1/asyncactor/backend/mqttproto.py +78 -0
  10. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/backend/serf.py +10 -12
  11. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/events.py +61 -51
  12. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/exceptions.py +7 -6
  13. asyncactor-0.25.1/asyncactor/messages.py +175 -0
  14. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor/nodelist.py +11 -7
  15. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor.egg-info/PKG-INFO +30 -9
  16. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor.egg-info/SOURCES.txt +4 -3
  17. asyncactor-0.25.1/asyncactor.egg-info/requires.txt +20 -0
  18. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor.egg-info/top_level.txt +0 -1
  19. {asyncactor-0.24.0 → asyncactor-0.25.1}/docs/source/conf.py +34 -28
  20. {asyncactor-0.24.0 → asyncactor-0.25.1}/docs/source/index.rst +1 -3
  21. asyncactor-0.25.1/pyproject.toml +123 -0
  22. asyncactor-0.25.1/setup.cfg +4 -0
  23. {asyncactor-0.24.0 → asyncactor-0.25.1}/tests/__init__.py +4 -1
  24. {asyncactor-0.24.0 → asyncactor-0.25.1}/tests/mock_serf.py +12 -9
  25. {asyncactor-0.24.0 → asyncactor-0.25.1}/tests/test_actor.py +11 -8
  26. {asyncactor-0.24.0 → asyncactor-0.25.1}/tests/test_mosquitto.py +8 -5
  27. asyncactor-0.25.1/tests/test_mqttproto.py +130 -0
  28. {asyncactor-0.24.0 → asyncactor-0.25.1}/tests/test_serf.py +7 -5
  29. asyncactor-0.24.0/asyncactor/__init__.py +0 -9
  30. asyncactor-0.24.0/asyncactor/backend/distkv.py +0 -55
  31. asyncactor-0.24.0/asyncactor/messages.py +0 -143
  32. asyncactor-0.24.0/asyncactor.egg-info/requires.txt +0 -8
  33. asyncactor-0.24.0/setup.cfg +0 -18
  34. asyncactor-0.24.0/setup.py +0 -51
  35. {asyncactor-0.24.0 → asyncactor-0.25.1}/.coveragerc +0 -0
  36. {asyncactor-0.24.0 → asyncactor-0.25.1}/.gitignore +0 -0
  37. {asyncactor-0.24.0 → asyncactor-0.25.1}/.pylintrc +0 -0
  38. {asyncactor-0.24.0 → asyncactor-0.25.1}/.readthedocs.yml +0 -0
  39. {asyncactor-0.24.0 → asyncactor-0.25.1}/.style.yapf +0 -0
  40. {asyncactor-0.24.0 → asyncactor-0.25.1}/.travis.yml +0 -0
  41. {asyncactor-0.24.0 → asyncactor-0.25.1}/CHANGELOG.rst +0 -0
  42. {asyncactor-0.24.0 → asyncactor-0.25.1}/LICENSE +0 -0
  43. {asyncactor-0.24.0 → asyncactor-0.25.1}/Makefile +0 -0
  44. {asyncactor-0.24.0 → asyncactor-0.25.1}/README.rst +0 -0
  45. {asyncactor-0.24.0 → asyncactor-0.25.1}/asyncactor.egg-info/dependency_links.txt +0 -0
  46. {asyncactor-0.24.0 → asyncactor-0.25.1}/ci/rtd-requirements.txt +0 -0
  47. {asyncactor-0.24.0 → asyncactor-0.25.1}/ci/test-requirements.txt +0 -0
  48. {asyncactor-0.24.0 → asyncactor-0.25.1}/ci/travis.sh +0 -0
  49. {asyncactor-0.24.0 → asyncactor-0.25.1}/docs/Makefile +0 -0
  50. {asyncactor-0.24.0 → asyncactor-0.25.1}/docs/make.bat +0 -0
  51. {asyncactor-0.24.0 → asyncactor-0.25.1}/docs/source/_static/.git_empty +0 -0
  52. {asyncactor-0.24.0 → asyncactor-0.25.1}/docs/source/actor.rst +0 -0
  53. {asyncactor-0.24.0 → asyncactor-0.25.1}/docs/source/history.rst +0 -0
  54. {asyncactor-0.24.0 → asyncactor-0.25.1}/docs/source/rationale.rst +0 -0
  55. {asyncactor-0.24.0 → asyncactor-0.25.1}/mktag +0 -0
  56. {asyncactor-0.24.0 → asyncactor-0.25.1}/tox.ini +0 -0
@@ -1,16 +1,37 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: asyncactor
3
- Version: 0.24.0
3
+ Version: 0.25.1
4
4
  Summary: Async decentralized actor
5
- Home-page: https://github.com/smurfix/asyncactor
6
- Author: Matthias Urlichs
7
- Author-email: matthias@urlichs.de
8
- Maintainer: Matthias Urlichs
9
- Maintainer-email: matthias@urlichs.de
10
- License: GPL3
11
- Keywords: Serf,MQTT,orchestration,service discovery,anyio
12
- Requires-Python: >=3.7
5
+ Author-email: Matthias Urlichs <matthias@urlichs.de>
6
+ Project-URL: homepage, https://github.com/smurfix/asyncactor
7
+ Project-URL: repository, https://github.com/smurfix/asyncactor.git
8
+ Keywords: MQTT,orchestration,service discovery,anyio
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Framework :: Trio
13
+ Classifier: Framework :: AnyIO
14
+ Classifier: License :: OSI Approved
15
+ Classifier: Development Status :: 4 - Beta
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/x-rst
13
18
  License-File: LICENSE
19
+ Requires-Dist: anyio~=4.2
20
+ Requires-Dist: moat-lib-codec
21
+ Requires-Dist: moat-util~=0.53
22
+ Provides-Extra: moat-kv
23
+ Requires-Dist: moat-kv; extra == "moat-kv"
24
+ Provides-Extra: moat-link
25
+ Requires-Dist: moat-link; extra == "moat-link"
26
+ Provides-Extra: mqtt
27
+ Requires-Dist: mqttproto; extra == "mqtt"
28
+ Provides-Extra: dev
29
+ Requires-Dist: moat-mqtt; extra == "dev"
30
+ Requires-Dist: mqttproto; extra == "dev"
31
+ Requires-Dist: pytest-cov>=2.3; extra == "dev"
32
+ Requires-Dist: pytest-trio; extra == "dev"
33
+ Requires-Dist: pytest>=2.5.2; extra == "dev"
34
+ Requires-Dist: trio>=0.23; extra == "dev"
14
35
 
15
36
  asyncactor
16
37
  ==========
@@ -0,0 +1,15 @@
1
+ """
2
+ AsyncActor: decentral somewhat-round-robin next-responsible-node selection
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from .actor import * # noqa:F403
8
+ from .actor import __all__ as _actor_all
9
+ from .backend import get_transport
10
+ from .events import * # noqa:F403
11
+ from .events import __all__ as _events_all
12
+ from .nodelist import * # noqa:F403
13
+ from .nodelist import __all__ as _nodelist_all
14
+
15
+ __all__ = _actor_all + _events_all + _nodelist_all + ["get_transport"] # noqa:PLE0605
@@ -1,13 +1,28 @@
1
- #
2
- # Abstract classes for the underlying transport
3
- #
1
+ """
2
+ Abstract classes for the underlying transport
3
+ """
4
+
5
+ from __future__ import annotations
4
6
 
5
7
  from abc import ABCMeta, abstractmethod
6
8
 
7
- from typing import Dict, Union, Tuple, List
9
+ import anyio
10
+
11
+ from typing import TYPE_CHECKING, Union
12
+
13
+ if TYPE_CHECKING:
14
+ from typing import Self
8
15
 
9
- Packable = Union[
10
- str, bytes, type(None), int, float, Dict[str, "Packable"], Tuple["Packable"], List["Packable"]
16
+ Packable = Union[ # noqa:UP007
17
+ str,
18
+ bytes,
19
+ bool,
20
+ type(None),
21
+ int,
22
+ float,
23
+ dict[str | int, "Packable"],
24
+ tuple["Packable"],
25
+ list["Packable"],
11
26
  ]
12
27
 
13
28
 
@@ -21,13 +36,20 @@ class Transport(metaclass=ABCMeta):
21
36
  """
22
37
 
23
38
  @abstractmethod
24
- def monitor(self) -> "MonitorStream":
39
+ def monitor(self) -> MonitorStream:
25
40
  """Return a MonitorStream that async-iterates payloads sent to this channel"""
26
41
 
27
42
  @abstractmethod
28
43
  async def send(self, payload: Packable):
29
44
  """send this payload to this channel"""
30
45
 
46
+ async def receiver(self, *, task_status=anyio.TASK_STATUS_IGNORED):
47
+ """
48
+ A dummy receiver which the transport may override,
49
+ otherwise it's the client's job
50
+ """
51
+ task_status.started()
52
+
31
53
 
32
54
  class MonitorStream(metaclass=ABCMeta):
33
55
  """Async context manager and iterator that attaches to a channel and
@@ -37,15 +59,7 @@ class MonitorStream(metaclass=ABCMeta):
37
59
  def __init__(self, transport: Transport):
38
60
  self.transport = transport
39
61
 
40
- @abstractmethod
41
- async def __aenter__(self):
42
- return self
43
-
44
- @abstractmethod
45
- async def __aexit__(self, *tb):
46
- pass
47
-
48
- def __aiter__(self):
62
+ def __aiter__(self) -> Self:
49
63
  return self
50
64
 
51
65
  @abstractmethod
@@ -1,22 +1,41 @@
1
+ """
2
+ The main Actor module.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
1
7
  import anyio
8
+ import logging
9
+ import os
2
10
  import time
3
11
  from random import Random
4
- import os
5
- import logging
6
- from contextlib import asynccontextmanager
7
12
 
8
- from .abc import Transport
9
- from .exceptions import ActorTimeoutError, ActorCollisionError
10
- from .events import * # pylint: disable=wildcard-import,unused-wildcard-import
11
- from .nodelist import * # pylint: disable=wildcard-import,unused-wildcard-import
12
- from .messages import * # pylint: disable=wildcard-import,unused-wildcard-import
13
+ from moat.util import CtxObj, create_queue
13
14
 
14
- from moat.util import create_queue
15
+ from .events import (
16
+ DetagEvent,
17
+ GoodNodeEvent,
18
+ PingEvent,
19
+ RawMsgEvent,
20
+ RecoverEvent,
21
+ SetupEvent,
22
+ TagEvent,
23
+ UntagEvent,
24
+ )
25
+ from .exceptions import ActorCollisionError, ActorTimeoutError
26
+ from .messages import HistoryMessage, InitMessage, Message, PingMessage, SetupMessage
27
+ from .nodelist import NodeList
28
+
29
+ from typing import TYPE_CHECKING
30
+
31
+ if TYPE_CHECKING:
32
+ from .abc import Transport
15
33
 
16
34
  __all__ = [
17
35
  "Actor",
18
36
  ]
19
37
 
38
+
20
39
  async def spawn(taskgroup, proc, *args, **kw):
21
40
  """
22
41
  Run a task within this object's task group.
@@ -36,7 +55,7 @@ async def spawn(taskgroup, proc, *args, **kw):
36
55
  return await taskgroup.start(_run, proc, args, kw)
37
56
 
38
57
 
39
- class Actor:
58
+ class Actor(CtxObj):
40
59
  """
41
60
  Some jobs need a single controller so that tasks don't step on each other's
42
61
  toes.
@@ -57,7 +76,7 @@ class Actor:
57
76
  name (str): This node's name. **Must** be unambiguous.
58
77
  cfg (dict): a dict containing additional configuration values.
59
78
  enabled (bool): if False, start the actor in passive monitoring mode.
60
- send_raw (bool): flag whether to send raw-message events (debugging)
79
+ send_raw (bool): flag whether to forward raw-message events (debugging)
61
80
 
62
81
  The config dict may contain these values.
63
82
 
@@ -92,28 +111,21 @@ class Actor:
92
111
  _tg = None
93
112
  _ae = None
94
113
 
95
- DEFAULTS = dict(cycle=10, gap=1.5, nodes=5, splits=4, n_hosts=10, version=0, force_in=0)
96
-
97
114
  def __init__( # pylint: disable=dangerous-default-value
98
115
  self,
99
116
  client: Transport,
100
117
  name: str,
101
- cfg: dict = {},
118
+ cfg: dict | None = None,
102
119
  *,
103
120
  enabled: bool = True,
104
121
  send_raw: bool = False,
105
122
  ):
106
123
  self._client = client
107
124
  self._name = name
108
- self.logger = logging.getLogger("asyncactor.%s.%s" % (self._name, client.topic[-1]))
109
-
110
- self._cfg = {}
111
- self._cfg.update(self.DEFAULTS)
112
- self._cfg.update(cfg)
125
+ self.logger = logging.getLogger(f"asyncactor.{self._name}.{client.topic[-1]}")
113
126
 
114
- self._version = SetupMessage(node=self._name, **self._cfg)
127
+ self._version = SetupMessage(node=self._name, **(cfg or {}))
115
128
  self._version_job = None
116
- self._version.verify()
117
129
  self._off_nodes = self._version.nodes
118
130
  self._self_seen = False
119
131
 
@@ -128,47 +140,47 @@ class Actor:
128
140
  self._values = {} # map names to steps
129
141
  self._history = NodeList(self._nodes) # those in the loop
130
142
  self._prev_history = None
131
- rs = os.environ.get("PYTHONHASHSEED", None)
132
- if rs is None:
133
- self._rand = Random()
134
- else:
135
- import trio._core._run as tcr
136
-
137
- self._rand = tcr._r
143
+ self._rand = Random() # noqa:S311
138
144
 
139
145
  self._next_ping_time = 0
140
146
  self._recover_pings = {}
141
147
  self._count_not_on = 0
142
148
 
143
149
  def __repr__(self):
144
- return "<Actor %s %r>" % (self._name, self._client)
150
+ return f"<Actor {self._name} {self._client!r}>"
145
151
 
146
152
  # Accessors for config values
147
153
 
148
154
  @property
149
- def _cycle(self):
155
+ def _cycle(self) -> float:
156
+ "'cycle' from config"
150
157
  return self._version.cycle
151
158
 
152
159
  @property
153
- def _gap(self):
160
+ def _gap(self) -> float:
161
+ "'gap' from config"
154
162
  return self._version.gap
155
163
 
156
164
  @property
157
- def _nodes(self):
165
+ def _nodes(self) -> int:
166
+ "'nodes' from config"
158
167
  if self._tagged < 0:
159
168
  return self._off_nodes
160
169
  return self._version.nodes
161
170
 
162
171
  @property
163
- def _splits(self):
172
+ def _splits(self) -> int:
173
+ "'splits' from config"
164
174
  return self._version.splits
165
175
 
166
176
  @property
167
- def _n_hosts(self):
177
+ def _n_hosts(self) -> int:
178
+ "'n_hosts' from config"
168
179
  return self._version.n_hosts
169
180
 
170
181
  @property
171
- def _force_in(self):
182
+ def _force_in(self) -> bool:
183
+ "'force_in' from config"
172
184
  return self._version.force_in
173
185
 
174
186
  @property
@@ -178,10 +190,12 @@ class Actor:
178
190
 
179
191
  @property
180
192
  def name(self):
193
+ "my name"
181
194
  return self._name
182
195
 
183
196
  @property
184
197
  def cycle_time(self):
198
+ "'cycle' from config"
185
199
  return self._cycle
186
200
 
187
201
  @property
@@ -240,30 +254,20 @@ class Actor:
240
254
  self._valid_pings += 1
241
255
  await self.post_event(PingEvent(msg))
242
256
 
243
- def __aenter__(self):
244
- @asynccontextmanager
245
- async def work(self):
246
- if self._worker is not None or self._reader is not None:
247
- raise RuntimeError("You can't enter me twice")
248
- async with anyio.create_task_group() as tg:
249
- try:
250
- evt = anyio.Event()
251
- self._tg = tg
252
- self._reader = await spawn(tg, self.read_task, evt)
253
- await evt.wait()
254
- self._worker = await spawn(tg, self._run)
255
- self._pinger = await spawn(tg, self._ping)
256
- yield self
257
- finally:
258
- with anyio.fail_after(2, shield=True):
259
- self._tg = None
260
- tg.cancel_scope.cancel()
261
-
262
- self._ae = work(self)
263
- return self._ae.__aenter__() # pylint: disable=E1101
264
-
265
- def __aexit__(self, *tb):
266
- return self._ae.__aexit__(*tb) # pylint: disable=E1101
257
+ async def _ctx(self):
258
+ async with anyio.create_task_group() as tg:
259
+ try:
260
+ evt = anyio.Event()
261
+ self._tg = tg
262
+ self._reader = await spawn(tg, self.read_task, evt)
263
+
264
+ await evt.wait()
265
+ self._worker = await spawn(tg, self._run)
266
+ self._pinger = await spawn(tg, self._ping)
267
+ yield self
268
+ finally:
269
+ self._tg = None
270
+ tg.cancel_scope.cancel()
267
271
 
268
272
  def __aiter__(self):
269
273
  return self
@@ -309,24 +313,23 @@ class Actor:
309
313
  """
310
314
  if self._send_raw:
311
315
  await self.post_event(RawMsgEvent(msg))
312
- msg = Message(**msg)
316
+ msg = Message.read(msg)
313
317
  if type(msg) is InitMessage and msg.node == self._name:
314
318
  if self._self_seen:
315
319
  # We may see our own Hello only once
316
- raise ActorCollisionError()
320
+ raise ActorCollisionError
317
321
  self._self_seen = True
318
322
 
319
323
  await self._rdr_q.put(msg)
320
324
 
321
325
  async def _run(self):
322
- """
323
- """
326
+ """ """
324
327
  await self._send_msg(InitMessage(node=self._name))
325
328
  await self._send_msg(self._version)
326
329
 
327
330
  await anyio.sleep((self.random / 2 + 1.5) * self._gap + self._cycle)
328
331
  if not self._self_seen:
329
- raise ActorTimeoutError()
332
+ raise ActorTimeoutError
330
333
  await self._send_ping()
331
334
 
332
335
  t_dest = 0
@@ -390,13 +393,9 @@ class Actor:
390
393
 
391
394
  It is an error to not supersede the old config.
392
395
  """
393
- self._cfg = {}
394
- self._cfg.update(self.DEFAULTS)
395
- self._cfg.update(cfg)
396
-
397
- v = SetupMessage(**self._cfg)
396
+ v = SetupMessage(**cfg)
398
397
  if v.version <= self._version.version:
399
- raise ValueError("You need a version > %s" % (self._version.version,))
398
+ raise ValueError(f"You need a version > {self._version.version}")
400
399
  self._version = v
401
400
  await self._send_msg(v)
402
401
 
@@ -407,11 +406,10 @@ class Actor:
407
406
  Args:
408
407
  length (int): New max length of the history. Default: Leave alone.
409
408
  """
410
- if length is not None:
411
- if self._version.nodes < length:
412
- self._version.nodes = length
413
- self._version.version += 1
414
- await self._send_msg(self._version)
409
+ if length is not None and self._version.nodes < length:
410
+ self._version.nodes = length
411
+ self._version.version += 1
412
+ await self._send_msg(self._version)
415
413
 
416
414
  if self._tagged != -1:
417
415
  return
@@ -657,7 +655,7 @@ class Actor:
657
655
  history = self._prev_history = self._history
658
656
  self._history += self._name
659
657
  self._get_next_ping_time()
660
- msg.history = history[0 : self._splits] # noqa: E203
658
+ msg.history = history[0 : self._splits]
661
659
  await self._send_msg(msg)
662
660
 
663
661
  async def _send_msg(self, msg: Message):
@@ -753,10 +751,9 @@ class Actor:
753
751
  lv += 1
754
752
  if self._nodes > 0 and lv > self._nodes:
755
753
  break
756
- if not self._ready:
757
- if p > lv // 2:
758
- # No, the first active host is too far back.
759
- return 2 + self.random / 3
754
+ if not self._ready and p > lv // 2:
755
+ # No, the first active host is too far back.
756
+ return 2 + self.random / 3
760
757
 
761
758
  return self.ping_delay(
762
759
  s - 1,
@@ -1,4 +1,12 @@
1
+ """
2
+ Select a transport backend for asyncactor
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+
1
8
  def get_transport(name):
9
+ "import the named transport class"
2
10
  from importlib import import_module
3
11
 
4
12
  if "." not in name:
@@ -1,42 +1,52 @@
1
- #
2
- # Listener on top of a MoaT-KV transport
3
- #
4
- from asyncactor.abc import Transport, MonitorStream
1
+ """
2
+ Listener on top of a MoaT-KV transport
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from asyncactor.abc import MonitorStream, Transport
8
+
9
+ from moat.util import CtxObj
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import AsyncIterator
15
+
16
+ from typing import Self
17
+
5
18
  import msgpack
6
19
 
7
20
 
8
21
  class MoatKVTransport(Transport):
22
+ "Transport"
23
+
9
24
  def __init__(self, conn, *topic):
10
25
  self.conn = conn
11
26
  self.topic = topic
12
27
 
13
28
  def monitor(self):
29
+ "monitor"
14
30
  return MoatKVMonitor(self)
15
31
 
16
32
  async def send(self, payload):
33
+ "send message"
17
34
  payload = msgpack.packb(payload, use_bin_type=True)
18
35
  await self.conn.send(*self.topic, payload=payload)
19
36
 
20
37
  def __repr__(self):
21
- return "<Moat-KV:%s %r>" % (self.topic, self.conn)
38
+ return f"<Moat-KV:{self.topic} {self.conn!r}>"
22
39
 
23
40
 
24
- class MoatKVMonitor(MonitorStream):
25
- _mon1 = None
26
- _mon2 = None
27
- _it = None
41
+ class MoatKVMonitor(MonitorStream, CtxObj):
42
+ "Monitor"
28
43
 
29
- async def __aenter__(self):
30
- self._mon1 = self.transport.conn.monitor(*self.transport.topic)
31
- if hasattr(self._mon1, "__aenter__"):
32
- self._mon2 = await self._mon1.__aenter__()
33
- else:
34
- self._mon2 = self._mon1
35
- return self
44
+ _mon = None
45
+ _it = None
36
46
 
37
- async def __aexit__(self, *tb):
38
- if hasattr(self._mon1, "__aexit__"):
39
- return await self._mon1.__aexit__(*tb)
47
+ async def _ctx(self) -> AsyncIterator[Self]:
48
+ async with self.transport.conn.monitor(*self.transport.topic) as self._mon:
49
+ yield self
40
50
 
41
51
  def __aiter__(self):
42
52
  self._it = self._mon2.__aiter__()
@@ -48,7 +58,7 @@ class MoatKVMonitor(MonitorStream):
48
58
  return msg
49
59
 
50
60
  def __repr__(self):
51
- return "<Mon:%r>" % (self.transport,)
61
+ return f"<Mon:{self.transport}>"
52
62
 
53
63
 
54
64
  Transport = MoatKVTransport
@@ -0,0 +1,77 @@
1
+ """
2
+ Listener on top of an async mqttproto connection
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from asyncactor.abc import MonitorStream, Transport
8
+
9
+ from mqttproto import QoS
10
+
11
+ from moat.util import CtxObj, Path
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import AsyncIterator, Awaitable
17
+
18
+ from moat.lib.codec import Codec
19
+ from moat.link.client import Link
20
+
21
+ from typing import Self
22
+
23
+
24
+ class MQTTPTransport(Transport):
25
+ """
26
+ MQTTProto Transport for AsyncActor.
27
+ """
28
+
29
+ def __init__(self, conn: Link, topic: Path, codec: Codec | str = None):
30
+ self.conn = conn
31
+ self.topic = topic
32
+ self.codec = codec
33
+
34
+ def monitor(self):
35
+ """
36
+ Accept incoming messages::
37
+
38
+ async with conn.monitor() as m:
39
+ async for msg in m:
40
+ await process(msg)
41
+ """
42
+ return MQTTMonitor(self)
43
+
44
+ def send(self, payload) -> Awaitable[None]:
45
+ """
46
+ Send a message.
47
+ """
48
+ return self.conn.send(self.topic, payload, qos=QoS.AT_LEAST_ONCE, codec=self.codec)
49
+
50
+ def __repr__(self):
51
+ return f"<MQTT:[self.tag] @{self.conn!r}>"
52
+
53
+
54
+ class MQTTMonitor(MonitorStream, CtxObj):
55
+ "Monitor"
56
+
57
+ async def _ctx(self) -> AsyncIterator[Self]:
58
+ c = self.transport
59
+ async with c.conn.monitor(c.tag, maximum_qos=QoS.AT_LEAST_ONCE, codec=self.codec) as mon:
60
+ self._it = aiter(mon)
61
+ try:
62
+ yield self
63
+ finally:
64
+ self._it = None
65
+
66
+ def __aiter__(self):
67
+ return self
68
+
69
+ async def __anext__(self):
70
+ msg = (await anext(self._it)).data
71
+ return msg
72
+
73
+ def __repr__(self):
74
+ return f"<Mon:{self.transport}>"
75
+
76
+
77
+ Transport = MQTTPTransport