moat-link-gate 0.0.1__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,366 @@
1
+ """
2
+ Gateway to wherever
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import anyio
8
+ import logging
9
+
10
+ from attrs import define, field
11
+
12
+ from moat.util import NotGiven, P, Path, to_attrdict
13
+ from moat.lib.codec import get_codec
14
+ from moat.link.meta import MsgMeta
15
+ from moat.link.node import Node
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from moat.util import attrdict
21
+ from moat.lib.codec import Codec
22
+
23
+ from .client import Link, Watcher
24
+
25
+ from typing import Any
26
+
27
+ __all__ = ["Gate"]
28
+
29
+
30
+ class GateVanished(RuntimeError):
31
+ "internal error: gate got dropped, or driver changed"
32
+
33
+ pass
34
+
35
+
36
+ @define
37
+ class GateNode(Node):
38
+ """
39
+ A gatewayed node. It stores the external value and metadata.
40
+
41
+ Data and meta
42
+ """
43
+
44
+ ext_meta: dict[str, Any] | None = field(init=False, default=None)
45
+ ext_data: Any = field(init=False, default=NotGiven)
46
+ lock: anyio.abc.Lock = field(init=False, factory=anyio.Lock)
47
+
48
+ todo: bool = field(init=False, default=False)
49
+
50
+ @property
51
+ def has_src(self):
52
+ "Check whether source data is present"
53
+ return self.data_ is not NotGiven or self.meta not in (None, NotGiven)
54
+
55
+ @property
56
+ def has_dst(self):
57
+ "Check whether destination data is present"
58
+ return self.ext_data is not NotGiven or self.ext_meta not in (None, NotGiven)
59
+
60
+ @property
61
+ def has_both(self):
62
+ "Check whether both source and destination data are present"
63
+ if self.data_ is NotGiven and self.meta is None:
64
+ return False
65
+ if self.ext_data is NotGiven and self.ext_meta is None:
66
+ return False
67
+ return True
68
+
69
+
70
+ class Gate:
71
+ """
72
+ This is the base class for data gateways.
73
+
74
+ Gateways are described by a dict in ``:r.gate.NAME`` with the following
75
+ entries:
76
+
77
+ * src: source path, covered by ``moat.link``
78
+ * dst: destination, *must not* be at or under the ``moat.link`` root (if MQTT)
79
+ * driver:
80
+ * mqtt: the destination is a raw MQTT thing
81
+ * codec: Encoding of the destination (source is always ``std-cbor``).
82
+ * retain: ``True/False/None``; the latter is the default and copies
83
+ the data's retain flag
84
+
85
+ The gateway works thus:
86
+ * if a data item is not in the source or arrives from dest, copy to source
87
+ * if a data item is not in the destination or arrives from source, copy to dest
88
+ * if the values are equal, do nothing
89
+ * if the source metadata say the data is from the destination, copy dest to source
90
+ * otherwise copy source to dest.
91
+
92
+ Subclasses override
93
+ * get_dst
94
+ * set_dst
95
+ * newer_dst
96
+ """
97
+
98
+ state: Node
99
+ src: Node
100
+ tg: anyio.abc.TaskGroup
101
+ codec: Codec
102
+
103
+ _src_done: anyio.Event
104
+ _dst_done: anyio.Event
105
+
106
+ cfg: attrdict
107
+ cf: attrdict
108
+
109
+ def __init__(self, cfg: dict[str, Any], cf: dict[str, Any], path: Path, link: Link):
110
+ """
111
+ Setup.
112
+ @cfg: initial data for this gateway.
113
+ """
114
+ self.cfg = cfg
115
+ self.cf = to_attrdict(cf)
116
+ self.codec = cf.get("codec", "cbor")
117
+ if isinstance(self.codec, str):
118
+ self.codec = get_codec(self.codec)
119
+
120
+ self.link = link
121
+ self.path = path
122
+ self.origin = str(Path("GATE") + (P(cf["name"]) if "name" in cf else self.path[1:]))
123
+
124
+ self.logger = logging.getLogger(f"moat.link.{path}")
125
+
126
+ async def get_src(self, *, task_status=anyio.TASK_STATUS_IGNORED):
127
+ """
128
+ Fetch the internal data.
129
+ """
130
+ async with self.link.d_watch(
131
+ self.cf.src, subtree=True, state=None, meta=True, mark=True
132
+ ) as mon:
133
+ task_status.started()
134
+ async for pdm in mon:
135
+ if pdm is None:
136
+ self._src_done.set()
137
+ continue
138
+ p, d, m = pdm
139
+ if m is not None and m.origin == self.origin and self._src_done.is_set():
140
+ # mine, so skip
141
+ continue
142
+
143
+ node = self.data.get(p)
144
+ if self.running or node.has_src:
145
+ # self.logger.debug("S NOW %r %r %r",p,d,m)
146
+ await self._set_dst(p, node, d, m)
147
+ else:
148
+ # self.logger.debug("S DLY %r %r %r",p,d,m)
149
+ node.set_(p, d, m)
150
+ node.todo = True
151
+
152
+ async def _set_dst(self, path: Path, node: GateNode, data: Any, meta: MsgMeta):
153
+ node.ext_data = NotGiven
154
+ node.ext_meta = NotGiven
155
+ node.set_(path, data, meta)
156
+ node.todo = False
157
+
158
+ async with node.lock:
159
+ await self.set_dst(path, data, meta, node)
160
+
161
+ def dst_is_current(self): # noqa: D102
162
+ self._dst_done.set()
163
+
164
+ async def get_dst(self, *, task_status=anyio.TASK_STATUS_IGNORED):
165
+ """
166
+ Fetch the external data.
167
+
168
+ Override this; call `set_src` with each item.
169
+
170
+ You must call `dst_is_current` when the current state has been read
171
+ and you're now waiting for updates. If your backend doesn't support
172
+ this, use a timeout *and* an update counter; `set_src` returns True
173
+
174
+ """
175
+ raise NotImplementedError
176
+
177
+ async def set_src(self, path: Path, data: Any, aux: MsgMeta):
178
+ """
179
+ Update source state (possibly). @aux is additional metadata that
180
+ the destination resolver can use to disambiguate.
181
+ """
182
+ node = self.data.get(path)
183
+
184
+ if self.running or node.has_dst:
185
+ await self._set_src(self.cf.src + path, node, data, aux)
186
+ else:
187
+ node.ext_data = data
188
+ node.ext_meta = aux or NotGiven
189
+ node.todo = True
190
+
191
+ async def _set_src(self, path: Path, node: GateNode, data: Any, aux: MsgMeta):
192
+ async with node.lock:
193
+ if not self.is_update(node, data, aux):
194
+ return
195
+ meta = MsgMeta(origin=self.origin)
196
+ if aux not in (None, NotGiven):
197
+ meta["gw"] = aux
198
+
199
+ await self.link.d_set(path, data, meta)
200
+
201
+ node.set_((), NotGiven, NotGiven)
202
+ node.ext_data = data
203
+ node.ext_meta = aux or NotGiven
204
+ node.todo = False
205
+
206
+ async def set_dst(self, path: Path, data: Any, meta: MsgMeta, node: GateNode):
207
+ """
208
+ Update destination state. @meta is the source metadata, in case
209
+ it is useful in some way.
210
+ """
211
+ raise NotImplementedError
212
+
213
+ def is_update(self, node: GateNode, data: Any, aux: MsgMeta): # noqa: ARG002
214
+ """
215
+ Check whether this new destination data is an update.
216
+ """
217
+ return True
218
+
219
+ def newer_dst(self, node) -> bool | None:
220
+ """
221
+ Test whether the destination data is newer, based on the node's
222
+ metadata. Return `True` if the data should be copied to the source,
223
+ `False` if the source should be copied to the destination, or
224
+ `None` if inconclusive.
225
+
226
+ This method is only called when starting up.
227
+ """
228
+ raise NotImplementedError
229
+
230
+ async def run(self, *, task_status=anyio.TASK_STATUS_IGNORED):
231
+ """
232
+ Run a bidirectional copy.
233
+
234
+ This method auto-restarts the gateway if its data changes.
235
+ It ends if the gateway node is removed or the driver changes.
236
+
237
+ The task status is set when the initial sync has completed.
238
+
239
+ Called by the system.
240
+ """
241
+ run = True
242
+ while run:
243
+ self.state = Node()
244
+ self.data = GateNode()
245
+ self._src_done = anyio.Event()
246
+ self._dst_done = anyio.Event()
247
+ self.running = False
248
+
249
+ try:
250
+ async with anyio.create_task_group() as self.tg:
251
+ await self.tg.start(self._restart)
252
+ await self.run_(task_status=task_status)
253
+ except* GateVanished:
254
+ run = False
255
+ else:
256
+ task_status = anyio.TASK_STATUS_IGNORED
257
+ await anyio.sleep(1)
258
+
259
+ async def _restart(self, *, task_status=anyio.TASK_STATUS_IGNORED):
260
+ "Restart the thing when the root changes."
261
+ async with self.link.d_watch(self.path) as mon:
262
+ task_status.started()
263
+ async for d in mon:
264
+ if self.cf == d:
265
+ continue
266
+ if d is NotGiven or d.get("driver") != self.cf.driver:
267
+ raise GateVanished(str(self.path))
268
+ self.cf = d
269
+ self.tg.cancel_scope.cancel()
270
+ return
271
+
272
+ async def run_(self, *, task_status=anyio.TASK_STATUS_IGNORED):
273
+ """
274
+ The core runner for the gateway.
275
+
276
+ If your implementation needs a context or a support task,
277
+ override this and call the original. `tg` can be used.
278
+ """
279
+ # start initial loops
280
+ await self.tg.start(self.get_src)
281
+ await self.tg.start(self.get_dst)
282
+
283
+ # wait for initial scans to be done
284
+ await self._src_done.wait()
285
+ await self._dst_done.wait()
286
+ self.running = True
287
+
288
+ # resolve any conflicts in the initial data
289
+ async def visit(path, node):
290
+ if not node.todo:
291
+ return
292
+
293
+ if not node.has_src:
294
+ # no source data
295
+ if not node.has_dst:
296
+ # no destination data
297
+ return
298
+
299
+ # copy dest to source
300
+ d = True
301
+
302
+ elif not node.has_dst:
303
+ # copy source to dest
304
+ d = False
305
+
306
+ else:
307
+ # both are set. Ugh.
308
+ d = self.newer_dst(node)
309
+
310
+ if d is False:
311
+ self.logger.debug("SRC %s %s %r/%r", self.path, path, node.data_, node.meta)
312
+ await self._set_dst(path, node, node.data_, node.meta)
313
+
314
+ elif d is True:
315
+ self.logger.debug("DST %s %s %r/%r", self.path, path, node.ext_data, node.ext_meta)
316
+
317
+ meta = MsgMeta(origin=self.origin)
318
+ if node.ext_meta:
319
+ meta["gw"] = node.ext_meta
320
+ await self.link.d_set(self.cf.src + path, node.ext_data, meta)
321
+
322
+ elif node.data_ != node.ext_data:
323
+ self.logger.warning(
324
+ "Conflict %s %s %r/%r vs %r/%r",
325
+ self.path,
326
+ path,
327
+ node.data_,
328
+ node.meta,
329
+ node.ext_data,
330
+ node.ext_meta,
331
+ )
332
+
333
+ await self.data.walk(visit, force=True)
334
+ task_status.started()
335
+
336
+ async def state_updater(self, mon: Watcher, *, task_status=anyio.TASK_STATUS_IGNORED):
337
+ """
338
+ Status update handler.
339
+
340
+ By default this just gets the monitor node and updates the raw
341
+ node data in the background.
342
+ """
343
+ node = await mon.get_node()
344
+ task_status.started(node)
345
+
346
+ # nothing further to do
347
+
348
+
349
+ async def run_gate(
350
+ cfg: dict, link: Link, cf: Path | str, *, task_status=anyio.TASK_STATUS_IGNORED
351
+ ):
352
+ """
353
+ Run a gate in @link, described by @name.
354
+ """
355
+ from importlib import import_module # noqa: PLC0415
356
+
357
+ if isinstance(cf, str):
358
+ cf = P("gate") / cf
359
+ path = cf
360
+ cf = await link.d_get(path)
361
+
362
+ drv = cf["driver"]
363
+ if "." not in drv:
364
+ drv = "moat.link.gate." + drv
365
+ gate = import_module(drv).Gate(cfg, cf, path, link)
366
+ await gate.run(task_status=task_status)
@@ -0,0 +1,140 @@
1
+ """
2
+ Command line for gatewaying
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import sys
8
+
9
+ import asyncclick as click
10
+
11
+ from moat.util import NotGiven, P, yprint
12
+ from moat.lib.run import attr_args
13
+ from moat.link._data import data_get, node_attr
14
+ from moat.link.client import Link
15
+ from moat.link.meta import MsgMeta
16
+
17
+
18
+ @click.group(short_help="Manage gateways.", invoke_without_command=True)
19
+ @click.argument("path", type=P, nargs=1)
20
+ @click.pass_context
21
+ async def cli(ctx, path):
22
+ """
23
+ This subcommand gates MoaT-Link data to/from other channels.
24
+ """
25
+ obj = ctx.obj
26
+ cfg = obj.cfg["link"]
27
+ if obj.get("port", None) is not None:
28
+ cfg.client.port = obj.port
29
+ obj.conn = await ctx.with_async_resource(Link(cfg, common=True))
30
+ obj.path = path
31
+ if ctx.invoked_subcommand is None:
32
+ await _list(obj)
33
+
34
+
35
+ @cli.command()
36
+ @click.pass_obj
37
+ async def run(obj):
38
+ """
39
+ Run a gateway setup.
40
+ """
41
+ from moat.link.announce import announcing # noqa: PLC0415
42
+ from moat.link.gate import run_gate # noqa: PLC0415
43
+ from moat.util.systemd import as_service # noqa: PLC0415
44
+
45
+ gate = P("gate") + obj.path
46
+ async with (
47
+ as_service(obj) as srv,
48
+ announcing(obj.conn, host=False, name=gate, force=True, via=srv.evt),
49
+ ):
50
+ await run_gate(obj.cfg, obj.conn, gate, task_status=srv)
51
+
52
+
53
+ @cli.command("list")
54
+ @click.pass_obj
55
+ async def list_(obj):
56
+ """
57
+ List gates / a gate's data.
58
+ """
59
+ await _list(obj)
60
+
61
+
62
+ async def _list(obj):
63
+ if not obj.path:
64
+ seen = False
65
+ async with obj.conn.d_walk(P("gate"), min_depth=1, max_depth=1) as mon:
66
+ async for p, _d in mon:
67
+ seen = True
68
+ print(p[-1])
69
+ if not seen and obj.debug:
70
+ print("- no data.", file=sys.stderr)
71
+ return
72
+
73
+ k = {}
74
+ k["recursive"] = True
75
+ k["raw"] = True
76
+ k["empty"] = True
77
+ await data_get(obj.conn, P("gate") + obj.path, **k)
78
+
79
+
80
+ @cli.command("set", short_help="Add or update a gate entry")
81
+ @attr_args
82
+ @click.option("-S", "--src", type=P, help="Source (in Moat-Link)")
83
+ @click.option("-D", "--dst", type=P, help="Destination (driver specific)")
84
+ @click.option("-d", "--driver", type=str, help="Driver")
85
+ @click.pass_obj
86
+ async def set_(obj, src, dst, driver, **kw):
87
+ """
88
+ Add/change a gateway entry.
89
+ """
90
+
91
+ if not len(obj.path):
92
+ raise SyntaxError("Can't set the top level")
93
+ if driver:
94
+ kw["vars_"] += ((P("driver"), driver),)
95
+ if src:
96
+ kw["path_"] += ((P("src"), src),)
97
+ if dst:
98
+ kw["path_"] += ((P("dst"), dst),)
99
+ await node_attr(obj, P("gate") + obj.path, **kw)
100
+
101
+
102
+ class nstr:
103
+ def __new__(cls, val):
104
+ if val is NotGiven:
105
+ return val
106
+ return str(val)
107
+
108
+
109
+ @cli.command(short_help="Delete an entry / subtree")
110
+ @click.option(
111
+ "-b",
112
+ "--before",
113
+ type=float,
114
+ help="Don't delete entries created after this timestamp",
115
+ )
116
+ @click.option("-r", "--recursive", is_flag=True, help="Delete a complete subtree")
117
+ @click.pass_obj
118
+ async def delete(obj, before, recursive):
119
+ """
120
+ Delete an entry, or a subtree.
121
+
122
+ You really should use "--before" flag to ensure that no other change
123
+ arrived after you viewed the data in question.
124
+
125
+ Non-recursively deleting an entry with children works and does *not*
126
+ affect the child entries.
127
+
128
+ The root entry cannot be deleted.
129
+ """
130
+ args = {}
131
+ if recursive:
132
+ args["rec"] = recursive
133
+ if before:
134
+ args["ts"] = before
135
+ res = await obj.conn.d.delete(obj.path, **args)
136
+ if obj.meta:
137
+ res = dict(data=res[0], meta=MsgMeta.restore(res[1:]).repr())
138
+ else:
139
+ res = res[0]
140
+ yprint(res, stream=obj.stdout)
moat/link/gate/kv.py ADDED
@@ -0,0 +1,97 @@
1
+ """
2
+ MoaT gateway
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import anyio
8
+
9
+ from moat.util import NotGiven, Path, PathLongener
10
+ from moat.kv.client import Client, open_client
11
+ from moat.link.meta import MsgMeta
12
+
13
+ from . import Gate as _Gate
14
+
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from moat.link.gate import GateNode
19
+
20
+ from typing import Any
21
+
22
+
23
+ class Gate(_Gate): # noqa: D101
24
+ kv: Client
25
+
26
+ async def run_(self, *, task_status=anyio.TASK_STATUS_IGNORED):
27
+ "Main loop. Overridden to start a Moat-KV client"
28
+ async with open_client("moat.link.gate.kv", **self.cfg["kv"]) as self.kv:
29
+ await super().run_(task_status=task_status)
30
+
31
+ async def get_dst(self, task_status=anyio.TASK_STATUS_IGNORED): # noqa: D102
32
+ pl = PathLongener()
33
+ # This chops the `self.cf.dst` prefix off the resulting path
34
+ async with self.kv.watch(self.cf.dst, fetch=True, long_path=False, nchain=2) as mon:
35
+ task_status.started()
36
+ async for msg in mon:
37
+ if "path" not in msg:
38
+ if msg.get("state", "") == "uptodate":
39
+ self.dst_is_current()
40
+ continue
41
+ path = pl.long(msg.depth, msg.path)
42
+ await self.set_src(
43
+ path,
44
+ msg.get("value", NotGiven),
45
+ MsgMeta(origin=msg.chain.node, t=msg.chain.tick),
46
+ )
47
+
48
+ async def set_dst(self, path: Path, data: Any, meta: MsgMeta, node: GateNode):
49
+ "Set KV data."
50
+
51
+ meta # noqa:B018
52
+
53
+ # XXX ideally we should have the previous value's external chain
54
+ # available here, just to be able to complain when there's a conflict
55
+ if data is NotGiven:
56
+ res = await self.kv.delete(self.cf.dst + path, nchain=1)
57
+ else:
58
+ res = await self.kv.set(self.cf.dst + path, value=data, nchain=1)
59
+
60
+ node.ext_meta = res.chain
61
+
62
+ def is_update(self, node: GateNode, data: Any, meta: MsgMeta):
63
+ "Check for update"
64
+ data # noqa:B018
65
+ # If the message is an echo of what we sent earlier, ignore it.
66
+ try:
67
+ if meta.origin == node.ext_meta.node and meta["t"] == node.ext_meta.tick:
68
+ return False
69
+ except (AttributeError, KeyError):
70
+ pass
71
+ return True
72
+
73
+ def newer_dst(self, node): # noqa: D102
74
+ # If the internal message has a copy of the outside metadata, it
75
+ # should be either unmodified or older. Test the data to be sure.
76
+ # Otherwise compare the chains.
77
+ if node.meta.origin == self.origin:
78
+ return None
79
+
80
+ if "gw" in node.meta:
81
+ last_node = node.meta["gw"]
82
+ if last_node.origin != node.ext_meta.origin:
83
+ return True
84
+ if last_node["t"] < node.ext_meta["t"]:
85
+ return True
86
+ if last_node["t"] > node.ext_meta["t"]:
87
+ return False
88
+ return None
89
+
90
+ # same data = nothing to do
91
+ # XXX may require inexact comparison for floats
92
+ if node.data == node.ext_data:
93
+ return None
94
+
95
+ # Otherwise assume that the local data is newer because there's no
96
+ # gateway data in it.
97
+ return False
moat/link/gate/mqtt.py ADDED
@@ -0,0 +1,167 @@
1
+ """
2
+ MoaT gateway
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import anyio
8
+ from contextlib import AsyncExitStack
9
+
10
+ from moat.util import NotGiven, P, Path
11
+ from moat.link.meta import MsgMeta
12
+
13
+ from . import Gate as _Gate
14
+
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from moat.link.node.codec import CodecNode
19
+
20
+ from . import GateNode
21
+
22
+ from typing import Any
23
+
24
+
25
+ class Gate(_Gate): # noqa: D101
26
+ codecs: CodecNode | None = None
27
+
28
+ async def run_(self, *, task_status=anyio.TASK_STATUS_IGNORED):
29
+ "Main loop. Overridden to fetch the codecs"
30
+ async with AsyncExitStack() as ex:
31
+ if isinstance(self.cf.codec, Path):
32
+ cdv = await ex.enter_async_context(
33
+ self.link.d_watch(
34
+ P("conv") + self.cf.codec, subtree=True, state=None, meta=False
35
+ )
36
+ )
37
+ self.codec_vecs = await cdv.get_node()
38
+
39
+ self.codecs = await self.link.get_codec_tree()
40
+
41
+ await super().run_(task_status=task_status)
42
+
43
+ async def get_dst(self, *, task_status=anyio.TASK_STATUS_IGNORED): # noqa: D102
44
+ async with AsyncExitStack() as ex:
45
+ if self.codecs is not None:
46
+ codec = "noop"
47
+
48
+ def conv(p, d):
49
+ # two step
50
+ # (a) look up the codec type in the vector
51
+ try:
52
+ vd = self.codec_vecs.search(p)
53
+ cd = self.codecs.get(Path.build(vd.data["codec"]))
54
+ except (KeyError, ValueError):
55
+ return NotGiven
56
+ try:
57
+ return cd.dec_value(d)
58
+ except Exception as exc:
59
+ self.logger.error("Decode: %s %r: %r", p, d, exc)
60
+ return NotGiven
61
+
62
+ else:
63
+ codec = self.codec
64
+
65
+ def conv(p, d):
66
+ p # noqa:B018
67
+ return d
68
+
69
+ mon = await ex.enter_async_context(
70
+ self.link.monitor(self.cf.dst, subtree=True, codec=codec)
71
+ )
72
+ task_status.started()
73
+ ld = len(self.cf.dst)
74
+ while True:
75
+ try:
76
+ with anyio.fail_after(self.cf.get("timeout", 0.5)):
77
+ msg = await anext(mon)
78
+ except TimeoutError:
79
+ break
80
+ p = Path.build(msg.topic[ld:])
81
+ res = conv(p, msg.data)
82
+ if res is NotGiven:
83
+ continue
84
+ await self.set_src(p, res, msg.meta)
85
+ self.dst_is_current()
86
+
87
+ async for msg in mon:
88
+ if msg.meta is not None and msg.meta.origin == self.origin:
89
+ # mine, so skip
90
+ continue
91
+ p = Path.build(msg.topic[ld:])
92
+ if msg.data == b"":
93
+ res = NotGiven
94
+ else:
95
+ res = conv(p, msg.data)
96
+ if res is NotGiven:
97
+ continue
98
+ await self.set_src(p, res, msg.meta)
99
+
100
+ async def set_dst(self, path: Path, data: Any, meta: MsgMeta, node: GateNode): # noqa: D102
101
+ meta = MsgMeta(origin=self.origin, timestamp=meta.timestamp)
102
+ if data is NotGiven:
103
+ await self.link.send(self.cf.dst + path, b"", retain=True, codec="noop", meta=meta)
104
+ elif self.codecs is not None:
105
+ try:
106
+ vd = self.codec_vecs.search(path)
107
+ cd = self.codecs.get(Path.build(vd.data["codec"]))
108
+ except (ValueError, KeyError):
109
+ self.logger.error("No codec: %s %r", path, data)
110
+ return
111
+ res = cd.enc_value(data)
112
+ if isinstance(res, (str, bytes, bytearray)):
113
+ await self.link.send(self.cf.dst + path, res, retain=True, codec="noop", meta=meta)
114
+ else:
115
+ self.logger.error("Bad codec: %s %r > %r", path, data, res)
116
+
117
+ else:
118
+ await self.link.send(
119
+ self.cf.dst + path, data, retain=True, codec=self.codec, meta=meta
120
+ )
121
+
122
+ node.ext_meta = meta
123
+
124
+ def is_update(self, node: GateNode, data: Any, meta: MsgMeta):
125
+ """
126
+ Test whether this is an update.
127
+
128
+ @data is currently ignored.
129
+ """
130
+ data # noqa:B018
131
+ # if the old metadata match the new, it's not an update.
132
+ try:
133
+ if node.ext_meta.origin == meta.origin and node.ext_meta.timestamp == meta.timestamp:
134
+ return False
135
+ except (AttributeError, KeyError):
136
+ pass
137
+ return True
138
+
139
+ def newer_dst(self, node): # noqa: D102
140
+ # If the external message has no metadata, it can't be from us,
141
+ # thus assume it's newer.
142
+ if not node.ext_meta:
143
+ return True
144
+
145
+ # If the internal and external metadata match, the message is from
146
+ # us, so nothing to do.
147
+ if self.origin == node.ext_meta.origin:
148
+ return None
149
+
150
+ # If the internal message has a copy of the outside metadata, it's
151
+ # either unmodified or older. Test the data to be sure.
152
+ if "gw" in node.meta:
153
+ if node.meta["gw"] == node.ext_meta:
154
+ return None if node.data_ == node.ext_data else True
155
+ else:
156
+ return True
157
+
158
+ # Otherwise, if the external message is ours, it's old.
159
+ if node.ext_meta.origin == self.origin:
160
+ return False
161
+
162
+ # if the timestamps are too close, there might be a problem.
163
+ if abs(node.ext_meta.timestamp - node.meta.timestamp) < 0.1:
164
+ return None
165
+
166
+ # Otherwise use the message with the newer timestamp.
167
+ return node.ext_meta.timestamp > node.meta.timestamp
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: moat-link-gate
3
+ Version: 0.0.1
4
+ Summary: Gateway modules for MoaT-Link (KV and MQTT)
5
+ Author-email: Matthias Urlichs <matthias@urlichs.de>
6
+ Project-URL: homepage, https://m-o-a-t.org
7
+ Project-URL: repository, https://github.com/M-o-a-T/moat
8
+ Keywords: MoaT
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: Development Status :: 4 - Beta
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.txt
18
+ Requires-Dist: anyio~=4.2
19
+ Requires-Dist: asyncclick
20
+ Requires-Dist: attrs
21
+ Requires-Dist: moat-util~=0.62.2
22
+ Requires-Dist: moat-link~=0.2.46
23
+ Requires-Dist: moat-lib-mqtt~=0.0.1
24
+ Dynamic: license-file
25
+
26
+ # moat-link-gate
27
+
28
+ Gateway modules for MoaT-Link providing KV and MQTT integrations.
29
+
30
+ This package provides gateway functionality for MoaT-Link, including:
31
+ - KV (key-value) gateway for data storage integration
32
+ - MQTT gateway for MQTT broker integration
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install moat-link-gate
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ The gateways can be configured through MoaT-Link's configuration system.
43
+
44
+ ## Dependencies
45
+
46
+ - moat-link: Core MoaT-Link functionality
47
+ - moat-lib-mqtt: MQTT client library
48
+
49
+ ## License
50
+
51
+ This project is licensed under the same terms as MoaT.
@@ -0,0 +1,9 @@
1
+ moat/link/gate/__init__.py,sha256=vOKlZRcowuUSe8eCtlwpT98MUCXjsWYFkFHKtpZSb0o,11395
2
+ moat/link/gate/_main.py,sha256=u0ydGxvG85UU7fQ3kPLL8ymsH0P-z3056d9MRL5kCoI,3686
3
+ moat/link/gate/kv.py,sha256=JYrMQEMggyByJXOD4WNZXQ9GddIprSRYHknW_V_xFJ4,3270
4
+ moat/link/gate/mqtt.py,sha256=IcziqygqwC0kU9Zswwa25ysYbw8MpNJnhTY-5xfFS9A,5844
5
+ moat_link_gate-0.0.1.dist-info/licenses/LICENSE.txt,sha256=L5vKJLVOg5t0CEEPpW9-O_0vzbP0PEjEF06tLvnIDuk,541
6
+ moat_link_gate-0.0.1.dist-info/METADATA,sha256=9laWCjLJRDyvCHzN_nO8QBBtp114gYZnJxbMSo8KOSU,1353
7
+ moat_link_gate-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ moat_link_gate-0.0.1.dist-info/top_level.txt,sha256=pcs9fl5w5AB5GVi4SvBqIVmFrkRwQkVw_dEvW0Q0cSA,5
9
+ moat_link_gate-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,14 @@
1
+ The code in this repository, and all MoaT submodules it refers to,
2
+ is part of the MoaT project.
3
+
4
+ Unless a submodule's LICENSE.txt states otherwise, all included files are
5
+ licensed under the LGPL V3, as published by the FSF at
6
+ https://www.gnu.org/licenses/lgpl-3.0.html .
7
+
8
+ In addition to the LGPL's terms, the author(s) respectfully ask all users of
9
+ this code to contribute any bug fixes or enhancements. Also, please link back to
10
+ https://M-o-a-T.org.
11
+
12
+ Thank you.
13
+
14
+ Copyright © 2021 ff.: the MoaT contributor(s), as per the git changelog(s).
@@ -0,0 +1 @@
1
+ moat