moat-kv 0.71.0__py3-none-any.whl → 0.71.7__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.
- moat/kv/__init__.py +6 -7
- moat/kv/_cfg.yaml +3 -2
- moat/kv/actor/__init__.py +2 -1
- moat/kv/actor/deletor.py +4 -1
- moat/kv/auth/__init__.py +12 -13
- moat/kv/auth/_test.py +4 -1
- moat/kv/auth/password.py +11 -7
- moat/kv/backend/mqtt.py +4 -5
- moat/kv/client.py +20 -39
- moat/kv/code.py +3 -3
- moat/kv/command/data.py +4 -3
- moat/kv/command/dump/__init__.py +36 -34
- moat/kv/command/internal.py +2 -3
- moat/kv/command/job.py +1 -2
- moat/kv/command/type.py +3 -6
- moat/kv/data.py +9 -8
- moat/kv/errors.py +16 -8
- moat/kv/mock/__init__.py +2 -12
- moat/kv/model.py +29 -33
- moat/kv/obj/__init__.py +3 -3
- moat/kv/obj/command.py +3 -3
- moat/kv/runner.py +4 -5
- moat/kv/server.py +106 -126
- moat/kv/types.py +10 -12
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/METADATA +6 -2
- moat_kv-0.71.7.dist-info/RECORD +47 -0
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/WHEEL +1 -1
- moat_kv-0.71.7.dist-info/licenses/LICENSE +3 -0
- moat_kv-0.71.7.dist-info/licenses/LICENSE.APACHE2 +202 -0
- moat_kv-0.71.7.dist-info/licenses/LICENSE.MIT +20 -0
- moat_kv-0.71.7.dist-info/top_level.txt +1 -0
- build/lib/docs/source/conf.py +0 -201
- build/lib/examples/pathify.py +0 -45
- build/lib/moat/kv/__init__.py +0 -19
- build/lib/moat/kv/_cfg.yaml +0 -93
- build/lib/moat/kv/_main.py +0 -91
- build/lib/moat/kv/actor/__init__.py +0 -98
- build/lib/moat/kv/actor/deletor.py +0 -139
- build/lib/moat/kv/auth/__init__.py +0 -444
- build/lib/moat/kv/auth/_test.py +0 -166
- build/lib/moat/kv/auth/password.py +0 -234
- build/lib/moat/kv/auth/root.py +0 -58
- build/lib/moat/kv/backend/__init__.py +0 -67
- build/lib/moat/kv/backend/mqtt.py +0 -71
- build/lib/moat/kv/client.py +0 -1025
- build/lib/moat/kv/code.py +0 -236
- build/lib/moat/kv/codec.py +0 -11
- build/lib/moat/kv/command/__init__.py +0 -1
- build/lib/moat/kv/command/acl.py +0 -180
- build/lib/moat/kv/command/auth.py +0 -261
- build/lib/moat/kv/command/code.py +0 -293
- build/lib/moat/kv/command/codec.py +0 -186
- build/lib/moat/kv/command/data.py +0 -265
- build/lib/moat/kv/command/dump/__init__.py +0 -143
- build/lib/moat/kv/command/error.py +0 -149
- build/lib/moat/kv/command/internal.py +0 -248
- build/lib/moat/kv/command/job.py +0 -433
- build/lib/moat/kv/command/log.py +0 -53
- build/lib/moat/kv/command/server.py +0 -114
- build/lib/moat/kv/command/type.py +0 -201
- build/lib/moat/kv/config.py +0 -46
- build/lib/moat/kv/data.py +0 -216
- build/lib/moat/kv/errors.py +0 -561
- build/lib/moat/kv/exceptions.py +0 -126
- build/lib/moat/kv/mock/__init__.py +0 -101
- build/lib/moat/kv/mock/mqtt.py +0 -159
- build/lib/moat/kv/mock/tracer.py +0 -63
- build/lib/moat/kv/model.py +0 -1069
- build/lib/moat/kv/obj/__init__.py +0 -646
- build/lib/moat/kv/obj/command.py +0 -241
- build/lib/moat/kv/runner.py +0 -1347
- build/lib/moat/kv/server.py +0 -2809
- build/lib/moat/kv/types.py +0 -513
- ci/rtd-requirements.txt +0 -4
- ci/test-requirements.txt +0 -7
- ci/travis.sh +0 -96
- debian/.gitignore +0 -7
- debian/changelog +0 -1435
- debian/control +0 -43
- debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +0 -201
- debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +0 -45
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +0 -19
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +0 -93
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +0 -91
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +0 -98
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +0 -139
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +0 -444
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +0 -166
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +0 -234
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +0 -58
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +0 -67
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +0 -71
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +0 -1025
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +0 -236
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +0 -11
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +0 -1
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +0 -180
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +0 -261
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +0 -293
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +0 -186
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +0 -265
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +0 -143
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +0 -149
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +0 -248
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +0 -433
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +0 -53
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +0 -114
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +0 -201
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +0 -46
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +0 -216
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +0 -561
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +0 -126
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +0 -101
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +0 -159
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +0 -63
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +0 -1069
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +0 -646
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +0 -241
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +0 -1347
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +0 -2809
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +0 -513
- debian/moat-kv.postinst +0 -3
- debian/rules +0 -20
- debian/source/format +0 -1
- debian/watch +0 -4
- docs/Makefile +0 -20
- docs/make.bat +0 -36
- docs/source/TODO.rst +0 -61
- docs/source/_static/.gitkeep +0 -0
- docs/source/acls.rst +0 -80
- docs/source/auth.rst +0 -84
- docs/source/client_protocol.rst +0 -456
- docs/source/code.rst +0 -341
- docs/source/command_line.rst +0 -1187
- docs/source/common_protocol.rst +0 -47
- docs/source/conf.py +0 -201
- docs/source/debugging.rst +0 -70
- docs/source/extend.rst +0 -37
- docs/source/history.rst +0 -36
- docs/source/index.rst +0 -75
- docs/source/model.rst +0 -54
- docs/source/overview.rst +0 -83
- docs/source/related.rst +0 -89
- docs/source/server_protocol.rst +0 -450
- docs/source/startup.rst +0 -31
- docs/source/translator.rst +0 -244
- docs/source/tutorial.rst +0 -711
- docs/source/v3.rst +0 -168
- examples/code/transform.scale.yml +0 -21
- examples/code/transform.switch.yml +0 -82
- examples/code/transform.timeslot.yml +0 -63
- examples/pathify.py +0 -45
- moat/kv/codec.py +0 -11
- moat_kv-0.71.0.dist-info/RECORD +0 -188
- moat_kv-0.71.0.dist-info/top_level.txt +0 -9
- scripts/current +0 -15
- scripts/env +0 -8
- scripts/init +0 -39
- scripts/recover +0 -17
- scripts/rotate +0 -33
- scripts/run +0 -29
- scripts/run-all +0 -10
- scripts/run-any +0 -10
- scripts/run-single +0 -15
- scripts/success +0 -4
- systemd/moat-kv-recover.service +0 -21
- systemd/moat-kv-rotate.service +0 -20
- systemd/moat-kv-rotate.timer +0 -10
- systemd/moat-kv-run-all.service +0 -26
- systemd/moat-kv-run-all@.service +0 -25
- systemd/moat-kv-run-any.service +0 -26
- systemd/moat-kv-run-any@.service +0 -25
- systemd/moat-kv-run-single.service +0 -26
- systemd/moat-kv-run-single@.service +0 -25
- systemd/moat-kv.service +0 -27
- systemd/postinst +0 -7
- systemd/sysusers +0 -3
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,1025 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Client code.
|
3
|
-
|
4
|
-
Main entry point: :func:`open_client`.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from __future__ import annotations
|
8
|
-
|
9
|
-
import logging
|
10
|
-
import os
|
11
|
-
import socket
|
12
|
-
from contextlib import AsyncExitStack, asynccontextmanager
|
13
|
-
from inspect import iscoroutine
|
14
|
-
|
15
|
-
import anyio
|
16
|
-
from asyncscope import Scope, main_scope, scope
|
17
|
-
from moat.util import ( # pylint: disable=no-name-in-module
|
18
|
-
CFG,
|
19
|
-
DelayedRead,
|
20
|
-
DelayedWrite,
|
21
|
-
NotGiven,
|
22
|
-
OptCtx,
|
23
|
-
PathLongener,
|
24
|
-
ValueEvent,
|
25
|
-
attrdict,
|
26
|
-
byte2num,
|
27
|
-
combine_dict,
|
28
|
-
create_queue,
|
29
|
-
ensure_cfg,
|
30
|
-
gen_ssl,
|
31
|
-
gen_ident,al_lower,
|
32
|
-
num2byte,
|
33
|
-
)
|
34
|
-
|
35
|
-
from .codec import packer, stream_unpacker
|
36
|
-
from .exceptions import (
|
37
|
-
CancelledError,
|
38
|
-
ClientAuthMethodError,
|
39
|
-
ClientAuthRequiredError,
|
40
|
-
ServerClosedError,
|
41
|
-
ServerConnectionError,
|
42
|
-
ServerError,
|
43
|
-
error_types,
|
44
|
-
)
|
45
|
-
|
46
|
-
logger = logging.getLogger(__name__)
|
47
|
-
|
48
|
-
ClosedResourceError = anyio.ClosedResourceError
|
49
|
-
|
50
|
-
__all__ = ["NoData", "ManyData", "open_client", "client_scope", "StreamedRequest"]
|
51
|
-
|
52
|
-
|
53
|
-
class AsyncValueEvent(ValueEvent):
|
54
|
-
def cancel(self):
|
55
|
-
if self.scope is not None:
|
56
|
-
self.scope.cancel()
|
57
|
-
super().set_error(CancelledError())
|
58
|
-
|
59
|
-
|
60
|
-
class NoData(ValueError):
|
61
|
-
"""No reply arrived"""
|
62
|
-
|
63
|
-
|
64
|
-
class ManyData(ValueError):
|
65
|
-
"""More than one reply arrived"""
|
66
|
-
|
67
|
-
|
68
|
-
@asynccontextmanager
|
69
|
-
async def open_client(_main_name="moat.kv.client", **cfg):
|
70
|
-
"""
|
71
|
-
This async context manager returns an opened client connection.
|
72
|
-
|
73
|
-
There is no attempt to reconnect if the connection should fail.
|
74
|
-
"""
|
75
|
-
|
76
|
-
async with OptCtx(main_scope(name=_main_name) if scope.get() is None else None):
|
77
|
-
yield await client_scope(**cfg)
|
78
|
-
|
79
|
-
|
80
|
-
async def _scoped_client(_name=None, **cfg):
|
81
|
-
"""
|
82
|
-
AsyncScope service for a client connection.
|
83
|
-
"""
|
84
|
-
client = Client(cfg)
|
85
|
-
async with client._connected() as client:
|
86
|
-
scope.register(client)
|
87
|
-
await scope.wait_no_users()
|
88
|
-
|
89
|
-
|
90
|
-
_cid = 0
|
91
|
-
|
92
|
-
|
93
|
-
async def client_scope(_name=None, **cfg):
|
94
|
-
"""
|
95
|
-
Returns an opened client connection, by way of an asyncscope service.
|
96
|
-
|
97
|
-
The configuration's 'connect' dict may include a name to disambiguate
|
98
|
-
multiple connections.
|
99
|
-
|
100
|
-
There is no attempt to reconnect if the connection should fail.
|
101
|
-
"""
|
102
|
-
|
103
|
-
if _name is None:
|
104
|
-
_name = cfg.get("conn", {}).get("name", "conn")
|
105
|
-
if _name is None:
|
106
|
-
global _cid
|
107
|
-
_cid += 1
|
108
|
-
_name = f"_{_cid}"
|
109
|
-
# uniqueness required for testing.
|
110
|
-
# TODO replace with a dependency on the test server.
|
111
|
-
return await scope.service(f"moat.kv.client.{_name}", _scoped_client, _name=_name, **cfg)
|
112
|
-
|
113
|
-
|
114
|
-
class StreamedRequest:
|
115
|
-
"""
|
116
|
-
This class represents a bidirectional multi-message request.
|
117
|
-
|
118
|
-
stream: True if you need to send a multi-message request.
|
119
|
-
Set to None if you already sent a single-message request.
|
120
|
-
report_start: True if the initial state=start message of a multi-reply
|
121
|
-
should be included in the iterator.
|
122
|
-
If False, the message is available as ``.start_msg``.
|
123
|
-
TODO: add rate limit.
|
124
|
-
|
125
|
-
Call ``.send(**params)`` to send something; call ``.recv()``
|
126
|
-
or async-iterate for receiving.
|
127
|
-
"""
|
128
|
-
|
129
|
-
start_msg = None
|
130
|
-
end_msg = None
|
131
|
-
qr = None
|
132
|
-
dw = None
|
133
|
-
|
134
|
-
def __init__(self, client, seq, stream: bool = False, report_start: bool = False):
|
135
|
-
self._stream = stream
|
136
|
-
self._client = client
|
137
|
-
self.seq = seq
|
138
|
-
self._open = False
|
139
|
-
self._client._handlers[seq] = self
|
140
|
-
self._reply_stream = None
|
141
|
-
self.n_msg = 0
|
142
|
-
self._report_start = report_start
|
143
|
-
self._started = anyio.Event()
|
144
|
-
self._path_long = lambda x: x
|
145
|
-
if client.qlen > 0:
|
146
|
-
self.dw = DelayedWrite(client.qlen)
|
147
|
-
self.qr = DelayedRead(client.qlen, get_seq=self._get_seq, send_ack=self._send_ack)
|
148
|
-
else:
|
149
|
-
self.qr = create_queue(client.config.server.buffer)
|
150
|
-
|
151
|
-
@staticmethod
|
152
|
-
def _get_seq(msg):
|
153
|
-
return msg.pop("wseq", 0)
|
154
|
-
|
155
|
-
async def _send_ack(self, seq):
|
156
|
-
msg = dict(seq=self.seq, state="ack", ack=seq)
|
157
|
-
self._client.logger.debug("Send %s", msg)
|
158
|
-
await self._client._send(**msg)
|
159
|
-
|
160
|
-
async def set(self, msg):
|
161
|
-
"""Called by the read loop to process a command's result"""
|
162
|
-
self.n_msg += 1
|
163
|
-
if "error" in msg:
|
164
|
-
logger.info("ErrorMsg: %s", msg)
|
165
|
-
try:
|
166
|
-
cls = error_types[msg["etype"]]
|
167
|
-
except KeyError:
|
168
|
-
cls = ServerError
|
169
|
-
try:
|
170
|
-
await self.qr.put_error(cls(msg.error))
|
171
|
-
except anyio.BrokenResourceError:
|
172
|
-
raise cls(msg.error)
|
173
|
-
return
|
174
|
-
self._client.logger.debug("Reply %s", msg)
|
175
|
-
state = msg.get("state", "")
|
176
|
-
|
177
|
-
if state == "start":
|
178
|
-
if self._reply_stream is not None: # pragma: no cover
|
179
|
-
raise RuntimeError("Recv state 2", self._reply_stream, msg)
|
180
|
-
self._reply_stream = True
|
181
|
-
self.start_msg = msg
|
182
|
-
self._started.set()
|
183
|
-
if self._report_start:
|
184
|
-
await self.qr.put(msg)
|
185
|
-
|
186
|
-
elif state == "end":
|
187
|
-
if self._reply_stream is not True: # pragma: no cover
|
188
|
-
raise RuntimeError("Recv state 3", self._reply_stream, msg)
|
189
|
-
self._reply_stream = None
|
190
|
-
self.end_msg = msg
|
191
|
-
if self.qr is not None:
|
192
|
-
self.qr.close_sender()
|
193
|
-
return False
|
194
|
-
|
195
|
-
elif state == "ack":
|
196
|
-
if self.dw is not None:
|
197
|
-
await self.dw.recv_ack(msg["ack"])
|
198
|
-
|
199
|
-
else:
|
200
|
-
if state not in ("", "uptodate"): # pragma: no cover
|
201
|
-
logger.warning("Unknown state: %s", msg)
|
202
|
-
|
203
|
-
if self._reply_stream is False: # pragma: no cover
|
204
|
-
raise RuntimeError("Recv state 1", self._reply_stream, msg)
|
205
|
-
elif self._reply_stream is None:
|
206
|
-
self._reply_stream = False
|
207
|
-
try:
|
208
|
-
with anyio.fail_after(1):
|
209
|
-
await self.qr.put(msg)
|
210
|
-
except (anyio.BrokenResourceError, anyio.ClosedResourceError):
|
211
|
-
logger.warning("Reader for %s closed: %s", self.seq, msg)
|
212
|
-
if self._reply_stream is False:
|
213
|
-
self.qr.close_sender()
|
214
|
-
|
215
|
-
async def get(self):
|
216
|
-
"""Receive a single reply"""
|
217
|
-
# receive reply
|
218
|
-
if self._reply_stream:
|
219
|
-
raise RuntimeError("Unexpected multi stream msg")
|
220
|
-
msg = await self.recv()
|
221
|
-
if self._reply_stream or self.n_msg != 1:
|
222
|
-
raise RuntimeError("Unexpected multi stream msg")
|
223
|
-
return msg
|
224
|
-
|
225
|
-
def __iter__(self):
|
226
|
-
raise RuntimeError("You need to use 'async for …'")
|
227
|
-
|
228
|
-
__next__ = __iter__
|
229
|
-
|
230
|
-
def __aiter__(self):
|
231
|
-
return self
|
232
|
-
|
233
|
-
async def __anext__(self):
|
234
|
-
try:
|
235
|
-
res = await self.qr.get()
|
236
|
-
except (anyio.EndOfStream, anyio.ClosedResourceError, EOFError):
|
237
|
-
raise StopAsyncIteration
|
238
|
-
except CancelledError:
|
239
|
-
raise StopAsyncIteration # just terminate
|
240
|
-
self._path_long(res)
|
241
|
-
return res
|
242
|
-
|
243
|
-
async def send(self, **msg):
|
244
|
-
# self._client.logger.debug("Send %s", msg)
|
245
|
-
if not self._open:
|
246
|
-
if self._stream:
|
247
|
-
msg["state"] = "start"
|
248
|
-
self._open = True
|
249
|
-
elif msg.get("state", "") == "end":
|
250
|
-
self._open = False
|
251
|
-
|
252
|
-
if self.dw is not None:
|
253
|
-
msg["wseq"] = await self.dw.next_seq()
|
254
|
-
msg["seq"] = self.seq
|
255
|
-
self._client.logger.debug("Send %s", msg)
|
256
|
-
await self._client._send(**msg)
|
257
|
-
|
258
|
-
async def recv(self):
|
259
|
-
return await self.__anext__()
|
260
|
-
|
261
|
-
async def cancel(self):
|
262
|
-
try:
|
263
|
-
await self.qr.put_error(CancelledError())
|
264
|
-
except (anyio.BrokenResourceError, anyio.ClosedResourceError, EOFError):
|
265
|
-
pass
|
266
|
-
else:
|
267
|
-
try:
|
268
|
-
await self.aclose()
|
269
|
-
except ServerClosedError:
|
270
|
-
pass
|
271
|
-
|
272
|
-
async def wait_started(self):
|
273
|
-
await self._started.wait()
|
274
|
-
|
275
|
-
async def aclose(self):
|
276
|
-
try:
|
277
|
-
if self._stream:
|
278
|
-
msg = dict(seq=self.seq, state="end")
|
279
|
-
# self._client.logger.debug("SendE %s", msg)
|
280
|
-
await self._client._send(**msg)
|
281
|
-
if self._open:
|
282
|
-
msg = dict(action="stop", task=self.seq)
|
283
|
-
# self._client.logger.debug("SendC %s", msg)
|
284
|
-
try:
|
285
|
-
await self._client._request(**msg, _async=True)
|
286
|
-
except (ServerClosedError, anyio.BrokenResourceError):
|
287
|
-
pass
|
288
|
-
# ignore the reply
|
289
|
-
finally:
|
290
|
-
self.qr.close_sender()
|
291
|
-
self.qr.close_receiver()
|
292
|
-
|
293
|
-
|
294
|
-
class _SingleReply:
|
295
|
-
"""
|
296
|
-
This class represents a single-message reply.
|
297
|
-
It will delegate itself to a StreamedRequest if a multi message reply
|
298
|
-
arrives.
|
299
|
-
"""
|
300
|
-
|
301
|
-
def __init__(self, conn, seq, params):
|
302
|
-
self._conn = conn
|
303
|
-
self.seq = seq
|
304
|
-
self.q = AsyncValueEvent()
|
305
|
-
self._params = params
|
306
|
-
|
307
|
-
async def set(self, msg):
|
308
|
-
"""Called by the read loop to process a command's result"""
|
309
|
-
if msg.get("state") == "start":
|
310
|
-
res = StreamedRequest(self._conn, self.seq, stream=None)
|
311
|
-
await res.set(msg)
|
312
|
-
self.q.set(res)
|
313
|
-
return res
|
314
|
-
elif "error" in msg:
|
315
|
-
msg["request_params"] = self._params
|
316
|
-
logger.info("ErrorMsg: %s", msg)
|
317
|
-
self.q.set_error(ServerError(msg.error))
|
318
|
-
else:
|
319
|
-
self.q.set(msg)
|
320
|
-
return False
|
321
|
-
|
322
|
-
async def get(self):
|
323
|
-
"""Wait for and return the result.
|
324
|
-
|
325
|
-
This is a coroutine.
|
326
|
-
"""
|
327
|
-
return await self.q.get()
|
328
|
-
|
329
|
-
async def cancel(self):
|
330
|
-
pass
|
331
|
-
|
332
|
-
|
333
|
-
class ClientConfig:
|
334
|
-
"""Accessor for configuration, possibly stored in MoaT-KV."""
|
335
|
-
|
336
|
-
_changed = None # pylint
|
337
|
-
|
338
|
-
def __init__(self, client, *a, **k): # pylint: disable=unused-argument
|
339
|
-
self._init(client)
|
340
|
-
|
341
|
-
def _init(self, client):
|
342
|
-
self._client = client
|
343
|
-
self._current = {}
|
344
|
-
self._changed = anyio.Event()
|
345
|
-
|
346
|
-
def __getattr__(self, k):
|
347
|
-
if k.startswith("_"):
|
348
|
-
return object.__getattribute__(self, k)
|
349
|
-
v = self._current.get(k, NotGiven)
|
350
|
-
if v is NotGiven:
|
351
|
-
try:
|
352
|
-
v = self._client._cfg[k]
|
353
|
-
except KeyError:
|
354
|
-
raise AttributeError(k) from None
|
355
|
-
return v
|
356
|
-
|
357
|
-
def __contains__(self, k):
|
358
|
-
return k in self._current or k in self._client._cfg
|
359
|
-
|
360
|
-
async def _update(self, k, v):
|
361
|
-
"""
|
362
|
-
Update this config entry. The new data is combined with the static
|
363
|
-
configuration; the old data is discarded.
|
364
|
-
"""
|
365
|
-
self._current[k] = combine_dict(v, self._client._cfg.get(k, {}))
|
366
|
-
c, self._changed = self._changed, anyio.Event()
|
367
|
-
c.set()
|
368
|
-
|
369
|
-
async def _watch(self):
|
370
|
-
class CfgWatcher:
|
371
|
-
def __ainit__(slf): # pylint: disable=no-self-argument
|
372
|
-
return slf
|
373
|
-
|
374
|
-
async def __anext__(slf): # pylint: disable=no-self-argument
|
375
|
-
await self._changed.wait()
|
376
|
-
|
377
|
-
return CfgWatcher()
|
378
|
-
|
379
|
-
|
380
|
-
class Client:
|
381
|
-
"""
|
382
|
-
The client side of a MoaT-KV connection.
|
383
|
-
|
384
|
-
Use `open_client` or `client_scope` to use this class.
|
385
|
-
"""
|
386
|
-
|
387
|
-
_server_init = None # Server greeting
|
388
|
-
_dh_key = None
|
389
|
-
_config = None
|
390
|
-
_socket = None
|
391
|
-
tg: anyio.abc.TaskGroup = None
|
392
|
-
scope: Scope = None
|
393
|
-
_n = None
|
394
|
-
exit_stack = None
|
395
|
-
|
396
|
-
server_name = None
|
397
|
-
client_name = None
|
398
|
-
qlen: int = 0
|
399
|
-
|
400
|
-
def __init__(self, cfg: dict):
|
401
|
-
ensure_cfg("moat.kv")
|
402
|
-
self._cfg = combine_dict(cfg, CFG["kv"], cls=attrdict)
|
403
|
-
self.config = ClientConfig(self)
|
404
|
-
|
405
|
-
self._seq = 0
|
406
|
-
self._handlers = {}
|
407
|
-
self._send_lock = anyio.Lock()
|
408
|
-
self._helpers = {}
|
409
|
-
self._name = gen_ident(9, alphabet=al_lower)
|
410
|
-
self.logger = logging.getLogger(f"moat.kv.client.{self._name}")
|
411
|
-
|
412
|
-
@property
|
413
|
-
def name(self):
|
414
|
-
return self._name
|
415
|
-
|
416
|
-
@property
|
417
|
-
def node(self):
|
418
|
-
return self._server_init["node"]
|
419
|
-
|
420
|
-
async def get_tock(self):
|
421
|
-
"""Fetch the next tock value from the server."""
|
422
|
-
m = await self._request("get_tock")
|
423
|
-
return m.tock
|
424
|
-
|
425
|
-
async def unique_helper(self, name, factory):
|
426
|
-
"""
|
427
|
-
Run a (single) async context manager as a service.
|
428
|
-
"""
|
429
|
-
|
430
|
-
async def with_factory(f):
|
431
|
-
async with f() as r:
|
432
|
-
scope.register(r)
|
433
|
-
await scope.no_more_dependents()
|
434
|
-
|
435
|
-
return await scope.service(name, with_factory, factory)
|
436
|
-
|
437
|
-
async def _handle_msg(self, msg):
|
438
|
-
try:
|
439
|
-
seq = msg.seq
|
440
|
-
except AttributeError:
|
441
|
-
if "error" in msg:
|
442
|
-
raise RuntimeError("Server error", msg.error) from None
|
443
|
-
raise RuntimeError("Reader got out of sync: " + str(msg)) from None
|
444
|
-
try:
|
445
|
-
hdl = self._handlers[seq]
|
446
|
-
except KeyError:
|
447
|
-
logger.warning("Spurious message %s: %s", seq, msg)
|
448
|
-
return
|
449
|
-
|
450
|
-
res = hdl.set(msg)
|
451
|
-
if iscoroutine(res):
|
452
|
-
res = await res
|
453
|
-
elif res.__class__.__name__ == "DeprecatedAwaitable":
|
454
|
-
res = None
|
455
|
-
if res is False:
|
456
|
-
del self._handlers[seq]
|
457
|
-
elif res:
|
458
|
-
self._handlers[seq] = res
|
459
|
-
|
460
|
-
async def dh_secret(self, length=1024):
|
461
|
-
"""Exchange a diffie-hellman secret with the server"""
|
462
|
-
if self._dh_key is None:
|
463
|
-
from moat.lib.diffiehellman import DiffieHellman
|
464
|
-
|
465
|
-
def gen_key():
|
466
|
-
k = DiffieHellman(key_length=length, group=(5 if length < 32 else 14))
|
467
|
-
k.generate_public_key()
|
468
|
-
return k
|
469
|
-
|
470
|
-
k = await anyio.to_thread.run_sync(gen_key)
|
471
|
-
res = await self._request(
|
472
|
-
"diffie_hellman",
|
473
|
-
pubkey=num2byte(k.public_key),
|
474
|
-
length=length,
|
475
|
-
) # length=k.key_length
|
476
|
-
await anyio.to_thread.run_sync(k.generate_shared_secret, byte2num(res.pubkey))
|
477
|
-
self._dh_key = num2byte(k.shared_secret)[0:32]
|
478
|
-
return self._dh_key
|
479
|
-
|
480
|
-
async def _send(self, **params):
|
481
|
-
async with self._send_lock:
|
482
|
-
sock = self._socket
|
483
|
-
if sock is None:
|
484
|
-
raise ServerClosedError("Disconnected")
|
485
|
-
|
486
|
-
try:
|
487
|
-
p = packer(params)
|
488
|
-
except TypeError as e:
|
489
|
-
raise ValueError(f"Unable to pack: {params!r}") from e
|
490
|
-
await sock.send(p)
|
491
|
-
|
492
|
-
async def _reader(self, *, evt=None):
|
493
|
-
"""Main loop for reading"""
|
494
|
-
unpacker = stream_unpacker()
|
495
|
-
|
496
|
-
with anyio.CancelScope():
|
497
|
-
# XXX store the scope so that the redaer may get cancelled?
|
498
|
-
if evt is not None:
|
499
|
-
await evt.set()
|
500
|
-
try:
|
501
|
-
while True:
|
502
|
-
for msg in unpacker:
|
503
|
-
# self.logger.debug("Recv %s", msg)
|
504
|
-
try:
|
505
|
-
await self._handle_msg(msg)
|
506
|
-
except ClosedResourceError as exc:
|
507
|
-
logger.warning("Reader closed in handler", exc_info=exc)
|
508
|
-
return
|
509
|
-
|
510
|
-
if self._socket is None:
|
511
|
-
logger.warning("Reader socket closed")
|
512
|
-
break
|
513
|
-
try:
|
514
|
-
buf = await self._socket.receive(4096)
|
515
|
-
except anyio.EndOfStream:
|
516
|
-
raise ServerClosedError("Connection closed by peer")
|
517
|
-
except ClosedResourceError:
|
518
|
-
return # closed by us
|
519
|
-
if len(buf) == 0: # Connection was closed.
|
520
|
-
raise ServerClosedError("Connection closed by peer")
|
521
|
-
unpacker.feed(buf)
|
522
|
-
|
523
|
-
except BaseException as exc:
|
524
|
-
logger.warning("Reader died: %r", exc, exc_info=exc)
|
525
|
-
raise
|
526
|
-
finally:
|
527
|
-
with anyio.fail_after(2, shield=True):
|
528
|
-
hdl, self._handlers = self._handlers, None
|
529
|
-
for m in hdl.values():
|
530
|
-
try:
|
531
|
-
res = m.cancel()
|
532
|
-
if iscoroutine(res):
|
533
|
-
await res
|
534
|
-
except ClosedResourceError:
|
535
|
-
pass
|
536
|
-
|
537
|
-
async def _request(self, action, iter=None, seq=None, _async=False, **params): # pylint: disable=redefined-builtin # iter
|
538
|
-
"""Send a request. Wait for a reply.
|
539
|
-
|
540
|
-
Args:
|
541
|
-
action (str): what to do. If ``seq`` is set, this is the stream's
|
542
|
-
state, which should be ``None`` or ``'end'``.
|
543
|
-
seq: Sequence number to use. Only when terminating a
|
544
|
-
multi-message request.
|
545
|
-
_async: don't wait for a reply (internal!)
|
546
|
-
params: whatever other data the action needs
|
547
|
-
iter: A flag how to treat multi-line replies.
|
548
|
-
``True``: always return an iterator
|
549
|
-
``False``: Never return an iterator, raise an error
|
550
|
-
if no or more than on reply arrives
|
551
|
-
Default: ``None``: return a StreamedRequest if multi-line
|
552
|
-
otherwise return directly
|
553
|
-
|
554
|
-
Any other keywords are forwarded to the server.
|
555
|
-
"""
|
556
|
-
if self._handlers is None:
|
557
|
-
raise ClosedResourceError()
|
558
|
-
if seq is None:
|
559
|
-
act = "action"
|
560
|
-
self._seq += 1
|
561
|
-
seq = self._seq
|
562
|
-
else:
|
563
|
-
act = "state"
|
564
|
-
|
565
|
-
if action is not None:
|
566
|
-
params[act] = action
|
567
|
-
params["seq"] = seq
|
568
|
-
res = _SingleReply(self, seq, params)
|
569
|
-
self._handlers[seq] = res
|
570
|
-
|
571
|
-
self.logger.debug("Send %s", params)
|
572
|
-
await self._send(**params)
|
573
|
-
if _async:
|
574
|
-
return res
|
575
|
-
|
576
|
-
res = await res.get()
|
577
|
-
if isinstance(res, dict):
|
578
|
-
self.logger.debug("Result %s", res)
|
579
|
-
|
580
|
-
if iter is True and not isinstance(res, StreamedRequest):
|
581
|
-
|
582
|
-
async def send_one(res):
|
583
|
-
yield res
|
584
|
-
|
585
|
-
res = send_one(res)
|
586
|
-
|
587
|
-
elif iter is False and isinstance(res, StreamedRequest):
|
588
|
-
rr = None
|
589
|
-
try:
|
590
|
-
async for r in res:
|
591
|
-
if rr is not None:
|
592
|
-
raise ManyData(action)
|
593
|
-
rr = r
|
594
|
-
finally:
|
595
|
-
await res.aclose()
|
596
|
-
if rr is None:
|
597
|
-
raise NoData(action)
|
598
|
-
res = rr
|
599
|
-
return res
|
600
|
-
|
601
|
-
@asynccontextmanager
|
602
|
-
async def _stream(self, action, stream=False, **params):
|
603
|
-
"""Send and receive a multi-message request.
|
604
|
-
|
605
|
-
Args:
|
606
|
-
``action``: what to do
|
607
|
-
``params``: whatever other data the action needs
|
608
|
-
``stream``: whether to enable multi-line requests
|
609
|
-
via ``await stream.send(**params)``
|
610
|
-
|
611
|
-
This is a context manager. Use it like this::
|
612
|
-
|
613
|
-
async with client._stream("update", path=P("private.storage"),
|
614
|
-
stream=True) as req:
|
615
|
-
with MsgReader("/tmp/msgs.pack") as f:
|
616
|
-
for msg in f:
|
617
|
-
await req.send(msg)
|
618
|
-
# … or …
|
619
|
-
async with client._stream("get_tree", path=P("private.storage)) as req:
|
620
|
-
for msg in req:
|
621
|
-
await process_entry(msg)
|
622
|
-
# … or maybe … (auth does this)
|
623
|
-
async with client._stream("interactive_thing", path=P(':n.foo)) as req:
|
624
|
-
msg = await req.recv()
|
625
|
-
while msg.get(s,"") == "more":
|
626
|
-
await foo.send(s="more",value="some data")
|
627
|
-
msg = await req.recv()
|
628
|
-
await foo.send(s="that's all then")
|
629
|
-
|
630
|
-
Any server-side exception will be raised on recv.
|
631
|
-
|
632
|
-
The server-side command will be killed if you leave the loop
|
633
|
-
without having read a "state=end" message.
|
634
|
-
"""
|
635
|
-
self._seq += 1
|
636
|
-
seq = self._seq
|
637
|
-
|
638
|
-
# self.logger.debug("Send %s", params)
|
639
|
-
if self._handlers is None:
|
640
|
-
raise ClosedResourceError("Closed already")
|
641
|
-
res = StreamedRequest(self, seq, stream=stream)
|
642
|
-
if "path" in params and params.get("long_path", False):
|
643
|
-
res._path_long = PathLongener(params["path"])
|
644
|
-
await res.send(action=action, **params)
|
645
|
-
await res.wait_started()
|
646
|
-
try:
|
647
|
-
yield res
|
648
|
-
except BaseException as exc:
|
649
|
-
if stream:
|
650
|
-
try:
|
651
|
-
await res.send(error=repr(exc))
|
652
|
-
except anyio.ClosedResourceError:
|
653
|
-
pass
|
654
|
-
raise
|
655
|
-
finally:
|
656
|
-
with anyio.fail_after(2, shield=True):
|
657
|
-
try:
|
658
|
-
await res.aclose()
|
659
|
-
except anyio.ClosedResourceError:
|
660
|
-
pass
|
661
|
-
|
662
|
-
async def _run_auth(self, auth=None):
|
663
|
-
"""
|
664
|
-
As the name implies: process authorization.
|
665
|
-
"""
|
666
|
-
hello = self._server_init
|
667
|
-
sa = hello.get("auth", ())
|
668
|
-
if not sa or not sa[0]:
|
669
|
-
# no auth required
|
670
|
-
if auth:
|
671
|
-
logger.info("Tried to use auth=%s, but not required.", auth._auth_method)
|
672
|
-
return
|
673
|
-
if not auth:
|
674
|
-
raise ClientAuthRequiredError("You need to log in using:", sa[0])
|
675
|
-
if auth._auth_method != sa[0]:
|
676
|
-
raise ClientAuthMethodError(f"You cannot use {auth._auth_method!r} auth", sa)
|
677
|
-
if getattr(auth, "_DEBUG", False):
|
678
|
-
auth._length = 16
|
679
|
-
await auth.auth(self)
|
680
|
-
|
681
|
-
@asynccontextmanager
|
682
|
-
async def _connected(self):
|
683
|
-
"""
|
684
|
-
This async context manager handles the actual TCP connection to
|
685
|
-
the MoaT-KV server.
|
686
|
-
"""
|
687
|
-
hello = AsyncValueEvent()
|
688
|
-
self._handlers[0] = hello
|
689
|
-
|
690
|
-
cfg = self._cfg["conn"]
|
691
|
-
host = cfg["host"]
|
692
|
-
port = cfg["port"]
|
693
|
-
auth = cfg["auth"]
|
694
|
-
if auth is not None:
|
695
|
-
from .auth import gen_auth
|
696
|
-
|
697
|
-
auth = gen_auth(auth)
|
698
|
-
init_timeout = cfg["init_timeout"]
|
699
|
-
ssl = gen_ssl(cfg["ssl"], server=False)
|
700
|
-
|
701
|
-
# self.logger.debug("Conn %s %s",self.host,self.port)
|
702
|
-
try:
|
703
|
-
ctx = await anyio.connect_tcp(host, port)
|
704
|
-
except socket.gaierror:
|
705
|
-
raise ServerConnectionError(host, port)
|
706
|
-
if ssl:
|
707
|
-
raise NotImplementedError("XXX TODO fix SSL")
|
708
|
-
# ctx = await anyio.streams.tls.TLSStream(ctx, ssl_context=ssl, server_side=False)
|
709
|
-
try:
|
710
|
-
async with ctx as stream, AsyncExitStack() as ex:
|
711
|
-
self.scope = scope.get()
|
712
|
-
# self.tg = tg # TODO might not be necessary
|
713
|
-
self.exit_stack = ex
|
714
|
-
|
715
|
-
try:
|
716
|
-
self._socket = stream
|
717
|
-
await self.scope.spawn(self._reader)
|
718
|
-
with anyio.fail_after(init_timeout):
|
719
|
-
self._server_init = msg = await hello.get()
|
720
|
-
self.logger.debug("Hello %s", msg)
|
721
|
-
self.server_name = msg.node
|
722
|
-
self.client_name = cfg["name"] or socket.gethostname() or self.server_name
|
723
|
-
if "qlen" in msg:
|
724
|
-
self.qlen = min(msg.qlen, 99) # self.config.server.buffer
|
725
|
-
await self._send(seq=0, qlen=self.qlen)
|
726
|
-
await self._run_auth(auth)
|
727
|
-
|
728
|
-
from .config import ConfigRoot
|
729
|
-
|
730
|
-
self._config = await ConfigRoot.as_handler(self, require_client=False)
|
731
|
-
|
732
|
-
except TimeoutError:
|
733
|
-
raise
|
734
|
-
except OSError as e:
|
735
|
-
raise ServerConnectionError(host, port) from e
|
736
|
-
else:
|
737
|
-
yield self
|
738
|
-
finally:
|
739
|
-
# Clean up our hacked config
|
740
|
-
try:
|
741
|
-
del self._config
|
742
|
-
except AttributeError:
|
743
|
-
pass
|
744
|
-
self.config = ClientConfig(self)
|
745
|
-
finally:
|
746
|
-
self._socket = None
|
747
|
-
self.tg = None
|
748
|
-
|
749
|
-
# externally visible interface ##########################
|
750
|
-
|
751
|
-
def get(self, path, *, nchain=0):
|
752
|
-
"""
|
753
|
-
Retrieve the data at a particular subtree position.
|
754
|
-
|
755
|
-
Usage::
|
756
|
-
res = await client.get(P("foo.bar"))
|
757
|
-
|
758
|
-
If you want to update this value, you should retrieve its change chain entry
|
759
|
-
so that a competing update can be detected::
|
760
|
-
|
761
|
-
res = await client.get("foo","bar", nchain=-1)
|
762
|
-
res = await client.set("foo","bar", value=res.value+1, chain=res.chain)
|
763
|
-
|
764
|
-
For lower overhead and set-directly-after-get change, nchain may be 1 or 2.
|
765
|
-
|
766
|
-
Arguments:
|
767
|
-
path (Path): the path to update.
|
768
|
-
nchain: set to retrieve the node's chain tag, for later updates.
|
769
|
-
"""
|
770
|
-
if isinstance(path, str):
|
771
|
-
raise RuntimeError("You need a path, not a string")
|
772
|
-
return self._request(action="get_value", path=path, iter=False, nchain=nchain)
|
773
|
-
|
774
|
-
def set(
|
775
|
-
self,
|
776
|
-
path,
|
777
|
-
value=NotGiven,
|
778
|
-
*,
|
779
|
-
chain=NotGiven,
|
780
|
-
prev=NotGiven,
|
781
|
-
nchain=0,
|
782
|
-
idem=None,
|
783
|
-
):
|
784
|
-
"""
|
785
|
-
Set or update a value.
|
786
|
-
|
787
|
-
Usage::
|
788
|
-
await client.set(P("foo.bar"), value="baz", chain=None)
|
789
|
-
|
790
|
-
Arguments:
|
791
|
-
path (Path): the path to update.
|
792
|
-
value: the value to set. Duh. ;-)
|
793
|
-
chain: the previous value's change chain. Use ``None`` for new values.
|
794
|
-
prev: the previous value. Discouraged; use ``chain`` instead.
|
795
|
-
nchain: set to retrieve the node's chain tag, for further updates.
|
796
|
-
idem: if True, no-op if the value doesn't change
|
797
|
-
"""
|
798
|
-
if isinstance(path, str):
|
799
|
-
raise RuntimeError("You need a path, not a string")
|
800
|
-
if value is NotGiven:
|
801
|
-
raise RuntimeError("You need to supply a value, or call 'delete'")
|
802
|
-
|
803
|
-
kw = {}
|
804
|
-
if prev is not NotGiven:
|
805
|
-
kw["prev"] = prev
|
806
|
-
if chain is not NotGiven:
|
807
|
-
kw["chain"] = chain
|
808
|
-
if idem is not None:
|
809
|
-
kw["idem"] = idem
|
810
|
-
|
811
|
-
return self._request(
|
812
|
-
action="set_value",
|
813
|
-
path=path,
|
814
|
-
value=value,
|
815
|
-
iter=False,
|
816
|
-
nchain=nchain,
|
817
|
-
**kw,
|
818
|
-
)
|
819
|
-
|
820
|
-
def delete(self, path, *, chain=NotGiven, prev=NotGiven, nchain=0, recursive=False):
|
821
|
-
"""
|
822
|
-
Delete a node.
|
823
|
-
|
824
|
-
Usage::
|
825
|
-
await client.delete(P("foo.bar"))
|
826
|
-
|
827
|
-
Arguments:
|
828
|
-
path (Path): the path of the entry to remove.
|
829
|
-
chain: the previous value's change chain.
|
830
|
-
prev: the previous value. Discouraged; use ``chain`` instead.
|
831
|
-
nchain: set to retrieve the node's chain, for setting a new value.
|
832
|
-
recursive: delete the whole subtree. Cannot be used with
|
833
|
-
``chain`` and/or ``prev``.
|
834
|
-
"""
|
835
|
-
if isinstance(path, str):
|
836
|
-
raise RuntimeError("You need a path, not a string")
|
837
|
-
kw = {}
|
838
|
-
if prev is not NotGiven:
|
839
|
-
kw["prev"] = prev
|
840
|
-
if chain is not NotGiven:
|
841
|
-
kw["chain"] = chain
|
842
|
-
|
843
|
-
return self._request(
|
844
|
-
action="delete_tree" if recursive else "delete_value",
|
845
|
-
path=path,
|
846
|
-
iter=False,
|
847
|
-
nchain=nchain,
|
848
|
-
**kw,
|
849
|
-
)
|
850
|
-
|
851
|
-
async def list(self, path, *, with_data=False, empty=None, **kw):
|
852
|
-
"""
|
853
|
-
Retrieve the next data level.
|
854
|
-
|
855
|
-
Args:
|
856
|
-
path (Path): the path to retrieve the entries from.
|
857
|
-
with_data (bool): Return the data along with the keys. Default False.
|
858
|
-
empty (bool): Return [names of] empty nodes. Default True if
|
859
|
-
with_data is not set.
|
860
|
-
"""
|
861
|
-
if isinstance(path, str):
|
862
|
-
raise RuntimeError("You need a path, not a string")
|
863
|
-
if empty is None:
|
864
|
-
empty = not with_data
|
865
|
-
res = await self._request(action="enum", path=path, with_data=with_data, empty=empty, **kw)
|
866
|
-
try:
|
867
|
-
return res.result
|
868
|
-
except AttributeError:
|
869
|
-
raise res.q.value.error from None # XXX fix this
|
870
|
-
|
871
|
-
async def get_tree(self, path, *, long_path=True, **kw):
|
872
|
-
"""
|
873
|
-
Retrieve a complete MoaT-KV subtree.
|
874
|
-
|
875
|
-
This call results in a stream of tree nodes. Storage of these nodes,
|
876
|
-
if required, is up to the caller. Also, the server does not
|
877
|
-
take a snapshot for you, thus the data may be inconsistent.
|
878
|
-
|
879
|
-
Use :meth:`mirror` if you want this tree to be kept up-to-date or
|
880
|
-
if you need a consistent snapshot.
|
881
|
-
|
882
|
-
Args:
|
883
|
-
path (Path): the path to retrieve the entries from.
|
884
|
-
nchain (int): Length of change chain to add to the results, for updating.
|
885
|
-
min_depth (int): min level of nodes to retrieve.
|
886
|
-
max_depth (int): max level of nodes to retrieve.
|
887
|
-
long_path (bool): if set (the default), pass the result through PathLongener
|
888
|
-
|
889
|
-
"""
|
890
|
-
if isinstance(path, str):
|
891
|
-
raise RuntimeError("You need a path, not a string")
|
892
|
-
if long_path:
|
893
|
-
lp = PathLongener()
|
894
|
-
async for r in await self._request(
|
895
|
-
action="get_tree",
|
896
|
-
path=path,
|
897
|
-
iter=True,
|
898
|
-
long_path=True,
|
899
|
-
**kw,
|
900
|
-
):
|
901
|
-
if long_path:
|
902
|
-
lp(r)
|
903
|
-
yield r
|
904
|
-
|
905
|
-
def delete_tree(self, path, *, nchain=0):
|
906
|
-
"""
|
907
|
-
Delete a whole subtree.
|
908
|
-
|
909
|
-
If you set ``nchain``, this call will return an async iterator over
|
910
|
-
the deleted nodes; if not, the single return value only contains the
|
911
|
-
number of deleted nodes.
|
912
|
-
"""
|
913
|
-
if isinstance(path, str):
|
914
|
-
raise RuntimeError("You need a path, not a string")
|
915
|
-
return self._request(action="delete_tree", path=path, nchain=nchain)
|
916
|
-
|
917
|
-
def stop(self, seq: int):
|
918
|
-
"""End this stream or request.
|
919
|
-
|
920
|
-
Args:
|
921
|
-
seq: the sequence number of the request in question.
|
922
|
-
|
923
|
-
TODO: MoaT-KV doesn't do per-command flow control yet, so you should
|
924
|
-
call this method from a different task if you don't want to risk a
|
925
|
-
deadlock.
|
926
|
-
"""
|
927
|
-
return self._request(action="stop", task=seq)
|
928
|
-
|
929
|
-
def watch(self, path, *, long_path=True, **kw):
|
930
|
-
"""
|
931
|
-
Return an async iterator of changes to a subtree.
|
932
|
-
|
933
|
-
Args:
|
934
|
-
path (Path): the path to monitor entries at.
|
935
|
-
fetch (bool): if ``True``, also send the currect state. Be aware
|
936
|
-
that this may overlap with processing changes: you may get
|
937
|
-
updates before the current state is completely transmitted.
|
938
|
-
nchain: add the nodes' change chains.
|
939
|
-
min_depth (int): min level of nodes to retrieve.
|
940
|
-
max_depth (int): max level of nodes to retrieve.
|
941
|
-
|
942
|
-
The result should be passed through a :class:`moat.kv.util.PathLongener`.
|
943
|
-
|
944
|
-
If ``fetch`` is set, a ``state="uptodate"`` message will be sent
|
945
|
-
as soon as sending the current state is completed.
|
946
|
-
|
947
|
-
MoaT-KV will not send stale data, so you may always replace a path's
|
948
|
-
old cached state with the newly-arrived data.
|
949
|
-
"""
|
950
|
-
if isinstance(path, str):
|
951
|
-
raise RuntimeError("You need a path, not a string")
|
952
|
-
return self._stream(action="watch", path=path, iter=True, long_path=long_path, **kw)
|
953
|
-
|
954
|
-
def mirror(self, path, *, root_type=None, **kw):
|
955
|
-
"""An async context manager that affords an update-able mirror
|
956
|
-
of part of a MoaT-KV store.
|
957
|
-
|
958
|
-
Arguments:
|
959
|
-
root_type (type): The class to use for the root. Must be
|
960
|
-
:class:`MirrorRoot` or a :class:`ClientRoot` subclass.
|
961
|
-
|
962
|
-
Returns: the root of this tree.
|
963
|
-
|
964
|
-
Usage::
|
965
|
-
async with moat.kv.open_client() as c:
|
966
|
-
async with c.mirror("foo", "bar", need_wait=True) as foobar:
|
967
|
-
r = await c.set_value("foo", "bar", "baz", value="test")
|
968
|
-
await foobar.wait_chain(r.chain)
|
969
|
-
assert foobar['baz'].value == "test"
|
970
|
-
pass
|
971
|
-
# At this point you can still access the tree's data
|
972
|
-
# via ``foobar``, but they will no longer be kept up-to-date.
|
973
|
-
|
974
|
-
"""
|
975
|
-
if isinstance(path, str):
|
976
|
-
raise RuntimeError("You need a path, not a string")
|
977
|
-
if root_type is None:
|
978
|
-
from .obj import MirrorRoot
|
979
|
-
|
980
|
-
root_type = MirrorRoot
|
981
|
-
root = root_type(self, path, **kw)
|
982
|
-
return root.run()
|
983
|
-
|
984
|
-
def msg_monitor(self, topic: tuple[str], raw: bool = False):
|
985
|
-
"""
|
986
|
-
Return an async iterator of tunneled messages. This receives
|
987
|
-
all messages sent using :meth:`msg_send` with the same topic.
|
988
|
-
|
989
|
-
Args:
|
990
|
-
topic: the topic to monitor.
|
991
|
-
raw: If ``True``, will not try to msgpack-decode incoming messages.
|
992
|
-
|
993
|
-
Returns: an iterator yielding a dict.
|
994
|
-
topic: actual topic the message was received at.
|
995
|
-
data: decoded data. Not present when ``raw`` is set or the
|
996
|
-
decoder raised an exception.
|
997
|
-
raw: un-decoded data. Not present when '`raw`` is not set and
|
998
|
-
decoding succeeded.
|
999
|
-
error: Error message. Not present when ``raw`` is set or
|
1000
|
-
``data`` is present.
|
1001
|
-
usage::
|
1002
|
-
async with client.msg_monitor(["test"]) as cl:
|
1003
|
-
async for msg in cl:
|
1004
|
-
if 'error' in msg:
|
1005
|
-
raise RuntimeError(msg.error)
|
1006
|
-
await process_test(msg.data)
|
1007
|
-
"""
|
1008
|
-
return self._stream(action="msg_monitor", topic=topic, raw=raw)
|
1009
|
-
|
1010
|
-
def msg_send(self, topic: tuple[str], data=None, raw: bytes = None):
|
1011
|
-
"""
|
1012
|
-
Tunnel a user-tagged message. This sends the message
|
1013
|
-
to all active callers of :meth:`msg_monitor` which use the same topic.
|
1014
|
-
|
1015
|
-
Args:
|
1016
|
-
topic: the MQTT topic to send to.
|
1017
|
-
data: to-be-encoded data (anything ``msgpack`` can process).
|
1018
|
-
raw: raw binary data to send, mutually exclusive with ``data``.
|
1019
|
-
"""
|
1020
|
-
if raw is None:
|
1021
|
-
return self._request(action="msg_send", topic=topic, data=data)
|
1022
|
-
elif data is not None and data is not NotGiven:
|
1023
|
-
raise RuntimeError("You can either send raw or non-raw data")
|
1024
|
-
else:
|
1025
|
-
return self._request(action="msg_send", topic=topic, raw=raw)
|