blissdata 0.3.4__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.
- blissdata/__init__.py +17 -0
- blissdata/beacon/__init__.py +1 -0
- blissdata/beacon/_base.py +106 -0
- blissdata/beacon/config.py +24 -0
- blissdata/beacon/data.py +45 -0
- blissdata/beacon/files.py +141 -0
- blissdata/client.py +62 -0
- blissdata/common/__init__.py +15 -0
- blissdata/common/utils.py +159 -0
- blissdata/data/__init__.py +15 -0
- blissdata/data/events/__init__.py +22 -0
- blissdata/data/events/channel.py +149 -0
- blissdata/data/events/lima.py +478 -0
- blissdata/data/events/node.py +28 -0
- blissdata/data/events/scan.py +49 -0
- blissdata/data/events/walk.py +42 -0
- blissdata/data/expiration.py +42 -0
- blissdata/data/lima_image.py +465 -0
- blissdata/data/node.py +1619 -0
- blissdata/data/nodes/__init__.py +11 -0
- blissdata/data/nodes/channel.py +394 -0
- blissdata/data/nodes/dataset.py +82 -0
- blissdata/data/nodes/dataset_collection.py +12 -0
- blissdata/data/nodes/lima.py +422 -0
- blissdata/data/nodes/node_ref_channel.py +34 -0
- blissdata/data/nodes/proposal.py +12 -0
- blissdata/data/nodes/scan.py +192 -0
- blissdata/data/nodes/scan_group.py +12 -0
- blissdata/data/remote_node.py +204 -0
- blissdata/data/scan.py +666 -0
- blissdata/h5api/__init__.py +1 -0
- blissdata/h5api/abstract.py +97 -0
- blissdata/h5api/dynamic_hdf5.py +153 -0
- blissdata/h5api/file_arguments.py +28 -0
- blissdata/h5api/static_hdf5.py +139 -0
- blissdata/h5api/utils/__init__.py +0 -0
- blissdata/h5api/utils/bliss.py +138 -0
- blissdata/h5api/utils/hdf5.py +280 -0
- blissdata/h5api/utils/hdf5_retry.py +98 -0
- blissdata/h5api/utils/lima.py +286 -0
- blissdata/h5api/utils/types.py +13 -0
- blissdata/redis/__init__.py +12 -0
- blissdata/redis/caching.py +390 -0
- blissdata/redis/connection.py +169 -0
- blissdata/redis/manager.py +164 -0
- blissdata/redis/proxy.py +971 -0
- blissdata/redis/scripting.py +24 -0
- blissdata/settings.py +1174 -0
- blissdata/streaming.py +819 -0
- blissdata/streaming_events.py +355 -0
- blissdata/tests/__init__.py +0 -0
- blissdata/tests/beacon/__init__.py +0 -0
- blissdata/tests/beacon/test_config.py +26 -0
- blissdata/tests/beacon/test_data.py +50 -0
- blissdata/tests/beacon/test_files.py +80 -0
- blissdata/tests/conftest.py +0 -0
- blissdata/tests/h5api/__init__.py +0 -0
- blissdata/tests/h5api/scanner.py +390 -0
- blissdata/tests/h5api/test_dynamic_files.py +309 -0
- blissdata/tests/h5api/test_static_files.py +169 -0
- blissdata/tests/redis/__init__.py +0 -0
- blissdata/tests/redis/manager.py +23 -0
- blissdata-0.3.4.dist-info/LICENSE +165 -0
- blissdata-0.3.4.dist-info/METADATA +79 -0
- blissdata-0.3.4.dist-info/RECORD +67 -0
- blissdata-0.3.4.dist-info/WHEEL +5 -0
- blissdata-0.3.4.dist-info/top_level.txt +1 -0
blissdata/data/node.py
ADDED
|
@@ -0,0 +1,1619 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of the bliss project
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2015-2023 Beamline Control Unit, ESRF
|
|
6
|
+
# Distributed under the GNU LGPLv3. See LICENSE for more info.
|
|
7
|
+
"""
|
|
8
|
+
Redis data node structure
|
|
9
|
+
|
|
10
|
+
--session_name (DataNodeContainer - inherits from DataNode)
|
|
11
|
+
...
|
|
12
|
+
|
|
|
13
|
+
--sample_0001 (DataNodeContainer - inherits from DataNode)
|
|
14
|
+
|
|
|
15
|
+
--1_loopscan (ScanNode - inherits from DataNodeContainer)
|
|
16
|
+
|
|
|
17
|
+
--timer (DataNodeContainer - inherits from DataNode)
|
|
18
|
+
|
|
|
19
|
+
-- epoch (ChannelDataNode - inherits from _ChannelDataNodeBase)
|
|
20
|
+
|
|
|
21
|
+
-- frelon (LimaChannelDataNode - inherits from _ChannelDataNodeBase)
|
|
22
|
+
|
|
|
23
|
+
--P201 (DataNodeContainer - inherits from DataNode)
|
|
24
|
+
|
|
|
25
|
+
--c0 (ChannelDataNode - inherits from _ChannelDataNodeBase)
|
|
26
|
+
|
|
27
|
+
A DataNode is represented by 2 Redis keys:
|
|
28
|
+
|
|
29
|
+
{db_name} -> Struct { name, db_name, node_type, parent=(parent db_name) }
|
|
30
|
+
{db_name}_info -> HashObjSetting, free dictionary
|
|
31
|
+
|
|
32
|
+
A DataNodeContainer is represented by 3 Redis keys:
|
|
33
|
+
|
|
34
|
+
{db_name} -> see DataNode
|
|
35
|
+
{db_name}_info -> see DataNode
|
|
36
|
+
{db_name}_children -> DataStream, list of db names
|
|
37
|
+
|
|
38
|
+
A ScanNode is represented by 4 Redis keys:
|
|
39
|
+
|
|
40
|
+
{db_name} -> see DataNodeContainer
|
|
41
|
+
{db_name}_info -> see DataNodeContainer
|
|
42
|
+
{db_name}_children -> see DataNodeContainer
|
|
43
|
+
{db_name}_end -> contains the END event
|
|
44
|
+
{db_name}_prepared -> contains the PREPARED event
|
|
45
|
+
|
|
46
|
+
A ChannelDataNode is represented by 3 Redis keys:
|
|
47
|
+
|
|
48
|
+
{db_name} -> see DataNode
|
|
49
|
+
{db_name}_info -> see DataNode
|
|
50
|
+
{db_name}_data -> DataStream, list of channel values
|
|
51
|
+
|
|
52
|
+
A LimaChannelDataNode is represented by 4 Redis keys:
|
|
53
|
+
|
|
54
|
+
{db_name} -> see DataNode
|
|
55
|
+
{db_name}_info -> see DataNode, with some extra keys like reference: True
|
|
56
|
+
{db_name}_data -> DataStream, list of reference data
|
|
57
|
+
{db_name}_data_ref -> QueueObjSetting, the 'live' reference info
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
Each DataNode can be iterated over to yield nodes (walk, walk_from_last)
|
|
61
|
+
or events (walk_events, walk_on_new_events) during or after a scan.
|
|
62
|
+
|
|
63
|
+
This is achieved by subscribing (i.e. adding to the active stream reader)
|
|
64
|
+
to "children_list" streams and when yielding events, "data" streams.
|
|
65
|
+
The subscribing is done whenever a new block of raw events is yielded by
|
|
66
|
+
the reader:
|
|
67
|
+
|
|
68
|
+
DataNode:
|
|
69
|
+
subscribe/create to the "data" stream when yielding events
|
|
70
|
+
|
|
71
|
+
DataNodeContainer(DataNode):
|
|
72
|
+
subscribe/create to the "children_list" stream
|
|
73
|
+
subscribe to existing "children_list" streams of children
|
|
74
|
+
subscribe to existing "data" streams of children when yielding events
|
|
75
|
+
|
|
76
|
+
Scan(DataNodeContainer):
|
|
77
|
+
subscribe/create to the "children_list" stream
|
|
78
|
+
subscribe to existing "children_list" streams of children
|
|
79
|
+
subscribe to existing "data" streams of children when yielding events
|
|
80
|
+
subscribe/create to the "data" stream
|
|
81
|
+
|
|
82
|
+
The walk methods have the following filtering arguments:
|
|
83
|
+
|
|
84
|
+
include_filter: only yield nodes/events for these nodes
|
|
85
|
+
exclude_children: no events from the children of these nodes (recursive)
|
|
86
|
+
exclude_existing_children: no events from existing children of these
|
|
87
|
+
nodes (recursive). Defaults to `exclude_children`.
|
|
88
|
+
|
|
89
|
+
Use the following utility functions to instantiate a DataNode:
|
|
90
|
+
|
|
91
|
+
Absolute Redis key name:
|
|
92
|
+
get_node: None when not in required state
|
|
93
|
+
get_nodes: None when not in required state
|
|
94
|
+
|
|
95
|
+
Absolute or relative Redis key name:
|
|
96
|
+
create_node: create in Redis regardless of what exists
|
|
97
|
+
get_or_create_node: create in Redis when not in required state
|
|
98
|
+
datanode_factory: when not in required state:
|
|
99
|
+
return DataNode and create
|
|
100
|
+
return DataNode but don't create
|
|
101
|
+
return None
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
import importlib
|
|
105
|
+
import weakref
|
|
106
|
+
import warnings
|
|
107
|
+
from blissdata.common.utils import grouped
|
|
108
|
+
from blissdata.client import get_redis_proxy
|
|
109
|
+
from blissdata import settings
|
|
110
|
+
from blissdata import streaming
|
|
111
|
+
from blissdata.data.events import Event, EventType, NewNodeEvent
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
node_plugins = {
|
|
115
|
+
"channel": "ChannelDataNode",
|
|
116
|
+
"dataset": "DatasetNode",
|
|
117
|
+
"dataset_collection": "DatasetCollectionNode",
|
|
118
|
+
"lima": "LimaImageChannelDataNode",
|
|
119
|
+
"node_ref_channel": "NodeRefChannel",
|
|
120
|
+
"proposal": "ProposalNode",
|
|
121
|
+
"scan": "ScanNode",
|
|
122
|
+
"scan_group": "GroupScanNode",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class NodeStruct(settings.Struct):
|
|
127
|
+
def __init__(self, db_name, **kwargs):
|
|
128
|
+
# /!\ it is important to not modify redis keys here,
|
|
129
|
+
# otherwise calls to '._get_struct' in pipelines (for example)
|
|
130
|
+
# fail, since there are extra return values (corresponding to
|
|
131
|
+
# the return values of redis calls in this constructor)
|
|
132
|
+
super().__init__(db_name, **kwargs)
|
|
133
|
+
object.__setattr__(self, "_NodeStruct__db_name", db_name)
|
|
134
|
+
# self.__name is initialized to None => attempt to read the "name" property
|
|
135
|
+
# will pass through the underlying HashSetting
|
|
136
|
+
object.__setattr__(self, "_NodeStruct__name", None)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def db_name(self):
|
|
140
|
+
return self.__db_name
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def name(self):
|
|
144
|
+
if self.__name is None:
|
|
145
|
+
self.__name = self._proxy.get("name")
|
|
146
|
+
return self.__name
|
|
147
|
+
|
|
148
|
+
def _update(self, mapping):
|
|
149
|
+
return self._proxy.update(mapping)
|
|
150
|
+
|
|
151
|
+
def _init(self, **mapping):
|
|
152
|
+
name = mapping.get("name")
|
|
153
|
+
if name is None:
|
|
154
|
+
_, _, name = self.db_name.rpartition(":")
|
|
155
|
+
object.__setattr__(self, "_NodeStruct__name", name)
|
|
156
|
+
mapping["name"] = name
|
|
157
|
+
# hash setting needs to have `db_name` field,
|
|
158
|
+
# since it is expected when doing `hgetall` (cf. test publishing)
|
|
159
|
+
mapping["db_name"] = self.db_name
|
|
160
|
+
self._update(mapping)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_node_object(node_type, name, parent, connection, create=False, **kwargs):
|
|
164
|
+
"""Instantiate a DataNode class and optionally create it in Redis.
|
|
165
|
+
This does not perform any checks on what already exists in Redis.
|
|
166
|
+
|
|
167
|
+
:returns DataNode:
|
|
168
|
+
"""
|
|
169
|
+
if node_type in node_plugins.keys():
|
|
170
|
+
module = importlib.import_module("blissdata.data.nodes." + node_type)
|
|
171
|
+
klass = getattr(module, node_plugins[node_type])
|
|
172
|
+
return klass(
|
|
173
|
+
name, parent=parent, connection=connection, create=create, **kwargs
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
return DataNodeContainer(
|
|
177
|
+
node_type,
|
|
178
|
+
name,
|
|
179
|
+
parent=parent,
|
|
180
|
+
connection=connection,
|
|
181
|
+
create=create,
|
|
182
|
+
**kwargs,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_node(db_name, **kwargs):
|
|
187
|
+
"""Do not create in Redis.
|
|
188
|
+
|
|
189
|
+
:param str db_name: Redis key
|
|
190
|
+
:param **kwargs: see `get_nodes`
|
|
191
|
+
:returns DataNode or None: `None` when node not in `state`
|
|
192
|
+
"""
|
|
193
|
+
return get_nodes(db_name, **kwargs)[0]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def create_node(name, node_type=None, parent=None, connection=None, **kwargs):
|
|
197
|
+
"""Create in Redis regardless of its state.
|
|
198
|
+
|
|
199
|
+
:returns DataNode:
|
|
200
|
+
"""
|
|
201
|
+
if connection is None:
|
|
202
|
+
connection = get_redis_proxy(db=1)
|
|
203
|
+
return _get_node_object(node_type, name, parent, connection, create=True, **kwargs)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_or_create_node(name, **kwargs):
|
|
207
|
+
"""Create in Redis when node not in `state`.
|
|
208
|
+
|
|
209
|
+
:param str name: absolute or relative to parent (if any)
|
|
210
|
+
:param **kwargs:
|
|
211
|
+
:returns DataNode:
|
|
212
|
+
"""
|
|
213
|
+
return datanode_factory(name, create_not_state=True, **kwargs)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _default_datanode_state(state):
|
|
217
|
+
"""DataNode state in Redis
|
|
218
|
+
|
|
219
|
+
:param str or None state:
|
|
220
|
+
:returns str:
|
|
221
|
+
"""
|
|
222
|
+
if state is None:
|
|
223
|
+
return "supported"
|
|
224
|
+
if state not in {"exists", "initialized", "supported"}:
|
|
225
|
+
raise ValueError("State should be 'exists', 'initialized' or 'supported'")
|
|
226
|
+
return state
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_nodes(*db_names, connection=None, state=None, **kwargs):
|
|
230
|
+
"""Do not create in Redis.
|
|
231
|
+
|
|
232
|
+
:param `*db_names`: Redis keys (str)
|
|
233
|
+
:param Connection connection:
|
|
234
|
+
:param str state: "exists" < "initialized" < "supported"
|
|
235
|
+
:param **kwargs: see `_get_node_object`
|
|
236
|
+
:return list(DataNode or None): `None` when node not in `state`
|
|
237
|
+
"""
|
|
238
|
+
state = _default_datanode_state(state)
|
|
239
|
+
if connection is None:
|
|
240
|
+
connection = get_redis_proxy(db=1)
|
|
241
|
+
|
|
242
|
+
# Get attributes from the principal representations in 1 call (pipeline)
|
|
243
|
+
pipeline = connection.pipeline()
|
|
244
|
+
for db_name in db_names:
|
|
245
|
+
pipeline.exists(db_name)
|
|
246
|
+
struct = DataNode._get_struct(db_name, connection=pipeline)
|
|
247
|
+
struct.version
|
|
248
|
+
struct.node_type
|
|
249
|
+
iter_result = grouped(pipeline.execute(), 3)
|
|
250
|
+
it = enumerate(zip(db_names, iter_result))
|
|
251
|
+
|
|
252
|
+
# Instantiate a DataNode when it is in `state`.
|
|
253
|
+
nodes = [None] * len(db_names)
|
|
254
|
+
for i, (db_name, (valid, version, node_type)) in it:
|
|
255
|
+
if state != "exists":
|
|
256
|
+
valid &= bool(version) # initialized
|
|
257
|
+
if state == "supported":
|
|
258
|
+
valid &= DataNode.supported_version(version)
|
|
259
|
+
if valid:
|
|
260
|
+
if node_type:
|
|
261
|
+
node_type = node_type.decode()
|
|
262
|
+
nodes[i] = _get_node_object(node_type, db_name, None, connection, **kwargs)
|
|
263
|
+
return nodes
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def get_filtered_nodes(
|
|
267
|
+
*db_names,
|
|
268
|
+
include_filter=None,
|
|
269
|
+
recursive_exclude=None,
|
|
270
|
+
strict_recursive_exclude=True,
|
|
271
|
+
**kw,
|
|
272
|
+
):
|
|
273
|
+
"""Get nodes filtered on node properties. String filtering applies to the type property.
|
|
274
|
+
The default required node state is "exists".
|
|
275
|
+
|
|
276
|
+
:param `*db_names`: Redis keys (str)
|
|
277
|
+
:param tuple(str) or callable include_filter:
|
|
278
|
+
:param tuple(str) or callable recursive_exclude: exclude children as well
|
|
279
|
+
:param bool strict_recursive_exclude: exclude only the children when False
|
|
280
|
+
:param **kw: see `get_nodes`
|
|
281
|
+
:yields DataNode:
|
|
282
|
+
"""
|
|
283
|
+
kw.setdefault("state", "exists")
|
|
284
|
+
if not include_filter and not recursive_exclude:
|
|
285
|
+
for node in get_nodes(*db_names, **kw):
|
|
286
|
+
if node is not None:
|
|
287
|
+
yield node
|
|
288
|
+
elif callable(include_filter) or callable(recursive_exclude):
|
|
289
|
+
yield from _filtered_nodes(
|
|
290
|
+
*db_names,
|
|
291
|
+
include_filter=include_filter,
|
|
292
|
+
recursive_exclude=recursive_exclude,
|
|
293
|
+
strict_recursive_exclude=strict_recursive_exclude,
|
|
294
|
+
**kw,
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
if kw.get("connection") is None:
|
|
298
|
+
kw["connection"] = get_redis_proxy(db=1)
|
|
299
|
+
db_names = filter_node_names(
|
|
300
|
+
*db_names,
|
|
301
|
+
include_types=include_filter,
|
|
302
|
+
recursive_exclude_types=recursive_exclude,
|
|
303
|
+
strict_recursive_exclude=strict_recursive_exclude,
|
|
304
|
+
connection=kw["connection"],
|
|
305
|
+
)
|
|
306
|
+
for node in get_nodes(*db_names, **kw):
|
|
307
|
+
if node is not None:
|
|
308
|
+
yield node
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _filtered_nodes(
|
|
312
|
+
*db_names,
|
|
313
|
+
include_filter=None,
|
|
314
|
+
recursive_exclude=None,
|
|
315
|
+
strict_recursive_exclude=True,
|
|
316
|
+
**kw,
|
|
317
|
+
):
|
|
318
|
+
"""Get nodes filtered on node properties. String filtering applies to the type property.
|
|
319
|
+
|
|
320
|
+
:param `*db_names`: Redis keys (str)
|
|
321
|
+
:param tuple(str) or callable include_filter:
|
|
322
|
+
:param tuple(str) or callable recursive_exclude: exclude children as well
|
|
323
|
+
:param bool strict_recursive_exclude: exclude only the children when False
|
|
324
|
+
:param **kw: see `get_nodes`
|
|
325
|
+
:yields DataNode:
|
|
326
|
+
"""
|
|
327
|
+
nodes = get_nodes(*db_names, **kw)
|
|
328
|
+
|
|
329
|
+
if not include_filter and not recursive_exclude:
|
|
330
|
+
for node in nodes:
|
|
331
|
+
if node is not None:
|
|
332
|
+
yield node
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
if recursive_exclude:
|
|
336
|
+
exclude_prefixes = [
|
|
337
|
+
node.db_name
|
|
338
|
+
for node in nodes
|
|
339
|
+
if node is not None and node._excluded(recursive_exclude)
|
|
340
|
+
]
|
|
341
|
+
else:
|
|
342
|
+
exclude_prefixes = []
|
|
343
|
+
|
|
344
|
+
def include(node):
|
|
345
|
+
if node is None:
|
|
346
|
+
return False
|
|
347
|
+
if not node._included(include_filter):
|
|
348
|
+
return False
|
|
349
|
+
db_name = node.db_name
|
|
350
|
+
for exclude_prefix in exclude_prefixes:
|
|
351
|
+
if db_name.startswith(exclude_prefix):
|
|
352
|
+
if strict_recursive_exclude:
|
|
353
|
+
return False
|
|
354
|
+
else:
|
|
355
|
+
# Exclude only the children
|
|
356
|
+
return ":" not in db_name[len(exclude_prefix) :]
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
# Subscribe to the streams associated to the nodes
|
|
360
|
+
for node in nodes:
|
|
361
|
+
if include(node):
|
|
362
|
+
yield node
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def filter_node_names(
|
|
366
|
+
*db_names,
|
|
367
|
+
include_types=None,
|
|
368
|
+
recursive_exclude_types=None,
|
|
369
|
+
strict_recursive_exclude=True,
|
|
370
|
+
connection=None,
|
|
371
|
+
):
|
|
372
|
+
"""Filter node names based on node type.
|
|
373
|
+
|
|
374
|
+
:param `*db_names`: Redis keys (str)
|
|
375
|
+
:param tuple(str) include_types:
|
|
376
|
+
:param tuple(str) recursive_exclude_types: exclude children as well
|
|
377
|
+
:param bool strict_recursive_exclude: exclude only the children when False
|
|
378
|
+
:param Connection connection:
|
|
379
|
+
:return list(str):
|
|
380
|
+
"""
|
|
381
|
+
if not include_types and not recursive_exclude_types:
|
|
382
|
+
return db_names
|
|
383
|
+
|
|
384
|
+
if not include_types:
|
|
385
|
+
include_types = tuple()
|
|
386
|
+
elif isinstance(include_types, str):
|
|
387
|
+
include_types = (include_types,)
|
|
388
|
+
if not recursive_exclude_types:
|
|
389
|
+
recursive_exclude_types = tuple()
|
|
390
|
+
elif isinstance(recursive_exclude_types, str):
|
|
391
|
+
recursive_exclude_types = (recursive_exclude_types,)
|
|
392
|
+
|
|
393
|
+
if connection is None:
|
|
394
|
+
connection = get_redis_proxy(db=1)
|
|
395
|
+
|
|
396
|
+
# Get attributes from the principal representations in 1 call (pipeline)
|
|
397
|
+
pipeline = connection.pipeline()
|
|
398
|
+
for db_name in db_names:
|
|
399
|
+
struct = DataNode._get_struct(db_name, connection=pipeline)
|
|
400
|
+
struct.node_type
|
|
401
|
+
iter_result = grouped(pipeline.execute(), 1)
|
|
402
|
+
it = zip(db_names, iter_result)
|
|
403
|
+
|
|
404
|
+
# Filter names based on type
|
|
405
|
+
exclude_prefixes = []
|
|
406
|
+
ret_names = []
|
|
407
|
+
for db_name, (node_type,) in it:
|
|
408
|
+
if node_type:
|
|
409
|
+
node_type = node_type.decode()
|
|
410
|
+
if recursive_exclude_types and node_type in recursive_exclude_types:
|
|
411
|
+
exclude_prefixes.append(db_name)
|
|
412
|
+
if include_types and node_type not in include_types:
|
|
413
|
+
continue
|
|
414
|
+
ret_names.append(db_name)
|
|
415
|
+
|
|
416
|
+
if not exclude_prefixes:
|
|
417
|
+
return ret_names
|
|
418
|
+
|
|
419
|
+
def include(db_name):
|
|
420
|
+
for exclude_prefix in exclude_prefixes:
|
|
421
|
+
if db_name.startswith(exclude_prefix):
|
|
422
|
+
if strict_recursive_exclude:
|
|
423
|
+
return False
|
|
424
|
+
else:
|
|
425
|
+
# Exclude only the children
|
|
426
|
+
return ":" not in db_name[len(exclude_prefix) :]
|
|
427
|
+
return True
|
|
428
|
+
|
|
429
|
+
return [db_name for db_name in ret_names if include(db_name)]
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def datanode_factory(
|
|
433
|
+
name,
|
|
434
|
+
node_type=None,
|
|
435
|
+
parent=None,
|
|
436
|
+
connection=None,
|
|
437
|
+
state=None,
|
|
438
|
+
create_not_state=False,
|
|
439
|
+
**kwargs,
|
|
440
|
+
):
|
|
441
|
+
"""Instantiate a DataNode class. When not in `state`, optionally
|
|
442
|
+
(re)create the node in Redis.
|
|
443
|
+
|
|
444
|
+
:param str name: absolute or relative to parent (if any)
|
|
445
|
+
:param str node_type: ignored when node already in `state`
|
|
446
|
+
:param DataNode parent:
|
|
447
|
+
:param Connection connection: a new one will be created when `None`
|
|
448
|
+
:param str state: default is "supported"
|
|
449
|
+
:param bool create_not_state:
|
|
450
|
+
:param **kwargs: see `_get_node_object`
|
|
451
|
+
:returns DataNode:
|
|
452
|
+
"""
|
|
453
|
+
if connection is None:
|
|
454
|
+
connection = get_redis_proxy(db=1)
|
|
455
|
+
db_name = DataNode._principal_db_name(name, parent=parent)
|
|
456
|
+
node = get_node(db_name, connection=connection, state=state, **kwargs)
|
|
457
|
+
if node is None:
|
|
458
|
+
node = _get_node_object(
|
|
459
|
+
node_type, name, parent, connection, create=create_not_state, **kwargs
|
|
460
|
+
)
|
|
461
|
+
elif parent is not None and create_not_state and node.parent is None:
|
|
462
|
+
node._struct.parent = parent.db_name
|
|
463
|
+
return node
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def get_session_node(session_name):
|
|
467
|
+
"""Do not create in Redis but instantiate even when it does not exist yet.
|
|
468
|
+
|
|
469
|
+
:returns DataNodeContainer:
|
|
470
|
+
"""
|
|
471
|
+
if session_name.find(":") > -1:
|
|
472
|
+
raise ValueError(f"Session name can't contains ':' -> ({session_name})")
|
|
473
|
+
return DataNodeContainer(None, session_name)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def sessions_list():
|
|
477
|
+
"""Return all available session node(s).
|
|
478
|
+
Return only sessions having data published in Redis.
|
|
479
|
+
Session may or may not be running.
|
|
480
|
+
"""
|
|
481
|
+
session_names = []
|
|
482
|
+
conn = get_redis_proxy(db=1)
|
|
483
|
+
for node_name in settings.scan("*_children_list", connection=conn):
|
|
484
|
+
if node_name.find(":") > -1: # can't be a session node
|
|
485
|
+
continue
|
|
486
|
+
session_names.append(node_name[:-14])
|
|
487
|
+
return [n for n in get_nodes(*session_names, connection=conn) if n is not None]
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def get_last_saved_scan(parent):
|
|
491
|
+
"""
|
|
492
|
+
:param DataNodeContainer parent:
|
|
493
|
+
:returns ScanNode or None:
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
def include_filter(node):
|
|
497
|
+
return node.type == "scan" and node.info.get("save")
|
|
498
|
+
|
|
499
|
+
return parent.get_last_child_container(
|
|
500
|
+
include_filter=include_filter, exclude_children=("scan", "scan_group")
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def get_last_scan_filename(parent):
|
|
505
|
+
"""
|
|
506
|
+
:param DataNodeContainer parent:
|
|
507
|
+
:returns str or None:
|
|
508
|
+
"""
|
|
509
|
+
last_scan_node = get_last_saved_scan(parent)
|
|
510
|
+
if last_scan_node is None:
|
|
511
|
+
return None
|
|
512
|
+
else:
|
|
513
|
+
return last_scan_node.info.get("filename")
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _get_or_create_node(*args, **kwargs):
|
|
517
|
+
warnings.warn(
|
|
518
|
+
"'_get_or_create_node' is deprecated. Use 'get_or_create_node' instead.",
|
|
519
|
+
FutureWarning,
|
|
520
|
+
)
|
|
521
|
+
return get_or_create_node(*args, **kwargs)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _create_node(*args, **kwargs):
|
|
525
|
+
warnings.warn(
|
|
526
|
+
"'_create_node' is deprecated. Use 'create_node' instead.", FutureWarning
|
|
527
|
+
)
|
|
528
|
+
return create_node(*args, **kwargs)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class DataNodeAsyncHelper:
|
|
532
|
+
"""This context manager helps to create and use DataNode's in a pipeline.
|
|
533
|
+
It can be used as a context manager. Inside the context, you use the
|
|
534
|
+
`replace_connection` method to replace the DataNode's connection with
|
|
535
|
+
an asynchronous proxy. The DataNode's connection will be replaced
|
|
536
|
+
again upon exiting the context with the synchronous proxy provided
|
|
537
|
+
to this helper.
|
|
538
|
+
|
|
539
|
+
Usage:
|
|
540
|
+
|
|
541
|
+
with DataNodeAsyncHelper(sync_proxy) as helper:
|
|
542
|
+
helper.replace_connection(node1)
|
|
543
|
+
helper.replace_connection(node2)
|
|
544
|
+
# ... all Redis calls of node1 and node2 are asynchronous
|
|
545
|
+
|
|
546
|
+
# ... all Redis calls of node1 and node2 are synchronous
|
|
547
|
+
|
|
548
|
+
Warning: the DataNode's are no longer thread-safe inside the context.
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
def __init__(self, sync_proxy):
|
|
552
|
+
self.sync_proxy = sync_proxy
|
|
553
|
+
self._nodes = []
|
|
554
|
+
self._async_proxy = None
|
|
555
|
+
self._results = None
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def results(self):
|
|
559
|
+
self._raise_inside_context()
|
|
560
|
+
return self._results
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def async_proxy(self):
|
|
564
|
+
self._raise_outside_context()
|
|
565
|
+
return self._async_proxy
|
|
566
|
+
|
|
567
|
+
def _raise_outside_context(self):
|
|
568
|
+
if self._async_proxy is None:
|
|
569
|
+
raise RuntimeError(
|
|
570
|
+
f"Can only be done inside the {self.__class__.__name__} context"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def _raise_inside_context(self):
|
|
574
|
+
if self._async_proxy is None:
|
|
575
|
+
raise RuntimeError(
|
|
576
|
+
f"Can only be done outside the {self.__class__.__name__} context"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def __enter__(self):
|
|
580
|
+
"""Create asynchronous proxy"""
|
|
581
|
+
if self._async_proxy is not None:
|
|
582
|
+
raise RuntimeError("You cannot enter this context more than once")
|
|
583
|
+
if self._nodes:
|
|
584
|
+
raise RuntimeError(
|
|
585
|
+
"Node connections were not reset in the previous context"
|
|
586
|
+
)
|
|
587
|
+
self._nodes = []
|
|
588
|
+
self._async_proxy = self.sync_proxy.pipeline()
|
|
589
|
+
self._results = None
|
|
590
|
+
return self
|
|
591
|
+
|
|
592
|
+
def replace_connection(self, *nodes):
|
|
593
|
+
"""Replace all connections with the asynchronous proxy. When
|
|
594
|
+
the context exits, all connections are replace with the synchronous
|
|
595
|
+
proxy.
|
|
596
|
+
"""
|
|
597
|
+
self._raise_outside_context()
|
|
598
|
+
for node in nodes:
|
|
599
|
+
if node not in self._nodes:
|
|
600
|
+
self._nodes.append(node)
|
|
601
|
+
node.replace_connection(self._async_proxy)
|
|
602
|
+
|
|
603
|
+
def __exit__(self, *args):
|
|
604
|
+
"""Execute the pipeline and reset the node connections"""
|
|
605
|
+
try:
|
|
606
|
+
self._results = self._async_proxy.execute()
|
|
607
|
+
finally:
|
|
608
|
+
self._async_proxy = None
|
|
609
|
+
for node in self._nodes:
|
|
610
|
+
node.replace_connection(self.sync_proxy)
|
|
611
|
+
self._nodes = None
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class DataNodeMetaClass(type):
|
|
615
|
+
def __call__(cls, *args, **kwargs):
|
|
616
|
+
"""This wraps the __init__ execution"""
|
|
617
|
+
instance = super().__call__(*args, **kwargs)
|
|
618
|
+
instance._finalize_init(**kwargs)
|
|
619
|
+
return instance
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class DataNode(metaclass=DataNodeMetaClass):
|
|
623
|
+
"""The DataNode can have these states, depending associated Redis keys:
|
|
624
|
+
|
|
625
|
+
1. exists: the principal Redis key is created in Redis
|
|
626
|
+
2. initialized: all Redis keys are created and initialized
|
|
627
|
+
3. supported: initialized + version can be handled by the current implementation
|
|
628
|
+
|
|
629
|
+
Use the utility methods `get_node`, `get_nodes`, ... to instantiate
|
|
630
|
+
a `DataNode` depending on its state.
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
VERSION = (1, 1) # change major version for incompatible API changes
|
|
634
|
+
|
|
635
|
+
@staticmethod
|
|
636
|
+
def _principal_db_name(name, parent=None):
|
|
637
|
+
"""Redis key of the principal representation of a `DataNode` in Redis"""
|
|
638
|
+
if parent:
|
|
639
|
+
return f"{parent.db_name}:{name}"
|
|
640
|
+
else:
|
|
641
|
+
return name
|
|
642
|
+
|
|
643
|
+
def __init__(
|
|
644
|
+
self,
|
|
645
|
+
node_type,
|
|
646
|
+
name,
|
|
647
|
+
parent=None,
|
|
648
|
+
add_to_parent=True,
|
|
649
|
+
create=False,
|
|
650
|
+
connection=None,
|
|
651
|
+
**kwargs,
|
|
652
|
+
):
|
|
653
|
+
"""
|
|
654
|
+
:param str node_type:
|
|
655
|
+
:param str name: used in the associated Redis keys
|
|
656
|
+
:param DataNode parent:
|
|
657
|
+
:param bool create: create the associated Redis keys
|
|
658
|
+
:param bool add_to_parent: only applicable when `create=True`.
|
|
659
|
+
:param connection:
|
|
660
|
+
:param kwargs: see `_init_info`. The `kwargs["info"]` will become `node.info`.
|
|
661
|
+
All other keys from `kwargs` are skipped, except for derived classes
|
|
662
|
+
that overwrite `_init_info`. They can take keys from `kwargs`
|
|
663
|
+
to populate the `kwargs["info"]`.
|
|
664
|
+
"""
|
|
665
|
+
# The DataNode's Redis connection, used by all Redis queries
|
|
666
|
+
if connection is None:
|
|
667
|
+
connection = get_redis_proxy(db=1)
|
|
668
|
+
self.db_connection = connection
|
|
669
|
+
|
|
670
|
+
# The DataNode's Redis key and type
|
|
671
|
+
db_name = self._principal_db_name(name, parent=parent)
|
|
672
|
+
self.__db_name = db_name
|
|
673
|
+
self.node_type = node_type
|
|
674
|
+
|
|
675
|
+
self._priorities = {}
|
|
676
|
+
"""Hold priorities per streams."""
|
|
677
|
+
|
|
678
|
+
# The info dictionary associated to the DataNode
|
|
679
|
+
self._info = settings.HashObjSetting(f"{db_name}_info", connection=connection)
|
|
680
|
+
info_dict = self._init_info(create=create, **kwargs)
|
|
681
|
+
if info_dict:
|
|
682
|
+
info_dict["node_name"] = db_name
|
|
683
|
+
self._info.update(info_dict)
|
|
684
|
+
|
|
685
|
+
# The DataNode itself is represented by a Redis dictionary
|
|
686
|
+
if create:
|
|
687
|
+
self.__new_node = True
|
|
688
|
+
self._struct = self._create_struct(db_name, node_type)
|
|
689
|
+
else:
|
|
690
|
+
self.__new_node = False
|
|
691
|
+
self._struct = self._get_struct(db_name, connection=self.db_connection)
|
|
692
|
+
|
|
693
|
+
def _register_stream_priority(self, fullname: str, priority: int):
|
|
694
|
+
"""
|
|
695
|
+
Register the stream priority which will be used on the reader side.
|
|
696
|
+
|
|
697
|
+
:paran str fullname: Full name of the stream
|
|
698
|
+
:param int priority: data from streams with a lower priority is never
|
|
699
|
+
yielded as long as higher priority streams have
|
|
700
|
+
data. Lower number means higher priority.
|
|
701
|
+
"""
|
|
702
|
+
self._priorities[fullname] = priority
|
|
703
|
+
|
|
704
|
+
def add_prefetch(self, async_proxy=None):
|
|
705
|
+
"""As long as caching on the proxy level exists in CachingRedisDbProxy,
|
|
706
|
+
we need to prefetch settings like this.
|
|
707
|
+
"""
|
|
708
|
+
if async_proxy is None:
|
|
709
|
+
async_proxy = self.db_connection
|
|
710
|
+
async_proxy.add_prefetch(self._struct, self._info)
|
|
711
|
+
|
|
712
|
+
def remove_prefetch(self, async_proxy=None):
|
|
713
|
+
"""Undo `add_prefetch`."""
|
|
714
|
+
if async_proxy is None:
|
|
715
|
+
async_proxy = self.db_connection
|
|
716
|
+
async_proxy.remove_prefetch(self._struct, self._info)
|
|
717
|
+
|
|
718
|
+
def _init_info(self, **kwargs):
|
|
719
|
+
return kwargs.pop("info", {})
|
|
720
|
+
|
|
721
|
+
def _finalize_init(self, parent=None, add_to_parent=True, **kwargs):
|
|
722
|
+
if self.__new_node:
|
|
723
|
+
# Mark node as "initialized" in Redis
|
|
724
|
+
self._mark_initialized()
|
|
725
|
+
# Add to the children_list stream of the parent
|
|
726
|
+
if parent is not None:
|
|
727
|
+
self._struct.parent = parent.db_name
|
|
728
|
+
if add_to_parent:
|
|
729
|
+
parent.add_children(self)
|
|
730
|
+
|
|
731
|
+
def get_nodes(self, *db_names, **kw):
|
|
732
|
+
"""
|
|
733
|
+
:param `*db_names`: str
|
|
734
|
+
:param `**kw`: see `get_nodes`
|
|
735
|
+
:return list(DataNode):
|
|
736
|
+
"""
|
|
737
|
+
kw.setdefault("connection", self.db_connection)
|
|
738
|
+
return get_nodes(*db_names, **kw)
|
|
739
|
+
|
|
740
|
+
def get_filtered_nodes(self, *db_names, **kw):
|
|
741
|
+
"""
|
|
742
|
+
:param `*db_names`: str
|
|
743
|
+
:param `**kw`: see `get_nodes`
|
|
744
|
+
:yields DataNode:
|
|
745
|
+
"""
|
|
746
|
+
kw.setdefault("connection", self.db_connection)
|
|
747
|
+
yield from get_filtered_nodes(*db_names, **kw)
|
|
748
|
+
|
|
749
|
+
def get_node(self, db_name, **kw):
|
|
750
|
+
"""
|
|
751
|
+
:param str db_name:
|
|
752
|
+
:param `**kw`: see `get_node`
|
|
753
|
+
:return DataNode:
|
|
754
|
+
"""
|
|
755
|
+
kw.setdefault("connection", self.db_connection)
|
|
756
|
+
return get_node(db_name, **kw)
|
|
757
|
+
|
|
758
|
+
def _create_nonassociated_stream(self, name, **kw):
|
|
759
|
+
"""Create any stream, not necessarily associated to this DataNode
|
|
760
|
+
(but use the DataNode's Redis connection).
|
|
761
|
+
|
|
762
|
+
:param str name:
|
|
763
|
+
:param `**kw`: see `DataStream`
|
|
764
|
+
:returns DataStream:
|
|
765
|
+
"""
|
|
766
|
+
kw.setdefault("connection", self.db_connection)
|
|
767
|
+
kw.setdefault("create", self.__new_node)
|
|
768
|
+
return streaming.DataStream(name, **kw)
|
|
769
|
+
|
|
770
|
+
def _create_stream(self, suffix, **kw):
|
|
771
|
+
"""Create a stream associated to this DataNode.
|
|
772
|
+
|
|
773
|
+
:param str suffix:
|
|
774
|
+
:param `**kw`: see `_create_nonassociated_stream`
|
|
775
|
+
:returns DataStream:
|
|
776
|
+
"""
|
|
777
|
+
stream = self._create_nonassociated_stream(f"{self.db_name}_{suffix}", **kw)
|
|
778
|
+
return stream
|
|
779
|
+
|
|
780
|
+
@classmethod
|
|
781
|
+
def _streamid_to_idx(cls, streamID):
|
|
782
|
+
"""
|
|
783
|
+
:param bytes streamID:
|
|
784
|
+
:returns int:
|
|
785
|
+
"""
|
|
786
|
+
return int(streamID.split(b"-")[0])
|
|
787
|
+
|
|
788
|
+
def search_redis(self, pattern):
|
|
789
|
+
"""Look for Redis keys that match a pattern.
|
|
790
|
+
|
|
791
|
+
:param str pattern:
|
|
792
|
+
:returns generator: db_name generator
|
|
793
|
+
"""
|
|
794
|
+
# TODO: Redis SCAN too slow
|
|
795
|
+
return (x.decode() for x in self.db_connection.keys(pattern))
|
|
796
|
+
|
|
797
|
+
def scan_redis(self, *args, **kw):
|
|
798
|
+
warnings.warn(
|
|
799
|
+
"'scan_redis' is deprecated. Use 'search_redis' instead.", FutureWarning
|
|
800
|
+
)
|
|
801
|
+
return self.search_redis(*args, **kw)
|
|
802
|
+
|
|
803
|
+
@property
|
|
804
|
+
def exists(self):
|
|
805
|
+
return bool(self.db_connection.exists(self.db_name))
|
|
806
|
+
|
|
807
|
+
@property
|
|
808
|
+
def initialized(self):
|
|
809
|
+
return bool(self.version)
|
|
810
|
+
|
|
811
|
+
def _mark_initialized(self):
|
|
812
|
+
self._struct.version = self.encode_version(self.VERSION)
|
|
813
|
+
|
|
814
|
+
@property
|
|
815
|
+
def supported(self):
|
|
816
|
+
return self.supported_version(self.version)
|
|
817
|
+
|
|
818
|
+
@classmethod
|
|
819
|
+
def supported_version(cls, version):
|
|
820
|
+
"""Version can be handled by the current implementation
|
|
821
|
+
|
|
822
|
+
:param tuple, bytes or None version:
|
|
823
|
+
:returns bool:
|
|
824
|
+
"""
|
|
825
|
+
if not isinstance(version, tuple):
|
|
826
|
+
version = cls.decode_version(version)
|
|
827
|
+
if version:
|
|
828
|
+
return version[0] == cls.VERSION[0]
|
|
829
|
+
return False
|
|
830
|
+
|
|
831
|
+
@classmethod
|
|
832
|
+
def _get_struct(cls, db_name, connection=None, **kwargs):
|
|
833
|
+
"""Principal Redis representation of a `DataNode`"""
|
|
834
|
+
if connection is None:
|
|
835
|
+
connection = get_redis_proxy(db=1)
|
|
836
|
+
return NodeStruct(db_name, connection=connection, **kwargs)
|
|
837
|
+
|
|
838
|
+
def _create_struct(self, db_name, node_type, name=None):
|
|
839
|
+
"""Create principal Redis representation of a `DataNode`"""
|
|
840
|
+
struct = self._get_struct(db_name, connection=self.db_connection)
|
|
841
|
+
# the following call finalize initialization
|
|
842
|
+
# 1) sets db_name
|
|
843
|
+
# 2) sets version to None => means the node is uninitialized
|
|
844
|
+
# 3) if name is None, it is assigned to the last part of "db_name" (default)
|
|
845
|
+
# - this is useful for Channel nodes only
|
|
846
|
+
struct._init(version=None, node_type=node_type, name=name)
|
|
847
|
+
return struct
|
|
848
|
+
|
|
849
|
+
@staticmethod
|
|
850
|
+
def decode_version(version):
|
|
851
|
+
"""
|
|
852
|
+
:param str, bytes or None version:
|
|
853
|
+
:returns tuple or None:
|
|
854
|
+
"""
|
|
855
|
+
if version:
|
|
856
|
+
if isinstance(version, bytes):
|
|
857
|
+
version = version.decode()
|
|
858
|
+
if version[0] == "v":
|
|
859
|
+
return tuple(map(int, version[1:].split(".")))
|
|
860
|
+
|
|
861
|
+
@staticmethod
|
|
862
|
+
def encode_version(version):
|
|
863
|
+
"""
|
|
864
|
+
:param tuple or None version:
|
|
865
|
+
:returns str or None:
|
|
866
|
+
"""
|
|
867
|
+
if version:
|
|
868
|
+
# Prefix is needed: float on decoding otherwise
|
|
869
|
+
return "v" + ".".join(map(str, version))
|
|
870
|
+
|
|
871
|
+
@property
|
|
872
|
+
def db_name(self):
|
|
873
|
+
return self.__db_name
|
|
874
|
+
|
|
875
|
+
@property
|
|
876
|
+
def connection(self):
|
|
877
|
+
return self.db_connection
|
|
878
|
+
|
|
879
|
+
@property
|
|
880
|
+
def name(self):
|
|
881
|
+
return self._struct.name
|
|
882
|
+
|
|
883
|
+
@property
|
|
884
|
+
def fullname(self):
|
|
885
|
+
return self._struct.fullname
|
|
886
|
+
|
|
887
|
+
@property
|
|
888
|
+
def type(self):
|
|
889
|
+
if self.node_type is not None:
|
|
890
|
+
return self.node_type
|
|
891
|
+
return self._struct.node_type
|
|
892
|
+
|
|
893
|
+
@property
|
|
894
|
+
def version(self):
|
|
895
|
+
"""`
|
|
896
|
+
:returns None or tuple:
|
|
897
|
+
"""
|
|
898
|
+
return self.decode_version(self._struct.version)
|
|
899
|
+
|
|
900
|
+
@property
|
|
901
|
+
def iterator(self):
|
|
902
|
+
warnings.warn(
|
|
903
|
+
"DataNodeIterator is deprecated. Use 'DataNode' itself.", FutureWarning
|
|
904
|
+
)
|
|
905
|
+
return self
|
|
906
|
+
|
|
907
|
+
@property
|
|
908
|
+
def parent(self):
|
|
909
|
+
parent_name = self._struct.parent
|
|
910
|
+
if parent_name:
|
|
911
|
+
parent = self.get_node(parent_name, state="exists")
|
|
912
|
+
if parent is None: # clean
|
|
913
|
+
del self._struct.parent
|
|
914
|
+
return parent
|
|
915
|
+
|
|
916
|
+
@property
|
|
917
|
+
def new_node(self):
|
|
918
|
+
return self.__new_node
|
|
919
|
+
|
|
920
|
+
@property
|
|
921
|
+
def info(self):
|
|
922
|
+
return self._info
|
|
923
|
+
|
|
924
|
+
def get_db_names(self, include_parents=True):
|
|
925
|
+
"""All associated Redis keys, including the associated keys of the parents."""
|
|
926
|
+
db_name = self.db_name
|
|
927
|
+
db_names = [db_name, "%s_info" % db_name]
|
|
928
|
+
if include_parents:
|
|
929
|
+
parent = self.parent
|
|
930
|
+
if parent:
|
|
931
|
+
db_names.extend(parent.get_db_names())
|
|
932
|
+
return db_names
|
|
933
|
+
|
|
934
|
+
def get_settings(self):
|
|
935
|
+
return [self._struct, self._info]
|
|
936
|
+
|
|
937
|
+
def replace_connection(self, redis_proxy):
|
|
938
|
+
"""Replace the connection of this nodes and all associated
|
|
939
|
+
Bliss settings.
|
|
940
|
+
"""
|
|
941
|
+
# A hard reference to the Redis proxy
|
|
942
|
+
self.db_connection = redis_proxy
|
|
943
|
+
# Weak references to the Redis proxy
|
|
944
|
+
cnx = weakref.ref(redis_proxy)
|
|
945
|
+
for setting in self.get_settings():
|
|
946
|
+
setting._cnx = cnx
|
|
947
|
+
|
|
948
|
+
def walk(
|
|
949
|
+
self,
|
|
950
|
+
filter=None,
|
|
951
|
+
include_filter=None,
|
|
952
|
+
exclude_children=None,
|
|
953
|
+
exclude_existing_children=None,
|
|
954
|
+
wait=True,
|
|
955
|
+
stop_handler=None,
|
|
956
|
+
active_streams=None,
|
|
957
|
+
excluded_stream_names=None,
|
|
958
|
+
first_index=0,
|
|
959
|
+
started_event=None,
|
|
960
|
+
):
|
|
961
|
+
"""Iterate over child nodes that match the `include_filter` argument.
|
|
962
|
+
|
|
963
|
+
:param filter: deprecated in favor of include_filter
|
|
964
|
+
:param include_filter: only these nodes are included (all by default)
|
|
965
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
966
|
+
:param exclude_existing_children: defaults to `exclude_children`.
|
|
967
|
+
:param bool wait:
|
|
968
|
+
:param DataStreamReaderStopHandler stop_handler:
|
|
969
|
+
:param dict active_streams: stream name (str) -> stream info (dict)
|
|
970
|
+
:param set excluded_stream_names:
|
|
971
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
972
|
+
:param Event started_event: set when subscribed to initial streams
|
|
973
|
+
:yields DataNode:
|
|
974
|
+
"""
|
|
975
|
+
with streaming.DataStreamReader(
|
|
976
|
+
wait=wait,
|
|
977
|
+
stop_handler=stop_handler,
|
|
978
|
+
active_streams=active_streams,
|
|
979
|
+
excluded_stream_names=excluded_stream_names,
|
|
980
|
+
) as reader:
|
|
981
|
+
yield from self._iter_reader(
|
|
982
|
+
reader,
|
|
983
|
+
filter=filter,
|
|
984
|
+
include_filter=include_filter,
|
|
985
|
+
exclude_children=exclude_children,
|
|
986
|
+
exclude_existing_children=exclude_existing_children,
|
|
987
|
+
first_index=first_index,
|
|
988
|
+
yield_events=False,
|
|
989
|
+
started_event=started_event,
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
def walk_from_last(
|
|
993
|
+
self,
|
|
994
|
+
filter=None,
|
|
995
|
+
include_filter=None,
|
|
996
|
+
exclude_children=None,
|
|
997
|
+
exclude_existing_children=None,
|
|
998
|
+
wait=True,
|
|
999
|
+
include_last=True,
|
|
1000
|
+
stop_handler=None,
|
|
1001
|
+
started_event=None,
|
|
1002
|
+
):
|
|
1003
|
+
"""Like `walk` but start from the last node.
|
|
1004
|
+
|
|
1005
|
+
:param filter: deprecated in favor of include_filter
|
|
1006
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1007
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1008
|
+
:param exclude_existing_children: defaults to `exclude_children`.
|
|
1009
|
+
:param bool wait: if wait is True (default), the function blocks
|
|
1010
|
+
until a new node appears
|
|
1011
|
+
:param bool include_last:
|
|
1012
|
+
:param DataStreamReaderStopHandler stop_handler:
|
|
1013
|
+
:param Event started_event: set when subscribed to initial streams
|
|
1014
|
+
:yields DataNode:
|
|
1015
|
+
"""
|
|
1016
|
+
# Start walking from "now":
|
|
1017
|
+
first_index = streaming.DataStream.now_index()
|
|
1018
|
+
if include_last:
|
|
1019
|
+
last_node, active_streams, excluded_stream_names = self._get_last_child(
|
|
1020
|
+
filter=filter,
|
|
1021
|
+
include_filter=include_filter,
|
|
1022
|
+
exclude_children=exclude_children,
|
|
1023
|
+
exclude_existing_children=exclude_existing_children,
|
|
1024
|
+
)
|
|
1025
|
+
if last_node is not None:
|
|
1026
|
+
exclude_existing_children = None
|
|
1027
|
+
yield last_node
|
|
1028
|
+
# Start walking from this node's index:
|
|
1029
|
+
first_index = last_node.get_children_stream_index()
|
|
1030
|
+
if first_index is None:
|
|
1031
|
+
raise RuntimeError(
|
|
1032
|
+
f"{last_node.db_name} was not added to the children stream of its parent"
|
|
1033
|
+
)
|
|
1034
|
+
else:
|
|
1035
|
+
started_event = None
|
|
1036
|
+
active_streams = dict()
|
|
1037
|
+
excluded_stream_names = set()
|
|
1038
|
+
|
|
1039
|
+
yield from self.walk(
|
|
1040
|
+
filter=filter,
|
|
1041
|
+
include_filter=include_filter,
|
|
1042
|
+
exclude_children=exclude_children,
|
|
1043
|
+
exclude_existing_children=exclude_existing_children,
|
|
1044
|
+
wait=wait,
|
|
1045
|
+
active_streams=active_streams,
|
|
1046
|
+
excluded_stream_names=excluded_stream_names,
|
|
1047
|
+
first_index=first_index,
|
|
1048
|
+
stop_handler=stop_handler,
|
|
1049
|
+
started_event=started_event,
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
def walk_events(
|
|
1053
|
+
self,
|
|
1054
|
+
filter=None,
|
|
1055
|
+
include_filter=None,
|
|
1056
|
+
exclude_children=None,
|
|
1057
|
+
exclude_existing_children=None,
|
|
1058
|
+
wait=True,
|
|
1059
|
+
first_index=0,
|
|
1060
|
+
active_streams=None,
|
|
1061
|
+
excluded_stream_names=None,
|
|
1062
|
+
stop_handler=None,
|
|
1063
|
+
started_event=None,
|
|
1064
|
+
):
|
|
1065
|
+
"""Iterate over node and children node events.
|
|
1066
|
+
|
|
1067
|
+
:param filter: deprecated in favor of include_filter
|
|
1068
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1069
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1070
|
+
:param exclude_existing_children: defaults to `exclude_children`.
|
|
1071
|
+
:param bool wait:
|
|
1072
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
1073
|
+
:param dict active_streams: stream name (str) -> stream info (dict)
|
|
1074
|
+
:param set excluded_stream_names:
|
|
1075
|
+
:param DataStreamReaderStopHandler stop_handler:
|
|
1076
|
+
:param Event started_event: set when subscribed to initial streams
|
|
1077
|
+
:yields Event:
|
|
1078
|
+
"""
|
|
1079
|
+
with streaming.DataStreamReader(
|
|
1080
|
+
wait=wait,
|
|
1081
|
+
stop_handler=stop_handler,
|
|
1082
|
+
active_streams=active_streams,
|
|
1083
|
+
excluded_stream_names=excluded_stream_names,
|
|
1084
|
+
) as reader:
|
|
1085
|
+
yield from self._iter_reader(
|
|
1086
|
+
reader,
|
|
1087
|
+
filter=filter,
|
|
1088
|
+
include_filter=include_filter,
|
|
1089
|
+
exclude_children=exclude_children,
|
|
1090
|
+
exclude_existing_children=exclude_existing_children,
|
|
1091
|
+
first_index=first_index,
|
|
1092
|
+
yield_events=True,
|
|
1093
|
+
started_event=started_event,
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
def walk_on_new_events(self, **kw):
|
|
1097
|
+
"""Like `walk_en_events` but yield only new event.
|
|
1098
|
+
|
|
1099
|
+
:param `**kw`: see `walk_events`
|
|
1100
|
+
:yields Event:
|
|
1101
|
+
"""
|
|
1102
|
+
yield from self.walk_events(first_index=streaming.DataStream.now_index(), **kw)
|
|
1103
|
+
|
|
1104
|
+
def _iter_reader(
|
|
1105
|
+
self,
|
|
1106
|
+
reader,
|
|
1107
|
+
filter=None,
|
|
1108
|
+
include_filter=None,
|
|
1109
|
+
exclude_children=None,
|
|
1110
|
+
exclude_existing_children=None,
|
|
1111
|
+
first_index=0,
|
|
1112
|
+
yield_events=False,
|
|
1113
|
+
started_event=None,
|
|
1114
|
+
):
|
|
1115
|
+
"""Iterate over the DataStreamReader
|
|
1116
|
+
|
|
1117
|
+
:param DataStreamReader reader:
|
|
1118
|
+
:param filter: deprecated in favor of include_filter
|
|
1119
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1120
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1121
|
+
:param exclude_existing_children: defaults to `exclude_children`.
|
|
1122
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
1123
|
+
:param bool yield_events: yield Event or DataNode
|
|
1124
|
+
:param Event started_event: set when subscribed to initial streams
|
|
1125
|
+
:yields Event or DataNode:
|
|
1126
|
+
"""
|
|
1127
|
+
if filter:
|
|
1128
|
+
if include_filter:
|
|
1129
|
+
raise ValueError("Only use include_filter")
|
|
1130
|
+
else:
|
|
1131
|
+
warnings.warn(
|
|
1132
|
+
"'filter' is deprecated. Use 'include_filter' instead.",
|
|
1133
|
+
FutureWarning,
|
|
1134
|
+
)
|
|
1135
|
+
include_filter = filter
|
|
1136
|
+
if exclude_existing_children is None:
|
|
1137
|
+
exclude_existing_children = exclude_children
|
|
1138
|
+
self._subscribe_streams(
|
|
1139
|
+
reader,
|
|
1140
|
+
include_filter=include_filter,
|
|
1141
|
+
exclude_children=exclude_existing_children,
|
|
1142
|
+
first_index=first_index,
|
|
1143
|
+
yield_events=yield_events,
|
|
1144
|
+
)
|
|
1145
|
+
if started_event is not None:
|
|
1146
|
+
started_event.set()
|
|
1147
|
+
for stream, events in reader:
|
|
1148
|
+
node = reader.get_stream_info(stream, "node")
|
|
1149
|
+
handler = node.get_stream_event_handler(stream)
|
|
1150
|
+
yield from handler(
|
|
1151
|
+
reader,
|
|
1152
|
+
events,
|
|
1153
|
+
include_filter=include_filter,
|
|
1154
|
+
exclude_children=exclude_children,
|
|
1155
|
+
first_index=first_index,
|
|
1156
|
+
yield_events=yield_events,
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
def get_stream_event_handler(self, stream):
|
|
1160
|
+
"""
|
|
1161
|
+
:param DataStream stream:
|
|
1162
|
+
:returns callable:
|
|
1163
|
+
"""
|
|
1164
|
+
if stream.name == f"{self.db_name}_data":
|
|
1165
|
+
return self._iter_data_stream_events
|
|
1166
|
+
else:
|
|
1167
|
+
raise RuntimeError(f"Unknown stream {stream.name}")
|
|
1168
|
+
|
|
1169
|
+
def _filter(self, fltr, default=True):
|
|
1170
|
+
"""When the filter is a string or sequence, the node type is filtered.
|
|
1171
|
+
|
|
1172
|
+
:param None, callable, str or sequence fltr:
|
|
1173
|
+
:param bool default: returned when filter is `None`
|
|
1174
|
+
:returns bool:
|
|
1175
|
+
"""
|
|
1176
|
+
if callable(fltr):
|
|
1177
|
+
return fltr(self)
|
|
1178
|
+
elif isinstance(fltr, str):
|
|
1179
|
+
return self.type == fltr
|
|
1180
|
+
elif fltr:
|
|
1181
|
+
return self.type in fltr
|
|
1182
|
+
else:
|
|
1183
|
+
return default
|
|
1184
|
+
|
|
1185
|
+
def _included(self, include_filter):
|
|
1186
|
+
"""When the filter is a string or sequence, the node type is filtered.
|
|
1187
|
+
|
|
1188
|
+
:param None, callable, str or sequence include_filter:
|
|
1189
|
+
:returns bool: True by default
|
|
1190
|
+
"""
|
|
1191
|
+
return self._filter(include_filter, default=True)
|
|
1192
|
+
|
|
1193
|
+
def _excluded(self, exclude_filter):
|
|
1194
|
+
"""When the filter is a string or sequence, the node type is filtered.
|
|
1195
|
+
|
|
1196
|
+
:param None, callable, str or sequence exclude_filter:
|
|
1197
|
+
:returns bool: False by default
|
|
1198
|
+
"""
|
|
1199
|
+
return self._filter(exclude_filter, default=False)
|
|
1200
|
+
|
|
1201
|
+
def _yield_on_new_node(
|
|
1202
|
+
self, reader, include_filter, exclude_children, first_index, yield_events
|
|
1203
|
+
):
|
|
1204
|
+
"""
|
|
1205
|
+
:param DataStreamReader reader:
|
|
1206
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1207
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1208
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
1209
|
+
:param bool yield_events: yield Event or DataNode
|
|
1210
|
+
"""
|
|
1211
|
+
self._subscribe_streams(
|
|
1212
|
+
reader,
|
|
1213
|
+
include_filter=include_filter,
|
|
1214
|
+
exclude_children=exclude_children,
|
|
1215
|
+
first_index=first_index,
|
|
1216
|
+
yield_events=yield_events,
|
|
1217
|
+
)
|
|
1218
|
+
if self._included(include_filter):
|
|
1219
|
+
if yield_events:
|
|
1220
|
+
yield Event(type=EventType.NEW_NODE, node=self)
|
|
1221
|
+
else:
|
|
1222
|
+
yield self
|
|
1223
|
+
|
|
1224
|
+
def _iter_data_stream_events(
|
|
1225
|
+
self,
|
|
1226
|
+
reader,
|
|
1227
|
+
events,
|
|
1228
|
+
include_filter=None,
|
|
1229
|
+
exclude_children=None,
|
|
1230
|
+
first_index=None,
|
|
1231
|
+
yield_events=False,
|
|
1232
|
+
):
|
|
1233
|
+
"""
|
|
1234
|
+
:param DataStreamReader reader:
|
|
1235
|
+
:param list(2-tuple) events:
|
|
1236
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1237
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1238
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
1239
|
+
:param bool yield_events: yield Event or DataNode
|
|
1240
|
+
:yields Event:
|
|
1241
|
+
"""
|
|
1242
|
+
if yield_events and self._included(include_filter):
|
|
1243
|
+
data = self.decode_raw_events(events)
|
|
1244
|
+
yield Event(type=EventType.NEW_DATA, node=self, data=data)
|
|
1245
|
+
|
|
1246
|
+
def _get_last_child(
|
|
1247
|
+
self,
|
|
1248
|
+
filter=None,
|
|
1249
|
+
include_filter=None,
|
|
1250
|
+
exclude_children=None,
|
|
1251
|
+
exclude_existing_children=None,
|
|
1252
|
+
):
|
|
1253
|
+
"""Get the last child added to the _children_list stream of this node or its children.
|
|
1254
|
+
|
|
1255
|
+
:param filter: deprecated in favor of include_filter
|
|
1256
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1257
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1258
|
+
:param exclude_existing_children: defaults to `exclude_children`.
|
|
1259
|
+
:returns 2-tuple: DataNode, active streams
|
|
1260
|
+
"""
|
|
1261
|
+
return None, None
|
|
1262
|
+
|
|
1263
|
+
def _subscribe_stream(
|
|
1264
|
+
self, stream_suffix, reader, create=False, first_index=None, **kw
|
|
1265
|
+
):
|
|
1266
|
+
"""Subscribe to a particular stream associated with this node.
|
|
1267
|
+
|
|
1268
|
+
:param str stream_suffix: stream to add is "{db_name}_{stream_suffix}"
|
|
1269
|
+
:param DataStreamReader reader:
|
|
1270
|
+
:param bool create: create when missing
|
|
1271
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
1272
|
+
:param `**kw`: see `DataStreamReader.add_streams`
|
|
1273
|
+
"""
|
|
1274
|
+
stream_name = f"{self.db_name}_{stream_suffix}"
|
|
1275
|
+
if not create:
|
|
1276
|
+
if not self.db_connection.exists(stream_name):
|
|
1277
|
+
return
|
|
1278
|
+
stream = self._create_nonassociated_stream(stream_name)
|
|
1279
|
+
|
|
1280
|
+
# Use the priority as it was setup
|
|
1281
|
+
priority = self._priorities.get(stream.name, 0)
|
|
1282
|
+
if priority is not None:
|
|
1283
|
+
kw["priority"] = priority
|
|
1284
|
+
|
|
1285
|
+
reader.add_streams(stream, node=self, first_index=first_index, **kw)
|
|
1286
|
+
|
|
1287
|
+
def _subscribe_streams(
|
|
1288
|
+
self,
|
|
1289
|
+
reader,
|
|
1290
|
+
include_filter=None,
|
|
1291
|
+
exclude_children=None,
|
|
1292
|
+
first_index=None,
|
|
1293
|
+
yield_events=False,
|
|
1294
|
+
):
|
|
1295
|
+
"""Subscribe to all associated streams of this node.
|
|
1296
|
+
|
|
1297
|
+
:param DataStreamReader reader:
|
|
1298
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1299
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1300
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
1301
|
+
:param bool yield_events: yield Event or DataNode
|
|
1302
|
+
"""
|
|
1303
|
+
pass
|
|
1304
|
+
|
|
1305
|
+
def get_children_stream_index(self):
|
|
1306
|
+
"""Get the node's stream ID in parent node's _children_list stream
|
|
1307
|
+
|
|
1308
|
+
:returns bytes or None: stream ID
|
|
1309
|
+
"""
|
|
1310
|
+
parent = self.parent
|
|
1311
|
+
if parent is None:
|
|
1312
|
+
return None
|
|
1313
|
+
# Higher priority than PREPARED scan
|
|
1314
|
+
children_stream = parent._create_stream("children_list")
|
|
1315
|
+
self_db_name = self.db_name
|
|
1316
|
+
for index, raw in children_stream.rev_range():
|
|
1317
|
+
db_name = NewNodeEvent(raw=raw).db_name
|
|
1318
|
+
if db_name == self_db_name:
|
|
1319
|
+
break
|
|
1320
|
+
else:
|
|
1321
|
+
return None
|
|
1322
|
+
return index
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
class DataNodeContainer(DataNode):
|
|
1326
|
+
def __init__(
|
|
1327
|
+
self, node_type, name, parent=None, connection=None, create=False, **kwargs
|
|
1328
|
+
):
|
|
1329
|
+
super().__init__(
|
|
1330
|
+
node_type,
|
|
1331
|
+
name,
|
|
1332
|
+
parent=parent,
|
|
1333
|
+
connection=connection,
|
|
1334
|
+
create=create,
|
|
1335
|
+
**kwargs,
|
|
1336
|
+
)
|
|
1337
|
+
db_name = name if parent is None else self.db_name
|
|
1338
|
+
self._children_stream = self._create_nonassociated_stream(
|
|
1339
|
+
f"{db_name}_children_list"
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
def get_db_names(self, **kw):
|
|
1343
|
+
db_names = super().get_db_names(**kw)
|
|
1344
|
+
db_names.append("%s_children_list" % self.db_name)
|
|
1345
|
+
return db_names
|
|
1346
|
+
|
|
1347
|
+
def get_settings(self):
|
|
1348
|
+
return super().get_settings() + [self._children_stream]
|
|
1349
|
+
|
|
1350
|
+
def add_children(self, *children):
|
|
1351
|
+
"""Publish new (direct) child in Redis"""
|
|
1352
|
+
for child in children:
|
|
1353
|
+
self._children_stream.add_event(NewNodeEvent(child.db_name))
|
|
1354
|
+
|
|
1355
|
+
def get_children(self, events=None, purge=False):
|
|
1356
|
+
"""Get direct children as published in the _children_list
|
|
1357
|
+
DataStream. When purging missing nodes, you can no longer
|
|
1358
|
+
verify whether all data is still there.
|
|
1359
|
+
|
|
1360
|
+
:param dict events: list((streamID, dict))
|
|
1361
|
+
:param bool purge: purge missing nodes
|
|
1362
|
+
:yields DataNode:
|
|
1363
|
+
"""
|
|
1364
|
+
if events is None:
|
|
1365
|
+
events = self._children_stream.range()
|
|
1366
|
+
node_dict = {NewNodeEvent(raw=raw).db_name: index for index, raw in events}
|
|
1367
|
+
nodes = self.get_nodes(*node_dict)
|
|
1368
|
+
if purge:
|
|
1369
|
+
with settings.pipeline(self._children_stream):
|
|
1370
|
+
for index, node in zip(node_dict.values(), nodes):
|
|
1371
|
+
if node is None:
|
|
1372
|
+
# When the index is not present
|
|
1373
|
+
# it is silently ignored.
|
|
1374
|
+
self._children_stream.remove(index)
|
|
1375
|
+
for index, node in zip(node_dict.values(), nodes):
|
|
1376
|
+
if node is not None:
|
|
1377
|
+
yield node
|
|
1378
|
+
|
|
1379
|
+
def children(self, purge=False):
|
|
1380
|
+
"""
|
|
1381
|
+
:yields DataNode:
|
|
1382
|
+
"""
|
|
1383
|
+
yield from self.get_children(purge=purge)
|
|
1384
|
+
|
|
1385
|
+
def get_stream_event_handler(self, stream):
|
|
1386
|
+
"""
|
|
1387
|
+
:param DataStream stream:
|
|
1388
|
+
:returns callable:
|
|
1389
|
+
"""
|
|
1390
|
+
if stream.name == f"{self.db_name}_children_list":
|
|
1391
|
+
return self._iter_children_stream_events
|
|
1392
|
+
else:
|
|
1393
|
+
return super().get_stream_event_handler(stream)
|
|
1394
|
+
|
|
1395
|
+
def _iter_children_stream_events(
|
|
1396
|
+
self,
|
|
1397
|
+
reader,
|
|
1398
|
+
events,
|
|
1399
|
+
include_filter=None,
|
|
1400
|
+
exclude_children=None,
|
|
1401
|
+
first_index=None,
|
|
1402
|
+
yield_events=False,
|
|
1403
|
+
):
|
|
1404
|
+
"""
|
|
1405
|
+
:param DataStreamReader reader:
|
|
1406
|
+
:param list(2-tuple) events:
|
|
1407
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1408
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
1409
|
+
:param bool yield_events: yield Event or DataNode
|
|
1410
|
+
:yields Event or DataNode:
|
|
1411
|
+
"""
|
|
1412
|
+
for node in self.get_children(events):
|
|
1413
|
+
yield from node._yield_on_new_node(
|
|
1414
|
+
reader, include_filter, exclude_children, first_index, yield_events
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
def _subscribe_streams(
|
|
1418
|
+
self,
|
|
1419
|
+
reader,
|
|
1420
|
+
include_filter=None,
|
|
1421
|
+
exclude_children=None,
|
|
1422
|
+
first_index=None,
|
|
1423
|
+
yield_events=False,
|
|
1424
|
+
):
|
|
1425
|
+
"""Subscribe to all associated streams of this node.
|
|
1426
|
+
|
|
1427
|
+
:param DataStreamReader reader:
|
|
1428
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1429
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1430
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
1431
|
+
:param bool yield_events: yield Event or DataNode
|
|
1432
|
+
"""
|
|
1433
|
+
search_child_streams = not reader.n_subscribed_streams and first_index
|
|
1434
|
+
|
|
1435
|
+
# Do not use the include_filter for *_children_list. Maybe we don't
|
|
1436
|
+
# want the events from the direct children but may want the events
|
|
1437
|
+
# from their children.
|
|
1438
|
+
exclude_my_children = self._excluded(exclude_children)
|
|
1439
|
+
if not exclude_my_children:
|
|
1440
|
+
self._subscribe_stream(
|
|
1441
|
+
"children_list", reader, create=True, first_index=first_index
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
# Subscribing to child streams requires searching for Redis keys
|
|
1445
|
+
# which is an expensive operation for the Redis server so skip it
|
|
1446
|
+
# when possible.
|
|
1447
|
+
if not search_child_streams:
|
|
1448
|
+
return
|
|
1449
|
+
|
|
1450
|
+
# Delay subscribing to *_data streams to the moment we receive the
|
|
1451
|
+
# NEW_NODE events of those nodes. Same reason as above: search Redis
|
|
1452
|
+
# keys is expensive.
|
|
1453
|
+
# search_data_streams = yield_events
|
|
1454
|
+
search_data_streams = False
|
|
1455
|
+
|
|
1456
|
+
# Subscribe to the streams of all children, not only the direct children.
|
|
1457
|
+
# TODO: this makes assumptions about the data nodes and their streams.
|
|
1458
|
+
# Any change in streams (rename stream, add new streams, ...) will
|
|
1459
|
+
# affect this code.
|
|
1460
|
+
|
|
1461
|
+
# Subscribe to streams found by a recursive search
|
|
1462
|
+
nodes_with_data = dict()
|
|
1463
|
+
search_suffixes = {"data": ["data"], "end": ["prepared", "end"]}
|
|
1464
|
+
nodes_with_children = list()
|
|
1465
|
+
excluded_stream_names = set(reader.excluded_stream_names)
|
|
1466
|
+
if search_data_streams:
|
|
1467
|
+
# Make sure the NEW_NODE event always arrives before any other node event:
|
|
1468
|
+
# - assume "...:parent_children_list" is created BEFORE "...parent:child_data"
|
|
1469
|
+
# - search for *_children_list AFTER searching for *_data
|
|
1470
|
+
# - subscribe to *_children_list BEFORE subscribing to *_data
|
|
1471
|
+
for suffix in search_suffixes:
|
|
1472
|
+
node_names = self._search_nodes_with_streams(
|
|
1473
|
+
suffix, excluded_stream_names, include_parent=False
|
|
1474
|
+
)
|
|
1475
|
+
nodes_with_data[suffix] = list(
|
|
1476
|
+
self.get_filtered_nodes(
|
|
1477
|
+
*node_names,
|
|
1478
|
+
include_filter=include_filter,
|
|
1479
|
+
recursive_exclude=exclude_children,
|
|
1480
|
+
strict_recursive_exclude=False,
|
|
1481
|
+
)
|
|
1482
|
+
)
|
|
1483
|
+
if not exclude_my_children:
|
|
1484
|
+
node_names = self._search_nodes_with_streams(
|
|
1485
|
+
"children_list", excluded_stream_names, include_parent=False
|
|
1486
|
+
)
|
|
1487
|
+
nodes_with_children = self.get_filtered_nodes(
|
|
1488
|
+
*node_names,
|
|
1489
|
+
include_filter=None,
|
|
1490
|
+
recursive_exclude=exclude_children,
|
|
1491
|
+
strict_recursive_exclude=True,
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
# Subscribe to the streams that were searched
|
|
1495
|
+
for node in nodes_with_children:
|
|
1496
|
+
node._subscribe_stream("children_list", reader, first_index=first_index)
|
|
1497
|
+
for search_suffix, nodes in nodes_with_data.items():
|
|
1498
|
+
subscribe_suffixes = search_suffixes[search_suffix]
|
|
1499
|
+
for node in nodes:
|
|
1500
|
+
for subscribe_suffix in subscribe_suffixes:
|
|
1501
|
+
node._subscribe_stream(
|
|
1502
|
+
subscribe_suffix, reader, first_index=first_index
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
# Exclude searched Redis keys from further subscription attempts
|
|
1506
|
+
reader.excluded_stream_names |= excluded_stream_names
|
|
1507
|
+
|
|
1508
|
+
def _search_nodes_with_streams(
|
|
1509
|
+
self, stream_suffix, excluded_stream_names=None, include_parent=False
|
|
1510
|
+
):
|
|
1511
|
+
"""Find all children nodes recursively (optionally including self)
|
|
1512
|
+
which have associated streams with a particular suffix.
|
|
1513
|
+
|
|
1514
|
+
:param str stream_suffix: streams to add have the name
|
|
1515
|
+
"{db_name}_{stream_suffix}"
|
|
1516
|
+
:param set excluded_stream_names: will be updated with the found redis keys
|
|
1517
|
+
:param bool include_parent: include self
|
|
1518
|
+
:returns list(str):
|
|
1519
|
+
"""
|
|
1520
|
+
# Get existing stream names
|
|
1521
|
+
if include_parent:
|
|
1522
|
+
pattern = f"{self.db_name}*_{stream_suffix}"
|
|
1523
|
+
else:
|
|
1524
|
+
pattern = f"{self.db_name}:*_{stream_suffix}"
|
|
1525
|
+
found_names = set(self.search_redis(pattern))
|
|
1526
|
+
if excluded_stream_names is None:
|
|
1527
|
+
stream_names = sorted(found_names, key=self._node_sort_key)
|
|
1528
|
+
else:
|
|
1529
|
+
stream_names = sorted(
|
|
1530
|
+
found_names - excluded_stream_names, key=self._node_sort_key
|
|
1531
|
+
)
|
|
1532
|
+
excluded_stream_names |= found_names
|
|
1533
|
+
|
|
1534
|
+
# Get associated DataNode key names
|
|
1535
|
+
nsuffix = len(stream_suffix) + 1 # +1 for the underscore
|
|
1536
|
+
# Warning: some nodes may be None because a Redis key could end with
|
|
1537
|
+
# the suffix and not be a stream associated to a node.
|
|
1538
|
+
return [db_name[:-nsuffix] for db_name in stream_names]
|
|
1539
|
+
|
|
1540
|
+
@staticmethod
|
|
1541
|
+
def _node_sort_key(db_name):
|
|
1542
|
+
"""For hierarchical sort of node names"""
|
|
1543
|
+
return db_name.count(":")
|
|
1544
|
+
|
|
1545
|
+
def _get_last_child(
|
|
1546
|
+
self,
|
|
1547
|
+
filter=None,
|
|
1548
|
+
include_filter=None,
|
|
1549
|
+
exclude_children=None,
|
|
1550
|
+
exclude_existing_children=None,
|
|
1551
|
+
):
|
|
1552
|
+
"""Get the last child added to the _children_list stream of this node or its children.
|
|
1553
|
+
|
|
1554
|
+
:param filter: deprecated in favor of include_filter
|
|
1555
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1556
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1557
|
+
:param exclude_existing_children: defaults to `exclude_children`
|
|
1558
|
+
:returns 3-tuple: DataNode, active streams, excluded stream names
|
|
1559
|
+
"""
|
|
1560
|
+
last_node = None
|
|
1561
|
+
active_streams = dict()
|
|
1562
|
+
excluded_stream_names = set()
|
|
1563
|
+
# Higher priority than PREPARED scan
|
|
1564
|
+
children_stream = self._create_stream("children_list")
|
|
1565
|
+
first_index = children_stream.before_last_index()
|
|
1566
|
+
if first_index is None:
|
|
1567
|
+
return last_node, active_streams, excluded_stream_names
|
|
1568
|
+
for last_node in self.walk(
|
|
1569
|
+
filter=filter,
|
|
1570
|
+
include_filter=include_filter,
|
|
1571
|
+
exclude_children=exclude_children,
|
|
1572
|
+
exclude_existing_children=exclude_existing_children,
|
|
1573
|
+
wait=False,
|
|
1574
|
+
active_streams=active_streams,
|
|
1575
|
+
excluded_stream_names=excluded_stream_names,
|
|
1576
|
+
first_index=first_index,
|
|
1577
|
+
):
|
|
1578
|
+
pass
|
|
1579
|
+
return last_node, active_streams, excluded_stream_names
|
|
1580
|
+
|
|
1581
|
+
def get_child_containers(self, include_filter=None, exclude_children=None):
|
|
1582
|
+
"""Get the child `DataNodeContainer` of this node and its children.
|
|
1583
|
+
|
|
1584
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1585
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1586
|
+
:yields DataNodeContainer:
|
|
1587
|
+
"""
|
|
1588
|
+
node_names = self._search_nodes_with_streams(
|
|
1589
|
+
"children_list", include_parent=True
|
|
1590
|
+
)
|
|
1591
|
+
yield from self.get_filtered_nodes(
|
|
1592
|
+
*node_names,
|
|
1593
|
+
include_filter=include_filter,
|
|
1594
|
+
recursive_exclude=exclude_children,
|
|
1595
|
+
strict_recursive_exclude=False,
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
def get_last_child_container(self, include_filter=None, exclude_children=None):
|
|
1599
|
+
"""Get the last child `DataNodeContainer` of this node or its children.
|
|
1600
|
+
The order is based on the Redis streamid in the `*_children_list` streams.
|
|
1601
|
+
|
|
1602
|
+
:param include_filter: only these nodes are included (all by default)
|
|
1603
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
1604
|
+
:returns DataNodeContainer:
|
|
1605
|
+
"""
|
|
1606
|
+
last_node = None
|
|
1607
|
+
last_id = 0, 0
|
|
1608
|
+
containers = self.get_child_containers(
|
|
1609
|
+
include_filter=include_filter, exclude_children=exclude_children
|
|
1610
|
+
)
|
|
1611
|
+
for node in containers:
|
|
1612
|
+
streamid = node.get_children_stream_index()
|
|
1613
|
+
if streamid is None:
|
|
1614
|
+
continue
|
|
1615
|
+
node_id = tuple(map(int, streamid.decode().split("-")))
|
|
1616
|
+
if node_id > last_id:
|
|
1617
|
+
last_node = node
|
|
1618
|
+
last_id = node_id
|
|
1619
|
+
return last_node
|