moat-kv 0.70.24__py3-none-any.whl → 0.71.6__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 (127) hide show
  1. moat/kv/__init__.py +6 -7
  2. moat/kv/_cfg.yaml +5 -8
  3. moat/kv/actor/__init__.py +2 -1
  4. moat/kv/actor/deletor.py +4 -1
  5. moat/kv/auth/__init__.py +12 -13
  6. moat/kv/auth/_test.py +4 -1
  7. moat/kv/auth/password.py +11 -7
  8. moat/kv/backend/mqtt.py +4 -8
  9. moat/kv/client.py +20 -39
  10. moat/kv/code.py +3 -3
  11. moat/kv/command/data.py +4 -3
  12. moat/kv/command/dump/__init__.py +29 -29
  13. moat/kv/command/internal.py +2 -3
  14. moat/kv/command/job.py +1 -2
  15. moat/kv/command/type.py +3 -6
  16. moat/kv/data.py +9 -8
  17. moat/kv/errors.py +16 -8
  18. moat/kv/mock/__init__.py +2 -12
  19. moat/kv/model.py +28 -32
  20. moat/kv/obj/__init__.py +3 -3
  21. moat/kv/obj/command.py +3 -3
  22. moat/kv/runner.py +4 -5
  23. moat/kv/server.py +106 -126
  24. moat/kv/types.py +8 -6
  25. {moat_kv-0.70.24.dist-info → moat_kv-0.71.6.dist-info}/METADATA +7 -6
  26. moat_kv-0.71.6.dist-info/RECORD +47 -0
  27. {moat_kv-0.70.24.dist-info → moat_kv-0.71.6.dist-info}/WHEEL +1 -1
  28. moat_kv-0.71.6.dist-info/licenses/LICENSE +3 -0
  29. moat_kv-0.71.6.dist-info/licenses/LICENSE.APACHE2 +202 -0
  30. moat_kv-0.71.6.dist-info/licenses/LICENSE.MIT +20 -0
  31. moat_kv-0.71.6.dist-info/top_level.txt +1 -0
  32. build/lib/docs/source/conf.py +0 -201
  33. build/lib/examples/pathify.py +0 -45
  34. build/lib/moat/kv/__init__.py +0 -19
  35. build/lib/moat/kv/_cfg.yaml +0 -97
  36. build/lib/moat/kv/_main.py +0 -91
  37. build/lib/moat/kv/actor/__init__.py +0 -98
  38. build/lib/moat/kv/actor/deletor.py +0 -139
  39. build/lib/moat/kv/auth/__init__.py +0 -444
  40. build/lib/moat/kv/auth/_test.py +0 -166
  41. build/lib/moat/kv/auth/password.py +0 -234
  42. build/lib/moat/kv/auth/root.py +0 -58
  43. build/lib/moat/kv/backend/__init__.py +0 -67
  44. build/lib/moat/kv/backend/mqtt.py +0 -74
  45. build/lib/moat/kv/backend/serf.py +0 -45
  46. build/lib/moat/kv/client.py +0 -1025
  47. build/lib/moat/kv/code.py +0 -236
  48. build/lib/moat/kv/codec.py +0 -11
  49. build/lib/moat/kv/command/__init__.py +0 -1
  50. build/lib/moat/kv/command/acl.py +0 -180
  51. build/lib/moat/kv/command/auth.py +0 -261
  52. build/lib/moat/kv/command/code.py +0 -293
  53. build/lib/moat/kv/command/codec.py +0 -186
  54. build/lib/moat/kv/command/data.py +0 -265
  55. build/lib/moat/kv/command/dump/__init__.py +0 -143
  56. build/lib/moat/kv/command/error.py +0 -149
  57. build/lib/moat/kv/command/internal.py +0 -248
  58. build/lib/moat/kv/command/job.py +0 -433
  59. build/lib/moat/kv/command/log.py +0 -53
  60. build/lib/moat/kv/command/server.py +0 -114
  61. build/lib/moat/kv/command/type.py +0 -201
  62. build/lib/moat/kv/config.py +0 -46
  63. build/lib/moat/kv/data.py +0 -216
  64. build/lib/moat/kv/errors.py +0 -561
  65. build/lib/moat/kv/exceptions.py +0 -126
  66. build/lib/moat/kv/mock/__init__.py +0 -101
  67. build/lib/moat/kv/mock/mqtt.py +0 -159
  68. build/lib/moat/kv/mock/serf.py +0 -250
  69. build/lib/moat/kv/mock/tracer.py +0 -63
  70. build/lib/moat/kv/model.py +0 -1069
  71. build/lib/moat/kv/obj/__init__.py +0 -646
  72. build/lib/moat/kv/obj/command.py +0 -241
  73. build/lib/moat/kv/runner.py +0 -1347
  74. build/lib/moat/kv/server.py +0 -2809
  75. build/lib/moat/kv/types.py +0 -513
  76. debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +0 -201
  77. debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +0 -45
  78. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +0 -19
  79. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +0 -97
  80. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +0 -91
  81. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +0 -98
  82. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +0 -139
  83. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +0 -444
  84. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +0 -166
  85. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +0 -234
  86. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +0 -58
  87. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +0 -67
  88. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +0 -74
  89. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/serf.py +0 -45
  90. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +0 -1025
  91. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +0 -236
  92. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +0 -11
  93. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +0 -1
  94. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +0 -180
  95. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +0 -261
  96. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +0 -293
  97. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +0 -186
  98. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +0 -265
  99. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +0 -143
  100. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +0 -149
  101. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +0 -248
  102. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +0 -433
  103. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +0 -53
  104. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +0 -114
  105. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +0 -201
  106. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +0 -46
  107. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +0 -216
  108. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +0 -561
  109. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +0 -126
  110. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +0 -101
  111. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +0 -159
  112. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/serf.py +0 -250
  113. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +0 -63
  114. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +0 -1069
  115. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +0 -646
  116. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +0 -241
  117. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +0 -1347
  118. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +0 -2809
  119. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +0 -513
  120. docs/source/conf.py +0 -201
  121. examples/pathify.py +0 -45
  122. moat/kv/backend/serf.py +0 -45
  123. moat/kv/codec.py +0 -11
  124. moat/kv/mock/serf.py +0 -250
  125. moat_kv-0.70.24.dist-info/RECORD +0 -137
  126. moat_kv-0.70.24.dist-info/top_level.txt +0 -9
  127. {moat_kv-0.70.24.dist-info → moat_kv-0.71.6.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)