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/type.py
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# command line interface
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import json
|
5
|
+
|
6
|
+
import asyncclick as click
|
7
|
+
from moat.util import NotGiven, P, Path, PathLongener, yload, yprint
|
8
|
+
|
9
|
+
|
10
|
+
@click.group() # pylint: disable=undefined-variable
|
11
|
+
async def cli():
|
12
|
+
"""Manage types and type matches. Usage: … type …"""
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
@cli.command()
|
17
|
+
@click.option("-s", "--script", type=click.File(mode="w", lazy=True), help="Save the script here")
|
18
|
+
@click.option("-S", "--schema", type=click.File(mode="w", lazy=True), help="Save the schema here")
|
19
|
+
@click.option("-y", "--yaml", "yaml_", is_flag=True, help="Write schema as YAML. Default: JSON.")
|
20
|
+
@click.argument("path", type=P, nargs=1)
|
21
|
+
@click.pass_obj
|
22
|
+
async def get(obj, path, script, schema, yaml_):
|
23
|
+
"""Read type checker information"""
|
24
|
+
if not len(path):
|
25
|
+
raise click.UsageError("You need a non-empty path.")
|
26
|
+
res = await obj.client._request(
|
27
|
+
action="get_internal",
|
28
|
+
path=Path("type") + path,
|
29
|
+
iter=False,
|
30
|
+
nchain=obj.meta,
|
31
|
+
)
|
32
|
+
try:
|
33
|
+
r = res.value
|
34
|
+
except AttributeError:
|
35
|
+
raise click.UsageError(f"No data at {Path('type') + path}") from None
|
36
|
+
|
37
|
+
if not obj.meta:
|
38
|
+
res = res.value
|
39
|
+
if script:
|
40
|
+
script.write(r.pop("code"))
|
41
|
+
if schema:
|
42
|
+
if yaml_:
|
43
|
+
yprint(r.pop("schema"), stream=schema)
|
44
|
+
else:
|
45
|
+
json.dump(r.pop("schema"), schema)
|
46
|
+
yprint(res, stream=obj.stdout)
|
47
|
+
|
48
|
+
|
49
|
+
@cli.command("set")
|
50
|
+
@click.option("-g", "--good", multiple=True, help="Example for passing values")
|
51
|
+
@click.option("-b", "--bad", multiple=True, help="Example for failing values")
|
52
|
+
@click.option("-d", "--data", type=click.File(mode="r"), help="Load metadata from this YAML file.")
|
53
|
+
@click.option("-s", "--script", type=click.File(mode="r"), help="File with the checking script")
|
54
|
+
@click.option("-S", "--schema", type=click.File(mode="r"), help="File with the JSON schema")
|
55
|
+
@click.option("-y", "--yaml", "yaml_", is_flag=True, help="load the schema as YAML. Default: JSON")
|
56
|
+
@click.argument("path", type=P, nargs=1)
|
57
|
+
@click.pass_obj
|
58
|
+
async def set_(obj, path, good, bad, script, schema, yaml_, data):
|
59
|
+
"""Write type checker information."""
|
60
|
+
if not len(path):
|
61
|
+
raise click.UsageError("You need a non-empty path.")
|
62
|
+
|
63
|
+
if data:
|
64
|
+
msg = yload(data)
|
65
|
+
else:
|
66
|
+
msg = {}
|
67
|
+
chain = NotGiven
|
68
|
+
if "value" in msg:
|
69
|
+
chain = msg.get("chain", NotGiven)
|
70
|
+
msg = msg["value"]
|
71
|
+
|
72
|
+
msg.setdefault("good", [])
|
73
|
+
msg.setdefault("bad", [])
|
74
|
+
for x in good:
|
75
|
+
msg["good"].append(eval(x)) # pylint: disable=eval-used
|
76
|
+
for x in bad:
|
77
|
+
msg["bad"].append(eval(x)) # pylint: disable=eval-used
|
78
|
+
|
79
|
+
if "code" in msg:
|
80
|
+
if script:
|
81
|
+
raise click.UsageError("Duplicate script")
|
82
|
+
elif script:
|
83
|
+
msg["code"] = script.read()
|
84
|
+
|
85
|
+
if "schema" in msg:
|
86
|
+
raise click.UsageError("Missing schema")
|
87
|
+
elif schema:
|
88
|
+
if yaml_:
|
89
|
+
msg["schema"] = yload(schema)
|
90
|
+
else:
|
91
|
+
msg["schema"] = json.load(schema)
|
92
|
+
|
93
|
+
if "schema" not in msg and "code" not in msg:
|
94
|
+
raise click.UsageError("I need a schema, Python code, or both.")
|
95
|
+
|
96
|
+
if len(msg["good"]) < 2:
|
97
|
+
raise click.UsageError("Missing known-good test values (at least two)")
|
98
|
+
if not msg["bad"]:
|
99
|
+
raise click.UsageError("Missing known-bad test values")
|
100
|
+
|
101
|
+
res = await obj.client._request(
|
102
|
+
action="set_internal",
|
103
|
+
value=msg,
|
104
|
+
path=Path("type") + path,
|
105
|
+
iter=False,
|
106
|
+
nchain=obj.meta,
|
107
|
+
**({} if chain is NotGiven else {"chain": chain}),
|
108
|
+
)
|
109
|
+
if obj.meta:
|
110
|
+
yprint(res, stream=obj.stdout)
|
111
|
+
|
112
|
+
|
113
|
+
@cli.command()
|
114
|
+
@click.option("-R", "--raw", is_flag=True, help="Print just the path.")
|
115
|
+
@click.option("-t", "--type", "type_", help="Type path to link to.")
|
116
|
+
@click.option("-d", "--delete", help="Use to delete this mapping.")
|
117
|
+
@click.argument("path", type=P, nargs=1)
|
118
|
+
@click.pass_obj
|
119
|
+
async def match(obj, path, type_, delete, raw): # pylint: disable=redefined-builtin
|
120
|
+
"""Match a type to a path (read, if no type given; list if empty path)"""
|
121
|
+
if not len(path):
|
122
|
+
if raw or type_ or delete:
|
123
|
+
raise click.UsageError("No options allowed when dumping the match tree..")
|
124
|
+
y = {}
|
125
|
+
pl = PathLongener()
|
126
|
+
async for r in await obj.client._request(
|
127
|
+
"get_tree_internal",
|
128
|
+
path=Path("match") + path,
|
129
|
+
iter=True,
|
130
|
+
nchain=0,
|
131
|
+
):
|
132
|
+
pl(r)
|
133
|
+
path = r["path"]
|
134
|
+
yy = y
|
135
|
+
for p in path:
|
136
|
+
yy = yy.setdefault(p, {})
|
137
|
+
try:
|
138
|
+
yy["_"] = r["value"]
|
139
|
+
except KeyError:
|
140
|
+
pass
|
141
|
+
yprint(y, stream=obj.stdout)
|
142
|
+
return
|
143
|
+
|
144
|
+
if type_ and delete:
|
145
|
+
raise click.UsageError("You can't both set and delete a path.")
|
146
|
+
if raw and (type_ or delete):
|
147
|
+
raise click.UsageError("You can only print the raw path when reading a match.")
|
148
|
+
|
149
|
+
if delete:
|
150
|
+
res = await obj.client._request(action="delete_internal", path=Path("type") + path)
|
151
|
+
if obj.meta:
|
152
|
+
yprint(res, stream=obj.stdout)
|
153
|
+
return
|
154
|
+
|
155
|
+
msg = {}
|
156
|
+
if type_:
|
157
|
+
msg["type"] = P(type_)
|
158
|
+
act = "set_internal"
|
159
|
+
elif delete:
|
160
|
+
act = "delete_internal"
|
161
|
+
else:
|
162
|
+
act = "get_internal"
|
163
|
+
res = await obj.client._request(
|
164
|
+
action=act,
|
165
|
+
value=msg,
|
166
|
+
path=Path("match") + path,
|
167
|
+
iter=False,
|
168
|
+
nchain=obj.meta,
|
169
|
+
)
|
170
|
+
if obj.meta:
|
171
|
+
yprint(res, stream=obj.stdout)
|
172
|
+
elif type_ or delete:
|
173
|
+
pass
|
174
|
+
else:
|
175
|
+
print(" ".join(str(x) for x in res.type), file=obj.stdout)
|
176
|
+
|
177
|
+
|
178
|
+
@cli.command()
|
179
|
+
@click.argument("path", type=P, nargs=1)
|
180
|
+
@click.pass_obj
|
181
|
+
async def list(obj, path): # pylint: disable=redefined-builtin
|
182
|
+
"""Dump type data"""
|
183
|
+
|
184
|
+
y = {}
|
185
|
+
pl = PathLongener()
|
186
|
+
async for r in await obj.client._request(
|
187
|
+
"get_tree_internal",
|
188
|
+
path=Path("type") + path,
|
189
|
+
iter=True,
|
190
|
+
nchain=0,
|
191
|
+
):
|
192
|
+
pl(r)
|
193
|
+
path = r["path"]
|
194
|
+
yy = y
|
195
|
+
for p in path:
|
196
|
+
yy = yy.setdefault(p, {})
|
197
|
+
try:
|
198
|
+
yy["_"] = r["value"]
|
199
|
+
except KeyError:
|
200
|
+
pass
|
201
|
+
yprint(y, stream=obj.stdout)
|
moat/kv/mock/__init__.py
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# from asyncclick.testing import CliRunner
|
2
|
+
from __future__ import annotations
|
3
|
+
import io
|
4
|
+
import logging
|
5
|
+
import shlex
|
6
|
+
import socket
|
7
|
+
import sys
|
8
|
+
from pathlib import Path
|
9
|
+
|
10
|
+
import attr
|
11
|
+
from asyncscope import main_scope, scope
|
12
|
+
from moat.src.test import run # pylint:disable=import-error,no-name-in-module
|
13
|
+
from moat.util import ( # pylint:disable=no-name-in-module
|
14
|
+
CFG,
|
15
|
+
OptCtx,
|
16
|
+
attrdict,
|
17
|
+
combine_dict,
|
18
|
+
ensure_cfg,
|
19
|
+
list_ext,
|
20
|
+
load_ext,
|
21
|
+
wrap_main,
|
22
|
+
yload,
|
23
|
+
)
|
24
|
+
|
25
|
+
from moat.kv.client import _scoped_client, client_scope
|
26
|
+
|
27
|
+
logger = logging.getLogger(__name__)
|
28
|
+
try:
|
29
|
+
from contextlib import asynccontextmanager
|
30
|
+
except ImportError:
|
31
|
+
from async_generator import asynccontextmanager
|
32
|
+
|
33
|
+
ensure_cfg("moat.kv")
|
34
|
+
|
35
|
+
|
36
|
+
@attr.s
|
37
|
+
class S:
|
38
|
+
tg = attr.ib()
|
39
|
+
client_ctx = attr.ib()
|
40
|
+
s = attr.ib(factory=list) # servers
|
41
|
+
c = attr.ib(factory=list) # clients
|
42
|
+
_seq = 1
|
43
|
+
|
44
|
+
async def ready(self, i=None):
|
45
|
+
if i is not None:
|
46
|
+
await self.s[i].is_ready
|
47
|
+
return self.s[i]
|
48
|
+
for s in self.s:
|
49
|
+
if s is not None:
|
50
|
+
await s.is_ready
|
51
|
+
return self.s
|
52
|
+
|
53
|
+
def __iter__(self):
|
54
|
+
return iter(self.s)
|
55
|
+
|
56
|
+
@asynccontextmanager
|
57
|
+
async def client(self, i: int = 0, **kv):
|
58
|
+
"""Get a (new) client for the i'th server."""
|
59
|
+
await self.s[i].is_serving
|
60
|
+
self._seq += 1
|
61
|
+
for host, port, *_ in self.s[i].ports:
|
62
|
+
if host != "::" and host[0] == ":":
|
63
|
+
continue
|
64
|
+
try:
|
65
|
+
cfg = combine_dict(
|
66
|
+
dict(conn=dict(host=host, port=port, ssl=self.client_ctx, **kv)),
|
67
|
+
CFG["kv"],
|
68
|
+
)
|
69
|
+
|
70
|
+
async def scc(s, **cfg):
|
71
|
+
scope.requires(s._scope)
|
72
|
+
return await _scoped_client(scope.name, **cfg)
|
73
|
+
|
74
|
+
async with scope.using_scope():
|
75
|
+
c = await scope.service(
|
76
|
+
f"moat.kv.client.{i}.{self._seq}",
|
77
|
+
scc,
|
78
|
+
self.s[i],
|
79
|
+
**cfg,
|
80
|
+
)
|
81
|
+
yield c
|
82
|
+
return
|
83
|
+
except socket.gaierror:
|
84
|
+
pass
|
85
|
+
raise RuntimeError("Duh? no connection")
|
86
|
+
|
87
|
+
async def run(self, *args, do_stdout=True):
|
88
|
+
h = p = None
|
89
|
+
for s in self.s:
|
90
|
+
for h, p, *_ in s.ports:
|
91
|
+
if h[0] != ":":
|
92
|
+
break
|
93
|
+
else:
|
94
|
+
continue
|
95
|
+
break
|
96
|
+
if len(args) == 1:
|
97
|
+
args = args[0]
|
98
|
+
if isinstance(args, str):
|
99
|
+
args = args.split(" ")
|
100
|
+
async with scope.using_scope():
|
101
|
+
return await run("-VV", "kv", "-h", h, "-p", p, *args, do_stdout=do_stdout)
|
moat/kv/mock/mqtt.py
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import copy
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import time
|
6
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
7
|
+
from functools import partial
|
8
|
+
|
9
|
+
import anyio
|
10
|
+
from unittest import mock
|
11
|
+
import trio
|
12
|
+
from asyncscope import main_scope, scope
|
13
|
+
from moat.mqtt.broker import create_broker
|
14
|
+
from moat.util import NotGiven, attrdict, combine_dict
|
15
|
+
|
16
|
+
from moat.kv.mock import S
|
17
|
+
from moat.kv.server import Server
|
18
|
+
|
19
|
+
from . import CFG
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
otm = time.time
|
24
|
+
|
25
|
+
PORT = 40000 + (os.getpid() + 10) % 10000
|
26
|
+
|
27
|
+
broker_cfg = {
|
28
|
+
"listeners": {"default": {"type": "tcp", "bind": f"127.0.0.1:{PORT}"}},
|
29
|
+
"timeout-disconnect-delay": 2,
|
30
|
+
"auth": {"allow-anonymous": True, "password-file": None},
|
31
|
+
}
|
32
|
+
|
33
|
+
URI = f"mqtt://127.0.0.1:{PORT}/"
|
34
|
+
|
35
|
+
|
36
|
+
@asynccontextmanager
|
37
|
+
async def stdtest(n=1, run=True, ssl=False, tocks=20, **kw):
|
38
|
+
C_OUT = CFG.get("_stdout", NotGiven)
|
39
|
+
if C_OUT is not NotGiven:
|
40
|
+
del CFG["_stdout"]
|
41
|
+
TESTCFG = copy.deepcopy(CFG["kv"])
|
42
|
+
TESTCFG.server.port = None
|
43
|
+
TESTCFG.root = "test"
|
44
|
+
if C_OUT is not NotGiven:
|
45
|
+
CFG["_stdout"] = C_OUT
|
46
|
+
TESTCFG["_stdout"] = C_OUT
|
47
|
+
|
48
|
+
if ssl:
|
49
|
+
import ssl
|
50
|
+
|
51
|
+
import trustme
|
52
|
+
|
53
|
+
ca = trustme.CA()
|
54
|
+
cert = ca.issue_server_cert("127.0.0.1")
|
55
|
+
server_ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
56
|
+
client_ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
|
57
|
+
ca.configure_trust(client_ctx)
|
58
|
+
cert.configure_cert(server_ctx)
|
59
|
+
else:
|
60
|
+
server_ctx = client_ctx = False
|
61
|
+
|
62
|
+
clock = trio.lowlevel.current_clock()
|
63
|
+
try:
|
64
|
+
clock.autojump_threshold = 0.02 # networking
|
65
|
+
except Exception:
|
66
|
+
pass # test doesn't have autojump_clock fixture
|
67
|
+
|
68
|
+
async def mock_get_host_port(st, host):
|
69
|
+
i = int(host[host.rindex("_") + 1 :])
|
70
|
+
s = st.s[i]
|
71
|
+
await s.is_serving
|
72
|
+
for host, port, *_ in s.ports:
|
73
|
+
if host == "::" or host[0] != ":":
|
74
|
+
return host, port
|
75
|
+
|
76
|
+
def tm():
|
77
|
+
try:
|
78
|
+
return trio.current_time()
|
79
|
+
except RuntimeError:
|
80
|
+
return otm()
|
81
|
+
|
82
|
+
async def mock_set_tock(self, old):
|
83
|
+
assert self._tock < tocks, "Test didn't terminate. Limit:" + str(tocks)
|
84
|
+
await old()
|
85
|
+
|
86
|
+
done = False
|
87
|
+
async with main_scope("moat.kv.test.mqtt") as scp:
|
88
|
+
tg = scp._tg
|
89
|
+
st = S(tg, client_ctx)
|
90
|
+
async with AsyncExitStack() as ex:
|
91
|
+
st.ex = ex # pylint: disable=attribute-defined-outside-init
|
92
|
+
ex.enter_context(mock.patch("time.time", new=tm))
|
93
|
+
ex.enter_context(mock.patch("time.monotonic", new=tm))
|
94
|
+
logging._startTime = tm()
|
95
|
+
|
96
|
+
async def run_broker(cfg):
|
97
|
+
async with create_broker(config=cfg) as srv:
|
98
|
+
# NB: some services use "async with await …"
|
99
|
+
scope.register(srv)
|
100
|
+
await scope.no_more_dependents()
|
101
|
+
|
102
|
+
async def with_broker(s, *a, **k):
|
103
|
+
await scope.service("moat.mqtt.broker", run_broker, broker_cfg)
|
104
|
+
s._scope = scope.get()
|
105
|
+
return await s._scoped_serve(*a, **k)
|
106
|
+
|
107
|
+
args_def = kw.get("args", attrdict())
|
108
|
+
for i in range(n):
|
109
|
+
name = "test_" + str(i)
|
110
|
+
args = kw.get(name, args_def)
|
111
|
+
args = combine_dict(
|
112
|
+
args,
|
113
|
+
args_def,
|
114
|
+
{
|
115
|
+
"cfg": {
|
116
|
+
"conn": {"ssl": client_ctx},
|
117
|
+
"server": {
|
118
|
+
"bind_default": {
|
119
|
+
"host": "127.0.0.1",
|
120
|
+
"port": i + PORT + 1,
|
121
|
+
"ssl": server_ctx,
|
122
|
+
},
|
123
|
+
"backend": "mqtt",
|
124
|
+
"mqtt": {"uri": URI},
|
125
|
+
},
|
126
|
+
},
|
127
|
+
},
|
128
|
+
{"cfg": TESTCFG},
|
129
|
+
)
|
130
|
+
args_def.pop("init", None)
|
131
|
+
s = Server(name, **args)
|
132
|
+
ex.enter_context(
|
133
|
+
mock.patch.object(s, "_set_tock", new=partial(mock_set_tock, s, s._set_tock)),
|
134
|
+
)
|
135
|
+
ex.enter_context(
|
136
|
+
mock.patch.object(s, "_get_host_port", new=partial(mock_get_host_port, st)),
|
137
|
+
)
|
138
|
+
st.s.append(s)
|
139
|
+
|
140
|
+
evts = []
|
141
|
+
for i in range(n):
|
142
|
+
if kw.get(f"run_{i}", run):
|
143
|
+
evt = anyio.Event()
|
144
|
+
await scp.spawn_service(with_broker, st.s[i], ready_evt=evt)
|
145
|
+
evts.append(evt)
|
146
|
+
else:
|
147
|
+
setattr(st, f"run_{i}", partial(scp.spawn_service, with_broker, st.s[i]))
|
148
|
+
|
149
|
+
for e in evts:
|
150
|
+
await e.wait()
|
151
|
+
try:
|
152
|
+
done = True
|
153
|
+
yield st
|
154
|
+
finally:
|
155
|
+
with anyio.fail_after(2, shield=True):
|
156
|
+
logger.info("Runtime: %s", clock.current_time())
|
157
|
+
tg.cancel_scope.cancel()
|
158
|
+
if not done:
|
159
|
+
yield None
|
moat/kv/mock/serf.py
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import copy
|
3
|
+
import logging
|
4
|
+
import time
|
5
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
6
|
+
from functools import partial
|
7
|
+
|
8
|
+
import anyio
|
9
|
+
import attr
|
10
|
+
from unittest import mock
|
11
|
+
import trio
|
12
|
+
from asyncscope import main_scope, scope
|
13
|
+
from asyncserf.stream import SerfEvent
|
14
|
+
from moat.util import NotGiven, ValueEvent, attrdict, combine_dict, create_queue
|
15
|
+
|
16
|
+
from moat.kv.codec import unpacker
|
17
|
+
from moat.kv.mock import S as _S
|
18
|
+
from moat.kv.server import Server
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
otm = time.time
|
23
|
+
|
24
|
+
from . import CFG
|
25
|
+
|
26
|
+
|
27
|
+
@asynccontextmanager
|
28
|
+
async def stdtest(n=1, run=True, ssl=False, tocks=20, **kw):
|
29
|
+
C_OUT = CFG.get("_stdout", NotGiven)
|
30
|
+
if C_OUT is not NotGiven:
|
31
|
+
del CFG["_stdout"]
|
32
|
+
TESTCFG = copy.deepcopy(CFG["kv"])
|
33
|
+
TESTCFG.server.port = None
|
34
|
+
TESTCFG.server.backend = "serf"
|
35
|
+
TESTCFG.root = "test"
|
36
|
+
if C_OUT is not NotGiven:
|
37
|
+
CFG["_stdout"] = C_OUT
|
38
|
+
TESTCFG["_stdout"] = C_OUT
|
39
|
+
|
40
|
+
if ssl:
|
41
|
+
import ssl
|
42
|
+
|
43
|
+
import trustme
|
44
|
+
|
45
|
+
ca = trustme.CA()
|
46
|
+
cert = ca.issue_server_cert("127.0.0.1")
|
47
|
+
server_ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
48
|
+
client_ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
|
49
|
+
ca.configure_trust(client_ctx)
|
50
|
+
cert.configure_cert(server_ctx)
|
51
|
+
else:
|
52
|
+
server_ctx = client_ctx = False
|
53
|
+
|
54
|
+
clock = trio.lowlevel.current_clock()
|
55
|
+
clock.autojump_threshold = 0.0
|
56
|
+
# clock.rate = 5
|
57
|
+
|
58
|
+
@attr.s
|
59
|
+
class S(_S):
|
60
|
+
splits = attr.ib(factory=set)
|
61
|
+
serfs = attr.ib(factory=set)
|
62
|
+
|
63
|
+
def split(self, s):
|
64
|
+
assert s not in self.splits
|
65
|
+
logger.debug("Split: add %d", s)
|
66
|
+
self.splits.add(s)
|
67
|
+
|
68
|
+
def join(self, s):
|
69
|
+
logger.debug("Split: join %d", s)
|
70
|
+
self.splits.remove(s)
|
71
|
+
|
72
|
+
async def mock_get_host_port(st, host):
|
73
|
+
i = int(host[host.rindex("_") + 1 :])
|
74
|
+
s = st.s[i]
|
75
|
+
await s.is_serving
|
76
|
+
for host, port, *_ in s.ports:
|
77
|
+
if host == "::" or host[0] != ":":
|
78
|
+
return host, port
|
79
|
+
|
80
|
+
def tm():
|
81
|
+
try:
|
82
|
+
return trio.current_time()
|
83
|
+
except RuntimeError:
|
84
|
+
return otm()
|
85
|
+
|
86
|
+
async def mock_set_tock(self, old):
|
87
|
+
assert self._tock < tocks, "Test didn't terminate. Limit:" + str(tocks)
|
88
|
+
await old()
|
89
|
+
|
90
|
+
async with main_scope("moat.kv.test.serf") as scp:
|
91
|
+
tg = scp._tg
|
92
|
+
st = S(tg, client_ctx)
|
93
|
+
async with AsyncExitStack() as ex:
|
94
|
+
st.ex = ex # pylint: disable=attribute-defined-outside-init
|
95
|
+
ex.enter_context(mock.patch("time.time", new=tm))
|
96
|
+
ex.enter_context(mock.patch("time.monotonic", new=tm))
|
97
|
+
logging._startTime = tm()
|
98
|
+
|
99
|
+
ex.enter_context(
|
100
|
+
mock.patch("asyncserf.serf_client", new=partial(mock_serf_client, st)),
|
101
|
+
)
|
102
|
+
|
103
|
+
for i in range(n):
|
104
|
+
name = "test_" + str(i)
|
105
|
+
args = kw.get(name, kw.get("args", attrdict()))
|
106
|
+
args["cfg"] = combine_dict(
|
107
|
+
args.get("cfg", {}),
|
108
|
+
{
|
109
|
+
"kv": {
|
110
|
+
"conn": {"ssl": client_ctx},
|
111
|
+
},
|
112
|
+
"server": {
|
113
|
+
"bind_default": {
|
114
|
+
"host": "127.0.0.1",
|
115
|
+
"port": i + 50120,
|
116
|
+
"ssl": server_ctx,
|
117
|
+
},
|
118
|
+
"serf": {"i": i},
|
119
|
+
},
|
120
|
+
},
|
121
|
+
TESTCFG,
|
122
|
+
)
|
123
|
+
s = Server(name, **args)
|
124
|
+
ex.enter_context(
|
125
|
+
mock.patch.object(s, "_set_tock", new=partial(mock_set_tock, s, s._set_tock)),
|
126
|
+
)
|
127
|
+
ex.enter_context(
|
128
|
+
mock.patch.object(s, "_get_host_port", new=partial(mock_get_host_port, st)),
|
129
|
+
)
|
130
|
+
st.s.append(s)
|
131
|
+
|
132
|
+
async def with_serf(s, *a, **k):
|
133
|
+
s._scope = scope.get()
|
134
|
+
return await s._scoped_serve(*a, **k)
|
135
|
+
|
136
|
+
evts = []
|
137
|
+
for i in range(n):
|
138
|
+
if kw.get("run_" + str(i), run):
|
139
|
+
evt = anyio.Event()
|
140
|
+
await scp.spawn_service(with_serf, st.s[i], ready_evt=evt)
|
141
|
+
evts.append(evt)
|
142
|
+
for e in evts:
|
143
|
+
await e.wait()
|
144
|
+
try:
|
145
|
+
yield st
|
146
|
+
finally:
|
147
|
+
with anyio.fail_after(2, shield=True):
|
148
|
+
logger.info("Runtime: %s", clock.current_time())
|
149
|
+
tg.cancel_scope.cancel()
|
150
|
+
logger.info("End")
|
151
|
+
pass # unwinding ex:AsyncExitStack
|
152
|
+
|
153
|
+
|
154
|
+
@asynccontextmanager
|
155
|
+
async def mock_serf_client(master, **cfg):
|
156
|
+
async with scope.using_scope():
|
157
|
+
ms = MockServ(master, **cfg)
|
158
|
+
master.serfs.add(ms)
|
159
|
+
ms._scope = scope.get() # pylint:disable=attribute-defined-outside-init
|
160
|
+
try:
|
161
|
+
yield ms
|
162
|
+
finally:
|
163
|
+
master.serfs.remove(ms)
|
164
|
+
pass # terminating mock_serf_client nursery
|
165
|
+
|
166
|
+
|
167
|
+
class MockServ:
|
168
|
+
def __init__(self, master, **cfg):
|
169
|
+
self.cfg = cfg
|
170
|
+
self._tg = scope._tg
|
171
|
+
self.streams = {}
|
172
|
+
self._master = master
|
173
|
+
|
174
|
+
def __hash__(self):
|
175
|
+
return id(self)
|
176
|
+
|
177
|
+
async def spawn(self, fn, *args, **kw):
|
178
|
+
async def run(evt):
|
179
|
+
with anyio.CancelScope() as sc:
|
180
|
+
await evt.set(sc)
|
181
|
+
await fn(*args, **kw)
|
182
|
+
|
183
|
+
evt = ValueEvent()
|
184
|
+
self._tg.spawn(run, evt)
|
185
|
+
return await evt.get()
|
186
|
+
|
187
|
+
async def event(self, name, payload, coalesce=True):
|
188
|
+
try:
|
189
|
+
logger.debug("SERF:%s: %r", name, unpacker(payload))
|
190
|
+
except Exception:
|
191
|
+
logger.debug("SERF:%s: %r (raw)", name, payload)
|
192
|
+
assert not coalesce, "'coalesce' must be cleared!"
|
193
|
+
|
194
|
+
i_self = self.cfg.get("i", 0)
|
195
|
+
for s in list(self._master.serfs):
|
196
|
+
i_s = s.cfg.get("i", 0)
|
197
|
+
for x in self._master.splits:
|
198
|
+
if (i_s < x) != (i_self < x):
|
199
|
+
break
|
200
|
+
else:
|
201
|
+
n = tuple(name.split("."))
|
202
|
+
while n:
|
203
|
+
sl = s.streams.get(n, ())
|
204
|
+
for sn in sl:
|
205
|
+
await sn.q.put((name, payload))
|
206
|
+
n = n[:-1]
|
207
|
+
|
208
|
+
def stream(self, typ):
|
209
|
+
"""compat for supporting asyncactor"""
|
210
|
+
if not typ.startswith("user:"):
|
211
|
+
raise RuntimeError("not supported")
|
212
|
+
typ = typ[5:]
|
213
|
+
return self.serf_mon(typ)
|
214
|
+
|
215
|
+
def serf_mon(self, typ):
|
216
|
+
if "," in typ:
|
217
|
+
raise RuntimeError("not supported")
|
218
|
+
s = MockSerfStream(self, "user:" + typ)
|
219
|
+
return s
|
220
|
+
|
221
|
+
async def serf_send(self, typ, payload):
|
222
|
+
"""compat for supporting asyncactor"""
|
223
|
+
return await self.event(typ, payload)
|
224
|
+
|
225
|
+
|
226
|
+
class MockSerfStream:
|
227
|
+
q = None
|
228
|
+
|
229
|
+
def __init__(self, serf, typ):
|
230
|
+
self.serf = serf
|
231
|
+
assert typ.startswith("user:")
|
232
|
+
self.typ = tuple(typ[5:].split("."))
|
233
|
+
|
234
|
+
async def __aenter__(self):
|
235
|
+
self.q = create_queue(100)
|
236
|
+
self.serf.streams.setdefault(self.typ, []).append(self)
|
237
|
+
return self
|
238
|
+
|
239
|
+
async def __aexit__(self, *tb):
|
240
|
+
self.serf.streams[self.typ].remove(self)
|
241
|
+
del self.q
|
242
|
+
|
243
|
+
def __aiter__(self):
|
244
|
+
return self
|
245
|
+
|
246
|
+
async def __anext__(self):
|
247
|
+
res = await self.q.get()
|
248
|
+
evt = SerfEvent(self)
|
249
|
+
evt.topic, evt.payload = res
|
250
|
+
return evt
|