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.
Files changed (67) hide show
  1. blissdata/__init__.py +17 -0
  2. blissdata/beacon/__init__.py +1 -0
  3. blissdata/beacon/_base.py +106 -0
  4. blissdata/beacon/config.py +24 -0
  5. blissdata/beacon/data.py +45 -0
  6. blissdata/beacon/files.py +141 -0
  7. blissdata/client.py +62 -0
  8. blissdata/common/__init__.py +15 -0
  9. blissdata/common/utils.py +159 -0
  10. blissdata/data/__init__.py +15 -0
  11. blissdata/data/events/__init__.py +22 -0
  12. blissdata/data/events/channel.py +149 -0
  13. blissdata/data/events/lima.py +478 -0
  14. blissdata/data/events/node.py +28 -0
  15. blissdata/data/events/scan.py +49 -0
  16. blissdata/data/events/walk.py +42 -0
  17. blissdata/data/expiration.py +42 -0
  18. blissdata/data/lima_image.py +465 -0
  19. blissdata/data/node.py +1619 -0
  20. blissdata/data/nodes/__init__.py +11 -0
  21. blissdata/data/nodes/channel.py +394 -0
  22. blissdata/data/nodes/dataset.py +82 -0
  23. blissdata/data/nodes/dataset_collection.py +12 -0
  24. blissdata/data/nodes/lima.py +422 -0
  25. blissdata/data/nodes/node_ref_channel.py +34 -0
  26. blissdata/data/nodes/proposal.py +12 -0
  27. blissdata/data/nodes/scan.py +192 -0
  28. blissdata/data/nodes/scan_group.py +12 -0
  29. blissdata/data/remote_node.py +204 -0
  30. blissdata/data/scan.py +666 -0
  31. blissdata/h5api/__init__.py +1 -0
  32. blissdata/h5api/abstract.py +97 -0
  33. blissdata/h5api/dynamic_hdf5.py +153 -0
  34. blissdata/h5api/file_arguments.py +28 -0
  35. blissdata/h5api/static_hdf5.py +139 -0
  36. blissdata/h5api/utils/__init__.py +0 -0
  37. blissdata/h5api/utils/bliss.py +138 -0
  38. blissdata/h5api/utils/hdf5.py +280 -0
  39. blissdata/h5api/utils/hdf5_retry.py +98 -0
  40. blissdata/h5api/utils/lima.py +286 -0
  41. blissdata/h5api/utils/types.py +13 -0
  42. blissdata/redis/__init__.py +12 -0
  43. blissdata/redis/caching.py +390 -0
  44. blissdata/redis/connection.py +169 -0
  45. blissdata/redis/manager.py +164 -0
  46. blissdata/redis/proxy.py +971 -0
  47. blissdata/redis/scripting.py +24 -0
  48. blissdata/settings.py +1174 -0
  49. blissdata/streaming.py +819 -0
  50. blissdata/streaming_events.py +355 -0
  51. blissdata/tests/__init__.py +0 -0
  52. blissdata/tests/beacon/__init__.py +0 -0
  53. blissdata/tests/beacon/test_config.py +26 -0
  54. blissdata/tests/beacon/test_data.py +50 -0
  55. blissdata/tests/beacon/test_files.py +80 -0
  56. blissdata/tests/conftest.py +0 -0
  57. blissdata/tests/h5api/__init__.py +0 -0
  58. blissdata/tests/h5api/scanner.py +390 -0
  59. blissdata/tests/h5api/test_dynamic_files.py +309 -0
  60. blissdata/tests/h5api/test_static_files.py +169 -0
  61. blissdata/tests/redis/__init__.py +0 -0
  62. blissdata/tests/redis/manager.py +23 -0
  63. blissdata-0.3.4.dist-info/LICENSE +165 -0
  64. blissdata-0.3.4.dist-info/METADATA +79 -0
  65. blissdata-0.3.4.dist-info/RECORD +67 -0
  66. blissdata-0.3.4.dist-info/WHEEL +5 -0
  67. 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