moat-kv 0.70.23__py3-none-any.whl → 0.70.24__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.
- build/lib/docs/source/conf.py +201 -0
- build/lib/examples/pathify.py +45 -0
- build/lib/moat/kv/__init__.py +19 -0
- build/lib/moat/kv/_cfg.yaml +97 -0
- build/lib/moat/kv/_main.py +91 -0
- build/lib/moat/kv/actor/__init__.py +98 -0
- build/lib/moat/kv/actor/deletor.py +139 -0
- build/lib/moat/kv/auth/__init__.py +444 -0
- build/lib/moat/kv/auth/_test.py +166 -0
- build/lib/moat/kv/auth/password.py +234 -0
- build/lib/moat/kv/auth/root.py +58 -0
- build/lib/moat/kv/backend/__init__.py +67 -0
- build/lib/moat/kv/backend/mqtt.py +74 -0
- build/lib/moat/kv/backend/serf.py +45 -0
- build/lib/moat/kv/client.py +1025 -0
- build/lib/moat/kv/code.py +236 -0
- build/lib/moat/kv/codec.py +11 -0
- build/lib/moat/kv/command/__init__.py +1 -0
- build/lib/moat/kv/command/acl.py +180 -0
- build/lib/moat/kv/command/auth.py +261 -0
- build/lib/moat/kv/command/code.py +293 -0
- build/lib/moat/kv/command/codec.py +186 -0
- build/lib/moat/kv/command/data.py +265 -0
- build/lib/moat/kv/command/dump/__init__.py +143 -0
- build/lib/moat/kv/command/error.py +149 -0
- build/lib/moat/kv/command/internal.py +248 -0
- build/lib/moat/kv/command/job.py +433 -0
- build/lib/moat/kv/command/log.py +53 -0
- build/lib/moat/kv/command/server.py +114 -0
- build/lib/moat/kv/command/type.py +201 -0
- build/lib/moat/kv/config.py +46 -0
- build/lib/moat/kv/data.py +216 -0
- build/lib/moat/kv/errors.py +561 -0
- build/lib/moat/kv/exceptions.py +126 -0
- build/lib/moat/kv/mock/__init__.py +101 -0
- build/lib/moat/kv/mock/mqtt.py +159 -0
- build/lib/moat/kv/mock/serf.py +250 -0
- build/lib/moat/kv/mock/tracer.py +63 -0
- build/lib/moat/kv/model.py +1069 -0
- build/lib/moat/kv/obj/__init__.py +646 -0
- build/lib/moat/kv/obj/command.py +241 -0
- build/lib/moat/kv/runner.py +1347 -0
- build/lib/moat/kv/server.py +2809 -0
- build/lib/moat/kv/types.py +513 -0
- debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +201 -0
- debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +45 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +19 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +97 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +91 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +98 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +139 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +444 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +166 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +234 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +58 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +67 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +74 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/serf.py +45 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +1025 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +236 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +11 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +1 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +180 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +261 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +293 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +186 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +265 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +143 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +149 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +248 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +433 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +53 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +114 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +201 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +46 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +216 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +561 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +126 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +101 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +159 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/serf.py +250 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +63 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +1069 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +646 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +241 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +1347 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +2809 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +513 -0
- docs/source/conf.py +201 -0
- examples/pathify.py +45 -0
- moat/kv/actor/__init__.py +98 -0
- moat/kv/actor/deletor.py +139 -0
- moat/kv/auth/__init__.py +444 -0
- moat/kv/auth/_test.py +166 -0
- moat/kv/auth/password.py +234 -0
- moat/kv/auth/root.py +58 -0
- moat/kv/backend/__init__.py +67 -0
- moat/kv/backend/mqtt.py +74 -0
- moat/kv/backend/serf.py +45 -0
- moat/kv/command/__init__.py +1 -0
- moat/kv/command/acl.py +180 -0
- moat/kv/command/auth.py +261 -0
- moat/kv/command/code.py +293 -0
- moat/kv/command/codec.py +186 -0
- moat/kv/command/data.py +265 -0
- moat/kv/command/dump/__init__.py +143 -0
- moat/kv/command/error.py +149 -0
- moat/kv/command/internal.py +248 -0
- moat/kv/command/job.py +433 -0
- moat/kv/command/log.py +53 -0
- moat/kv/command/server.py +114 -0
- moat/kv/command/type.py +201 -0
- moat/kv/mock/__init__.py +101 -0
- moat/kv/mock/mqtt.py +159 -0
- moat/kv/mock/serf.py +250 -0
- moat/kv/mock/tracer.py +63 -0
- moat/kv/obj/__init__.py +646 -0
- moat/kv/obj/command.py +241 -0
- {moat_kv-0.70.23.dist-info → moat_kv-0.70.24.dist-info}/METADATA +2 -2
- moat_kv-0.70.24.dist-info/RECORD +137 -0
- moat_kv-0.70.24.dist-info/top_level.txt +9 -0
- moat_kv-0.70.23.dist-info/RECORD +0 -19
- moat_kv-0.70.23.dist-info/top_level.txt +0 -1
- {moat_kv-0.70.23.dist-info → moat_kv-0.70.24.dist-info}/WHEEL +0 -0
- {moat_kv-0.70.23.dist-info → moat_kv-0.70.24.dist-info}/licenses/LICENSE.txt +0 -0
moat/kv/command/codec.py
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
# command line interface
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import asyncclick as click
|
5
|
+
from moat.util import NotGiven, P, Path, PathLongener, yload, yprint
|
6
|
+
|
7
|
+
|
8
|
+
@click.group() # pylint: disable=undefined-variable
|
9
|
+
async def cli():
|
10
|
+
"""Manage codecs and converters. Usage: … codec …"""
|
11
|
+
pass
|
12
|
+
|
13
|
+
|
14
|
+
@cli.command()
|
15
|
+
@click.option("-e", "--encode", type=click.File(mode="w", lazy=True), help="Save the encoder here")
|
16
|
+
@click.option("-d", "--decode", type=click.File(mode="w", lazy=True), help="Save the decoder here")
|
17
|
+
@click.option("-s", "--script", type=click.File(mode="w", lazy=True), help="Save the data here")
|
18
|
+
@click.argument("path", nargs=1)
|
19
|
+
@click.pass_obj
|
20
|
+
async def get(obj, path, script, encode, decode):
|
21
|
+
"""Read type information"""
|
22
|
+
path = P(path)
|
23
|
+
if not len(path):
|
24
|
+
raise click.UsageError("You need a non-empty path.")
|
25
|
+
res = await obj.client._request(
|
26
|
+
action="get_internal",
|
27
|
+
path=Path("codec") + path,
|
28
|
+
iter=False,
|
29
|
+
nchain=obj.meta,
|
30
|
+
)
|
31
|
+
if encode and res.get("encode", None) is not None:
|
32
|
+
encode.write(res.pop("encode"))
|
33
|
+
if decode and res.get("decode", None) is not None:
|
34
|
+
decode.write(res.pop("decode"))
|
35
|
+
|
36
|
+
if not obj.meta:
|
37
|
+
res = res.value
|
38
|
+
yprint(res, stream=script or obj.stdout)
|
39
|
+
|
40
|
+
|
41
|
+
@cli.command(name="list")
|
42
|
+
@click.pass_obj
|
43
|
+
@click.argument("path", nargs=1)
|
44
|
+
async def list_(obj, path):
|
45
|
+
"""List type information entries"""
|
46
|
+
res = await obj.client._request(
|
47
|
+
action="get_tree_internal",
|
48
|
+
path=Path("codec") + P(path),
|
49
|
+
iter=True,
|
50
|
+
nchain=obj.meta,
|
51
|
+
)
|
52
|
+
pl = PathLongener(())
|
53
|
+
async for r in res:
|
54
|
+
pl(r)
|
55
|
+
print(" ".join(str(x) for x in r.path), file=obj.stdout)
|
56
|
+
|
57
|
+
|
58
|
+
@cli.command("set")
|
59
|
+
@click.option("-e", "--encode", type=click.File(mode="r"), help="File with the encoder")
|
60
|
+
@click.option("-d", "--decode", type=click.File(mode="r"), help="File with the decoder")
|
61
|
+
@click.option("-D", "--data", type=click.File(mode="r"), help="File with the rest")
|
62
|
+
@click.option("-i", "--in", "in_", nargs=2, multiple=True, help="Decoding sample")
|
63
|
+
@click.option("-o", "--out", nargs=2, multiple=True, help="Encoding sample")
|
64
|
+
@click.argument("path", nargs=1)
|
65
|
+
@click.pass_obj
|
66
|
+
async def set_(obj, path, encode, decode, data, in_, out):
|
67
|
+
"""Save codec information"""
|
68
|
+
path = P(path)
|
69
|
+
if not len(path):
|
70
|
+
raise click.UsageError("You need a non-empty path.")
|
71
|
+
|
72
|
+
if data:
|
73
|
+
msg = yload(data)
|
74
|
+
else:
|
75
|
+
msg = {}
|
76
|
+
chain = NotGiven
|
77
|
+
if "value" in msg:
|
78
|
+
chain = msg.get("chain", NotGiven)
|
79
|
+
msg = msg["value"]
|
80
|
+
|
81
|
+
if "encode" in msg:
|
82
|
+
if encode:
|
83
|
+
raise click.UsageError("Duplicate encode script")
|
84
|
+
else:
|
85
|
+
if not encode:
|
86
|
+
raise click.UsageError("Missing encode script")
|
87
|
+
msg["encode"] = encode.read()
|
88
|
+
if "decode" in msg:
|
89
|
+
if decode:
|
90
|
+
raise click.UsageError("Duplicate decode script")
|
91
|
+
else:
|
92
|
+
if not decode:
|
93
|
+
raise click.UsageError("Missing decode script")
|
94
|
+
msg["decode"] = decode.read()
|
95
|
+
if in_:
|
96
|
+
msg["in"] = [(eval(a), eval(b)) for a, b in in_] # pylint: disable=eval-used
|
97
|
+
if out:
|
98
|
+
msg["out"] = [(eval(a), eval(b)) for a, b in out] # pylint: disable=eval-used
|
99
|
+
|
100
|
+
if not msg["in"]:
|
101
|
+
raise click.UsageError("Missing decode tests")
|
102
|
+
if not msg["out"]:
|
103
|
+
raise click.UsageError("Missing encode tests")
|
104
|
+
|
105
|
+
res = await obj.client._request(
|
106
|
+
action="set_internal",
|
107
|
+
value=msg,
|
108
|
+
path=Path("codec") + path,
|
109
|
+
iter=False,
|
110
|
+
nchain=obj.meta,
|
111
|
+
**({} if chain is NotGiven else {"chain": chain}),
|
112
|
+
)
|
113
|
+
if obj.meta:
|
114
|
+
yprint(res, stream=obj.stdout)
|
115
|
+
|
116
|
+
|
117
|
+
@cli.command()
|
118
|
+
@click.option("-c", "--codec", type=P, help="Codec to link to.")
|
119
|
+
@click.option("-d", "--delete", is_flag=True, help="Use to delete this converter.")
|
120
|
+
@click.option(
|
121
|
+
"-l",
|
122
|
+
"--list",
|
123
|
+
"list_",
|
124
|
+
is_flag=True,
|
125
|
+
help="Use to list this converter; '-' to list all.",
|
126
|
+
)
|
127
|
+
@click.argument("name", nargs=1)
|
128
|
+
@click.argument("path", type=P, nargs=1)
|
129
|
+
@click.pass_obj
|
130
|
+
async def convert(obj, path, codec, name, delete, list_):
|
131
|
+
"""Match a codec to a path (read, if no codec given)"""
|
132
|
+
path = P(path)
|
133
|
+
if delete and list_:
|
134
|
+
raise click.UsageError("You can't both list and delete a path.")
|
135
|
+
if not len(path) and not list_:
|
136
|
+
raise click.UsageError("You need a non-empty path.")
|
137
|
+
if codec and delete:
|
138
|
+
raise click.UsageError("You can't both set and delete a path.")
|
139
|
+
|
140
|
+
if list_:
|
141
|
+
if name == "-":
|
142
|
+
if len(path):
|
143
|
+
raise click.UsageError("You can't use a path here.")
|
144
|
+
res = await obj.client._request(
|
145
|
+
action="enum_internal",
|
146
|
+
path=Path("conv"),
|
147
|
+
iter=False,
|
148
|
+
nchain=0,
|
149
|
+
empty=True,
|
150
|
+
)
|
151
|
+
for r in res.result:
|
152
|
+
print(r, file=obj.stdout)
|
153
|
+
|
154
|
+
else:
|
155
|
+
res = await obj.client._request(
|
156
|
+
action="get_tree_internal",
|
157
|
+
path=Path("conv", name) + path,
|
158
|
+
iter=True,
|
159
|
+
nchain=obj.meta,
|
160
|
+
)
|
161
|
+
pl = PathLongener(())
|
162
|
+
async for r in res:
|
163
|
+
pl(r)
|
164
|
+
try:
|
165
|
+
print(f"{r.path} : {Path.build(r.value['codec'])}", file=obj.stdout)
|
166
|
+
except Exception as e:
|
167
|
+
print(f"{Path(r.path)} {e!r}", file=obj.stdout)
|
168
|
+
|
169
|
+
return
|
170
|
+
if delete:
|
171
|
+
res = await obj.client._request(action="delete_internal", path=Path("conv", name) + path)
|
172
|
+
else:
|
173
|
+
msg = {"codec": codec}
|
174
|
+
res = await obj.client._request(
|
175
|
+
action="set_internal",
|
176
|
+
value=msg,
|
177
|
+
path=Path("conv", name) + path,
|
178
|
+
iter=False,
|
179
|
+
nchain=obj.meta,
|
180
|
+
)
|
181
|
+
if obj.meta:
|
182
|
+
yprint(res, stream=obj.stdout)
|
183
|
+
elif type or delete:
|
184
|
+
print(res.tock, file=obj.stdout)
|
185
|
+
else:
|
186
|
+
print(" ".join(res.type), file=obj.stdout)
|
moat/kv/command/data.py
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
# command line interface
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import datetime
|
5
|
+
import time
|
6
|
+
|
7
|
+
import asyncclick as click
|
8
|
+
from moat.util import MsgReader, NotGiven, P, PathLongener, attr_args, yprint
|
9
|
+
|
10
|
+
from moat.kv.client import StreamedRequest
|
11
|
+
from moat.kv.data import add_dates, data_get, node_attr
|
12
|
+
|
13
|
+
|
14
|
+
@click.group(short_help="Manage data.", invoke_without_command=True) # pylint: disable=undefined-variable
|
15
|
+
@click.argument("path", type=P, nargs=1)
|
16
|
+
@click.pass_context
|
17
|
+
async def cli(ctx, path):
|
18
|
+
"""
|
19
|
+
This subcommand accesses the actual user data stored in your MoaT-KV tree.
|
20
|
+
"""
|
21
|
+
if ctx.invoked_subcommand is None:
|
22
|
+
await data_get(ctx.obj, path, recursive=False)
|
23
|
+
else:
|
24
|
+
ctx.obj.path = path
|
25
|
+
|
26
|
+
|
27
|
+
@cli.command()
|
28
|
+
@click.option(
|
29
|
+
"-d",
|
30
|
+
"--as-dict",
|
31
|
+
default=None,
|
32
|
+
help="Structure as dictionary. The argument is the key to use "
|
33
|
+
"for values. Default: return as list",
|
34
|
+
)
|
35
|
+
@click.option(
|
36
|
+
"-m",
|
37
|
+
"--maxdepth",
|
38
|
+
type=int,
|
39
|
+
default=None,
|
40
|
+
help="Limit recursion depth. Default: whole tree",
|
41
|
+
)
|
42
|
+
@click.option(
|
43
|
+
"-M",
|
44
|
+
"--mindepth",
|
45
|
+
type=int,
|
46
|
+
default=None,
|
47
|
+
help="Starting depth. Default: whole tree",
|
48
|
+
)
|
49
|
+
@click.option("-r", "--recursive", is_flag=True, help="Read a complete subtree")
|
50
|
+
@click.option("-e", "--empty", is_flag=True, help="Include empty nodes")
|
51
|
+
@click.option("-R", "--raw", is_flag=True, help="Print string values without quotes etc.")
|
52
|
+
@click.option("-D", "--add-date", is_flag=True, help="Add *_date entries")
|
53
|
+
@click.pass_obj
|
54
|
+
async def get(obj, **k):
|
55
|
+
"""
|
56
|
+
Read a MoaT-KV value.
|
57
|
+
|
58
|
+
If you read a sub-tree recursively, be aware that the whole subtree
|
59
|
+
will be read before anything is printed. Use the "watch --state" subcommand
|
60
|
+
for incremental output.
|
61
|
+
"""
|
62
|
+
|
63
|
+
await data_get(obj, obj.path, **k)
|
64
|
+
|
65
|
+
|
66
|
+
@cli.command("list")
|
67
|
+
@click.option(
|
68
|
+
"-d",
|
69
|
+
"--as-dict",
|
70
|
+
default=None,
|
71
|
+
help="Structure as dictionary. The argument is the key to use "
|
72
|
+
"for values. Default: return as list",
|
73
|
+
)
|
74
|
+
@click.option(
|
75
|
+
"-m",
|
76
|
+
"--maxdepth",
|
77
|
+
type=int,
|
78
|
+
default=1,
|
79
|
+
help="Limit recursion depth. Default: 1 (single layer).",
|
80
|
+
)
|
81
|
+
@click.option(
|
82
|
+
"-M",
|
83
|
+
"--mindepth",
|
84
|
+
type=int,
|
85
|
+
default=1,
|
86
|
+
help="Starting depth. Default: 1 (single layer).",
|
87
|
+
)
|
88
|
+
@click.pass_obj
|
89
|
+
async def list_(obj, **k):
|
90
|
+
"""
|
91
|
+
List MoaT-KV values.
|
92
|
+
|
93
|
+
This is like "get" but with "--mindepth=1 --maxdepth=1 --recursive --empty"
|
94
|
+
|
95
|
+
If you read a sub-tree recursively, be aware that the whole subtree
|
96
|
+
will be read before anything is printed. Use the "watch --state" subcommand
|
97
|
+
for incremental output.
|
98
|
+
"""
|
99
|
+
|
100
|
+
k["recursive"] = True
|
101
|
+
k["raw"] = True
|
102
|
+
k["empty"] = True
|
103
|
+
await data_get(obj, obj.path, **k)
|
104
|
+
|
105
|
+
|
106
|
+
@cli.command("set", short_help="Add or update an entry")
|
107
|
+
@attr_args
|
108
|
+
@click.option("-l", "--last", nargs=2, help="Previous change entry (node serial)")
|
109
|
+
@click.option("-n", "--new", is_flag=True, help="This is a new entry.")
|
110
|
+
@click.pass_obj
|
111
|
+
async def set_(obj, last, new, **kw):
|
112
|
+
"""
|
113
|
+
Store a value at some MoaT-KV position.
|
114
|
+
|
115
|
+
If you update a value you can use "--last" to ensure that no other
|
116
|
+
change arrived between reading and writing the entry. (This is not
|
117
|
+
foolproof but reduces the window to a fraction of a second.)
|
118
|
+
|
119
|
+
When adding a new entry use "--new" to ensure that you don't
|
120
|
+
accidentally overwrite something.
|
121
|
+
|
122
|
+
MoaT-KV entries typically are mappings. Use a colon as the path if you
|
123
|
+
want to replace the top level.
|
124
|
+
"""
|
125
|
+
if new:
|
126
|
+
if last:
|
127
|
+
raise click.UsageError("'new' and 'last' are mutually exclusive")
|
128
|
+
kw["chain"] = None
|
129
|
+
else:
|
130
|
+
if last:
|
131
|
+
kw["chain"] = {"node": last[0], "tick": int(last[1])}
|
132
|
+
|
133
|
+
res = await node_attr(obj, obj.path, **kw)
|
134
|
+
|
135
|
+
if obj.meta:
|
136
|
+
yprint(res, stream=obj.stdout)
|
137
|
+
|
138
|
+
|
139
|
+
class nstr:
|
140
|
+
def __new__(cls, val):
|
141
|
+
if val is NotGiven:
|
142
|
+
return val
|
143
|
+
return str(val)
|
144
|
+
|
145
|
+
|
146
|
+
@cli.command(short_help="Delete an entry / subtree")
|
147
|
+
@click.option(
|
148
|
+
"-p",
|
149
|
+
"--prev",
|
150
|
+
type=nstr,
|
151
|
+
default=NotGiven,
|
152
|
+
help="Previous value. Deprecated; use 'last'",
|
153
|
+
)
|
154
|
+
@click.option("-l", "--last", nargs=2, help="Previous change entry (node serial)")
|
155
|
+
@click.option("-r", "--recursive", is_flag=True, help="Delete a complete subtree")
|
156
|
+
@click.option("--internal", is_flag=True, help="Affect the internal tree. DANGER.")
|
157
|
+
@click.option("-e", "--eval", "eval_", is_flag=True, help="The previous value shall be evaluated.")
|
158
|
+
@click.pass_obj
|
159
|
+
async def delete(obj, prev, last, recursive, eval_, internal):
|
160
|
+
"""
|
161
|
+
Delete an entry, or a whole subtree.
|
162
|
+
|
163
|
+
You really should use "--last" (preferred) or "--prev" (if you must) to
|
164
|
+
ensure that no other change arrived (but note that this doesn't work
|
165
|
+
when deleting a subtree).
|
166
|
+
|
167
|
+
Non-recursively deleting an entry with children works and does *not*
|
168
|
+
affect the child entries.
|
169
|
+
|
170
|
+
The root entry cannot be deleted.
|
171
|
+
"""
|
172
|
+
args = {}
|
173
|
+
if eval_ and prev is NotGiven:
|
174
|
+
raise click.UsageError("You need to add a value that can be evaluated")
|
175
|
+
if recursive:
|
176
|
+
if prev is not NotGiven or last:
|
177
|
+
raise click.UsageError("You can't use a prev value when deleting recursively.")
|
178
|
+
if internal:
|
179
|
+
raise click.UsageError("'internal' and 'recursive' are mutually exclusive")
|
180
|
+
else:
|
181
|
+
if prev is not NotGiven:
|
182
|
+
if eval_:
|
183
|
+
prev = eval(prev) # pylint: disable=eval-used
|
184
|
+
args["prev"] = prev
|
185
|
+
if last:
|
186
|
+
args["chain"] = {"node": last[0], "tick": int(last[1])}
|
187
|
+
|
188
|
+
res = await obj.client.delete(path=obj.path, nchain=obj.meta, recursive=recursive, **args)
|
189
|
+
if isinstance(res, StreamedRequest):
|
190
|
+
pl = PathLongener(obj.path)
|
191
|
+
async for r in res:
|
192
|
+
pl(r)
|
193
|
+
if obj.meta:
|
194
|
+
yprint(r, stream=obj.stdout)
|
195
|
+
else:
|
196
|
+
if obj.meta:
|
197
|
+
yprint(res, stream=obj.stdout)
|
198
|
+
|
199
|
+
|
200
|
+
@cli.command()
|
201
|
+
@click.option("-s", "--state", is_flag=True, help="Also get the current state.")
|
202
|
+
@click.option("-o", "--only", is_flag=True, help="Value only, nothing fancy.")
|
203
|
+
@click.option("-p", "--path-only", is_flag=True, help="Value only, nothing fancy.")
|
204
|
+
@click.option("-D", "--add-date", is_flag=True, help="Add *_date entries")
|
205
|
+
@click.option("-i", "--ignore", multiple=True, type=P, help="Skip this (sub)tree")
|
206
|
+
@click.pass_obj
|
207
|
+
async def monitor(obj, state, only, path_only, add_date, ignore):
|
208
|
+
"""Monitor a MoaT-KV subtree"""
|
209
|
+
|
210
|
+
flushing = not state
|
211
|
+
seen = False
|
212
|
+
|
213
|
+
async with obj.client.watch(
|
214
|
+
obj.path,
|
215
|
+
nchain=obj.meta,
|
216
|
+
fetch=state,
|
217
|
+
max_depth=0 if only else -1,
|
218
|
+
long_path=False,
|
219
|
+
) as res:
|
220
|
+
pl = PathLongener(() if path_only else obj.path)
|
221
|
+
async for r in res:
|
222
|
+
if add_date and "value" in r:
|
223
|
+
add_dates(r.value)
|
224
|
+
if any(p == r.path[: len(p)] for p in ignore):
|
225
|
+
continue
|
226
|
+
pl(r)
|
227
|
+
if r.get("state", "") == "uptodate":
|
228
|
+
if only and not seen:
|
229
|
+
# value doesn't exist
|
230
|
+
return
|
231
|
+
flushing = True
|
232
|
+
if only or path_only:
|
233
|
+
continue
|
234
|
+
else:
|
235
|
+
del r["seq"]
|
236
|
+
if only or path_only:
|
237
|
+
try:
|
238
|
+
if path_only:
|
239
|
+
print(f"{r.path} {r.value}", file=obj.stdout)
|
240
|
+
else:
|
241
|
+
print(r.value, file=obj.stdout)
|
242
|
+
continue
|
243
|
+
except AttributeError:
|
244
|
+
# value has been deleted
|
245
|
+
continue
|
246
|
+
if flushing:
|
247
|
+
r["time"] = time.time()
|
248
|
+
r["_time"] = datetime.datetime.now().isoformat(sep=" ", timespec="milliseconds")
|
249
|
+
yprint(r, stream=obj.stdout)
|
250
|
+
print("---", file=obj.stdout)
|
251
|
+
if flushing:
|
252
|
+
obj.stdout.flush()
|
253
|
+
seen = True
|
254
|
+
|
255
|
+
|
256
|
+
@cli.command()
|
257
|
+
@click.option("-i", "--infile", type=click.Path(), help="File to read (msgpack).")
|
258
|
+
@click.pass_obj
|
259
|
+
async def update(obj, infile):
|
260
|
+
"""Send a list of updates to a MoaT-KV subtree"""
|
261
|
+
async with MsgReader(path=infile) as reader:
|
262
|
+
async for msg in reader:
|
263
|
+
if not hasattr(msg, "path"):
|
264
|
+
continue
|
265
|
+
await obj.client.set(obj.path + msg.path, value=msg.value)
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# command line interface
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import datetime
|
5
|
+
import sys
|
6
|
+
from collections.abc import Mapping
|
7
|
+
|
8
|
+
import asyncclick as click
|
9
|
+
from moat.mqtt.codecs import MsgPackCodec
|
10
|
+
from moat.util import (
|
11
|
+
MsgReader,
|
12
|
+
MsgWriter,
|
13
|
+
P,
|
14
|
+
Path,
|
15
|
+
PathLongener,
|
16
|
+
load_subgroup,
|
17
|
+
yload,
|
18
|
+
yprint,
|
19
|
+
)
|
20
|
+
|
21
|
+
from moat.kv.codec import unpacker
|
22
|
+
|
23
|
+
|
24
|
+
@load_subgroup(short_help="Local data mangling", sub_pre="dump")
|
25
|
+
async def cli():
|
26
|
+
"""
|
27
|
+
Low-level tools that don't depend on a running MoaT-KV server.
|
28
|
+
"""
|
29
|
+
pass
|
30
|
+
|
31
|
+
|
32
|
+
@cli.command()
|
33
|
+
@click.argument("node", nargs=1)
|
34
|
+
@click.argument("file", type=click.Path(), nargs=1)
|
35
|
+
async def init(node, file):
|
36
|
+
"""Write an initial preload file.
|
37
|
+
|
38
|
+
Usage: moat kv dump init <node> <outfile>
|
39
|
+
|
40
|
+
Writes an initial MoaT-KV file that behaves as if it was generated by <node>.
|
41
|
+
|
42
|
+
Using this command, followed by "moat kv server -l <outfile> <node>", is
|
43
|
+
equivalent to running "moat kv server -i 'Initial data' <node>.
|
44
|
+
"""
|
45
|
+
async with MsgWriter(path=file) as f:
|
46
|
+
await f(
|
47
|
+
dict(
|
48
|
+
chain=dict(node=node, tick=1, prev=None),
|
49
|
+
depth=0,
|
50
|
+
path=[],
|
51
|
+
tock=1,
|
52
|
+
value="Initial data",
|
53
|
+
),
|
54
|
+
)
|
55
|
+
|
56
|
+
|
57
|
+
@cli.command("msg")
|
58
|
+
@click.argument("path", nargs=1)
|
59
|
+
@click.pass_obj
|
60
|
+
async def msg_(obj, path):
|
61
|
+
"""
|
62
|
+
Monitor the server-to-sever message stream.
|
63
|
+
|
64
|
+
Use ':' for the main server's "update" stream.
|
65
|
+
Use '+NAME' to monitor a different stream instead.
|
66
|
+
Use '+' to monitor all streams.
|
67
|
+
Otherwise use the given name as-is; Mosquitto wildcard rules apply.
|
68
|
+
|
69
|
+
\b
|
70
|
+
Common streams (prefix with '+'):
|
71
|
+
* ping all servers
|
72
|
+
* update data changes
|
73
|
+
* del nodes responsible for cleaning up deleted records
|
74
|
+
"""
|
75
|
+
from moat.kv.backend import get_backend
|
76
|
+
|
77
|
+
class _Unpack:
|
78
|
+
def __init__(self):
|
79
|
+
self._part_cache = dict()
|
80
|
+
|
81
|
+
import moat.kv.server
|
82
|
+
|
83
|
+
_Unpack._unpack_multiple = moat.kv.server.Server._unpack_multiple
|
84
|
+
_unpacker = _Unpack()._unpack_multiple
|
85
|
+
|
86
|
+
path = P(path)
|
87
|
+
if len(path) == 0:
|
88
|
+
path = P(obj.cfg.server["root"]) | "update"
|
89
|
+
elif len(path) == 1 and path[0].startswith("+"): # pylint: disable=no-member # owch
|
90
|
+
p = path[0][1:]
|
91
|
+
path = P(obj.cfg.server["root"])
|
92
|
+
path |= p or "#"
|
93
|
+
be = obj.cfg.server.backend
|
94
|
+
kw = obj.cfg.server[be]
|
95
|
+
|
96
|
+
async with get_backend(be)(**kw) as conn:
|
97
|
+
async with conn.monitor(*path) as stream:
|
98
|
+
async for msg in stream:
|
99
|
+
v = vars(msg)
|
100
|
+
if isinstance(v.get("payload"), (bytearray, bytes)):
|
101
|
+
t = msg.topic
|
102
|
+
v = unpacker(v["payload"])
|
103
|
+
v = _unpacker(v)
|
104
|
+
if v is None:
|
105
|
+
continue
|
106
|
+
if not isinstance(v, Mapping):
|
107
|
+
v = {"_data": v}
|
108
|
+
v["_topic"] = Path.build(t)
|
109
|
+
else:
|
110
|
+
v["_type"] = type(msg).__name__
|
111
|
+
|
112
|
+
v["_timestamp"] = datetime.datetime.now().isoformat(
|
113
|
+
sep=" ",
|
114
|
+
timespec="milliseconds",
|
115
|
+
)
|
116
|
+
|
117
|
+
yprint(v, stream=obj.stdout)
|
118
|
+
print("---", file=obj.stdout)
|
119
|
+
|
120
|
+
|
121
|
+
@cli.command("post")
|
122
|
+
@click.argument("path", nargs=1)
|
123
|
+
@click.pass_obj
|
124
|
+
async def post_(obj, path):
|
125
|
+
"""
|
126
|
+
Send a msgpack-encoded message (or several) to a specific MQTT topic.
|
127
|
+
|
128
|
+
Messages are read from YAML.
|
129
|
+
Common streams:
|
130
|
+
* ping: sync: all servers (default)
|
131
|
+
* update: data changes
|
132
|
+
* del: sync: nodes responsible for cleaning up deleted records
|
133
|
+
"""
|
134
|
+
from moat.kv.backend import get_backend
|
135
|
+
|
136
|
+
path = P(path)
|
137
|
+
be = obj.cfg.server.backend
|
138
|
+
kw = obj.cfg.server[be]
|
139
|
+
|
140
|
+
async with get_backend(be)(codec=MsgPackCodec, **kw) as conn:
|
141
|
+
for d in yload(sys.stdin, multi=True):
|
142
|
+
topic = d.pop("_topic", path)
|
143
|
+
await conn.send(*topic, payload=d)
|