moat-kv 0.71.0__py3-none-any.whl → 0.71.7__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.
- moat/kv/__init__.py +6 -7
- moat/kv/_cfg.yaml +3 -2
- moat/kv/actor/__init__.py +2 -1
- moat/kv/actor/deletor.py +4 -1
- moat/kv/auth/__init__.py +12 -13
- moat/kv/auth/_test.py +4 -1
- moat/kv/auth/password.py +11 -7
- moat/kv/backend/mqtt.py +4 -5
- moat/kv/client.py +20 -39
- moat/kv/code.py +3 -3
- moat/kv/command/data.py +4 -3
- moat/kv/command/dump/__init__.py +36 -34
- moat/kv/command/internal.py +2 -3
- moat/kv/command/job.py +1 -2
- moat/kv/command/type.py +3 -6
- moat/kv/data.py +9 -8
- moat/kv/errors.py +16 -8
- moat/kv/mock/__init__.py +2 -12
- moat/kv/model.py +29 -33
- moat/kv/obj/__init__.py +3 -3
- moat/kv/obj/command.py +3 -3
- moat/kv/runner.py +4 -5
- moat/kv/server.py +106 -126
- moat/kv/types.py +10 -12
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/METADATA +6 -2
- moat_kv-0.71.7.dist-info/RECORD +47 -0
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/WHEEL +1 -1
- moat_kv-0.71.7.dist-info/licenses/LICENSE +3 -0
- moat_kv-0.71.7.dist-info/licenses/LICENSE.APACHE2 +202 -0
- moat_kv-0.71.7.dist-info/licenses/LICENSE.MIT +20 -0
- moat_kv-0.71.7.dist-info/top_level.txt +1 -0
- build/lib/docs/source/conf.py +0 -201
- build/lib/examples/pathify.py +0 -45
- build/lib/moat/kv/__init__.py +0 -19
- build/lib/moat/kv/_cfg.yaml +0 -93
- build/lib/moat/kv/_main.py +0 -91
- build/lib/moat/kv/actor/__init__.py +0 -98
- build/lib/moat/kv/actor/deletor.py +0 -139
- build/lib/moat/kv/auth/__init__.py +0 -444
- build/lib/moat/kv/auth/_test.py +0 -166
- build/lib/moat/kv/auth/password.py +0 -234
- build/lib/moat/kv/auth/root.py +0 -58
- build/lib/moat/kv/backend/__init__.py +0 -67
- build/lib/moat/kv/backend/mqtt.py +0 -71
- build/lib/moat/kv/client.py +0 -1025
- build/lib/moat/kv/code.py +0 -236
- build/lib/moat/kv/codec.py +0 -11
- build/lib/moat/kv/command/__init__.py +0 -1
- build/lib/moat/kv/command/acl.py +0 -180
- build/lib/moat/kv/command/auth.py +0 -261
- build/lib/moat/kv/command/code.py +0 -293
- build/lib/moat/kv/command/codec.py +0 -186
- build/lib/moat/kv/command/data.py +0 -265
- build/lib/moat/kv/command/dump/__init__.py +0 -143
- build/lib/moat/kv/command/error.py +0 -149
- build/lib/moat/kv/command/internal.py +0 -248
- build/lib/moat/kv/command/job.py +0 -433
- build/lib/moat/kv/command/log.py +0 -53
- build/lib/moat/kv/command/server.py +0 -114
- build/lib/moat/kv/command/type.py +0 -201
- build/lib/moat/kv/config.py +0 -46
- build/lib/moat/kv/data.py +0 -216
- build/lib/moat/kv/errors.py +0 -561
- build/lib/moat/kv/exceptions.py +0 -126
- build/lib/moat/kv/mock/__init__.py +0 -101
- build/lib/moat/kv/mock/mqtt.py +0 -159
- build/lib/moat/kv/mock/tracer.py +0 -63
- build/lib/moat/kv/model.py +0 -1069
- build/lib/moat/kv/obj/__init__.py +0 -646
- build/lib/moat/kv/obj/command.py +0 -241
- build/lib/moat/kv/runner.py +0 -1347
- build/lib/moat/kv/server.py +0 -2809
- build/lib/moat/kv/types.py +0 -513
- ci/rtd-requirements.txt +0 -4
- ci/test-requirements.txt +0 -7
- ci/travis.sh +0 -96
- debian/.gitignore +0 -7
- debian/changelog +0 -1435
- debian/control +0 -43
- debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +0 -201
- debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +0 -45
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +0 -19
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +0 -93
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +0 -91
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +0 -98
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +0 -139
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +0 -444
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +0 -166
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +0 -234
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +0 -58
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +0 -67
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +0 -71
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +0 -1025
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +0 -236
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +0 -11
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +0 -1
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +0 -180
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +0 -261
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +0 -293
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +0 -186
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +0 -265
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +0 -143
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +0 -149
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +0 -248
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +0 -433
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +0 -53
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +0 -114
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +0 -201
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +0 -46
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +0 -216
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +0 -561
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +0 -126
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +0 -101
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +0 -159
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +0 -63
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +0 -1069
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +0 -646
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +0 -241
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +0 -1347
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +0 -2809
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +0 -513
- debian/moat-kv.postinst +0 -3
- debian/rules +0 -20
- debian/source/format +0 -1
- debian/watch +0 -4
- docs/Makefile +0 -20
- docs/make.bat +0 -36
- docs/source/TODO.rst +0 -61
- docs/source/_static/.gitkeep +0 -0
- docs/source/acls.rst +0 -80
- docs/source/auth.rst +0 -84
- docs/source/client_protocol.rst +0 -456
- docs/source/code.rst +0 -341
- docs/source/command_line.rst +0 -1187
- docs/source/common_protocol.rst +0 -47
- docs/source/conf.py +0 -201
- docs/source/debugging.rst +0 -70
- docs/source/extend.rst +0 -37
- docs/source/history.rst +0 -36
- docs/source/index.rst +0 -75
- docs/source/model.rst +0 -54
- docs/source/overview.rst +0 -83
- docs/source/related.rst +0 -89
- docs/source/server_protocol.rst +0 -450
- docs/source/startup.rst +0 -31
- docs/source/translator.rst +0 -244
- docs/source/tutorial.rst +0 -711
- docs/source/v3.rst +0 -168
- examples/code/transform.scale.yml +0 -21
- examples/code/transform.switch.yml +0 -82
- examples/code/transform.timeslot.yml +0 -63
- examples/pathify.py +0 -45
- moat/kv/codec.py +0 -11
- moat_kv-0.71.0.dist-info/RECORD +0 -188
- moat_kv-0.71.0.dist-info/top_level.txt +0 -9
- scripts/current +0 -15
- scripts/env +0 -8
- scripts/init +0 -39
- scripts/recover +0 -17
- scripts/rotate +0 -33
- scripts/run +0 -29
- scripts/run-all +0 -10
- scripts/run-any +0 -10
- scripts/run-single +0 -15
- scripts/success +0 -4
- systemd/moat-kv-recover.service +0 -21
- systemd/moat-kv-rotate.service +0 -20
- systemd/moat-kv-rotate.timer +0 -10
- systemd/moat-kv-run-all.service +0 -26
- systemd/moat-kv-run-all@.service +0 -25
- systemd/moat-kv-run-any.service +0 -26
- systemd/moat-kv-run-any@.service +0 -25
- systemd/moat-kv-run-single.service +0 -26
- systemd/moat-kv-run-single@.service +0 -25
- systemd/moat-kv.service +0 -27
- systemd/postinst +0 -7
- systemd/sysusers +0 -3
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,646 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Object interface to moat.kv data
|
3
|
-
|
4
|
-
"""
|
5
|
-
from __future__ import annotations
|
6
|
-
|
7
|
-
import heapq
|
8
|
-
import weakref
|
9
|
-
from collections.abc import Mapping
|
10
|
-
from functools import partial
|
11
|
-
|
12
|
-
import anyio
|
13
|
-
from asyncscope import scope
|
14
|
-
|
15
|
-
try:
|
16
|
-
from contextlib import asynccontextmanager
|
17
|
-
except ImportError:
|
18
|
-
from async_generator import asynccontextmanager
|
19
|
-
|
20
|
-
from moat.util import (
|
21
|
-
NoLock,
|
22
|
-
NotGiven,
|
23
|
-
Path,
|
24
|
-
PathLongener,
|
25
|
-
combine_dict,
|
26
|
-
yload,
|
27
|
-
ensure_cfg,
|
28
|
-
CFG,
|
29
|
-
)
|
30
|
-
|
31
|
-
__all__ = ["ClientEntry", "AttrClientEntry", "ClientRoot"]
|
32
|
-
|
33
|
-
|
34
|
-
class NamedRoot:
|
35
|
-
"""
|
36
|
-
This is a mix-on class for the root of a subhierarchy that caches named
|
37
|
-
sub-entries.
|
38
|
-
|
39
|
-
Named children should call `_add_name` on this entry.
|
40
|
-
"""
|
41
|
-
|
42
|
-
def __init__(self, *a, **k):
|
43
|
-
self.__named = {}
|
44
|
-
super().__init__(*a, **k)
|
45
|
-
|
46
|
-
def by_name(self, name):
|
47
|
-
if name is None:
|
48
|
-
return None
|
49
|
-
if not isinstance(name, str):
|
50
|
-
raise ValueError("No string: " + repr(name))
|
51
|
-
return self.__named.get(name)
|
52
|
-
|
53
|
-
def _add_name(self, obj):
|
54
|
-
n = obj.name
|
55
|
-
if n is None:
|
56
|
-
return
|
57
|
-
|
58
|
-
self.__named[n] = obj
|
59
|
-
obj.reg_del(self, "_del__name", obj, n)
|
60
|
-
|
61
|
-
def _del__name(self, obj, n):
|
62
|
-
old = self.__named.pop(n)
|
63
|
-
if old is None or old is obj:
|
64
|
-
return
|
65
|
-
# Oops, that has been superseded. Put it back.
|
66
|
-
self.__named[n] = old
|
67
|
-
|
68
|
-
|
69
|
-
class ClientEntry:
|
70
|
-
"""A helper class that represents a node on the server, as returned by
|
71
|
-
:meth:`Client.mirror`.
|
72
|
-
"""
|
73
|
-
|
74
|
-
value = NotGiven
|
75
|
-
chain = None
|
76
|
-
|
77
|
-
def __init__(self, parent, name=None):
|
78
|
-
self._init()
|
79
|
-
self._path = parent._path + (name,)
|
80
|
-
self._name = name
|
81
|
-
self._parent = weakref.ref(parent)
|
82
|
-
self._root = weakref.ref(parent.root)
|
83
|
-
self.client = parent.client
|
84
|
-
|
85
|
-
def _init(self):
|
86
|
-
self._lock = anyio.Lock() # for saving etc.
|
87
|
-
self.chain = None
|
88
|
-
self.value = NotGiven
|
89
|
-
self._children = dict()
|
90
|
-
|
91
|
-
@classmethod
|
92
|
-
def child_type(cls, name): # pylint: disable=unused-argument
|
93
|
-
"""Given a node, return the type which the child with that name should have.
|
94
|
-
The default is "same as this class".
|
95
|
-
"""
|
96
|
-
return cls
|
97
|
-
|
98
|
-
def value_or(self, default, typ=None):
|
99
|
-
"""
|
100
|
-
Shortcut to coerce the value to some type
|
101
|
-
"""
|
102
|
-
val = self.value
|
103
|
-
if val is NotGiven:
|
104
|
-
return default
|
105
|
-
if typ is not None and not isinstance(val, typ):
|
106
|
-
return default
|
107
|
-
return val
|
108
|
-
|
109
|
-
def val(self, *attr):
|
110
|
-
"""
|
111
|
-
Shortcut to get an attribute value
|
112
|
-
"""
|
113
|
-
return self.val_d(NotGiven, *attr)
|
114
|
-
|
115
|
-
def val_d(self, default, *attr):
|
116
|
-
"""
|
117
|
-
Shortcut to get an attribute value, or a default
|
118
|
-
"""
|
119
|
-
val = self.value
|
120
|
-
if val is NotGiven:
|
121
|
-
if default is NotGiven:
|
122
|
-
raise ValueError("no value set")
|
123
|
-
return default
|
124
|
-
for a in attr:
|
125
|
-
try:
|
126
|
-
val = val[a]
|
127
|
-
except KeyError:
|
128
|
-
if default is NotGiven:
|
129
|
-
raise
|
130
|
-
return default
|
131
|
-
return val
|
132
|
-
|
133
|
-
def find_cfg(self, *k, default=NotGiven):
|
134
|
-
"""
|
135
|
-
Convenience method to get a config value
|
136
|
-
|
137
|
-
It is retrieved first from this node's value, then from the parent.
|
138
|
-
"""
|
139
|
-
val = self.value_or({}, Mapping)
|
140
|
-
try:
|
141
|
-
for kk in k:
|
142
|
-
try:
|
143
|
-
val = val[kk]
|
144
|
-
except TypeError:
|
145
|
-
raise TypeError(self.value, k, val, kk)
|
146
|
-
return val
|
147
|
-
except KeyError:
|
148
|
-
return self.parent.find_cfg(*k, default=default)
|
149
|
-
|
150
|
-
@property
|
151
|
-
def parent(self):
|
152
|
-
return self._parent()
|
153
|
-
|
154
|
-
@property
|
155
|
-
def root(self):
|
156
|
-
return self._root()
|
157
|
-
|
158
|
-
@property
|
159
|
-
def subpath(self):
|
160
|
-
"""Return the path to this entry, starting with its :class:`ClientRoot` base."""
|
161
|
-
return self._path[len(self.root._path) :]
|
162
|
-
|
163
|
-
@property
|
164
|
-
def all_children(self):
|
165
|
-
"""Iterate all child nodes with data.
|
166
|
-
|
167
|
-
You can send ``True`` to the iterator if you want to skip a subtree.
|
168
|
-
"""
|
169
|
-
for k in self:
|
170
|
-
if k.value is not NotGiven:
|
171
|
-
res = yield k
|
172
|
-
if res is True:
|
173
|
-
continue
|
174
|
-
yield from k.all_children
|
175
|
-
|
176
|
-
def allocate(self, name: str, exists: bool = False):
|
177
|
-
"""
|
178
|
-
Create the child named "name". It is created (locally) if it doesn't exist.
|
179
|
-
|
180
|
-
Arguments:
|
181
|
-
name: The child node's name.
|
182
|
-
exists: return the existing value? otherwise error
|
183
|
-
|
184
|
-
If this returns ``None``, the subtree shall not be tracked.
|
185
|
-
|
186
|
-
"""
|
187
|
-
c = self._children.get(name, None)
|
188
|
-
if c is not None:
|
189
|
-
if exists:
|
190
|
-
return c
|
191
|
-
raise RuntimeError("Duplicate child", self, name, c)
|
192
|
-
c = self.child_type(name)
|
193
|
-
if c is None:
|
194
|
-
raise KeyError(name)
|
195
|
-
self._children[name] = c = c(self, name)
|
196
|
-
return c
|
197
|
-
|
198
|
-
def follow(self, path, *, create=None, empty_ok=False):
|
199
|
-
"""Look up a sub-entry.
|
200
|
-
|
201
|
-
Arguments:
|
202
|
-
path (Path): the path elements to follow.
|
203
|
-
create (bool): Create the entries. Default ``False``.
|
204
|
-
Otherwise return ``None`` if not found.
|
205
|
-
|
206
|
-
The path may not be empty. It also must not be a string,
|
207
|
-
because that indicates that you called ``.follow(*path)`` instead of
|
208
|
-
``.follow(path)``.
|
209
|
-
"""
|
210
|
-
if isinstance(path, str):
|
211
|
-
raise RuntimeError("You seem to have used '*path' instead of 'path'.")
|
212
|
-
if not empty_ok and not len(path):
|
213
|
-
raise RuntimeError("Empty path")
|
214
|
-
|
215
|
-
node = self
|
216
|
-
for n, elem in enumerate(path, start=1):
|
217
|
-
next_node = node.get(elem)
|
218
|
-
if next_node is None:
|
219
|
-
if create is False:
|
220
|
-
return None
|
221
|
-
next_node = node.allocate(elem)
|
222
|
-
elif create and n == len(path) and next_node.value is not NotGiven:
|
223
|
-
raise RuntimeError("Duplicate child", self, path, n)
|
224
|
-
|
225
|
-
node = next_node
|
226
|
-
|
227
|
-
return node
|
228
|
-
|
229
|
-
def __getitem__(self, name):
|
230
|
-
return self._children[name]
|
231
|
-
|
232
|
-
def by_name(self, name):
|
233
|
-
"""
|
234
|
-
Lookup by a human-readable name?
|
235
|
-
"""
|
236
|
-
return self._children[name]
|
237
|
-
|
238
|
-
def __delitem__(self, name):
|
239
|
-
del self._children[name]
|
240
|
-
|
241
|
-
def get(self, name):
|
242
|
-
return self._children.get(name, None)
|
243
|
-
|
244
|
-
def __iter__(self):
|
245
|
-
"""Iterating an entry returns its children."""
|
246
|
-
return iter(list(self._children.values()))
|
247
|
-
|
248
|
-
def __bool__(self):
|
249
|
-
return self.value is not NotGiven or bool(self._children)
|
250
|
-
|
251
|
-
def __len__(self):
|
252
|
-
return len(self._children)
|
253
|
-
|
254
|
-
def __contains__(self, k):
|
255
|
-
if isinstance(k, type(self)):
|
256
|
-
k = k._name
|
257
|
-
return k in self._children
|
258
|
-
|
259
|
-
async def update(self, value, _locked=False, wait=False):
|
260
|
-
"""Update (or simply set) this node's value.
|
261
|
-
|
262
|
-
This is a coroutine.
|
263
|
-
"""
|
264
|
-
async with NoLock if _locked else self._lock:
|
265
|
-
r = await self.root.client.set(
|
266
|
-
self._path,
|
267
|
-
chain=self.chain,
|
268
|
-
value=value,
|
269
|
-
nchain=3,
|
270
|
-
idem=True,
|
271
|
-
)
|
272
|
-
if wait:
|
273
|
-
await self.root.wait_chain(r.chain)
|
274
|
-
self.value = value
|
275
|
-
self.chain = r.chain
|
276
|
-
return r
|
277
|
-
|
278
|
-
async def delete(self, _locked=False, nchain=0, chain=True, wait=False, recursive=False):
|
279
|
-
"""Delete this node's value.
|
280
|
-
|
281
|
-
This is a coroutine.
|
282
|
-
"""
|
283
|
-
async with NoLock if _locked else self._lock:
|
284
|
-
r = await self.root.client.delete(
|
285
|
-
self._path,
|
286
|
-
nchain=nchain,
|
287
|
-
recursive=recursive,
|
288
|
-
**({"chain": self.chain} if chain else {}),
|
289
|
-
)
|
290
|
-
if wait:
|
291
|
-
await self.root.wait_chain(r.chain)
|
292
|
-
self.chain = None
|
293
|
-
return r
|
294
|
-
|
295
|
-
async def set_value(self, value):
|
296
|
-
"""Callback to set the value when data has arrived.
|
297
|
-
|
298
|
-
This method is strictly for overriding.
|
299
|
-
Don't call me, I'll call you.
|
300
|
-
|
301
|
-
This is a coroutine, for ease of integration.
|
302
|
-
"""
|
303
|
-
self.value = value
|
304
|
-
|
305
|
-
async def seen_value(self):
|
306
|
-
"""The current value was seen (again).
|
307
|
-
|
308
|
-
Useful for syncing.
|
309
|
-
|
310
|
-
The default action is to do nothing.
|
311
|
-
"""
|
312
|
-
pass
|
313
|
-
|
314
|
-
def mark_inconsistent(self, r):
|
315
|
-
"""There has been an inconsistent update.
|
316
|
-
|
317
|
-
This call will immediately be followed by a call to
|
318
|
-
:meth:`set_value`, thus it is not async.
|
319
|
-
|
320
|
-
The default action is to do nothing.
|
321
|
-
"""
|
322
|
-
pass
|
323
|
-
|
324
|
-
|
325
|
-
def _node_gt(self, other):
|
326
|
-
if other is None and self is not None:
|
327
|
-
return True
|
328
|
-
if self is None or self == other:
|
329
|
-
return False
|
330
|
-
while self["node"] != other["node"]:
|
331
|
-
self = self["prev"]
|
332
|
-
if self is None:
|
333
|
-
return False
|
334
|
-
return self["tick"] >= other["tick"]
|
335
|
-
|
336
|
-
|
337
|
-
class AttrClientEntry(ClientEntry):
|
338
|
-
"""A ClientEntry which expects a dict as value and sets (some of) the clients'
|
339
|
-
attributes appropriately.
|
340
|
-
|
341
|
-
Set the classvar ``ATTRS`` to a list of the attrs you want saved. Note
|
342
|
-
that these are not inherited: when you subclass, copy and extend the
|
343
|
-
``ATTRS`` of your superclass.
|
344
|
-
|
345
|
-
If the entry is deleted (value set to ``None``, the attributes listed in
|
346
|
-
``ATTRS`` will be deleted too, or revert to the class values.
|
347
|
-
"""
|
348
|
-
|
349
|
-
ATTRS = ()
|
350
|
-
|
351
|
-
async def update(self, value, **kw): # pylint: disable=arguments-differ
|
352
|
-
raise RuntimeError("Nope. Set attributes and call '.save()'.")
|
353
|
-
|
354
|
-
async def set_value(self, value):
|
355
|
-
"""Callback to set the value when data has arrived.
|
356
|
-
|
357
|
-
This method sets the actual attributes.
|
358
|
-
|
359
|
-
This method is strictly for overriding.
|
360
|
-
Don't call me, I'll call you.
|
361
|
-
"""
|
362
|
-
await super().set_value(value)
|
363
|
-
for k in self.ATTRS:
|
364
|
-
if value is not NotGiven and k in value:
|
365
|
-
setattr(self, k, value[k])
|
366
|
-
else:
|
367
|
-
try:
|
368
|
-
delattr(self, k)
|
369
|
-
except AttributeError:
|
370
|
-
pass
|
371
|
-
|
372
|
-
def get_value(self, skip_none=False, skip_empty=False):
|
373
|
-
"""
|
374
|
-
Extract value from attrs
|
375
|
-
"""
|
376
|
-
res = {}
|
377
|
-
for attr in type(self).ATTRS:
|
378
|
-
try:
|
379
|
-
v = getattr(self, attr)
|
380
|
-
except AttributeError:
|
381
|
-
pass
|
382
|
-
else:
|
383
|
-
if v is NotGiven:
|
384
|
-
continue
|
385
|
-
if skip_none and v is None:
|
386
|
-
continue
|
387
|
-
if skip_empty and v == ():
|
388
|
-
continue
|
389
|
-
res[attr] = v
|
390
|
-
return res
|
391
|
-
|
392
|
-
async def save(self, wait=False):
|
393
|
-
"""
|
394
|
-
Save myself to storage, by copying ATTRS to a new value.
|
395
|
-
"""
|
396
|
-
async with self._lock:
|
397
|
-
r = await super().update(value=self.get_value(), _locked=True)
|
398
|
-
if wait:
|
399
|
-
await self.root.wait_chain(r.chain)
|
400
|
-
return r
|
401
|
-
|
402
|
-
|
403
|
-
class MirrorRoot(ClientEntry):
|
404
|
-
"""
|
405
|
-
This class represents the root of a subsystem's storage, used for
|
406
|
-
object-agnostic data mirroring.
|
407
|
-
|
408
|
-
Used internally.
|
409
|
-
"""
|
410
|
-
|
411
|
-
_tg = None
|
412
|
-
|
413
|
-
CFG = None # You need to override this with a dict(prefix=('where','ever'))
|
414
|
-
|
415
|
-
def __init__(self, client, path, *, need_wait=False, cfg=None, require_client=True):
|
416
|
-
# pylint: disable=super-init-not-called
|
417
|
-
self._init()
|
418
|
-
self.client = client
|
419
|
-
self._path = path
|
420
|
-
self._need_wait = need_wait
|
421
|
-
self._loaded = anyio.Event()
|
422
|
-
self._require_client = require_client
|
423
|
-
|
424
|
-
if cfg is None:
|
425
|
-
cfg = {}
|
426
|
-
self._cfg = cfg
|
427
|
-
self._name = self.client.name
|
428
|
-
|
429
|
-
if need_wait:
|
430
|
-
self._waiters = dict()
|
431
|
-
self._seen = dict()
|
432
|
-
|
433
|
-
@classmethod
|
434
|
-
async def as_handler(cls, client, cfg=None, key="prefix", subpath=(), name=None, **kw):
|
435
|
-
"""Return an (or "the") instance of this class.
|
436
|
-
|
437
|
-
The handler is created if it doesn't exist.
|
438
|
-
|
439
|
-
Instances are distinguished by a key (from config), which
|
440
|
-
must contain their path, and an optional subpath.
|
441
|
-
"""
|
442
|
-
d = []
|
443
|
-
if cfg is not None:
|
444
|
-
d.append(cfg)
|
445
|
-
|
446
|
-
defcfg = client._cfg.get(cls.CFG)
|
447
|
-
if not defcfg:
|
448
|
-
# seems we didn't load the class' default config yet.
|
449
|
-
import inspect
|
450
|
-
from pathlib import Path as _Path
|
451
|
-
|
452
|
-
md = inspect.getmodule(cls)
|
453
|
-
ensure_cfg("moat.kv")
|
454
|
-
defcfg = CFG.kv.get(cls.CFG)
|
455
|
-
try:
|
456
|
-
f = (_Path(md.__file__).parent / "_nconfig.yaml").open("r")
|
457
|
-
except OSError:
|
458
|
-
pass
|
459
|
-
else:
|
460
|
-
with f:
|
461
|
-
defcfg = yload(f, attr=True).get("kv", {}).get(cls.CFG)
|
462
|
-
if cfg:
|
463
|
-
if defcfg:
|
464
|
-
cfg = combine_dict(cfg, defcfg)
|
465
|
-
else:
|
466
|
-
if not defcfg:
|
467
|
-
raise RuntimeError("no config for " + repr(cls))
|
468
|
-
cfg = defcfg
|
469
|
-
|
470
|
-
if name is None:
|
471
|
-
# if key != "prefix":
|
472
|
-
# subpath = Path(key) + subpath
|
473
|
-
name = str(Path("_moat.kv", client.name, cls.CFG, *subpath))
|
474
|
-
|
475
|
-
def make():
|
476
|
-
return client.mirror(cfg[key] + subpath, root_type=cls, need_wait=True, cfg=cfg, **kw)
|
477
|
-
|
478
|
-
return await client.unique_helper(name, factory=make)
|
479
|
-
|
480
|
-
@classmethod
|
481
|
-
def child_type(cls, name):
|
482
|
-
"""Given a node, return the type which the child with that name should have.
|
483
|
-
The default is :class:`ClientEntry`.
|
484
|
-
|
485
|
-
This may return ``None``. In that case the subtree with this name
|
486
|
-
shall not be tracked further.
|
487
|
-
"""
|
488
|
-
return ClientEntry
|
489
|
-
|
490
|
-
@property
|
491
|
-
def root(self):
|
492
|
-
"""Returns this instance."""
|
493
|
-
return self
|
494
|
-
|
495
|
-
def find_cfg(self, *k, default=NotGiven):
|
496
|
-
"""
|
497
|
-
Convenience method to get a config value.
|
498
|
-
|
499
|
-
It is retrieved first from this node's value, then from the configuration read via CFG.
|
500
|
-
"""
|
501
|
-
val = self.value_or({}, Mapping)
|
502
|
-
try:
|
503
|
-
for kk in k:
|
504
|
-
val = val[kk]
|
505
|
-
return val
|
506
|
-
except KeyError:
|
507
|
-
try:
|
508
|
-
val = self._cfg
|
509
|
-
for kk in k:
|
510
|
-
val = val[kk]
|
511
|
-
return val
|
512
|
-
except KeyError:
|
513
|
-
if default is NotGiven:
|
514
|
-
raise
|
515
|
-
return default
|
516
|
-
|
517
|
-
async def run_starting(self):
|
518
|
-
"""Hook for 'about to start reading'"""
|
519
|
-
pass
|
520
|
-
|
521
|
-
async def running(self):
|
522
|
-
"""Hook for 'done reading current state'"""
|
523
|
-
self._loaded.set()
|
524
|
-
|
525
|
-
@asynccontextmanager
|
526
|
-
async def run(self):
|
527
|
-
"""A coroutine that fetches, and continually updates, a subtree."""
|
528
|
-
if self._require_client:
|
529
|
-
scope.requires(self.client.scope)
|
530
|
-
|
531
|
-
async with anyio.create_task_group() as tg:
|
532
|
-
self._tg = tg
|
533
|
-
|
534
|
-
async def monitor(*, task_status):
|
535
|
-
pl = PathLongener(())
|
536
|
-
await self.run_starting()
|
537
|
-
async with self.client._stream(
|
538
|
-
"watch",
|
539
|
-
nchain=3,
|
540
|
-
path=self._path,
|
541
|
-
fetch=True,
|
542
|
-
) as w:
|
543
|
-
async for r in w:
|
544
|
-
if "path" not in r:
|
545
|
-
if r.get("state", "") == "uptodate":
|
546
|
-
await self.running()
|
547
|
-
task_status.started()
|
548
|
-
continue
|
549
|
-
pl(r)
|
550
|
-
val = r.get("value", NotGiven)
|
551
|
-
entry = self.follow(r.path, create=None, empty_ok=True)
|
552
|
-
if entry is not None:
|
553
|
-
# Test for consistency
|
554
|
-
try:
|
555
|
-
if entry.chain == r.chain:
|
556
|
-
# entry.update() has set this
|
557
|
-
await entry.seen_value()
|
558
|
-
elif _node_gt(entry.chain, r.chain):
|
559
|
-
# stale data
|
560
|
-
pass
|
561
|
-
elif not _node_gt(r.chain, entry.chain):
|
562
|
-
entry.mark_inconsistent(r)
|
563
|
-
except AttributeError:
|
564
|
-
pass
|
565
|
-
|
566
|
-
# update entry
|
567
|
-
entry.chain = None if val is NotGiven else r.get("chain", None)
|
568
|
-
await entry.set_value(val)
|
569
|
-
|
570
|
-
if val is NotGiven and not entry:
|
571
|
-
# the entry has no value and no children,
|
572
|
-
# so we delete it (and possibly its
|
573
|
-
# parents) from our tree.
|
574
|
-
n = list(entry.subpath)
|
575
|
-
while n:
|
576
|
-
# no-op except for class-specific side effects
|
577
|
-
# like setting an event
|
578
|
-
await entry.set_value(NotGiven)
|
579
|
-
|
580
|
-
entry = entry.parent
|
581
|
-
del entry[n.pop()]
|
582
|
-
if entry:
|
583
|
-
break
|
584
|
-
|
585
|
-
if not self._need_wait or "chain" not in r:
|
586
|
-
continue
|
587
|
-
c = r.chain
|
588
|
-
while c is not None:
|
589
|
-
if self._seen.get(c.node, 0) < c.tick:
|
590
|
-
self._seen[c.node] = c.tick
|
591
|
-
try:
|
592
|
-
w = self._waiters[c.node]
|
593
|
-
except KeyError:
|
594
|
-
pass
|
595
|
-
else:
|
596
|
-
while w and w[0][0] <= c.tick:
|
597
|
-
heapq.heappop(w)[1].set()
|
598
|
-
c = c.get("prev", None)
|
599
|
-
|
600
|
-
await tg.start(monitor)
|
601
|
-
try:
|
602
|
-
yield self
|
603
|
-
finally:
|
604
|
-
with anyio.fail_after(2, shield=True):
|
605
|
-
tg.cancel_scope.cancel()
|
606
|
-
pass # end of 'run', closing taskgroup
|
607
|
-
|
608
|
-
async def cancel(self):
|
609
|
-
"""Stop the monitor"""
|
610
|
-
await self._tg.cancel_scope.cancel()
|
611
|
-
|
612
|
-
async def wait_loaded(self):
|
613
|
-
"""Wait for the tree to be loaded completely."""
|
614
|
-
await self._loaded.wait()
|
615
|
-
|
616
|
-
async def wait_chain(self, chain):
|
617
|
-
"""Wait for a tree update containing this tick."""
|
618
|
-
try:
|
619
|
-
if chain.tick <= self._seen[chain.node]:
|
620
|
-
return
|
621
|
-
except KeyError:
|
622
|
-
pass
|
623
|
-
w = self._waiters.setdefault(chain.node, [])
|
624
|
-
e = anyio.Event()
|
625
|
-
heapq.heappush(w, (chain.tick, e))
|
626
|
-
await e.wait()
|
627
|
-
|
628
|
-
async def spawn(self, p, *a, **kw):
|
629
|
-
p = partial(p, *a, **kw)
|
630
|
-
self._tg.start_soon(p)
|
631
|
-
|
632
|
-
|
633
|
-
class ClientRoot(MirrorRoot):
|
634
|
-
"""
|
635
|
-
This class represents the root of a subsystem's storage.
|
636
|
-
|
637
|
-
To use this class, create a subclass that, at minimum, overrides
|
638
|
-
``CFG`` and ``child_type``. ``CFG`` must be a dict with at least a
|
639
|
-
``prefix`` tuple. You instantiate the entry using :meth:`as_handler`.
|
640
|
-
|
641
|
-
"""
|
642
|
-
|
643
|
-
def __init__(self, *a, **kw):
|
644
|
-
if self.CFG is None:
|
645
|
-
raise TypeError(f"You need to override .CFG in {type(self).__name__}")
|
646
|
-
super().__init__(*a, **kw)
|