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,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)
@@ -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