moat-kv 0.70.23__py3-none-any.whl → 0.71.0__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.
Files changed (177) hide show
  1. build/lib/docs/source/conf.py +201 -0
  2. build/lib/examples/pathify.py +45 -0
  3. build/lib/moat/kv/__init__.py +19 -0
  4. build/lib/moat/kv/_cfg.yaml +93 -0
  5. build/lib/moat/kv/_main.py +91 -0
  6. build/lib/moat/kv/actor/__init__.py +98 -0
  7. build/lib/moat/kv/actor/deletor.py +139 -0
  8. build/lib/moat/kv/auth/__init__.py +444 -0
  9. build/lib/moat/kv/auth/_test.py +166 -0
  10. build/lib/moat/kv/auth/password.py +234 -0
  11. build/lib/moat/kv/auth/root.py +58 -0
  12. build/lib/moat/kv/backend/__init__.py +67 -0
  13. build/lib/moat/kv/backend/mqtt.py +71 -0
  14. build/lib/moat/kv/client.py +1025 -0
  15. build/lib/moat/kv/code.py +236 -0
  16. build/lib/moat/kv/codec.py +11 -0
  17. build/lib/moat/kv/command/__init__.py +1 -0
  18. build/lib/moat/kv/command/acl.py +180 -0
  19. build/lib/moat/kv/command/auth.py +261 -0
  20. build/lib/moat/kv/command/code.py +293 -0
  21. build/lib/moat/kv/command/codec.py +186 -0
  22. build/lib/moat/kv/command/data.py +265 -0
  23. build/lib/moat/kv/command/dump/__init__.py +143 -0
  24. build/lib/moat/kv/command/error.py +149 -0
  25. build/lib/moat/kv/command/internal.py +248 -0
  26. build/lib/moat/kv/command/job.py +433 -0
  27. build/lib/moat/kv/command/log.py +53 -0
  28. build/lib/moat/kv/command/server.py +114 -0
  29. build/lib/moat/kv/command/type.py +201 -0
  30. build/lib/moat/kv/config.py +46 -0
  31. build/lib/moat/kv/data.py +216 -0
  32. build/lib/moat/kv/errors.py +561 -0
  33. build/lib/moat/kv/exceptions.py +126 -0
  34. build/lib/moat/kv/mock/__init__.py +101 -0
  35. build/lib/moat/kv/mock/mqtt.py +159 -0
  36. build/lib/moat/kv/mock/tracer.py +63 -0
  37. build/lib/moat/kv/model.py +1069 -0
  38. build/lib/moat/kv/obj/__init__.py +646 -0
  39. build/lib/moat/kv/obj/command.py +241 -0
  40. build/lib/moat/kv/runner.py +1347 -0
  41. build/lib/moat/kv/server.py +2809 -0
  42. build/lib/moat/kv/types.py +513 -0
  43. ci/rtd-requirements.txt +4 -0
  44. ci/test-requirements.txt +7 -0
  45. ci/travis.sh +96 -0
  46. debian/.gitignore +7 -0
  47. debian/changelog +1435 -0
  48. debian/control +43 -0
  49. debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +201 -0
  50. debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +45 -0
  51. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +19 -0
  52. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +93 -0
  53. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +91 -0
  54. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +98 -0
  55. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +139 -0
  56. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +444 -0
  57. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +166 -0
  58. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +234 -0
  59. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +58 -0
  60. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +67 -0
  61. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +71 -0
  62. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +1025 -0
  63. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +236 -0
  64. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +11 -0
  65. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +1 -0
  66. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +180 -0
  67. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +261 -0
  68. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +293 -0
  69. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +186 -0
  70. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +265 -0
  71. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +143 -0
  72. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +149 -0
  73. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +248 -0
  74. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +433 -0
  75. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +53 -0
  76. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +114 -0
  77. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +201 -0
  78. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +46 -0
  79. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +216 -0
  80. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +561 -0
  81. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +126 -0
  82. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +101 -0
  83. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +159 -0
  84. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +63 -0
  85. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +1069 -0
  86. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +646 -0
  87. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +241 -0
  88. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +1347 -0
  89. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +2809 -0
  90. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +513 -0
  91. debian/moat-kv.postinst +3 -0
  92. debian/rules +20 -0
  93. debian/source/format +1 -0
  94. debian/watch +4 -0
  95. docs/Makefile +20 -0
  96. docs/make.bat +36 -0
  97. docs/source/TODO.rst +61 -0
  98. docs/source/_static/.gitkeep +0 -0
  99. docs/source/acls.rst +80 -0
  100. docs/source/auth.rst +84 -0
  101. docs/source/client_protocol.rst +456 -0
  102. docs/source/code.rst +341 -0
  103. docs/source/command_line.rst +1187 -0
  104. docs/source/common_protocol.rst +47 -0
  105. docs/source/conf.py +201 -0
  106. docs/source/debugging.rst +70 -0
  107. docs/source/extend.rst +37 -0
  108. docs/source/history.rst +36 -0
  109. docs/source/index.rst +75 -0
  110. docs/source/model.rst +54 -0
  111. docs/source/overview.rst +83 -0
  112. docs/source/related.rst +89 -0
  113. docs/source/server_protocol.rst +450 -0
  114. docs/source/startup.rst +31 -0
  115. docs/source/translator.rst +244 -0
  116. docs/source/tutorial.rst +711 -0
  117. docs/source/v3.rst +168 -0
  118. examples/code/transform.scale.yml +21 -0
  119. examples/code/transform.switch.yml +82 -0
  120. examples/code/transform.timeslot.yml +63 -0
  121. examples/pathify.py +45 -0
  122. moat/kv/_cfg.yaml +2 -6
  123. moat/kv/actor/__init__.py +98 -0
  124. moat/kv/actor/deletor.py +139 -0
  125. moat/kv/auth/__init__.py +444 -0
  126. moat/kv/auth/_test.py +166 -0
  127. moat/kv/auth/password.py +234 -0
  128. moat/kv/auth/root.py +58 -0
  129. moat/kv/backend/__init__.py +67 -0
  130. moat/kv/backend/mqtt.py +71 -0
  131. moat/kv/command/__init__.py +1 -0
  132. moat/kv/command/acl.py +180 -0
  133. moat/kv/command/auth.py +261 -0
  134. moat/kv/command/code.py +293 -0
  135. moat/kv/command/codec.py +186 -0
  136. moat/kv/command/data.py +265 -0
  137. moat/kv/command/dump/__init__.py +143 -0
  138. moat/kv/command/error.py +149 -0
  139. moat/kv/command/internal.py +248 -0
  140. moat/kv/command/job.py +433 -0
  141. moat/kv/command/log.py +53 -0
  142. moat/kv/command/server.py +114 -0
  143. moat/kv/command/type.py +201 -0
  144. moat/kv/mock/__init__.py +101 -0
  145. moat/kv/mock/mqtt.py +159 -0
  146. moat/kv/mock/tracer.py +63 -0
  147. moat/kv/obj/__init__.py +646 -0
  148. moat/kv/obj/command.py +241 -0
  149. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/METADATA +2 -5
  150. moat_kv-0.71.0.dist-info/RECORD +188 -0
  151. moat_kv-0.71.0.dist-info/top_level.txt +9 -0
  152. scripts/current +15 -0
  153. scripts/env +8 -0
  154. scripts/init +39 -0
  155. scripts/recover +17 -0
  156. scripts/rotate +33 -0
  157. scripts/run +29 -0
  158. scripts/run-all +10 -0
  159. scripts/run-any +10 -0
  160. scripts/run-single +15 -0
  161. scripts/success +4 -0
  162. systemd/moat-kv-recover.service +21 -0
  163. systemd/moat-kv-rotate.service +20 -0
  164. systemd/moat-kv-rotate.timer +10 -0
  165. systemd/moat-kv-run-all.service +26 -0
  166. systemd/moat-kv-run-all@.service +25 -0
  167. systemd/moat-kv-run-any.service +26 -0
  168. systemd/moat-kv-run-any@.service +25 -0
  169. systemd/moat-kv-run-single.service +26 -0
  170. systemd/moat-kv-run-single@.service +25 -0
  171. systemd/moat-kv.service +27 -0
  172. systemd/postinst +7 -0
  173. systemd/sysusers +3 -0
  174. moat_kv-0.70.23.dist-info/RECORD +0 -19
  175. moat_kv-0.70.23.dist-info/top_level.txt +0 -1
  176. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/WHEEL +0 -0
  177. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,166 @@
1
+ #
2
+ """
3
+ Test auth method.
4
+
5
+ Does not limit anything, allows everything.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+ from ..client import Client
15
+ from . import (
16
+ BaseClientAuth,
17
+ BaseClientAuthMaker,
18
+ BaseServerAuthMaker,
19
+ RootServerUser,
20
+ null_client_login,
21
+ null_server_login,
22
+ )
23
+
24
+
25
+ def load(typ: str, *, make: bool = False, server: bool):
26
+ if typ == "client":
27
+ if server:
28
+ return null_server_login
29
+ else:
30
+ return null_client_login
31
+ if typ != "user":
32
+ raise NotImplementedError("This module only handles users")
33
+ if server:
34
+ if make:
35
+ return ServerUserMaker
36
+ else:
37
+ return ServerUser
38
+ else:
39
+ if make:
40
+ return ClientUserMaker
41
+ else:
42
+ return ClientUser
43
+
44
+
45
+ class ServerUserMaker(BaseServerAuthMaker):
46
+ name = None
47
+
48
+ @property
49
+ def ident(self):
50
+ return self.name
51
+
52
+ # Overly-complicated methods of exchanging the user name
53
+
54
+ @classmethod
55
+ async def recv(cls, cmd, data):
56
+ await cmd.send(step="GiveName")
57
+ msg = await cmd.recv()
58
+ assert msg.step == "HasName"
59
+ self = cls()
60
+ self.name = msg.name
61
+ return self
62
+
63
+ async def send(self, cmd):
64
+ await cmd.send(step="SendWant")
65
+ msg = await cmd.recv()
66
+ assert msg.step == "WantName"
67
+ await cmd.send(step="SendName", name=self.name, chain=self._chain.serialize(nchain=3))
68
+ msg = await cmd.recv()
69
+
70
+ # Annoying methods to read+save the user name from/to KV
71
+
72
+ @classmethod
73
+ def load(cls, data):
74
+ self = super().load(data)
75
+ self.name = data.name
76
+ return self
77
+
78
+
79
+ class ServerUser(RootServerUser):
80
+ pass
81
+
82
+
83
+ class ClientUserMaker(BaseClientAuthMaker):
84
+ gen_schema = dict(
85
+ type="object",
86
+ additionalProperties=False,
87
+ properties=dict(name=dict(type="string", minLength=1, pattern="^[a-zA-Z][a-zA-Z0-9_]*$")),
88
+ required=["name"],
89
+ )
90
+ mod_schema = dict(
91
+ type="object",
92
+ additionalProperties=False,
93
+ properties=dict(name=dict(type="string", minLength=1, pattern="^[a-zA-Z][a-zA-Z0-9_]*$")),
94
+ # required=[],
95
+ )
96
+ name = None
97
+
98
+ @property
99
+ def ident(self):
100
+ return self.name
101
+
102
+ # Overly-complicated methods of exchanging the user name
103
+
104
+ @classmethod
105
+ async def recv(cls, client: Client, ident: str, _kind: str = "user", _initial=True):
106
+ """Read a record representing a user from the server."""
107
+ async with client._stream(
108
+ action="auth_get",
109
+ typ=cls._auth_method,
110
+ kind=_kind,
111
+ ident=ident,
112
+ stream=True,
113
+ nchain=0 if _initial else 2,
114
+ ) as s:
115
+ m = await s.recv()
116
+ assert m.step == "SendWant", m
117
+ await s.send(step="WantName")
118
+ m = await s.recv()
119
+ assert m.step == "SendName", m
120
+ assert m.name == ident
121
+
122
+ self = cls(name=m.name, _initial=_initial)
123
+ self._chain = m.chain
124
+ return self
125
+
126
+ async def send(self, client: Client, _kind="user"):
127
+ """Send a record representing this user to the server."""
128
+ async with client._stream(
129
+ action="auth_set",
130
+ typ=type(self)._auth_method,
131
+ kind=_kind,
132
+ ident=self.ident,
133
+ stream=True,
134
+ ) as s:
135
+ # we could initially send the ident but don't here, for testing
136
+ m = await s.recv()
137
+ assert m.step == "GiveName", m
138
+ await s.send(step="HasName", name=self.name, chain=self._chain)
139
+ m = await s.recv()
140
+ assert m.chain.prev is None
141
+
142
+ def export(self):
143
+ """Return the data required to re-create the user via :meth:`build`."""
144
+ return {"name": self.name}
145
+
146
+
147
+ class ClientUser(BaseClientAuth):
148
+ name = None
149
+
150
+ schema = dict(
151
+ type="object",
152
+ additionalProperties=False,
153
+ properties=dict(name=dict(type="string", minLength=1, pattern="^[a-zA-Z][a-zA-Z0-9_]*$")),
154
+ required=["name"],
155
+ )
156
+ _name = None
157
+
158
+ @property
159
+ def ident(self):
160
+ return self.name
161
+
162
+ @classmethod
163
+ def build(cls, user):
164
+ self = super().build(user)
165
+ self.name = user["name"]
166
+ return self
@@ -0,0 +1,234 @@
1
+ #
2
+ """
3
+ Password-based auth method.
4
+
5
+ Does not limit anything, allows everything.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import nacl.secret
11
+
12
+ from ..client import Client, NoData
13
+ from ..exceptions import AuthFailedError
14
+ from ..model import Entry
15
+ from ..server import StreamCommand
16
+ from . import (
17
+ BaseClientAuth,
18
+ BaseClientAuthMaker,
19
+ BaseServerAuthMaker,
20
+ RootServerUser,
21
+ null_client_login,
22
+ null_server_login,
23
+ )
24
+
25
+
26
+ def load(typ: str, *, make: bool = False, server: bool):
27
+ if typ == "client":
28
+ if server:
29
+ return null_server_login
30
+ else:
31
+ return null_client_login
32
+ if typ != "user":
33
+ raise NotImplementedError("This module only handles users")
34
+ if server:
35
+ if make:
36
+ return ServerUserMaker
37
+ else:
38
+ return ServerUser
39
+ else:
40
+ if make:
41
+ return ClientUserMaker
42
+ else:
43
+ return ClientUser
44
+
45
+
46
+ async def pack_pwd(client, password, length):
47
+ """Client side: encrypt password"""
48
+ secret = await client.dh_secret(length=length)
49
+ from hashlib import sha256
50
+
51
+ pwd = sha256(password).digest()
52
+ box = nacl.secret.SecretBox(secret)
53
+ pwd = box.encrypt(pwd)
54
+ return pwd
55
+
56
+
57
+ async def unpack_pwd(client, password):
58
+ """Server side: extract password"""
59
+ box = nacl.secret.SecretBox(client.dh_key)
60
+ pwd = box.decrypt(password)
61
+ return pwd
62
+ # TODO check with Argon2
63
+
64
+
65
+ class ServerUserMaker(BaseServerAuthMaker):
66
+ _name = None
67
+ _aux = None
68
+ password: str = None
69
+
70
+ @property
71
+ def ident(self):
72
+ return self._name
73
+
74
+ @classmethod
75
+ async def recv(cls, cmd, data):
76
+ self = cls()
77
+ self._name = data["ident"]
78
+ self._aux = data.get("aux")
79
+ pwd = data.get("password")
80
+ pwd = await unpack_pwd(cmd.client, pwd)
81
+
82
+ # TODO use Argon2 to re-hash this
83
+ self.password = pwd
84
+ return self
85
+
86
+ async def send(self, cmd):
87
+ return # nothing to do, we don't share the hash
88
+
89
+ @classmethod
90
+ def load(cls, data):
91
+ self = super().load(data)
92
+ self._name = data.path[-1]
93
+ return self
94
+
95
+ def save(self):
96
+ res = super().save()
97
+ res["password"] = self.password
98
+ return res
99
+
100
+
101
+ class ServerUser(RootServerUser):
102
+ @classmethod
103
+ def load(cls, data: Entry):
104
+ """Create a ServerUser object from existing stored data"""
105
+ self = super().load(data)
106
+ self._name = data.name
107
+ return self
108
+
109
+ async def auth(self, cmd: StreamCommand, data):
110
+ """Verify that @data authenticates this user."""
111
+ await super().auth(cmd, data)
112
+
113
+ pwd = await unpack_pwd(cmd.client, data.password)
114
+ if pwd != self.password: # pylint: disable=no-member
115
+ # pylint: disable=no-member
116
+ raise AuthFailedError("Password hashes do not match", self._name)
117
+
118
+
119
+ class ClientUserMaker(BaseClientAuthMaker):
120
+ gen_schema = dict(
121
+ type="object",
122
+ additionalProperties=True,
123
+ properties=dict(
124
+ name=dict(type="string", minLength=1, pattern="^[a-zA-Z][a-zA-Z0-9_]*$"),
125
+ password=dict(type="string", minLength=5),
126
+ ),
127
+ required=["name", "password"],
128
+ )
129
+ mod_schema = dict(
130
+ type="object",
131
+ additionalProperties=True,
132
+ properties=dict(password=dict(type="string", minLength=5)),
133
+ # required=[],
134
+ )
135
+ _name = None
136
+ _pass = None
137
+ _length = 1024
138
+
139
+ @property
140
+ def ident(self):
141
+ return self._name
142
+
143
+ # Overly-complicated methods of exchanging the user name
144
+
145
+ @classmethod
146
+ def build(cls, user, _initial=True):
147
+ self = super().build(user, _initial=_initial)
148
+ self._name = user["name"]
149
+ if "password" in user:
150
+ self._pass = user["password"].encode("utf-8")
151
+ return self
152
+
153
+ @classmethod
154
+ async def recv(cls, client: Client, ident: str, _kind: str = "user", _initial=True):
155
+ """Read a record representing a user from the server."""
156
+ m = await client._request(
157
+ action="auth_get",
158
+ typ=cls._auth_method,
159
+ kind=_kind,
160
+ ident=ident,
161
+ nchain=0 if _initial else 2,
162
+ )
163
+ # just to verify that the user exists
164
+ # There's no reason to send the password hash back
165
+ self = cls(_initial=_initial)
166
+ self._name = m.name
167
+ try:
168
+ self._chain = m.chain
169
+ except AttributeError:
170
+ pass
171
+ return self
172
+
173
+ async def send(self, client: Client, _kind="user", **msg): # pylint: disable=unused-argument,arguments-differ
174
+ """Send a record representing this user to the server."""
175
+ if self._pass is not None:
176
+ msg["password"] = await pack_pwd(client, self._pass, self._length)
177
+
178
+ await client._request(
179
+ action="auth_set",
180
+ ident=self._name,
181
+ typ=type(self)._auth_method,
182
+ kind=_kind,
183
+ chain=self._chain,
184
+ **msg,
185
+ )
186
+
187
+ def export(self):
188
+ """Return the data required to re-create the user via :meth:`build`."""
189
+ res = super().export()
190
+ res["name"] = self._name
191
+ return res
192
+
193
+
194
+ class ClientUser(BaseClientAuth):
195
+ schema = dict(
196
+ type="object",
197
+ additionalProperties=True,
198
+ properties=dict(
199
+ name=dict(type="string", minLength=1, pattern="^[a-zA-Z][a-zA-Z0-9_]*$"),
200
+ password=dict(type="string", minLength=5),
201
+ ),
202
+ required=["name", "password"],
203
+ )
204
+ _name = None
205
+ _pass = None
206
+ _length = 1024
207
+
208
+ @property
209
+ def ident(self):
210
+ return self._name
211
+
212
+ @classmethod
213
+ def build(cls, user):
214
+ self = super().build(user)
215
+ self._name = user["name"]
216
+ self._pass = user["password"].encode("utf-8")
217
+ return self
218
+
219
+ async def auth(self, client: Client, chroot=()):
220
+ """
221
+ Authorizes this user with the server.
222
+ """
223
+ try:
224
+ pw = await pack_pwd(client, self._pass, self._length)
225
+ await client._request(
226
+ action="auth",
227
+ typ=self._auth_method,
228
+ iter=False,
229
+ ident=self.ident,
230
+ password=pw,
231
+ **self.auth_data(),
232
+ )
233
+ except NoData:
234
+ pass
@@ -0,0 +1,58 @@
1
+ #
2
+ """
3
+ Null auth method.
4
+
5
+ Does not limit anything, allows everything.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from . import (
11
+ BaseClientAuth,
12
+ BaseClientAuthMaker,
13
+ BaseServerAuthMaker,
14
+ RootServerUser,
15
+ null_client_login,
16
+ null_server_login,
17
+ )
18
+
19
+
20
+ def load(typ: str, *, make: bool = False, server: bool):
21
+ if typ == "client":
22
+ if server:
23
+ return null_server_login
24
+ else:
25
+ return null_client_login
26
+ if typ != "user":
27
+ raise NotImplementedError("This module only handles users")
28
+ if server:
29
+ if make:
30
+ return ServerUserMaker
31
+ else:
32
+ return ServerUser
33
+ else:
34
+ if make:
35
+ return ClientUserMaker
36
+ else:
37
+ return ClientUser
38
+
39
+
40
+ class ServerUserMaker(BaseServerAuthMaker):
41
+ schema = {"type": "object", "additionalProperties": False}
42
+
43
+
44
+ class ServerUser(RootServerUser):
45
+ schema = {"type": "object", "additionalProperties": False}
46
+
47
+
48
+ class ClientUserMaker(BaseClientAuthMaker):
49
+ gen_schema = {"type": "object", "additionalProperties": False}
50
+ mod_schema = {"type": "object", "additionalProperties": False}
51
+
52
+ @property
53
+ def ident(self):
54
+ return "*"
55
+
56
+
57
+ class ClientUser(BaseClientAuth):
58
+ schema = {"type": "object", "additionalProperties": False}
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+ from abc import ABCMeta, abstractmethod
3
+ from contextlib import asynccontextmanager
4
+
5
+ import anyio
6
+
7
+ __all__ = ["get_backend", "Backend"]
8
+
9
+
10
+ class Backend(metaclass=ABCMeta):
11
+ def __init__(self, tg):
12
+ self._tg = tg
13
+ self._njobs = 0
14
+ self._ended = None
15
+
16
+ @abstractmethod
17
+ @asynccontextmanager
18
+ async def connect(self, *a, **k):
19
+ """
20
+ This async context manager returns a connection.
21
+ """
22
+
23
+ async def aclose(self):
24
+ """
25
+ Force-close the connection.
26
+ """
27
+ self._tg.cancel_scope.cancel()
28
+ if self._njobs > 0:
29
+ with anyio.move_on_after(2):
30
+ await self._ended.wait()
31
+
32
+ async def spawn(self, p, *a, **kw):
33
+ async def _run(p, a, kw, *, task_status):
34
+ if self._ended is None:
35
+ self._ended = anyio.Event()
36
+ self._njobs += 1
37
+ task_status.started()
38
+ try:
39
+ return await p(*a, **kw)
40
+ finally:
41
+ self._njobs -= 1
42
+ if not self._njobs:
43
+ self._ended.set()
44
+ self._ended = None
45
+
46
+ return await self._tg.start(_run, p, a, kw)
47
+
48
+ @abstractmethod
49
+ @asynccontextmanager
50
+ async def monitor(self, *topic):
51
+ """
52
+ Return an async iterator that listens to this topic.
53
+ """
54
+
55
+ @abstractmethod
56
+ async def send(self, *topic, payload):
57
+ """
58
+ Send this payload to this topic.
59
+ """
60
+
61
+
62
+ def get_backend(name):
63
+ from importlib import import_module
64
+
65
+ if "." not in name:
66
+ name = "moat.kv.backend." + name
67
+ return import_module(name).connect
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ from contextlib import asynccontextmanager
4
+
5
+ import anyio
6
+ from moat.mqtt.client import MQTTClient
7
+ from moat.mqtt.codecs import NoopCodec
8
+ from moat.util import NotGiven
9
+
10
+ from . import Backend
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class MqttMessage:
16
+ def __init__(self, topic, payload):
17
+ self.topic = topic
18
+ self.payload = payload
19
+
20
+
21
+ class MqttBackend(Backend):
22
+ client = None
23
+
24
+ @asynccontextmanager
25
+ async def connect(self, *a, **kw):
26
+ codec = kw.pop("codec", NotGiven)
27
+ C = MQTTClient(self._tg, codec=codec)
28
+ try:
29
+ await C.connect(*a, **kw)
30
+ self.client = C
31
+ yield self
32
+ finally:
33
+ self.client = None
34
+ with anyio.CancelScope(shield=True):
35
+ await self.aclose()
36
+ await C.disconnect()
37
+
38
+ @asynccontextmanager
39
+ async def monitor(self, *topic):
40
+ topic = "/".join(str(x) for x in topic)
41
+ logger.info("Monitor %s start", topic)
42
+ try:
43
+ async with self.client.subscription(topic) as sub:
44
+
45
+ async def sub_get(sub):
46
+ async for msg in sub:
47
+ yield MqttMessage(msg.topic.split("/"), msg.data)
48
+
49
+ yield sub_get(sub)
50
+ except anyio.get_cancelled_exc_class():
51
+ raise
52
+ except BaseException as exc:
53
+ logger.exception("Monitor %s end: %r", topic, exc)
54
+ raise
55
+ else:
56
+ logger.info("Monitor %s end", topic)
57
+
58
+ def send(self, *topic, payload): # pylint: disable=invalid-overridden-method
59
+ """
60
+ Send this payload to this topic.
61
+ """
62
+ # client.publish is also async, pass-thru
63
+ return self.client.publish("/".join(str(x) for x in topic), message=payload)
64
+
65
+
66
+ @asynccontextmanager
67
+ async def connect(**kw):
68
+ async with anyio.create_task_group() as tg:
69
+ c = MqttBackend(tg)
70
+ async with c.connect(**kw):
71
+ yield c