ewoksdata 2.1.0__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 (34) hide show
  1. ewoksdata/__init__.py +0 -0
  2. ewoksdata/data/__init__.py +0 -0
  3. ewoksdata/data/bliss.py +341 -0
  4. ewoksdata/data/blissdata/__init__.py +31 -0
  5. ewoksdata/data/blissdata/blissdatav0.py +119 -0
  6. ewoksdata/data/blissdata/blissdatav1.py +279 -0
  7. ewoksdata/data/blissdata/blissdatav2.py +342 -0
  8. ewoksdata/data/contextiterator.py +43 -0
  9. ewoksdata/data/hdf5/__init__.py +23 -0
  10. ewoksdata/data/hdf5/config.py +186 -0
  11. ewoksdata/data/hdf5/dataset_writer/__init__.py +4 -0
  12. ewoksdata/data/hdf5/dataset_writer/_base.py +55 -0
  13. ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +139 -0
  14. ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +158 -0
  15. ewoksdata/data/hdf5/dataset_writer/_sequential.py +143 -0
  16. ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +195 -0
  17. ewoksdata/data/hdf5/types.py +14 -0
  18. ewoksdata/data/nexus.py +187 -0
  19. ewoksdata/data/url.py +30 -0
  20. ewoksdata/data/utils.py +53 -0
  21. ewoksdata/tests/__init__.py +0 -0
  22. ewoksdata/tests/conftest.py +30 -0
  23. ewoksdata/tests/data/__init__.py +1 -0
  24. ewoksdata/tests/data/bliss_scans.py +137 -0
  25. ewoksdata/tests/test_contextiterator.py +61 -0
  26. ewoksdata/tests/test_data_bliss.py +98 -0
  27. ewoksdata/tests/test_data_hdf5.py +14 -0
  28. ewoksdata/tests/test_data_nexus.py +80 -0
  29. ewoksdata/tests/test_dataset_writer.py +275 -0
  30. ewoksdata-2.1.0.dist-info/METADATA +70 -0
  31. ewoksdata-2.1.0.dist-info/RECORD +34 -0
  32. ewoksdata-2.1.0.dist-info/WHEEL +5 -0
  33. ewoksdata-2.1.0.dist-info/licenses/LICENSE.md +20 -0
  34. ewoksdata-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,279 @@
1
+ import logging
2
+ import sys
3
+ import time
4
+ from collections import Counter
5
+ from numbers import Number
6
+ from typing import Any
7
+ from typing import Dict
8
+ from typing import Generator
9
+ from typing import List
10
+ from typing import Optional
11
+ from typing import Tuple
12
+
13
+ import numpy
14
+ from blissdata.beacon.data import BeaconData
15
+ from blissdata.lima.client import lima_client_factory
16
+ from blissdata.redis_engine.exceptions import EndOfStream
17
+ from blissdata.redis_engine.exceptions import IndexNoMoreThereError
18
+ from blissdata.redis_engine.exceptions import IndexNotYetThereError
19
+ from blissdata.redis_engine.exceptions import IndexWontBeThereError
20
+ from blissdata.redis_engine.scan import Scan
21
+ from blissdata.redis_engine.scan import ScanState
22
+ from blissdata.redis_engine.store import DataStore
23
+ from blissdata.redis_engine.stream import StreamingClient
24
+ from numpy.typing import ArrayLike
25
+
26
+ from ..utils import validate_and_init_names
27
+
28
+ try:
29
+ from blissdata.stream import LimaStream
30
+ except ImportError:
31
+ from blissdata.streams.lima_stream import LimaStream
32
+
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ INFINITY = sys.maxsize
37
+
38
+
39
+ def _get_data_store() -> None:
40
+ redis_url = BeaconData().get_redis_data_db()
41
+ return DataStore(redis_url)
42
+
43
+
44
+ def iter_bliss_scan_data_from_memory(
45
+ scan_key: str,
46
+ lima_names: Optional[List[str]] = None,
47
+ counter_names: Optional[List[str]] = None,
48
+ retry_timeout: Optional[Number] = None,
49
+ retry_period: Optional[Number] = None,
50
+ buffer_in_memory: bool = True,
51
+ ) -> Generator[Dict[str, Any], None, None]:
52
+ if not buffer_in_memory:
53
+ raise NotImplementedError("Requires blissdata>=2.0.0rc1")
54
+ lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
55
+
56
+ data_store = _get_data_store()
57
+ scan = data_store.load_scan(scan_key, scan_cls=Scan)
58
+ buffers = {name: list() for name in lima_names + counter_names}
59
+ if not buffers:
60
+ return
61
+
62
+ while scan.state < ScanState.PREPARED:
63
+ scan.update()
64
+
65
+ lima_streams = dict()
66
+ lima_clients = dict()
67
+ counter_streams = dict()
68
+ for name, stream in scan.streams.items():
69
+ if stream.encoding["type"] == "json" and "lima" in stream.info["format"]:
70
+ if name.split(":")[-2] in lima_names:
71
+ lima_streams[name] = stream
72
+ lima_clients[name] = lima_client_factory(data_store, stream.info)
73
+ elif name.split(":")[-1] in counter_names:
74
+ counter_streams[name] = stream
75
+
76
+ client = StreamingClient({**lima_streams, **counter_streams})
77
+ client_timeout = retry_period or 0
78
+ lima_buffer_count = Counter()
79
+
80
+ while True:
81
+ try:
82
+ output = client.read(timeout=client_timeout)
83
+ except EndOfStream:
84
+ break
85
+ for stream, (_, payload) in output.items():
86
+ name_parts = stream.name.split(":")
87
+ if stream.name in lima_streams:
88
+ # payload is a sequence of JSON statuses
89
+ ctr_name = name_parts[-2]
90
+ last_status = payload[-1]
91
+ lima_client = lima_clients[stream.name]
92
+ lima_client.update(**last_status)
93
+ n_already_read = lima_buffer_count[ctr_name]
94
+ try:
95
+ data = lima_client[n_already_read:]
96
+ except IndexNoMoreThereError:
97
+ continue
98
+ buffers[ctr_name].extend(data)
99
+ lima_buffer_count[ctr_name] += len(data)
100
+ else:
101
+ # payload is a sequence of data points (0D, 1D, 2D)
102
+ ctr_name = name_parts[-1]
103
+ buffers[ctr_name].extend(payload)
104
+
105
+ nyield = min(len(v) for v in buffers.values())
106
+ if nyield:
107
+ for i in range(nyield):
108
+ yield {name: values[i] for name, values in buffers.items()}
109
+ buffers = {name: values[nyield:] for name, values in buffers.items()}
110
+
111
+
112
+ def last_lima_image(channel_info: dict) -> ArrayLike:
113
+ """Get last lima image from memory"""
114
+ data_store = _get_data_store()
115
+ lima_client = lima_client_factory(data_store, channel_info)
116
+ return lima_client.get_last_live_image().array
117
+
118
+
119
+ def _get_streams(
120
+ scan_key: str,
121
+ lima_names: List[str],
122
+ counter_names: List[str],
123
+ ):
124
+ data_store = _get_data_store()
125
+ scan = data_store.load_scan(scan_key, scan_cls=Scan)
126
+
127
+ while scan.state < ScanState.PREPARED:
128
+ scan.update()
129
+
130
+ lima_streams = dict()
131
+ counter_streams = dict()
132
+
133
+ for name, stream in scan.streams.items():
134
+ if stream.encoding["type"] == "json" and "lima" in stream.info["format"]:
135
+ if name.split(":")[-2] in lima_names:
136
+ lima_streams[name.split(":")[-2]] = LimaStream(stream)
137
+
138
+ elif name.split(":")[-1] in counter_names:
139
+ counter_streams[name.split(":")[-1]] = stream
140
+
141
+ return lima_streams, counter_streams
142
+
143
+
144
+ def iter_bliss_scan_data_from_memory_slice(
145
+ scan_key: str,
146
+ lima_names: List[str],
147
+ counter_names: List[str],
148
+ slice_range: Optional[Tuple[int, int]] = None,
149
+ retry_timeout: Optional[Number] = None,
150
+ retry_period: Optional[Number] = None,
151
+ yield_timeout: Optional[Number] = None,
152
+ max_slicing_size: Optional[Number] = None,
153
+ verbose: Optional[bool] = False,
154
+ ):
155
+ """Iterates over the data from a Bliss scan, slicing the streams associated to a lima detector or a counter between specific indexes of the scan (optional)
156
+
157
+ :param str scan_key: key of the Bliss scan (e.g. "esrf:scan:XXXX")
158
+ :param list lima_names: names of lima detectors
159
+ :param list counter_names: names of non-lima detectors (you need to provide at least one)
160
+ :param tuple slice_range: two elements which define the limits of the iteration along the scan. If None, it iterates along the whole scan
161
+ :param Number retry_timeout: timeout when it cannot access the data for `retry_timeout` seconds
162
+ :param Number retry_period: interval in seconds between data access retries
163
+ :param Number yield_timeout: timeout to stop slicing the stream and yield the buffered data
164
+ :param Number max_slicing_size: maximum size of frames to be sliced out of the stream in one single iteration. If None, it will slice all the available data in the stream
165
+ :yields dict: data
166
+ """
167
+ lima_streams, counter_streams = _get_streams(scan_key, lima_names, counter_names)
168
+
169
+ all_streams = {**lima_streams, **counter_streams}
170
+ if not all_streams:
171
+ logger.warning("There is no stream to slice")
172
+ return
173
+
174
+ if slice_range is None:
175
+ slice_range = (0, INFINITY)
176
+
177
+ if retry_period is None:
178
+ retry_period = 1
179
+
180
+ if yield_timeout is None:
181
+ yield_timeout = 0.01
182
+
183
+ buffers_count = Counter({counter: slice_range[0] for counter in all_streams.keys()})
184
+
185
+ # Read and yield continuously
186
+ stream_on = True
187
+
188
+ incoming_buffers = {stream_name: [] for stream_name in all_streams.keys()}
189
+ non_yielded_buffers = {stream_name: [] for stream_name in all_streams.keys()}
190
+
191
+ restart_buffer = time.perf_counter()
192
+ while stream_on:
193
+
194
+ # While loop will stop unless one single stream is successfully sliced
195
+ stream_on = False
196
+
197
+ for stream_name, stream in all_streams.items():
198
+ try:
199
+ # Stop condition for limited slices
200
+ if (
201
+ slice_range[1] is not INFINITY
202
+ and buffers_count[stream_name] >= slice_range[1]
203
+ ):
204
+ continue
205
+
206
+ # Test first index, (slicing between limits do not fall into Error)
207
+ _ = stream[buffers_count[stream_name]]
208
+ if max_slicing_size is None:
209
+ stream_data = stream[buffers_count[stream_name] : slice_range[1]]
210
+ else:
211
+ stream_data = stream[
212
+ buffers_count[stream_name] : min(
213
+ slice_range[1],
214
+ buffers_count[stream_name] + max_slicing_size,
215
+ )
216
+ ]
217
+ incoming_buffers[stream_name] = stream_data
218
+ buffers_count[stream_name] += len(stream_data)
219
+ stream_on = True
220
+
221
+ except IndexNotYetThereError:
222
+ stream_on = True
223
+ except IndexWontBeThereError:
224
+ pass
225
+ except IndexNoMoreThereError:
226
+ pass
227
+ except EndOfStream:
228
+ pass
229
+ except RuntimeError:
230
+ pass
231
+
232
+ for stream_name in incoming_buffers.keys():
233
+ if len(incoming_buffers[stream_name]) > 0:
234
+ if len(non_yielded_buffers[stream_name]) == 0:
235
+ non_yielded_buffers[stream_name] = numpy.array(
236
+ incoming_buffers[stream_name]
237
+ )
238
+ else:
239
+ non_yielded_buffers[stream_name] = numpy.concatenate(
240
+ (
241
+ non_yielded_buffers[stream_name],
242
+ incoming_buffers[stream_name],
243
+ )
244
+ )
245
+ incoming_buffers[stream_name] = []
246
+
247
+ if not stream_on or ((time.perf_counter() - restart_buffer) > yield_timeout):
248
+
249
+ frames_to_yield = min(
250
+ [len(value) for value in non_yielded_buffers.values()]
251
+ )
252
+
253
+ if frames_to_yield > 0:
254
+ if verbose:
255
+ for stream_name, stream_buffer in non_yielded_buffers.items():
256
+ print(
257
+ f"After slicing the stream: {stream_name} buffer contains {len(stream_buffer)} items"
258
+ )
259
+
260
+ # Yield point by point
261
+ for index in range(frames_to_yield):
262
+ yield {
263
+ stream_name: stream_buffer[index]
264
+ for stream_name, stream_buffer in non_yielded_buffers.items()
265
+ }
266
+
267
+ # Save the non-yielded points for the next iteration
268
+ for stream_name in non_yielded_buffers.keys():
269
+ non_yielded_buffers[stream_name] = non_yielded_buffers[stream_name][
270
+ frames_to_yield:
271
+ ]
272
+
273
+ if verbose:
274
+ for stream_name, stream_buffer in non_yielded_buffers.items():
275
+ print(
276
+ f"After yielding: {stream_name} buffer contains {len(stream_buffer)} non-yielded items"
277
+ )
278
+
279
+ restart_buffer = time.perf_counter()
@@ -0,0 +1,342 @@
1
+ import logging
2
+ import sys
3
+ import time
4
+ from collections import Counter
5
+ from collections.abc import Iterator
6
+ from importlib.metadata import version
7
+ from typing import Any
8
+ from typing import Dict
9
+ from typing import Generator
10
+ from typing import List
11
+ from typing import Optional
12
+ from typing import Tuple
13
+
14
+ import numpy
15
+ from blissdata.beacon.data import BeaconData
16
+ from blissdata.redis_engine.scan import ScanState
17
+ from blissdata.redis_engine.store import DataStore
18
+ from numpy.typing import ArrayLike
19
+ from packaging.version import Version
20
+
21
+ from ..utils import validate_and_init_names
22
+
23
+ _BLISSDATA_VERSION = Version(version("blissdata"))
24
+ if _BLISSDATA_VERSION >= Version("2.3.0"):
25
+ from blissdata.exceptions import EndOfStream
26
+ from blissdata.exceptions import IndexNoMoreThereError
27
+ from blissdata.exceptions import IndexNotYetThereError
28
+ from blissdata.exceptions import IndexWontBeThereError
29
+ from blissdata.streams import CursorGroup
30
+ else:
31
+ from blissdata.redis_engine.exceptions import EndOfStream
32
+ from blissdata.redis_engine.exceptions import IndexNoMoreThereError
33
+ from blissdata.redis_engine.exceptions import IndexNotYetThereError
34
+ from blissdata.redis_engine.exceptions import IndexWontBeThereError
35
+ from blissdata.streams.base import CursorGroup
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ INFINITY = sys.maxsize
40
+
41
+
42
+ def _get_data_store() -> None:
43
+ redis_url = BeaconData().get_redis_data_db()
44
+ return DataStore(redis_url)
45
+
46
+
47
+ def iter_bliss_scan_data_from_memory(
48
+ scan_key: str,
49
+ lima_names: Optional[List[str]] = None,
50
+ counter_names: Optional[List[str]] = None,
51
+ retry_timeout: Optional[float] = None,
52
+ retry_period: Optional[float] = None,
53
+ buffer_in_memory: bool = False,
54
+ ) -> Generator[Dict[str, Any], None, None]:
55
+ lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
56
+
57
+ streams = _get_streams(scan_key, lima_names, counter_names)
58
+ if not streams:
59
+ return
60
+
61
+ cursor = CursorGroup(list(streams.values()))
62
+ cursor_timeout = retry_period or 0
63
+ ctr_names = lima_names + counter_names
64
+ data_buffers = {ctr_name: _DataBuffer() for ctr_name in ctr_names}
65
+ stream_to_ctr_names = {
66
+ stream.name: ctr_name for ctr_name, stream in streams.items()
67
+ }
68
+
69
+ while True:
70
+ try:
71
+ views = cursor.read(timeout=cursor_timeout)
72
+ except EndOfStream:
73
+ # No more data - stop the generator
74
+ progress = [
75
+ f"{ctr_name}: {data_buffer.progress}"
76
+ for ctr_name, data_buffer in data_buffers.items()
77
+ ]
78
+ logger.info("Data progress on exit:\n %s", "\n".join(progress))
79
+ for ctr_name, data_buffer in data_buffers.items():
80
+ if data_buffer.length_left:
81
+ logger.warning(
82
+ "Data points for counter %r: %s",
83
+ ctr_name,
84
+ data_buffer.progress,
85
+ )
86
+ break
87
+
88
+ for stream, view in views.items():
89
+ ctr_name = stream_to_ctr_names[stream.name]
90
+ data_buffers[ctr_name].append_view(view)
91
+
92
+ if buffer_in_memory:
93
+ for data_buffer in data_buffers.values():
94
+ data_buffer.buffer_in_memory()
95
+
96
+ while True:
97
+ try:
98
+ ctr_values = [
99
+ next(data_buffer) for data_buffer in data_buffers.values()
100
+ ]
101
+ except StopIteration:
102
+ # One of the buffers ran out of data
103
+ break
104
+
105
+ yield dict(zip(ctr_names, ctr_values))
106
+
107
+
108
+ class _DataBuffer(Iterator):
109
+ """Data iterator with view and data buffering for a single counter."""
110
+
111
+ def __init__(self):
112
+ self._views = []
113
+ self._view_lengths = []
114
+ self._first_view_index = 0
115
+ self._buffer = []
116
+ self._total_length = 0
117
+ self._total_yielded = 0
118
+
119
+ @property
120
+ def length_left(self) -> int:
121
+ return self._total_length - self._total_yielded
122
+
123
+ @property
124
+ def progress(self) -> str:
125
+ return f"{self._total_yielded}/{self._total_length}"
126
+
127
+ def buffer_in_memory(self) -> None:
128
+ """Buffer and clear all view objects."""
129
+ if not self._views:
130
+ return
131
+ starts = [0] * len(self._views)
132
+ starts[0] = self._first_view_index
133
+ for view, start in zip(self._views, starts):
134
+ self._buffer.extend(view.get_data(start=start, stop=None))
135
+ self._views.clear()
136
+ self._view_lengths.clear()
137
+ self._first_view_index = 0
138
+
139
+ def append_view(self, view) -> None:
140
+ """Append view object when not empty."""
141
+ view_length = len(view)
142
+ if view_length:
143
+ self._views.append(view)
144
+ self._view_lengths.append(view_length)
145
+ self._total_length += view_length
146
+
147
+ def __iter__(self) -> "_DataBuffer":
148
+ return self
149
+
150
+ def __next__(self):
151
+ """Return next scan data point for this counter."""
152
+ if self._buffer:
153
+ return self._buffer.pop(0)
154
+
155
+ if len(self._views) == 0:
156
+ raise StopIteration
157
+
158
+ view = self._views[0]
159
+ start = self._first_view_index
160
+
161
+ self._total_yielded += 1
162
+ self._first_view_index += 1
163
+
164
+ if self._first_view_index == self._view_lengths[0]:
165
+ self._views.pop(0)
166
+ self._view_lengths.pop(0)
167
+ self._first_view_index = 0
168
+
169
+ data = view.get_data(start=start, stop=start + 1)
170
+ return data[0]
171
+
172
+
173
+ def last_lima_image(scan_key: str, lima_name: str) -> ArrayLike:
174
+ """Get last lima image from memory"""
175
+ streams = _get_streams(scan_key, [lima_name], [])
176
+ stream = list(streams.values())[0]
177
+ return stream.get_last_live_image().array
178
+
179
+
180
+ def _get_streams(
181
+ scan_key: str,
182
+ lima_names: List[str],
183
+ counter_names: List[str],
184
+ ):
185
+ data_store = _get_data_store()
186
+ scan = data_store.load_scan(scan_key)
187
+
188
+ while scan.state < ScanState.PREPARED:
189
+ scan.update()
190
+
191
+ streams = dict()
192
+
193
+ for name, stream in scan.streams.items():
194
+ if (
195
+ stream.event_stream.encoding["type"] == "json"
196
+ and "lima" in stream.info["format"]
197
+ ):
198
+ if name.split(":")[-2] in lima_names:
199
+ streams[name.split(":")[-2]] = stream
200
+
201
+ elif name.split(":")[-1] in counter_names:
202
+ streams[name.split(":")[-1]] = stream
203
+
204
+ nnames = len(lima_names) + len(counter_names)
205
+ nstreams = len(streams)
206
+ if nnames != nstreams:
207
+ logger.warning("asked for %d names but got %s streams", nnames, nstreams)
208
+ return dict()
209
+
210
+ return streams
211
+
212
+
213
+ def iter_bliss_scan_data_from_memory_slice(
214
+ scan_key: str,
215
+ lima_names: List[str],
216
+ counter_names: List[str],
217
+ slice_range: Optional[Tuple[int, int]] = None,
218
+ retry_timeout: Optional[float] = None,
219
+ retry_period: Optional[float] = None,
220
+ yield_timeout: Optional[float] = None,
221
+ max_slicing_size: Optional[float] = None,
222
+ verbose: Optional[bool] = False,
223
+ ):
224
+ """Iterates over the data from a Bliss scan, slicing the streams associated to a lima detector or a counter between specific indexes of the scan (optional)
225
+
226
+ :param str scan_key: key of the Bliss scan (e.g. "esrf:scan:XXXX")
227
+ :param list lima_names: names of lima detectors
228
+ :param list counter_names: names of non-lima detectors (you need to provide at least one)
229
+ :param tuple slice_range: two elements which define the limits of the iteration along the scan. If None, it iterates along the whole scan
230
+ :param float retry_timeout: timeout when it cannot access the data for `retry_timeout` seconds
231
+ :param float retry_period: interval in seconds between data access retries
232
+ :param float yield_timeout: timeout to stop slicing the stream and yield the buffered data
233
+ :param float max_slicing_size: maximum size of frames to be sliced out of the stream in one single iteration. If None, it will slice all the available data in the stream
234
+ :yields dict: data
235
+ """
236
+ streams = _get_streams(scan_key, lima_names, counter_names)
237
+ if not streams:
238
+ return
239
+
240
+ if slice_range is None:
241
+ slice_range = (0, INFINITY)
242
+
243
+ if yield_timeout is None:
244
+ yield_timeout = 0.01
245
+
246
+ buffers_count = Counter({counter: slice_range[0] for counter in streams.keys()})
247
+
248
+ # Read and yield continuously
249
+ stream_on = True
250
+
251
+ incoming_buffers = {stream_name: [] for stream_name in streams.keys()}
252
+ non_yielded_buffers = {stream_name: [] for stream_name in streams.keys()}
253
+
254
+ restart_buffer = time.perf_counter()
255
+ while stream_on:
256
+
257
+ # While loop will stop unless one single stream is successfully sliced
258
+ stream_on = False
259
+
260
+ for stream_name, stream in streams.items():
261
+ try:
262
+ # Stop condition for limited slices
263
+ if (
264
+ slice_range[1] is not INFINITY
265
+ and buffers_count[stream_name] >= slice_range[1]
266
+ ):
267
+ continue
268
+
269
+ # Test first index, (slicing between limits do not fall into Error)
270
+ _ = stream[buffers_count[stream_name]]
271
+ if max_slicing_size is None:
272
+ stream_data = stream[buffers_count[stream_name] : slice_range[1]]
273
+ else:
274
+ stream_data = stream[
275
+ buffers_count[stream_name] : min(
276
+ slice_range[1],
277
+ buffers_count[stream_name] + max_slicing_size,
278
+ )
279
+ ]
280
+ incoming_buffers[stream_name] = stream_data
281
+ buffers_count[stream_name] += len(stream_data)
282
+ stream_on = True
283
+
284
+ except IndexNotYetThereError:
285
+ stream_on = True
286
+ except IndexWontBeThereError:
287
+ pass
288
+ except IndexNoMoreThereError:
289
+ pass
290
+ except EndOfStream:
291
+ pass
292
+ except RuntimeError:
293
+ pass
294
+
295
+ for stream_name in incoming_buffers.keys():
296
+ if len(incoming_buffers[stream_name]) > 0:
297
+ if len(non_yielded_buffers[stream_name]) == 0:
298
+ non_yielded_buffers[stream_name] = numpy.array(
299
+ incoming_buffers[stream_name]
300
+ )
301
+ else:
302
+ non_yielded_buffers[stream_name] = numpy.concatenate(
303
+ (
304
+ non_yielded_buffers[stream_name],
305
+ incoming_buffers[stream_name],
306
+ )
307
+ )
308
+ incoming_buffers[stream_name] = []
309
+
310
+ if not stream_on or ((time.perf_counter() - restart_buffer) > yield_timeout):
311
+
312
+ frames_to_yield = min(
313
+ [len(value) for value in non_yielded_buffers.values()]
314
+ )
315
+
316
+ if frames_to_yield > 0:
317
+ if verbose:
318
+ for stream_name, stream_buffer in non_yielded_buffers.items():
319
+ print(
320
+ f"After slicing the stream: {stream_name} buffer contains {len(stream_buffer)} items"
321
+ )
322
+
323
+ # Yield point by point
324
+ for index in range(frames_to_yield):
325
+ yield {
326
+ stream_name: stream_buffer[index]
327
+ for stream_name, stream_buffer in non_yielded_buffers.items()
328
+ }
329
+
330
+ # Save the non-yielded points for the next iteration
331
+ for stream_name in non_yielded_buffers.keys():
332
+ non_yielded_buffers[stream_name] = non_yielded_buffers[stream_name][
333
+ frames_to_yield:
334
+ ]
335
+
336
+ if verbose:
337
+ for stream_name, stream_buffer in non_yielded_buffers.items():
338
+ print(
339
+ f"After yielding: {stream_name} buffer contains {len(stream_buffer)} non-yielded items"
340
+ )
341
+
342
+ restart_buffer = time.perf_counter()
@@ -0,0 +1,43 @@
1
+ import functools
2
+ from collections.abc import Generator
3
+ from typing import Iterator
4
+
5
+
6
+ class ContextIterator(Generator):
7
+ """Can wrap an iterator or generator to ensure cleanup when used as a context manager."""
8
+
9
+ def __init__(self, it: Iterator):
10
+ self._it = it
11
+
12
+ def __iter__(self):
13
+ return self
14
+
15
+ def __next__(self):
16
+ return next(self._it)
17
+
18
+ def __enter__(self):
19
+ return self
20
+
21
+ def __exit__(self, *_):
22
+ if hasattr(self._it, "close"):
23
+ return self._it.close()
24
+
25
+ def send(self, value):
26
+ if hasattr(self._it, "send"):
27
+ return self._it.send(value)
28
+
29
+ def throw(self, *args):
30
+ if hasattr(self._it, "throw"):
31
+ return self._it.throw(*args)
32
+
33
+
34
+ def contextiterator(iterator):
35
+ """Decorator that allows to use an iterator or generator as a context manager.
36
+ This ensures that the iterator is closed when exiting the context manager.
37
+ """
38
+
39
+ @functools.wraps(iterator)
40
+ def wrapper(*args, **kw):
41
+ return ContextIterator(iterator(*args, **kw))
42
+
43
+ return wrapper
@@ -0,0 +1,23 @@
1
+ import os
2
+ from contextlib import contextmanager
3
+ from typing import Optional
4
+
5
+ from silx.io import h5py_utils
6
+
7
+
8
+ @contextmanager
9
+ def h5context(filename: str, h5path: Optional[str] = None, **openargs):
10
+ with h5py_utils.File(filename, **openargs) as f:
11
+ if h5path:
12
+ yield f[h5path]
13
+ else:
14
+ yield f
15
+
16
+
17
+ def h5exists(filename: str, h5path: Optional[str] = None, **openargs) -> bool:
18
+ if not os.path.isfile(filename):
19
+ return False
20
+ with h5py_utils.open_item(filename, "/", **openargs) as f:
21
+ if h5path and h5path != "/":
22
+ return h5path in f
23
+ return True