moat-kv 0.70.20__py3-none-any.whl → 0.70.23__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 (52) hide show
  1. moat/kv/__init__.py +1 -0
  2. moat/kv/_cfg.yaml +97 -0
  3. moat/kv/_main.py +6 -9
  4. moat/kv/client.py +36 -52
  5. moat/kv/code.py +10 -3
  6. moat/kv/codec.py +1 -0
  7. moat/kv/config.py +2 -0
  8. moat/kv/data.py +8 -7
  9. moat/kv/errors.py +17 -9
  10. moat/kv/exceptions.py +1 -7
  11. moat/kv/model.py +16 -24
  12. moat/kv/runner.py +39 -36
  13. moat/kv/server.py +86 -90
  14. moat/kv/types.py +5 -8
  15. {moat_kv-0.70.20.dist-info → moat_kv-0.70.23.dist-info}/METADATA +22 -25
  16. moat_kv-0.70.23.dist-info/RECORD +19 -0
  17. {moat_kv-0.70.20.dist-info → moat_kv-0.70.23.dist-info}/WHEEL +1 -1
  18. moat_kv-0.70.23.dist-info/licenses/LICENSE.txt +14 -0
  19. moat/kv/_config.yaml +0 -98
  20. moat/kv/actor/__init__.py +0 -97
  21. moat/kv/actor/deletor.py +0 -137
  22. moat/kv/auth/__init__.py +0 -446
  23. moat/kv/auth/_test.py +0 -172
  24. moat/kv/auth/password.py +0 -232
  25. moat/kv/auth/root.py +0 -56
  26. moat/kv/backend/__init__.py +0 -66
  27. moat/kv/backend/mqtt.py +0 -74
  28. moat/kv/backend/serf.py +0 -44
  29. moat/kv/command/__init__.py +0 -1
  30. moat/kv/command/acl.py +0 -174
  31. moat/kv/command/auth.py +0 -258
  32. moat/kv/command/code.py +0 -306
  33. moat/kv/command/codec.py +0 -190
  34. moat/kv/command/data.py +0 -274
  35. moat/kv/command/dump/__init__.py +0 -141
  36. moat/kv/command/error.py +0 -156
  37. moat/kv/command/internal.py +0 -257
  38. moat/kv/command/job.py +0 -438
  39. moat/kv/command/log.py +0 -52
  40. moat/kv/command/server.py +0 -115
  41. moat/kv/command/type.py +0 -203
  42. moat/kv/mock/__init__.py +0 -97
  43. moat/kv/mock/mqtt.py +0 -164
  44. moat/kv/mock/serf.py +0 -253
  45. moat/kv/mock/tracer.py +0 -65
  46. moat/kv/obj/__init__.py +0 -636
  47. moat/kv/obj/command.py +0 -246
  48. moat_kv-0.70.20.dist-info/LICENSE +0 -3
  49. moat_kv-0.70.20.dist-info/LICENSE.APACHE2 +0 -202
  50. moat_kv-0.70.20.dist-info/LICENSE.MIT +0 -20
  51. moat_kv-0.70.20.dist-info/RECORD +0 -49
  52. {moat_kv-0.70.20.dist-info → moat_kv-0.70.23.dist-info}/top_level.txt +0 -0
moat/kv/command/job.py DELETED
@@ -1,438 +0,0 @@
1
- # command line interface
2
-
3
- import sys
4
- import time
5
- from functools import partial
6
-
7
- import anyio
8
- import asyncclick as click
9
- from moat.util import P, Path, attr_args, attrdict, process_args, yprint
10
-
11
- from moat.kv.code import CodeRoot
12
- from moat.kv.data import add_dates, data_get
13
- from moat.kv.runner import AllRunnerRoot, AnyRunnerRoot, SingleRunnerRoot
14
-
15
-
16
- @click.group() # pylint: disable=undefined-variable
17
- @click.option(
18
- "-n", "--node", help="node to run this code on. Empty: any one node, '-': all nodes"
19
- )
20
- @click.option("-g", "--group", help="group to run this code on. Empty: default")
21
- @click.pass_context
22
- async def cli(ctx, node, group):
23
- """Run code stored in MoaT-KV.
24
-
25
- \b
26
- The option '-n' is somewhat special:
27
- -n - Jobs for all hosts
28
- -n XXX Jobs for host XXX
29
- (no -n) Jobs for any host
30
-
31
- The default group is 'default'.
32
- """
33
- obj = ctx.obj
34
- if group is None:
35
- group = "default"
36
- if group == "-":
37
- if node is not None:
38
- raise click.UsageError("'-g -' doesn't make sense with '-n'")
39
- if ctx.invoked_subcommand != "info":
40
- raise click.UsageError("'-g -' only makes sense with the 'info' command")
41
- obj.runner_root = SingleRunnerRoot
42
- subpath = (None,)
43
- elif not node:
44
- obj.runner_root = AnyRunnerRoot
45
- subpath = (group,)
46
- elif node == "-":
47
- obj.runner_root = AllRunnerRoot
48
- subpath = (group,)
49
- else:
50
- obj.runner_root = SingleRunnerRoot
51
- subpath = (node, group)
52
-
53
- cfg = obj.cfg["kv"]["runner"]
54
- obj.subpath = Path(cfg["sub"][obj.runner_root.SUB]) + subpath
55
- obj.path = cfg["prefix"] + obj.subpath
56
- obj.statepath = cfg["state"] + obj.subpath
57
-
58
-
59
- @cli.group(
60
- "at", short_help="path of the job to operate on", invoke_without_command=True
61
- )
62
- @click.argument("path", nargs=1, type=P)
63
- @click.pass_context
64
- async def at_cli(ctx, path):
65
- """
66
- Add, list, modify, delete jobs at/under this path.
67
- """
68
- obj = ctx.obj
69
- obj.jobpath = path
70
-
71
- if ctx.invoked_subcommand is None:
72
- res = await obj.client.get(obj.path + path, nchain=obj.meta)
73
- yprint(res if obj.meta else res.value if 'value' in res else None, stream=obj.stdout)
74
-
75
-
76
- @cli.command("info")
77
- @click.pass_obj
78
- async def info_(obj):
79
- """
80
- List available groups for the node in question.
81
-
82
- \b
83
- Options (between 'job' and 'info')
84
- (none) list groups with jobs for any host
85
- -n - list groups with jobs for all hosts
86
- -g - list hosts that have specific jobs
87
- -n XXX list groups with jobs for a specific host
88
- """
89
- path = obj.path[:-1]
90
- async for r in obj.client.get_tree(path=path, min_depth=1, max_depth=1, empty=True):
91
- print(r.path[-1], file=obj.stdout)
92
-
93
-
94
- @at_cli.command("--help", hidden=True)
95
- @click.pass_context
96
- def help_(ctx): # pylint:disable=unused-variable # oh boy
97
- print(at_cli.get_help(ctx))
98
-
99
-
100
- @at_cli.command("path")
101
- @click.pass_obj
102
- async def path__(obj):
103
- """
104
- Emit the full path leading to the specified runner object.
105
-
106
- Useful for copying or for state monitoring.
107
-
108
- NEVER directly write to the state object. It's controlled by the
109
- runner. You'll confuse it if you do that.
110
-
111
- Updating the control object will cancel any running code.
112
- """
113
- path = obj.jobpath
114
- res = dict(command=obj.path + path, state=obj.statepath + path)
115
- yprint(res, stream=obj.stdout)
116
-
117
-
118
- @cli.command("run")
119
- @click.option(
120
- "-n",
121
- "--nodes",
122
- type=int,
123
- default=0,
124
- help="Size of the group (not for single-node runners)",
125
- )
126
- @click.pass_obj
127
- async def run(obj, nodes):
128
- """
129
- Run code that needs to run.
130
-
131
- This does not return.
132
- """
133
- from moat.util import as_service
134
-
135
- if obj.subpath[-1] == "-":
136
- raise click.UsageError("Group '-' can only be used for listing.")
137
- if nodes and obj.runner_root is SingleRunnerRoot:
138
- raise click.UsageError("A single-site runner doesn't have a size.")
139
-
140
- async with as_service(obj) as evt:
141
- c = obj.client
142
- cr = await CodeRoot.as_handler(c)
143
- await obj.runner_root.as_handler(
144
- c, subpath=obj.subpath, code=cr, **({"nodes": nodes} if nodes else {})
145
- )
146
- evt.set()
147
- await anyio.sleep_forever()
148
-
149
-
150
- async def _state_fix(obj, state, state_only, path, r):
151
- try:
152
- val = r.value
153
- except AttributeError:
154
- return
155
- if state:
156
- rs = await obj.client._request(
157
- action="get_value", path=state + r.path, iter=False, nchain=obj.meta
158
- )
159
- if state_only:
160
- r.value = rs
161
- else:
162
- if obj.meta:
163
- val["state"] = rs
164
- elif "value" in rs:
165
- val["state"] = rs.value
166
- if "value" in rs:
167
- add_dates(rs.value)
168
- if not state_only:
169
- if path:
170
- r.path = path + r.path
171
- add_dates(val)
172
-
173
- return r
174
-
175
-
176
- @at_cli.command("list")
177
- @click.option("-s", "--state", is_flag=True, help="Add state data")
178
- @click.option("-S", "--state-only", is_flag=True, help="Only output state data")
179
- @click.option("-t", "--table", is_flag=True, help="one-line output")
180
- @click.option(
181
- "-d",
182
- "--as-dict",
183
- default=None,
184
- help="Structure as dictionary. The argument is the key to use "
185
- "for values. Default: return as list",
186
- )
187
- @click.pass_obj
188
- async def list_(obj, state, state_only, table, as_dict):
189
- """List run entries."""
190
- if table and state:
191
- raise click.UsageError("'--table' and '--state' are mutually exclusive")
192
-
193
- path = obj.jobpath
194
-
195
- if state or state_only or table:
196
- state = obj.statepath + path
197
-
198
- if table:
199
- from moat.kv.errors import ErrorRoot
200
-
201
- err = await ErrorRoot.as_handler(obj.client)
202
-
203
- async for r in obj.client.get_tree(obj.path + path):
204
- p = path + r.path
205
- s = await obj.client.get(state + r.path)
206
- if "value" not in s:
207
- st = "-never-"
208
- elif s.value.started > s.value.stopped:
209
- st = s.value.node
210
- else:
211
- try:
212
- e = await err.get_error_record("run", obj.path + p, create=False)
213
- except KeyError:
214
- st = "-stopped-"
215
- else:
216
- if e is None or e.resolved:
217
- st = "-stopped-"
218
- else:
219
- st = " | ".join(
220
- "%s %s"
221
- % (
222
- Path.build(e.subpath)
223
- if e._path[-2] == ee._path[-1]
224
- else Path.build(ee.subpath),
225
- getattr(ee, "message", None)
226
- or getattr(ee, "comment", None)
227
- or "-",
228
- )
229
- for ee in e
230
- )
231
- print(p, r.value.code, st, file=obj.stdout)
232
-
233
- else:
234
- await data_get(
235
- obj,
236
- obj.path + path,
237
- as_dict=as_dict,
238
- item_mangle=partial(
239
- _state_fix, obj, state, state_only, None if as_dict else path
240
- ),
241
- )
242
-
243
-
244
- @at_cli.command("state")
245
- @click.option("-r", "--result", is_flag=True, help="Just print the actual result.")
246
- @click.pass_obj
247
- async def state_(obj, result):
248
- """Get the status of a runner entry."""
249
- path = obj.jobpath
250
-
251
- if obj.subpath[-1] == "-":
252
- raise click.UsageError("Group '-' can only be used for listing.")
253
- if result and obj.meta:
254
- raise click.UsageError("You can't use '-v' and '-r' at the same time.")
255
- if not len(path):
256
- raise click.UsageError("You need a non-empty path.")
257
- path = obj.statepath + obj.jobpath
258
-
259
- res = await obj.client.get(path, nchain=obj.meta)
260
- if "value" not in res:
261
- if obj.debug:
262
- print("Not found (yet?)", file=sys.stderr)
263
- sys.exit(1)
264
-
265
- add_dates(res.value)
266
- if not obj.meta:
267
- res = res.value
268
- yprint(res, stream=obj.stdout)
269
-
270
-
271
- @at_cli.command()
272
- @click.option("-s", "--state", is_flag=True, help="Add state data")
273
- @click.pass_obj
274
- async def get(obj, state):
275
- """Read a runner entry"""
276
- path = obj.jobpath
277
- if obj.subpath[-1] == "-":
278
- raise click.UsageError("Group '-' can only be used for listing.")
279
- if not path:
280
- raise click.UsageError("You need a non-empty path.")
281
-
282
- res = await obj.client._request(
283
- action="get_value", path=obj.path + path, iter=False, nchain=obj.meta
284
- )
285
- if "value" not in res:
286
- print("Not found.", file=sys.stderr)
287
- return
288
- res.path = path
289
- if state:
290
- state = obj.statepath
291
- await _state_fix(obj, state, False, path, res)
292
- if not obj.meta:
293
- res = res.value
294
-
295
- yprint(res, stream=obj.stdout)
296
-
297
-
298
- @at_cli.command()
299
- @click.option("-f", "--force", is_flag=True, help="Force deletion even if messy")
300
- @click.pass_obj
301
- async def delete(obj, force):
302
- """Remove a runner entry"""
303
- path = obj.jobpath
304
-
305
- if obj.subpath[-1] == "-":
306
- raise click.UsageError("Group '-' can only be used for listing.")
307
- if not path:
308
- raise click.UsageError("You need a non-empty path.")
309
-
310
- res = await obj.client.get(obj.path + path, nchain=3)
311
- if "value" not in res:
312
- res.info = "Does not exist."
313
- else:
314
- val = res.value
315
- if "target" not in val:
316
- val.target = None
317
- if val.target is not None:
318
- val.target = None
319
- res = await obj.client.set(
320
- obj.path + path, value=val, nchain=3, chain=res.chain
321
- )
322
- if not force:
323
- res.info = "'target' was set: cleared but not deleted."
324
- if force or val.target is None:
325
- sres = await obj.client.get(obj.statepath + path, nchain=3)
326
- if (
327
- not force
328
- and "value" in sres
329
- and sres.value.stopped < sres.value.started
330
- ):
331
- res.info = "Still running, not deleted."
332
- else:
333
- sres = await obj.client.delete(obj.statepath + path, chain=sres.chain)
334
- res = await obj.client.delete(obj.path + path, chain=res.chain)
335
- if "value" in res and res.value.stopped < res.value.started:
336
- res.info = "Deleted (unclean!)."
337
- else:
338
- res.info = "Deleted."
339
-
340
- if obj.meta:
341
- yprint(res, stream=obj.stdout)
342
- elif obj.debug:
343
- print(res.info)
344
-
345
-
346
- @at_cli.command("set")
347
- @click.option("-c", "--code", help="Path to the code that should run.")
348
- @click.option("-C", "--copy", help="Use this entry as a template.")
349
- @click.option("-t", "--time", "tm", help="time the code should next run at. '-':not")
350
- @click.option("-r", "--repeat", type=int, help="Seconds the code should re-run after")
351
- @click.option("-k", "--ok", type=float, help="Code is OK if it ran this many seconds")
352
- @click.option("-b", "--backoff", type=float, help="Back-off factor. Default: 1.4")
353
- @click.option(
354
- "-d", "--delay", type=int, help="Seconds the code should retry after (w/ backoff)"
355
- )
356
- @click.option("-i", "--info", help="Short human-readable information")
357
- @attr_args
358
- @click.pass_obj
359
- async def set_(
360
- obj, code, tm, info, ok, repeat, delay, backoff, copy, vars_, eval_, path_
361
- ):
362
- """Add or modify a runner.
363
-
364
- Code typically requires some input parameters.
365
-
366
- You should use '-v NAME VALUE' for string values, '-p NAME VALUE' for
367
- paths, and '-e NAME VALUE' for other data. '-e NAME -' deletes an item.
368
- """
369
- path = obj.jobpath
370
-
371
- if obj.subpath[-1] == "-":
372
- raise click.UsageError("Group '-' can only be used for listing.")
373
-
374
- if code is not None:
375
- code = P(code)
376
- if copy:
377
- copy = P(copy)
378
- path = obj.path + P(path)
379
-
380
- res = await obj.client._request(
381
- action="get_value", path=copy or path, iter=False, nchain=3
382
- )
383
- if "value" not in res:
384
- if copy:
385
- raise click.UsageError("--copy: use the complete path to an existing entry")
386
- elif code is None:
387
- raise click.UsageError("New entry, need code")
388
- res = {}
389
- chain = None
390
- else:
391
- chain = None if copy else res["chain"]
392
- res = res["value"]
393
- if copy and "code" not in res:
394
- raise click.UsageError("'--copy' needs a runner entry")
395
-
396
- vl = attrdict(**res.setdefault("data", {}))
397
- vl = process_args(vl, vars_, eval_, path_)
398
- res["data"] = vl
399
-
400
- if code is not None:
401
- res["code"] = code
402
- if ok is not None:
403
- res["ok_after"] = ok
404
- if info is not None:
405
- res["info"] = info
406
- if backoff is not None:
407
- res["backoff"] = backoff
408
- if delay is not None:
409
- res["delay"] = delay
410
- if repeat is not None:
411
- res["repeat"] = repeat
412
- if tm is not None:
413
- if tm == "-":
414
- res["target"] = None
415
- else:
416
- res["target"] = time.time() + float(tm)
417
-
418
- res = await obj.client.set(path, value=res, nchain=3, chain=chain)
419
- if obj.meta:
420
- yprint(res, stream=obj.stdout)
421
-
422
-
423
- @cli.command(short_help="Show runners' keepalive messages")
424
- @click.pass_obj
425
- async def monitor(obj):
426
- """
427
- Runners periodically send a keepalive message. Show them.
428
- """
429
-
430
- # TODO this does not watch changes in MoaT-KV.
431
- # It also should watch individual jobs' state changes.
432
- if obj.subpath[-1] == "-":
433
- raise click.UsageError("Group '-' can only be used for listing.")
434
-
435
- async with obj.client.msg_monitor("run") as cl:
436
- async for msg in cl:
437
- yprint(msg, stream=obj.stdout)
438
- print("---", file=obj.stdout)
moat/kv/command/log.py DELETED
@@ -1,52 +0,0 @@
1
- # command line interface
2
-
3
- import asyncclick as click
4
- from moat.util import yprint
5
-
6
-
7
- @click.group(short_help="Manage logging.") # pylint: disable=undefined-variable
8
- async def cli():
9
- """
10
- This subcommand controls a server's logging.
11
- """
12
- pass
13
-
14
-
15
- @cli.command()
16
- @click.option("-i", "--incremental", is_flag=True, help="Don't write the initial state")
17
- @click.argument("path", nargs=1)
18
- @click.pass_obj
19
- async def dest(obj, path, incremental):
20
- """
21
- Log changes to a file.
22
-
23
- Any previously open log (on the server you talk to) is closed as soon
24
- as the new one is opened and ready.
25
- """
26
- res = await obj.client._request("log", path=path, fetch=not incremental)
27
- if obj.meta:
28
- yprint(res, stream=obj.stdout)
29
-
30
-
31
- @cli.command()
32
- @click.option("-f", "--full", is_flag=1, help="Also dump internal state")
33
- @click.argument("path", nargs=1)
34
- @click.pass_obj
35
- async def save(obj, path, full):
36
- """
37
- Write the server's current state to a file.
38
- """
39
- res = await obj.client._request("save", path=path, full=full)
40
- if obj.meta:
41
- yprint(res, stream=obj.stdout)
42
-
43
-
44
- @cli.command()
45
- @click.pass_obj
46
- async def stop(obj):
47
- """
48
- Stop logging changes.
49
- """
50
- res = await obj.client._request("log") # no path == stop
51
- if obj.meta:
52
- yprint(res, stream=obj.stdout)
moat/kv/command/server.py DELETED
@@ -1,115 +0,0 @@
1
- # command line interface
2
-
3
- import asyncclick as click
4
-
5
- from moat.kv.server import Server
6
-
7
-
8
- @click.command(short_help="Run the MoaT-KV server.") # pylint: disable=undefined-variable
9
- @click.option(
10
- "-l",
11
- "--load",
12
- type=click.Path(readable=True, exists=True, allow_dash=False),
13
- default=None,
14
- help="Event log to preload.",
15
- )
16
- @click.option(
17
- "-s",
18
- "--save",
19
- type=click.Path(writable=True, allow_dash=False),
20
- default=None,
21
- help="Event log to write to.",
22
- hidden=True,
23
- )
24
- @click.option(
25
- "-i",
26
- "--incremental",
27
- default=None,
28
- help="Save incremental changes, not the complete state",
29
- hidden=True,
30
- )
31
- @click.option(
32
- "-I",
33
- "--init",
34
- default=None,
35
- help="Initial value to set the root to. Use only when setting up "
36
- "a cluster for the first time!",
37
- hidden=True,
38
- )
39
- @click.option(
40
- "-e",
41
- "--eval",
42
- "eval_",
43
- is_flag=True,
44
- help="The 'init' value shall be evaluated.",
45
- hidden=True,
46
- )
47
- @click.option(
48
- "-a",
49
- "--auth",
50
- "--authoritative",
51
- is_flag=True,
52
- help="Data in this file is complete: mark anything missing as known even if not.",
53
- )
54
- @click.option(
55
- "-f",
56
- "--force",
57
- is_flag=True,
58
- help="Force 'successful' startup even if data are missing.",
59
- )
60
- @click.argument("name", nargs=1)
61
- @click.argument("nodes", nargs=-1)
62
- @click.pass_obj
63
- async def cli(obj, name, load, save, init, incremental, eval_, auth, force, nodes):
64
- """
65
- This command starts a MoaT-KV server. It defaults to connecting to the local Serf
66
- agent.
67
-
68
- All MoaT-KV servers must have a unique name. Its uniqueness cannot be
69
- verified reliably.
70
-
71
- One server in your network needs either an initial datum, or a copy of
72
- a previously-saved MoaT-KV state. Otherwise, no client connections will
73
- be accepted until synchronization with the other servers in your MoaT-KV
74
- network is complete.
75
-
76
- This command requires a unique NAME argument. The name identifies this
77
- server on the network. Never start two servers with the same name!
78
-
79
- You can force the server to fetch its data from a specific node, in
80
- case some data are corrupted. (This should never be necessary.)
81
-
82
- A server will refuse to start up as long as it knows about missing
83
- entries. Use the 'force' flag to disable that. You should disable
84
- any clients which use this server until the situation is resolved!
85
-
86
- An auhthoritative server doesn't have missing data in its storage by
87
- definition. This flag is used in the 'run' script when loading from a
88
- file.
89
- """
90
-
91
- kw = {}
92
- if eval_:
93
- kw["init"] = eval(init) # pylint: disable=eval-used
94
- elif init == "-":
95
- kw["init"] = None
96
- elif init is not None:
97
- kw["init"] = init
98
-
99
- from moat.util import as_service
100
-
101
- if load and nodes:
102
- raise click.UsageError(
103
- "Either read from a file or fetch from a node. Not both."
104
- )
105
- if auth and force:
106
- raise click.UsageError("Using both '-a' and '-f' is redundant. Choose one.")
107
-
108
- async with as_service(obj) as evt:
109
- s = Server(name, cfg=obj.cfg["kv"], **kw)
110
- if load is not None:
111
- await s.load(path=load, local=True, authoritative=auth)
112
- if nodes:
113
- await s.fetch_data(nodes, authoritative=auth)
114
-
115
- await s.serve(log_path=save, log_inc=incremental, force=force, ready_evt=evt)