moat-link-gate 0.0.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.
- moat_link_gate-0.0.1/LICENSE.txt +14 -0
- moat_link_gate-0.0.1/Makefile +14 -0
- moat_link_gate-0.0.1/PKG-INFO +51 -0
- moat_link_gate-0.0.1/README.md +26 -0
- moat_link_gate-0.0.1/debian/.gitignore +6 -0
- moat_link_gate-0.0.1/debian/changelog +11 -0
- moat_link_gate-0.0.1/debian/control +26 -0
- moat_link_gate-0.0.1/debian/rules +5 -0
- moat_link_gate-0.0.1/pyproject.toml +42 -0
- moat_link_gate-0.0.1/setup.cfg +4 -0
- moat_link_gate-0.0.1/src/moat/link/gate/__init__.py +366 -0
- moat_link_gate-0.0.1/src/moat/link/gate/_main.py +140 -0
- moat_link_gate-0.0.1/src/moat/link/gate/kv.py +97 -0
- moat_link_gate-0.0.1/src/moat/link/gate/mqtt.py +167 -0
- moat_link_gate-0.0.1/src/moat_link_gate.egg-info/PKG-INFO +51 -0
- moat_link_gate-0.0.1/src/moat_link_gate.egg-info/SOURCES.txt +17 -0
- moat_link_gate-0.0.1/src/moat_link_gate.egg-info/dependency_links.txt +1 -0
- moat_link_gate-0.0.1/src/moat_link_gate.egg-info/requires.txt +6 -0
- moat_link_gate-0.0.1/src/moat_link_gate.egg-info/top_level.txt +1 -0
|
@@ -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,14 @@
|
|
|
1
|
+
#!/usr/bin/make -f
|
|
2
|
+
|
|
3
|
+
PACKAGE = moat-link-gate
|
|
4
|
+
MAKEINCL ?= $(shell python3 -mmoat src path)/make/py
|
|
5
|
+
|
|
6
|
+
ifneq ($(wildcard $(MAKEINCL)),)
|
|
7
|
+
include $(MAKEINCL)
|
|
8
|
+
# availabe via http://github.com/smurfix/sourcemgr
|
|
9
|
+
|
|
10
|
+
else
|
|
11
|
+
%:
|
|
12
|
+
@echo "Please fix 'python3 -mmoat src path'."
|
|
13
|
+
@exit 1
|
|
14
|
+
endif
|
|
@@ -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,26 @@
|
|
|
1
|
+
# moat-link-gate
|
|
2
|
+
|
|
3
|
+
Gateway modules for MoaT-Link providing KV and MQTT integrations.
|
|
4
|
+
|
|
5
|
+
This package provides gateway functionality for MoaT-Link, including:
|
|
6
|
+
- KV (key-value) gateway for data storage integration
|
|
7
|
+
- MQTT gateway for MQTT broker integration
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install moat-link-gate
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
The gateways can be configured through MoaT-Link's configuration system.
|
|
18
|
+
|
|
19
|
+
## Dependencies
|
|
20
|
+
|
|
21
|
+
- moat-link: Core MoaT-Link functionality
|
|
22
|
+
- moat-lib-mqtt: MQTT client library
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
This project is licensed under the same terms as MoaT.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
moat-link-gate (0.0.1-2) unstable; urgency=medium
|
|
2
|
+
|
|
3
|
+
* New release for 25.8.6
|
|
4
|
+
|
|
5
|
+
-- Matthias Urlichs <matthias@urlichs.de> Mon, 05 Jan 2026 16:50:24 +0100
|
|
6
|
+
|
|
7
|
+
moat-link-gate (0.0.0-0) unstable; urgency=medium
|
|
8
|
+
|
|
9
|
+
* Initial release as separate package
|
|
10
|
+
|
|
11
|
+
-- Matthias Urlichs <matthias@urlichs.de> Sun, 05 Jan 2025 13:00:00 +0100
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Source: moat-link-gate
|
|
2
|
+
Maintainer: Matthias Urlichs <matthias@urlichs.de>
|
|
3
|
+
Section: python
|
|
4
|
+
Priority: optional
|
|
5
|
+
Build-Depends: dh-python, python3-all, debhelper (>= 13),
|
|
6
|
+
python3-setuptools,
|
|
7
|
+
python3-wheel,
|
|
8
|
+
moat-src,
|
|
9
|
+
Standards-Version: 3.9.6
|
|
10
|
+
Homepage: https://github.com/smurfix/moat
|
|
11
|
+
X-DH-Compat: 13
|
|
12
|
+
|
|
13
|
+
Package: moat-link-gate
|
|
14
|
+
Architecture: all
|
|
15
|
+
Depends: ${misc:Depends}, ${python3:Depends},
|
|
16
|
+
moat-util,
|
|
17
|
+
moat-link,
|
|
18
|
+
python3-moat-lib-mqtt,
|
|
19
|
+
Recommends:
|
|
20
|
+
python3-trio (>= 0.22),
|
|
21
|
+
Description: Gateway modules for MoaT-Link (KV and MQTT)
|
|
22
|
+
This package provides gateway functionality for MoaT-Link, including KV
|
|
23
|
+
(key-value) gateway for data storage integration and MQTT gateway for
|
|
24
|
+
MQTT broker integration.
|
|
25
|
+
.
|
|
26
|
+
Part of the MoaT ecosystem for distributed home automation.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = [ "setuptools", "wheel",]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
classifiers = [
|
|
7
|
+
"Intended Audience :: Developers",
|
|
8
|
+
"Programming Language :: Python :: 3",
|
|
9
|
+
"Framework :: AsyncIO",
|
|
10
|
+
"Framework :: Trio",
|
|
11
|
+
"Framework :: AnyIO",
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"anyio ~= 4.2",
|
|
16
|
+
"asyncclick",
|
|
17
|
+
"attrs",
|
|
18
|
+
"moat-util ~= 0.62.2",
|
|
19
|
+
"moat-link ~= 0.2.46",
|
|
20
|
+
"moat-lib-mqtt ~= 0.0.1",
|
|
21
|
+
]
|
|
22
|
+
version = "0.0.1"
|
|
23
|
+
keywords = [ "MoaT",]
|
|
24
|
+
requires-python = ">=3.8"
|
|
25
|
+
name = "moat-link-gate"
|
|
26
|
+
description = "Gateway modules for MoaT-Link (KV and MQTT)"
|
|
27
|
+
readme = "README.md"
|
|
28
|
+
|
|
29
|
+
[[project.authors]]
|
|
30
|
+
name = "Matthias Urlichs"
|
|
31
|
+
email = "matthias@urlichs.de"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
homepage = "https://m-o-a-t.org"
|
|
35
|
+
repository = "https://github.com/M-o-a-T/moat"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
"*" = ["*.yaml"]
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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,17 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
Makefile
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
debian/.gitignore
|
|
6
|
+
debian/changelog
|
|
7
|
+
debian/control
|
|
8
|
+
debian/rules
|
|
9
|
+
src/moat/link/gate/__init__.py
|
|
10
|
+
src/moat/link/gate/_main.py
|
|
11
|
+
src/moat/link/gate/kv.py
|
|
12
|
+
src/moat/link/gate/mqtt.py
|
|
13
|
+
src/moat_link_gate.egg-info/PKG-INFO
|
|
14
|
+
src/moat_link_gate.egg-info/SOURCES.txt
|
|
15
|
+
src/moat_link_gate.egg-info/dependency_links.txt
|
|
16
|
+
src/moat_link_gate.egg-info/requires.txt
|
|
17
|
+
src/moat_link_gate.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
moat
|