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