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
@@ -0,0 +1,422 @@
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
+ import numpy
9
+ import warnings
10
+ import logging
11
+
12
+ from blissdata.data.nodes.channel import _ChannelDataNodeBase
13
+ from blissdata.data.events import EventData, LimaImageStatusEvent, ImageNotSaved
14
+ from blissdata.settings import QueueObjSetting
15
+ from blissdata.data import lima_image
16
+
17
+
18
+ _logger = logging.getLogger(__name__)
19
+
20
+
21
+ class LimaDataView:
22
+ def __init__(self, queue, queue_ref, from_index, to_index, from_stream=False):
23
+ """
24
+ :param DataStream queue: acquisition status events
25
+ :param QueueObjSetting queue_ref: acquisition info
26
+ :param int from_index:
27
+ :param int to_index: when <0 we will take the last image
28
+ that is "ready" which may change after `update`
29
+ :param bool from_stream:
30
+ """
31
+ self._queue = queue
32
+ self._queue_ref = queue_ref
33
+ self.from_index = from_index
34
+ self._to_index = to_index
35
+ self.from_stream = from_stream
36
+ self._status_event = None
37
+
38
+ def __getattr__(self, attr):
39
+ # Get attribute from the status event
40
+ try:
41
+ return getattr(self.status_event, attr)
42
+ except AttributeError:
43
+ raise AttributeError(attr)
44
+
45
+ @property
46
+ def to_index(self):
47
+ """Either a fixed number or the last image that is "ready" """
48
+ self.update()
49
+ last_index = self.last_image_ready
50
+
51
+ if self._to_index >= 0:
52
+ return min(self._to_index, last_index)
53
+ else:
54
+ return last_index
55
+
56
+ @property
57
+ def last_index(self):
58
+ """WARNING: this is the last image index + 1"""
59
+ return self.to_index + 1
60
+
61
+ @property
62
+ def connection(self):
63
+ return self._queue._cnx()
64
+
65
+ def update(self):
66
+ """Get the latest status event from the data stream
67
+ and add the first element of the reference settings.
68
+ It is safe to call this as much as you want.
69
+ """
70
+ events = self._queue.rev_range(count=1)
71
+ if events:
72
+ index, raw = events[-1] # [0]
73
+ ev = LimaImageStatusEvent(raw=raw)
74
+ else: # Lima acqusition has not yet started.
75
+ ev = LimaImageStatusEvent({})
76
+
77
+ try:
78
+ ev.info = self.first_ref_data
79
+ except IndexError:
80
+ pass
81
+ ev.connection = self.connection
82
+ self._status_event = ev
83
+
84
+ @property
85
+ def status_event(self):
86
+ if self._status_event is None:
87
+ self.update()
88
+ return self._status_event
89
+
90
+ @property
91
+ def ref_status(self):
92
+ warnings.warn(
93
+ "ref_status is deprecated. Use 'status_event.status' instead.",
94
+ FutureWarning,
95
+ )
96
+ return self.status_event.status
97
+
98
+ @property
99
+ def all_ref_data(self):
100
+ """
101
+ :returns list(dict):
102
+ """
103
+ return self._queue_ref[0:]
104
+
105
+ @property
106
+ def first_ref_data(self):
107
+ """
108
+ :returns dict:
109
+ """
110
+ return self._queue_ref[0]
111
+
112
+ def is_video_frame_have_meaning(self):
113
+ """Returns True if the frame number reached from the header from
114
+ the Lima video have a meaning in the full scan.
115
+
116
+ Returns a boolean, else None if this information is not yet known.
117
+ """
118
+ self.update()
119
+ return self.status_event.is_video_frame_have_meaning()
120
+
121
+ def get_last_live_image(self):
122
+ """Returns the last image data from stream within it's frame number.
123
+
124
+ If no data is available, the function returns tuple (None, None).
125
+
126
+ If camera device is not configured with INTERNAL_TRIGGER_MULTI, and
127
+ then the reached frame number have no meaning, a None is returned.
128
+
129
+ :returns Frame:
130
+ """
131
+ if not self.from_stream:
132
+ # FIXME: It should return None
133
+ return lima_image.Frame(None, None, None)
134
+ self.update()
135
+ return self.status_event.get_last_live_image()
136
+
137
+ def get_last_image(self):
138
+ """Returns the last image from the received one, together with the frame id.
139
+
140
+ :returns Frame:
141
+ """
142
+ self.update()
143
+ return self.status_event.get_last_image()
144
+
145
+ def get_image(self, image_index_in_scan):
146
+ """
147
+ :param int image_index_in_scan:
148
+ :returns numpy.ndarray:
149
+ """
150
+ if image_index_in_scan < 0:
151
+ raise ValueError("image_index_in_scan cannot be a negative number")
152
+ self.update()
153
+ return self.status_event.get_image(image_index_in_scan)
154
+
155
+ def __getitem__(self, idx):
156
+ """Get images from server or file"""
157
+ if isinstance(idx, slice):
158
+ start, stop, step = idx.indices(len(self))
159
+ start += self.from_index
160
+ stop += self.from_index
161
+ return numpy.asarray(list(self._image_range(start, stop, step)))
162
+ elif isinstance(idx, list):
163
+ idx = numpy.asarray(idx)
164
+ if isinstance(idx[0].item(), bool):
165
+ idx = numpy.nonzero(idx)
166
+ idx += self.from_index
167
+ return numpy.asarray(list(self._image_iter(idx)))
168
+ elif isinstance(idx, tuple):
169
+ # This would slice the image dimensions
170
+ raise NotImplementedError
171
+ else:
172
+ try:
173
+ idx = int(idx)
174
+ except Exception as e:
175
+ raise IndexError from e
176
+ if self.from_stream and idx == -1:
177
+ img = self.get_image(-1)
178
+ else:
179
+ if idx < 0:
180
+ index = self.to_index + 1 + idx
181
+ if index < 0:
182
+ raise IndexError("No image available")
183
+ else:
184
+ index = self.from_index + idx
185
+ img = self.get_image(index)
186
+ if img is None:
187
+ raise IndexError
188
+ return img
189
+
190
+ def __iter__(self):
191
+ """Iterator over images from server or file"""
192
+ yield from self._image_range(self.from_index, self.to_index + 1)
193
+
194
+ def _image_range(self, start, stop, step=1):
195
+ """Iterator over images from server or file"""
196
+ yield from self._image_iter(range(start, stop, step))
197
+
198
+ def _image_iter(self, image_nb_iterator):
199
+ """Iterator over images from server or file"""
200
+ for image_index_in_scan in image_nb_iterator:
201
+ try:
202
+ img = self.get_image(image_index_in_scan)
203
+ except IndexError:
204
+ img = None
205
+ if img is None:
206
+ break
207
+ yield img
208
+
209
+ def as_array(self):
210
+ if len(self) == 1:
211
+ # To be consistant with ChannelDataNode
212
+ return list(self)[0]
213
+ else:
214
+ return numpy.asarray(list(self))
215
+
216
+ def __len__(self):
217
+ length = self.to_index - self.from_index + 1
218
+ return 0 if length < 0 else length
219
+
220
+ def all_image_references(self, saved=False):
221
+ """Get the image references.
222
+
223
+ :param bool saved: ready or ready and saved
224
+ :returns list(tuple): file name, path-in-file, image index, file format
225
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
226
+ :raise RuntimeError: images will never be saved
227
+ """
228
+ self.update()
229
+ return self.status_event.all_image_references(saved=saved)
230
+
231
+ def image_references(self, image_indices_in_scan, saved=False):
232
+ """Get the image references.
233
+
234
+ :param sequence image_indices_in_scan:
235
+ :param bool saved: ready or ready and saved
236
+ :returns list(tuple): file name, path-in-file, image index, file format
237
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
238
+ :raise RuntimeError: some images are not ready or saved yet
239
+ or images will never be saved
240
+ """
241
+ self.update()
242
+ return self.status_event.image_references(image_indices_in_scan, saved=saved)
243
+
244
+ def image_reference(self, image_index_in_scan, saved=False):
245
+ """Get the image references.
246
+
247
+ :param int image_index_in_scan:
248
+ :param bool saved: ready or ready and saved
249
+ :returns tuple: file name, path-in-file, image index, file format
250
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
251
+ :raise RuntimeError: image is not ready or saved yet
252
+ or images will never be saved
253
+ """
254
+ self.update()
255
+ return self.status_event.image_reference(image_index_in_scan, saved=saved)
256
+
257
+ def iter_image_references(self, image_indices_in_scan=None, saved=False):
258
+ """Get the image references.
259
+
260
+ Stops iterating when it encounters an image that is not
261
+ ready or saved yet, regardless of how many images you
262
+ asked for.
263
+
264
+ :param sequence image_indices_in_scan:
265
+ :param bool saved: ready or ready and saved
266
+ :yields list(tuple): file name, path-in-file, image index, file format
267
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
268
+ :raises RuntimeError: images will never be saved
269
+ """
270
+ self.update()
271
+ yield from self.status_event.iter_image_references(
272
+ image_indices_in_scan=image_indices_in_scan, saved=saved
273
+ )
274
+
275
+ def get_filenames(self):
276
+ warnings.warn(
277
+ "'get_filenames' is deprecated. Use 'all_image_references' instead.",
278
+ FutureWarning,
279
+ )
280
+ return self.all_image_references()
281
+
282
+ def _get_filenames(self, ref_data, *image_indices_in_scan):
283
+ warnings.warn(
284
+ "'_get_filenames' is deprecated. Use 'image_references' itself.",
285
+ FutureWarning,
286
+ )
287
+ return self.image_references(image_indices_in_scan)
288
+
289
+
290
+ class LimaImageChannelDataNode(_ChannelDataNodeBase):
291
+ _NODE_TYPE = "lima"
292
+
293
+ def __init__(self, *args, **kwargs):
294
+ super().__init__(*args, **kwargs)
295
+ # There is good reason for this name not to end with "*_data"
296
+ # (see DataNodeContainer._subscribe_streams)
297
+ self._queue_ref = QueueObjSetting(
298
+ f"{self.db_name}_data_ref", connection=self.db_connection
299
+ )
300
+ self.from_stream = False
301
+ self._local_ref_status = dict()
302
+ self._stream_image_count = 0
303
+
304
+ def store(self, event_dict, cnx=None):
305
+ """Publish lima reference in Redis"""
306
+ data = event_dict["data"]
307
+ if data.get("in_prepare", False): # in prepare phase
308
+ ref_data = event_dict["description"]
309
+ self.info.update(ref_data)
310
+ self._queue_ref.append(ref_data)
311
+ self._local_ref_status = data
312
+ self._local_ref_status["lima_acq_nb"] = self.db_connection.incr(
313
+ data["server_url"]
314
+ )
315
+ else: # during acquisition
316
+ self._local_ref_status.update(data)
317
+ ev = LimaImageStatusEvent(self._local_ref_status)
318
+ self._queue.add_event(ev, id=self._last_index, cnx=cnx)
319
+ self._last_index += 1
320
+
321
+ def get(self, from_index, to_index=None):
322
+ """
323
+ Return a view on data references.
324
+
325
+ **from_index** from which image index you want to get
326
+ **to_index** to which index you want images
327
+ if to_index is None => only one image which as index from_index
328
+ if to_index < 0 => to the end of acquisition
329
+ """
330
+ return LimaDataView(
331
+ self._queue,
332
+ self._queue_ref,
333
+ from_index,
334
+ to_index if to_index is not None else from_index,
335
+ from_stream=self.from_stream,
336
+ )
337
+
338
+ def get_as_array(self, from_index, to_index=None):
339
+ """Like `get` but ensures the result is a numpy array."""
340
+ return numpy.asarray(self.get(from_index, to_index).as_array(), self.dtype)
341
+
342
+ def decode_raw_events(self, events):
343
+ """Decode raw stream data and get image URI's.
344
+
345
+ :param list((index, raw)) events:
346
+ :returns EventData:
347
+ """
348
+ data = list()
349
+ first_index = -1
350
+ description = None
351
+ if events:
352
+ # The number of events is NOT equal to the number of images
353
+ # The number of images can be derived from the event data though
354
+ # TODO: first_index is only accurate if we use the same DataNode instance!!!
355
+
356
+ if len(self._queue_ref) == 0:
357
+ # Workaround for the issue https://gitlab.esrf.fr/bliss/bliss/-/issues/2128
358
+ _logger.debug(
359
+ "Inconsistency on the Redis data structure. 'image_data_ref' was removed (or is empty)"
360
+ )
361
+ return EventData(
362
+ first_index=first_index, data=data, description=description
363
+ )
364
+
365
+ ev = LimaImageStatusEvent.merge(events)
366
+ ev.info = self.first_ref_data
367
+ first_index = self._stream_image_count
368
+ try:
369
+ data = ev.image_reference_range(first_index)
370
+ except ImageNotSaved:
371
+ # If images are not supposed to be saved we can't expect to
372
+ # find references
373
+ pass
374
+ self._stream_image_count += len(data)
375
+ description = ev.status
376
+ return EventData(first_index=first_index, data=data, description=description)
377
+
378
+ @property
379
+ def all_ref_data(self):
380
+ """
381
+ :returns list(dict):
382
+ """
383
+ return self._queue_ref[0:]
384
+
385
+ @property
386
+ def first_ref_data(self):
387
+ """
388
+ :returns dict:
389
+ """
390
+ return self._queue_ref[0]
391
+
392
+ @property
393
+ def images_per_file(self):
394
+ try:
395
+ return self.first_ref_data.get("saving_frame_per_file")
396
+ except IndexError:
397
+ return None
398
+
399
+ @property
400
+ def images_per_acquisition(self):
401
+ try:
402
+ return self.first_ref_data.get("saving_frame_per_acquisition")
403
+ except IndexError:
404
+ return None
405
+
406
+ def get_db_names(self, **kw):
407
+ db_names = super().get_db_names(**kw)
408
+ db_names.append(self.db_name + "_data_ref")
409
+ events = self._queue.range(count=1)
410
+ if events:
411
+ index, raw = events[0]
412
+ ev = LimaImageStatusEvent(raw=raw)
413
+ url = ev.server_url
414
+ if url:
415
+ db_names.append(url)
416
+ return db_names
417
+
418
+ def get_settings(self):
419
+ return super().get_settings() + [self._queue_ref]
420
+
421
+ def __len__(self):
422
+ return len(self.get(0, -1))
@@ -0,0 +1,34 @@
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
+ from blissdata.data.nodes.channel import ChannelDataNode
9
+ from blissdata.data.events import EventData
10
+
11
+
12
+ class NodeRefChannel(ChannelDataNode):
13
+ """
14
+ A data node that stores references to other DataNodes. It is intended to be
15
+ used to keep references e.g. of individual scans to group them together as
16
+ one group or sequence of scans.
17
+ """
18
+
19
+ _NODE_TYPE = "node_ref_channel"
20
+
21
+ def decode_raw_events(self, events):
22
+ """Decode raw stream data
23
+
24
+ :param list((index, raw)) events:
25
+ :returns EventData:
26
+ """
27
+ event_data = super().decode_raw_events(events)
28
+ nodes = self.get_nodes(*event_data.data)
29
+ return EventData(
30
+ first_index=event_data.first_index,
31
+ data=nodes,
32
+ description=event_data.description,
33
+ block_size=event_data.block_size,
34
+ )
@@ -0,0 +1,12 @@
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
+ from blissdata.data.nodes.dataset import _DataPolicyNode
9
+
10
+
11
+ class ProposalNode(_DataPolicyNode):
12
+ _NODE_TYPE = "proposal"
@@ -0,0 +1,192 @@
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
+ from blissdata.data.node import DataNodeContainer
9
+ from blissdata.data.nodes.channel import ChannelDataNode, RedisDataExpiredError
10
+ from blissdata.data.nodes.lima import LimaImageChannelDataNode
11
+ from blissdata.streaming_events import StreamEvent
12
+ from blissdata.data.events import (
13
+ Event,
14
+ EventType,
15
+ EventData,
16
+ EndScanEvent,
17
+ PreparedScanEvent,
18
+ )
19
+ from blissdata import settings
20
+ from blissdata.common.utils import update_node_info
21
+
22
+
23
+ class ScanNode(DataNodeContainer):
24
+ _NODE_TYPE = "scan"
25
+
26
+ _EVENT_TYPE_MAPPING = {
27
+ EndScanEvent.TYPE.decode("ascii"): EventType.END_SCAN,
28
+ PreparedScanEvent.TYPE.decode("ascii"): EventType.PREPARED_SCAN,
29
+ }
30
+ """Mapping from event name to EventType
31
+ """
32
+
33
+ def __init__(self, name, **kwargs):
34
+ super().__init__(self._NODE_TYPE, name, **kwargs)
35
+ self._end_stream = self._create_stream("end")
36
+ self._prepared_stream = self._create_stream("prepared")
37
+ # Register to priority as the following way: NEW DATA > PREPARED > NEW NODE > END
38
+ self._register_stream_priority(self._end_stream.name, 3)
39
+ self._register_stream_priority(self._prepared_stream.name, 1)
40
+
41
+ @property
42
+ def dataset(self):
43
+ return self.parent
44
+
45
+ def prepared(self, scan_info):
46
+ """Publish PREPARED event in Redis"""
47
+ if not self.new_node:
48
+ return
49
+ # to avoid to have multiple modification events
50
+ # TODO: what does the comment above mean?
51
+ with settings.pipeline(self._prepared_stream, self._info):
52
+ event = PreparedScanEvent()
53
+ update_node_info(self, scan_info)
54
+ self._prepared_stream.add_event(event)
55
+
56
+ def end(self, scan_info, exception=None):
57
+ """Update the info in Redis and publish the END event."""
58
+ if not self.new_node:
59
+ return
60
+ # to avoid to have multiple modification events
61
+ # TODO: what does the comment above mean?
62
+ with settings.pipeline(self._end_stream, self._info):
63
+ event = EndScanEvent(exception=exception)
64
+ scan_info["end_time"] = event.time
65
+ scan_info["end_time_str"] = event.strftime
66
+ scan_info["end_timestamp"] = event.timestamp
67
+ update_node_info(self, scan_info)
68
+ self._end_stream.add_event(event)
69
+
70
+ def decode_raw_events(self, events):
71
+ """Decode raw stream data
72
+
73
+ :param list((index, raw)) events:
74
+ :returns EventData:
75
+ """
76
+ if not events:
77
+ return None
78
+
79
+ assert len(events) == 1 # Else you are about to lose events
80
+ event = events[0]
81
+ timestamp, raw_data = event
82
+ first_index = self._streamid_to_idx(timestamp)
83
+ ev = StreamEvent.factory(raw_data)
84
+ data = type(ev).TYPE.decode()
85
+ return EventData(first_index=first_index, data=data, description=ev.description)
86
+
87
+ def get_db_names(self, **kw):
88
+ db_names = super().get_db_names(**kw)
89
+ db_names.append(self._end_stream.name)
90
+ db_names.append(self._prepared_stream.name)
91
+ return db_names
92
+
93
+ def get_settings(self):
94
+ return super().get_settings() + [self._end_stream, self._prepared_stream]
95
+
96
+ def _subscribe_streams(self, reader, first_index=None, **kw):
97
+ """Subscribe to all associated streams of this node.
98
+
99
+ :param DataStreamReader reader:
100
+ :param **kw: see DataNodeContainer
101
+ """
102
+ super()._subscribe_streams(reader, first_index=first_index, **kw)
103
+ suffix = self._end_stream.name.rsplit("_", 1)[-1]
104
+ self._subscribe_stream(
105
+ suffix, reader, first_index=0, create=True, ignore_excluded=True
106
+ )
107
+ suffix = self._prepared_stream.name.rsplit("_", 1)[-1]
108
+ self._subscribe_stream(
109
+ suffix, reader, first_index=0, create=True, ignore_excluded=True
110
+ )
111
+
112
+ def get_stream_event_handler(self, stream):
113
+ """
114
+ :param DataStream stream:
115
+ :returns callable:
116
+ """
117
+ if stream.name == self._end_stream.name:
118
+ return self._iter_data_stream_events
119
+ elif stream.name == self._prepared_stream.name:
120
+ return self._iter_data_stream_events
121
+ return super(ScanNode, self).get_stream_event_handler(stream)
122
+
123
+ def _iter_data_stream_events(
124
+ self,
125
+ reader,
126
+ events,
127
+ include_filter=None,
128
+ exclude_children=None,
129
+ first_index=None,
130
+ yield_events=False,
131
+ ):
132
+ """
133
+ :param DataStreamReader reader:
134
+ :param list(2-tuple) events:
135
+ :param include_filter: only these nodes are included (all by default)
136
+ :param exclude_children: ignore children of these nodes recursively
137
+ :param str or int first_index: Redis stream index (None is now)
138
+ :param bool yield_events: yield Event or DataNode
139
+ :yields Event:
140
+ """
141
+ for event in events:
142
+ data = self.decode_raw_events([event])
143
+ if data is None:
144
+ return
145
+ if yield_events and self._included(include_filter):
146
+ kind = data.data
147
+ event_id = self._EVENT_TYPE_MAPPING[kind]
148
+ event = Event(type=event_id, node=self, data=data)
149
+ yield event
150
+ if event_id is EventType.END_SCAN:
151
+ # Stop reading events from this node's streams
152
+ # and the streams of its children
153
+ reader.remove_matching_streams(f"{self.db_name}*")
154
+
155
+
156
+ def get_data_from_nodes(pipeline, *nodes):
157
+ scan_channel_get_data_func = dict() # { channel_name: function }
158
+ scan_image_get_view = dict()
159
+ for node in nodes:
160
+ if isinstance(node, LimaImageChannelDataNode):
161
+ scan_image_get_view[node.fullname] = node.get(0, -1)
162
+ elif isinstance(node, ChannelDataNode):
163
+ chan = node
164
+ channel_name = chan.fullname
165
+
166
+ try:
167
+ saved_db_connection = chan.db_connection
168
+ chan.db_connection = pipeline
169
+ # append channel name and get all data from channel;
170
+ # as it is in a Redis pipeline, .get() returns the
171
+ # conversion function only - data will be received
172
+ # after .execute()
173
+ scan_channel_get_data_func[channel_name] = chan.get(0, -1)
174
+ finally:
175
+ chan.db_connection = saved_db_connection
176
+
177
+ result = pipeline.execute()
178
+
179
+ for i, (channel_name, get_data_func) in enumerate(
180
+ scan_channel_get_data_func.items()
181
+ ):
182
+ if not result[i]:
183
+ raise RedisDataExpiredError("Redis data channel expired.")
184
+
185
+ if result[i][0][0] != b"1-0": # first event is 1-0, not 0-0
186
+ raise RedisDataExpiredError(
187
+ f"Redis data channel {channel_name} have been trimmed out."
188
+ )
189
+
190
+ yield channel_name, get_data_func(result[i])
191
+ for channel_name, view in scan_image_get_view.items():
192
+ yield channel_name, view
@@ -0,0 +1,12 @@
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
+ from blissdata.data.nodes.scan import ScanNode
9
+
10
+
11
+ class GroupScanNode(ScanNode):
12
+ _NODE_TYPE = "scan_group"