asyncactor 0.24.0__tar.gz → 0.25.0__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.0}/PKG-INFO +29 -9
  2. asyncactor-0.25.0/asyncactor/__init__.py +15 -0
  3. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/abc.py +30 -16
  4. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/actor.py +78 -75
  5. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/backend/__init__.py +8 -0
  6. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/backend/moat_kv.py +30 -20
  7. asyncactor-0.25.0/asyncactor/backend/moat_link.py +77 -0
  8. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/backend/mqtt.py +30 -20
  9. asyncactor-0.25.0/asyncactor/backend/mqttproto.py +78 -0
  10. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/backend/serf.py +10 -12
  11. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/events.py +61 -51
  12. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/exceptions.py +7 -6
  13. asyncactor-0.25.0/asyncactor/messages.py +175 -0
  14. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor/nodelist.py +11 -7
  15. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor.egg-info/PKG-INFO +29 -9
  16. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor.egg-info/SOURCES.txt +4 -3
  17. asyncactor-0.25.0/asyncactor.egg-info/requires.txt +19 -0
  18. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor.egg-info/top_level.txt +0 -1
  19. {asyncactor-0.24.0 → asyncactor-0.25.0}/docs/source/conf.py +34 -28
  20. asyncactor-0.25.0/pyproject.toml +123 -0
  21. asyncactor-0.25.0/setup.cfg +4 -0
  22. {asyncactor-0.24.0 → asyncactor-0.25.0}/tests/__init__.py +4 -1
  23. {asyncactor-0.24.0 → asyncactor-0.25.0}/tests/mock_serf.py +12 -9
  24. {asyncactor-0.24.0 → asyncactor-0.25.0}/tests/test_actor.py +11 -8
  25. {asyncactor-0.24.0 → asyncactor-0.25.0}/tests/test_mosquitto.py +8 -5
  26. asyncactor-0.25.0/tests/test_mqttproto.py +130 -0
  27. {asyncactor-0.24.0 → asyncactor-0.25.0}/tests/test_serf.py +7 -5
  28. asyncactor-0.24.0/asyncactor/__init__.py +0 -9
  29. asyncactor-0.24.0/asyncactor/backend/distkv.py +0 -55
  30. asyncactor-0.24.0/asyncactor/messages.py +0 -143
  31. asyncactor-0.24.0/asyncactor.egg-info/requires.txt +0 -8
  32. asyncactor-0.24.0/setup.cfg +0 -18
  33. asyncactor-0.24.0/setup.py +0 -51
  34. {asyncactor-0.24.0 → asyncactor-0.25.0}/.coveragerc +0 -0
  35. {asyncactor-0.24.0 → asyncactor-0.25.0}/.gitignore +0 -0
  36. {asyncactor-0.24.0 → asyncactor-0.25.0}/.pylintrc +0 -0
  37. {asyncactor-0.24.0 → asyncactor-0.25.0}/.readthedocs.yml +0 -0
  38. {asyncactor-0.24.0 → asyncactor-0.25.0}/.style.yapf +0 -0
  39. {asyncactor-0.24.0 → asyncactor-0.25.0}/.travis.yml +0 -0
  40. {asyncactor-0.24.0 → asyncactor-0.25.0}/CHANGELOG.rst +0 -0
  41. {asyncactor-0.24.0 → asyncactor-0.25.0}/LICENSE +0 -0
  42. {asyncactor-0.24.0 → asyncactor-0.25.0}/Makefile +0 -0
  43. {asyncactor-0.24.0 → asyncactor-0.25.0}/README.rst +0 -0
  44. {asyncactor-0.24.0 → asyncactor-0.25.0}/asyncactor.egg-info/dependency_links.txt +0 -0
  45. {asyncactor-0.24.0 → asyncactor-0.25.0}/ci/rtd-requirements.txt +0 -0
  46. {asyncactor-0.24.0 → asyncactor-0.25.0}/ci/test-requirements.txt +0 -0
  47. {asyncactor-0.24.0 → asyncactor-0.25.0}/ci/travis.sh +0 -0
  48. {asyncactor-0.24.0 → asyncactor-0.25.0}/docs/Makefile +0 -0
  49. {asyncactor-0.24.0 → asyncactor-0.25.0}/docs/make.bat +0 -0
  50. {asyncactor-0.24.0 → asyncactor-0.25.0}/docs/source/_static/.git_empty +0 -0
  51. {asyncactor-0.24.0 → asyncactor-0.25.0}/docs/source/actor.rst +0 -0
  52. {asyncactor-0.24.0 → asyncactor-0.25.0}/docs/source/history.rst +0 -0
  53. {asyncactor-0.24.0 → asyncactor-0.25.0}/docs/source/index.rst +0 -0
  54. {asyncactor-0.24.0 → asyncactor-0.25.0}/docs/source/rationale.rst +0 -0
  55. {asyncactor-0.24.0 → asyncactor-0.25.0}/mktag +0 -0
  56. {asyncactor-0.24.0 → asyncactor-0.25.0}/tox.ini +0 -0
@@ -1,16 +1,36 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: asyncactor
3
- Version: 0.24.0
3
+ Version: 0.25.0
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>=2.5.2; extra == "dev"
33
+ Requires-Dist: trio>=0.23; extra == "dev"
14
34
 
15
35
  asyncactor
16
36
  ==========
@@ -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 trio
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=trio.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
14
+
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
13
30
 
14
- from moat.util import create_queue
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]))
125
+ self.logger = logging.getLogger(f"asyncactor.{self._name}.{client.topic[-1]}")
109
126
 
110
- self._cfg = {}
111
- self._cfg.update(self.DEFAULTS)
112
- self._cfg.update(cfg)
113
-
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
 
@@ -130,45 +142,51 @@ class Actor:
130
142
  self._prev_history = None
131
143
  rs = os.environ.get("PYTHONHASHSEED", None)
132
144
  if rs is None:
133
- self._rand = Random()
145
+ self._rand = Random() # noqa:S311
134
146
  else:
135
147
  import trio._core._run as tcr
136
148
 
137
- self._rand = tcr._r
149
+ self._rand = tcr._r # noqa:SLF001
138
150
 
139
151
  self._next_ping_time = 0
140
152
  self._recover_pings = {}
141
153
  self._count_not_on = 0
142
154
 
143
155
  def __repr__(self):
144
- return "<Actor %s %r>" % (self._name, self._client)
156
+ return f"<Actor {self._name} {self._client!r}>"
145
157
 
146
158
  # Accessors for config values
147
159
 
148
160
  @property
149
- def _cycle(self):
161
+ def _cycle(self) -> float:
162
+ "'cycle' from config"
150
163
  return self._version.cycle
151
164
 
152
165
  @property
153
- def _gap(self):
166
+ def _gap(self) -> float:
167
+ "'gap' from config"
154
168
  return self._version.gap
155
169
 
156
170
  @property
157
- def _nodes(self):
171
+ def _nodes(self) -> int:
172
+ "'nodes' from config"
158
173
  if self._tagged < 0:
159
174
  return self._off_nodes
160
175
  return self._version.nodes
161
176
 
162
177
  @property
163
- def _splits(self):
178
+ def _splits(self) -> int:
179
+ "'splits' from config"
164
180
  return self._version.splits
165
181
 
166
182
  @property
167
- def _n_hosts(self):
183
+ def _n_hosts(self) -> int:
184
+ "'n_hosts' from config"
168
185
  return self._version.n_hosts
169
186
 
170
187
  @property
171
- def _force_in(self):
188
+ def _force_in(self) -> bool:
189
+ "'force_in' from config"
172
190
  return self._version.force_in
173
191
 
174
192
  @property
@@ -178,10 +196,12 @@ class Actor:
178
196
 
179
197
  @property
180
198
  def name(self):
199
+ "my name"
181
200
  return self._name
182
201
 
183
202
  @property
184
203
  def cycle_time(self):
204
+ "'cycle' from config"
185
205
  return self._cycle
186
206
 
187
207
  @property
@@ -240,30 +260,20 @@ class Actor:
240
260
  self._valid_pings += 1
241
261
  await self.post_event(PingEvent(msg))
242
262
 
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
263
+ async def _ctx(self):
264
+ async with anyio.create_task_group() as tg:
265
+ try:
266
+ evt = anyio.Event()
267
+ self._tg = tg
268
+ self._reader = await spawn(tg, self.read_task, evt)
269
+
270
+ await evt.wait()
271
+ self._worker = await spawn(tg, self._run)
272
+ self._pinger = await spawn(tg, self._ping)
273
+ yield self
274
+ finally:
275
+ self._tg = None
276
+ tg.cancel_scope.cancel()
267
277
 
268
278
  def __aiter__(self):
269
279
  return self
@@ -309,24 +319,23 @@ class Actor:
309
319
  """
310
320
  if self._send_raw:
311
321
  await self.post_event(RawMsgEvent(msg))
312
- msg = Message(**msg)
322
+ msg = Message.read(msg)
313
323
  if type(msg) is InitMessage and msg.node == self._name:
314
324
  if self._self_seen:
315
325
  # We may see our own Hello only once
316
- raise ActorCollisionError()
326
+ raise ActorCollisionError
317
327
  self._self_seen = True
318
328
 
319
329
  await self._rdr_q.put(msg)
320
330
 
321
331
  async def _run(self):
322
- """
323
- """
332
+ """ """
324
333
  await self._send_msg(InitMessage(node=self._name))
325
334
  await self._send_msg(self._version)
326
335
 
327
336
  await anyio.sleep((self.random / 2 + 1.5) * self._gap + self._cycle)
328
337
  if not self._self_seen:
329
- raise ActorTimeoutError()
338
+ raise ActorTimeoutError
330
339
  await self._send_ping()
331
340
 
332
341
  t_dest = 0
@@ -390,13 +399,9 @@ class Actor:
390
399
 
391
400
  It is an error to not supersede the old config.
392
401
  """
393
- self._cfg = {}
394
- self._cfg.update(self.DEFAULTS)
395
- self._cfg.update(cfg)
396
-
397
- v = SetupMessage(**self._cfg)
402
+ v = SetupMessage(**cfg)
398
403
  if v.version <= self._version.version:
399
- raise ValueError("You need a version > %s" % (self._version.version,))
404
+ raise ValueError(f"You need a version > {self._version.version}")
400
405
  self._version = v
401
406
  await self._send_msg(v)
402
407
 
@@ -407,11 +412,10 @@ class Actor:
407
412
  Args:
408
413
  length (int): New max length of the history. Default: Leave alone.
409
414
  """
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)
415
+ if length is not None and self._version.nodes < length:
416
+ self._version.nodes = length
417
+ self._version.version += 1
418
+ await self._send_msg(self._version)
415
419
 
416
420
  if self._tagged != -1:
417
421
  return
@@ -657,7 +661,7 @@ class Actor:
657
661
  history = self._prev_history = self._history
658
662
  self._history += self._name
659
663
  self._get_next_ping_time()
660
- msg.history = history[0 : self._splits] # noqa: E203
664
+ msg.history = history[0 : self._splits]
661
665
  await self._send_msg(msg)
662
666
 
663
667
  async def _send_msg(self, msg: Message):
@@ -753,10 +757,9 @@ class Actor:
753
757
  lv += 1
754
758
  if self._nodes > 0 and lv > self._nodes:
755
759
  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
760
+ if not self._ready and p > lv // 2:
761
+ # No, the first active host is too far back.
762
+ return 2 + self.random / 3
760
763
 
761
764
  return self.ping_delay(
762
765
  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