ewoksdata 0.5.0__tar.gz → 0.6.0__tar.gz

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 (41) hide show
  1. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/PKG-INFO +4 -1
  2. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/setup.cfg +3 -0
  3. ewoksdata-0.6.0/src/ewoksdata/__init__.py +1 -0
  4. ewoksdata-0.6.0/src/ewoksdata/data/blissdata/__init__.py +30 -0
  5. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/blissdata/blissdatav0.py +2 -8
  6. ewoksdata-0.6.0/src/ewoksdata/data/blissdata/blissdatav1.py +267 -0
  7. ewoksdata-0.6.0/src/ewoksdata/data/blissdata/blissdatav2.py +234 -0
  8. ewoksdata-0.6.0/src/ewoksdata/data/contextiterator.py +43 -0
  9. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/hdf5/dataset_writer.py +22 -14
  10. ewoksdata-0.6.0/src/ewoksdata/tests/test_contextiterator.py +61 -0
  11. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata.egg-info/PKG-INFO +4 -1
  12. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata.egg-info/SOURCES.txt +2 -2
  13. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata.egg-info/requires.txt +3 -0
  14. ewoksdata-0.5.0/src/ewoksdata/__init__.py +0 -1
  15. ewoksdata-0.5.0/src/ewoksdata/data/blissdata/__init__.py +0 -36
  16. ewoksdata-0.5.0/src/ewoksdata/data/blissdata/blissdatav1.py +0 -91
  17. ewoksdata-0.5.0/src/ewoksdata/data/blissdata/blissdatavid31.py +0 -105
  18. ewoksdata-0.5.0/src/ewoksdata/data/blissdata/exceptions.py +0 -4
  19. ewoksdata-0.5.0/src/ewoksdata/data/contextiterator.py +0 -35
  20. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/LICENSE.md +0 -0
  21. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/README.md +0 -0
  22. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/pyproject.toml +0 -0
  23. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/setup.py +0 -0
  24. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/__init__.py +0 -0
  25. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/bliss.py +0 -0
  26. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/hdf5/__init__.py +0 -0
  27. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/hdf5/config.py +0 -0
  28. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/hdf5/types.py +0 -0
  29. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/nexus.py +0 -0
  30. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/url.py +0 -0
  31. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/data/utils.py +0 -0
  32. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/tests/__init__.py +0 -0
  33. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/tests/conftest.py +0 -0
  34. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/tests/data/__init__.py +0 -0
  35. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/tests/data/bliss_scans.py +0 -0
  36. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/tests/test_data_bliss.py +0 -0
  37. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/tests/test_data_hdf5.py +0 -0
  38. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/tests/test_data_nexus.py +0 -0
  39. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata/tests/test_dataset_writer.py +0 -0
  40. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
  41. {ewoksdata-0.5.0 → ewoksdata-0.6.0}/src/ewoksdata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ewoksdata
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Ewoks tasks for data access
5
5
  Home-page: https://gitlab.esrf.fr/workflow/ewoksapps/ewoksdata/
6
6
  Author: ESRF
@@ -18,9 +18,12 @@ License-File: LICENSE.md
18
18
  Requires-Dist: h5py>=2.8
19
19
  Requires-Dist: silx>=1.1.0
20
20
  Requires-Dist: hdf5plugin
21
+ Requires-Dist: blissdata>=0.3.4
21
22
  Requires-Dist: blissdata<1.0.1; python_version < "3.8"
22
23
  Requires-Dist: blissdata; python_version >= "3.8"
23
24
  Requires-Dist: redis
25
+ Requires-Dist: packaging
26
+ Requires-Dist: importlib_metadata; python_version < "3.8"
24
27
  Provides-Extra: online
25
28
  Requires-Dist: blissdata[tango]; extra == "online"
26
29
  Provides-Extra: test
@@ -26,9 +26,12 @@ install_requires =
26
26
  h5py >=2.8
27
27
  silx >=1.1.0
28
28
  hdf5plugin
29
+ blissdata >=0.3.4
29
30
  blissdata <1.0.1; python_version < "3.8"
30
31
  blissdata; python_version >= "3.8"
31
32
  redis # blissdata<1
33
+ packaging
34
+ importlib_metadata; python_version < "3.8"
32
35
 
33
36
  [options.packages.find]
34
37
  where = src
@@ -0,0 +1 @@
1
+ __version__ = "0.6.0"
@@ -0,0 +1,30 @@
1
+ import sys
2
+
3
+ if sys.version_info >= (3, 8):
4
+ from importlib.metadata import version
5
+ else:
6
+ from importlib_metadata import version
7
+ from packaging.specifiers import SpecifierSet
8
+
9
+
10
+ def _not_supported_by_blissdata_version(*_, **kw):
11
+ raise RuntimeError(f"Not supported by blissdata {_BLISSDATA_VERSION}")
12
+
13
+
14
+ from blissdata.h5api import dynamic_hdf5 # noqa F401
15
+
16
+ _BLISSDATA_VERSION = version("blissdata")
17
+
18
+ if _BLISSDATA_VERSION in SpecifierSet("<1", prereleases=True):
19
+ from .blissdatav0 import iter_bliss_scan_data_from_memory # noqa F401
20
+ from .blissdatav0 import last_lima_image # noqa F401
21
+
22
+ iter_bliss_scan_data_from_memory_slice = _not_supported_by_blissdata_version
23
+ elif _BLISSDATA_VERSION in SpecifierSet("<2", prereleases=True):
24
+ from .blissdatav1 import iter_bliss_scan_data_from_memory # noqa F401
25
+ from .blissdatav1 import iter_bliss_scan_data_from_memory_slice # noqa F401
26
+ from .blissdatav1 import last_lima_image # noqa F401
27
+ else:
28
+ from .blissdatav2 import iter_bliss_scan_data_from_memory # noqa F401
29
+ from .blissdatav2 import iter_bliss_scan_data_from_memory_slice # noqa F401
30
+ from .blissdatav2 import last_lima_image # noqa F401
@@ -4,14 +4,8 @@ from typing import List, Optional
4
4
 
5
5
  from numpy.typing import ArrayLike
6
6
  from silx.utils import retry as retrymod
7
-
8
- from .exceptions import VersionError
9
-
10
- try:
11
- from blissdata.data.node import get_node
12
- from blissdata.data.events.lima import ImageNotSaved
13
- except ImportError as e:
14
- raise VersionError(str(e)) from e
7
+ from blissdata.data.node import get_node
8
+ from blissdata.data.events.lima import ImageNotSaved
15
9
 
16
10
 
17
11
  logger = logging.getLogger(__name__)
@@ -0,0 +1,267 @@
1
+ import sys
2
+ import time
3
+ import logging
4
+ from numbers import Number
5
+ from collections import Counter
6
+ from typing import List, Optional, Tuple
7
+
8
+ import numpy
9
+ from numpy.typing import ArrayLike
10
+ from blissdata.beacon.data import BeaconData
11
+ from blissdata.redis_engine.store import DataStore
12
+ from blissdata.redis_engine.scan import Scan
13
+ from blissdata.redis_engine.scan import ScanState
14
+ from blissdata.redis_engine.stream import StreamingClient
15
+ from blissdata.redis_engine.exceptions import (
16
+ EndOfStream,
17
+ IndexNoMoreThereError,
18
+ IndexNotYetThereError,
19
+ IndexWontBeThereError,
20
+ )
21
+ from blissdata.lima.client import lima_client_factory
22
+
23
+ try:
24
+ from blissdata.stream import LimaStream
25
+ except ImportError:
26
+ from blissdata.streams.lima_stream import LimaStream
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ INFINITY = sys.maxsize
32
+
33
+
34
+ def _get_data_store() -> None:
35
+ redis_url = BeaconData().get_redis_data_db()
36
+ return DataStore(redis_url)
37
+
38
+
39
+ def iter_bliss_scan_data_from_memory(
40
+ scan_key: str,
41
+ lima_names: List[str],
42
+ counter_names: List[str],
43
+ retry_timeout: Optional[Number] = None,
44
+ retry_period: Optional[Number] = None,
45
+ ):
46
+ data_store = _get_data_store()
47
+ scan = data_store.load_scan(scan_key, scan_cls=Scan)
48
+ buffers = {name: list() for name in lima_names + counter_names}
49
+
50
+ while scan.state < ScanState.PREPARED:
51
+ scan.update()
52
+
53
+ lima_streams = dict()
54
+ lima_clients = dict()
55
+ counter_streams = dict()
56
+ for name, stream in scan.streams.items():
57
+ if stream.encoding["type"] == "json" and "lima" in stream.info["format"]:
58
+ if name.split(":")[-2] in lima_names:
59
+ lima_streams[name] = stream
60
+ lima_clients[name] = lima_client_factory(data_store, stream.info)
61
+ elif name.split(":")[-1] in counter_names:
62
+ counter_streams[name] = stream
63
+
64
+ client = StreamingClient({**lima_streams, **counter_streams})
65
+ client_timeout = retry_period or 0
66
+ lima_buffer_count = Counter()
67
+
68
+ while True:
69
+ try:
70
+ output = client.read(timeout=client_timeout)
71
+ except EndOfStream:
72
+ break
73
+ for stream, (_, payload) in output.items():
74
+ name_parts = stream.name.split(":")
75
+ if stream.name in lima_streams:
76
+ # payload is a sequence of JSON statuses
77
+ ctr_name = name_parts[-2]
78
+ last_status = payload[-1]
79
+ lima_client = lima_clients[stream.name]
80
+ lima_client.update(**last_status)
81
+ n_already_read = lima_buffer_count[ctr_name]
82
+ try:
83
+ data = lima_client[n_already_read:]
84
+ except IndexNoMoreThereError:
85
+ continue
86
+ buffers[ctr_name].extend(data)
87
+ lima_buffer_count[ctr_name] += len(data)
88
+ else:
89
+ # payload is a sequence of data points (0D, 1D, 2D)
90
+ ctr_name = name_parts[-1]
91
+ buffers[ctr_name].extend(payload)
92
+
93
+ nyield = min(len(v) for v in buffers.values())
94
+ if nyield:
95
+ for i in range(nyield):
96
+ yield {name: values[i] for name, values in buffers.items()}
97
+ buffers = {name: values[nyield:] for name, values in buffers.items()}
98
+
99
+
100
+ def last_lima_image(channel_info: dict) -> ArrayLike:
101
+ """Get last lima image from memory"""
102
+ data_store = _get_data_store()
103
+ lima_client = lima_client_factory(data_store, channel_info)
104
+ return lima_client.get_last_live_image().array
105
+
106
+
107
+ def _get_streams(
108
+ scan_key: str,
109
+ lima_names: List[str],
110
+ counter_names: List[str],
111
+ ):
112
+ data_store = _get_data_store()
113
+ scan = data_store.load_scan(scan_key, scan_cls=Scan)
114
+
115
+ while scan.state < ScanState.PREPARED:
116
+ scan.update()
117
+
118
+ lima_streams = dict()
119
+ counter_streams = dict()
120
+
121
+ for name, stream in scan.streams.items():
122
+ if stream.encoding["type"] == "json" and "lima" in stream.info["format"]:
123
+ if name.split(":")[-2] in lima_names:
124
+ lima_streams[name.split(":")[-2]] = LimaStream(stream)
125
+
126
+ elif name.split(":")[-1] in counter_names:
127
+ counter_streams[name.split(":")[-1]] = stream
128
+
129
+ return lima_streams, counter_streams
130
+
131
+
132
+ def iter_bliss_scan_data_from_memory_slice(
133
+ scan_key: str,
134
+ lima_names: List[str],
135
+ counter_names: List[str],
136
+ slice_range: Optional[Tuple[int, int]] = None,
137
+ retry_timeout: Optional[Number] = None,
138
+ retry_period: Optional[Number] = None,
139
+ yield_timeout: Optional[Number] = None,
140
+ max_slicing_size: Optional[Number] = None,
141
+ verbose: Optional[bool] = False,
142
+ ):
143
+ """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)
144
+
145
+ :param str scan_key: key of the Bliss scan (e.g. "esrf:scan:XXXX")
146
+ :param list lima_names: names of lima detectors
147
+ :param list counter_names: names of non-lima detectors (you need to provide at least one)
148
+ :param tuple slice_range: two elements which define the limits of the iteration along the scan. If None, it iterates along the whole scan
149
+ :param Number retry_timeout: timeout when it cannot access the data for `retry_timeout` seconds
150
+ :param Number retry_period: interval in seconds between data access retries
151
+ :param Number yield_timeout: timeout to stop slicing the stream and yield the buffered data
152
+ :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
153
+ :yields dict: data
154
+ """
155
+ lima_streams, counter_streams = _get_streams(scan_key, lima_names, counter_names)
156
+
157
+ all_streams = {**lima_streams, **counter_streams}
158
+ if not all_streams:
159
+ logger.warning("There is no stream to slice")
160
+ return
161
+
162
+ if slice_range is None:
163
+ slice_range = (0, INFINITY)
164
+
165
+ if retry_period is None:
166
+ retry_period = 1
167
+
168
+ if yield_timeout is None:
169
+ yield_timeout = 0.01
170
+
171
+ buffers_count = Counter({counter: slice_range[0] for counter in all_streams.keys()})
172
+
173
+ # Read and yield continuously
174
+ stream_on = True
175
+
176
+ incoming_buffers = {stream_name: [] for stream_name in all_streams.keys()}
177
+ non_yielded_buffers = {stream_name: [] for stream_name in all_streams.keys()}
178
+
179
+ restart_buffer = time.perf_counter()
180
+ while stream_on:
181
+
182
+ # While loop will stop unless one single stream is successfully sliced
183
+ stream_on = False
184
+
185
+ for stream_name, stream in all_streams.items():
186
+ try:
187
+ # Stop condition for limited slices
188
+ if (
189
+ slice_range[1] is not INFINITY
190
+ and buffers_count[stream_name] >= slice_range[1]
191
+ ):
192
+ continue
193
+
194
+ # Test first index, (slicing between limits do not fall into Error)
195
+ _ = stream[buffers_count[stream_name]]
196
+ if max_slicing_size is None:
197
+ stream_data = stream[buffers_count[stream_name] : slice_range[1]]
198
+ else:
199
+ stream_data = stream[
200
+ buffers_count[stream_name] : min(
201
+ slice_range[1],
202
+ buffers_count[stream_name] + max_slicing_size,
203
+ )
204
+ ]
205
+ incoming_buffers[stream_name] = stream_data
206
+ buffers_count[stream_name] += len(stream_data)
207
+ stream_on = True
208
+
209
+ except IndexNotYetThereError:
210
+ stream_on = True
211
+ except IndexWontBeThereError:
212
+ pass
213
+ except IndexNoMoreThereError:
214
+ pass
215
+ except EndOfStream:
216
+ pass
217
+ except RuntimeError:
218
+ pass
219
+
220
+ for stream_name in incoming_buffers.keys():
221
+ if len(incoming_buffers[stream_name]) > 0:
222
+ if len(non_yielded_buffers[stream_name]) == 0:
223
+ non_yielded_buffers[stream_name] = numpy.array(
224
+ incoming_buffers[stream_name]
225
+ )
226
+ else:
227
+ non_yielded_buffers[stream_name] = numpy.concatenate(
228
+ (
229
+ non_yielded_buffers[stream_name],
230
+ incoming_buffers[stream_name],
231
+ )
232
+ )
233
+ incoming_buffers[stream_name] = []
234
+
235
+ if not stream_on or ((time.perf_counter() - restart_buffer) > yield_timeout):
236
+
237
+ frames_to_yield = min(
238
+ [len(value) for value in non_yielded_buffers.values()]
239
+ )
240
+
241
+ if frames_to_yield > 0:
242
+ if verbose:
243
+ for stream_name, stream_buffer in non_yielded_buffers.items():
244
+ print(
245
+ f"After slicing the stream: {stream_name} buffer contains {len(stream_buffer)} items"
246
+ )
247
+
248
+ # Yield point by point
249
+ for index in range(frames_to_yield):
250
+ yield {
251
+ stream_name: stream_buffer[index]
252
+ for stream_name, stream_buffer in non_yielded_buffers.items()
253
+ }
254
+
255
+ # Save the non-yielded points for the next iteration
256
+ for stream_name in non_yielded_buffers.keys():
257
+ non_yielded_buffers[stream_name] = non_yielded_buffers[stream_name][
258
+ frames_to_yield:
259
+ ]
260
+
261
+ if verbose:
262
+ for stream_name, stream_buffer in non_yielded_buffers.items():
263
+ print(
264
+ f"After yielding: {stream_name} buffer contains {len(stream_buffer)} non-yielded items"
265
+ )
266
+
267
+ restart_buffer = time.perf_counter()
@@ -0,0 +1,234 @@
1
+ import sys
2
+ import time
3
+ import logging
4
+ from collections import Counter
5
+ from typing import List, Optional, Tuple
6
+
7
+ import numpy
8
+ from numpy.typing import ArrayLike
9
+
10
+ from blissdata.beacon.data import BeaconData
11
+ from blissdata.redis_engine.store import DataStore
12
+ from blissdata.redis_engine.scan import ScanState
13
+ from blissdata.redis_engine.exceptions import (
14
+ EndOfStream,
15
+ IndexNoMoreThereError,
16
+ IndexNotYetThereError,
17
+ IndexWontBeThereError,
18
+ )
19
+ from blissdata.streams.base import CursorGroup
20
+
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ INFINITY = sys.maxsize
25
+
26
+
27
+ def _get_data_store() -> None:
28
+ redis_url = BeaconData().get_redis_data_db()
29
+ return DataStore(redis_url)
30
+
31
+
32
+ def iter_bliss_scan_data_from_memory(
33
+ scan_key: str,
34
+ lima_names: List[str],
35
+ counter_names: List[str],
36
+ retry_timeout: Optional[float] = None,
37
+ retry_period: Optional[float] = None,
38
+ ):
39
+ streams = _get_streams(scan_key, lima_names, counter_names)
40
+ if not streams:
41
+ return
42
+
43
+ cursor = CursorGroup(list(streams.values()))
44
+ cursor_timeout = retry_period or 0
45
+ buffers = {ctr_name: list() for ctr_name in lima_names + counter_names}
46
+ ctr_names = {stream.name: ctr_name for ctr_name, stream in streams.items()}
47
+
48
+ while True:
49
+ try:
50
+ views = cursor.read(timeout=cursor_timeout)
51
+ except EndOfStream:
52
+ break
53
+
54
+ for stream, view in views.items():
55
+ ctr_name = ctr_names[stream.name]
56
+ buffers[ctr_name].extend(view.get_data())
57
+
58
+ nyield = min(len(v) for v in buffers.values())
59
+ if nyield:
60
+ for i in range(nyield):
61
+ yield {name: values[i] for name, values in buffers.items()}
62
+ buffers = {name: values[nyield:] for name, values in buffers.items()}
63
+
64
+
65
+ def last_lima_image(scan_key: str, lima_name: str) -> ArrayLike:
66
+ """Get last lima image from memory"""
67
+ streams = _get_streams(scan_key, [lima_name], [])
68
+ stream = list(streams.values())[0]
69
+ return stream.get_last_live_image().array
70
+
71
+
72
+ def _get_streams(
73
+ scan_key: str,
74
+ lima_names: List[str],
75
+ counter_names: List[str],
76
+ ):
77
+ data_store = _get_data_store()
78
+ scan = data_store.load_scan(scan_key)
79
+
80
+ while scan.state < ScanState.PREPARED:
81
+ scan.update()
82
+
83
+ streams = dict()
84
+
85
+ for name, stream in scan.streams.items():
86
+ if (
87
+ stream.event_stream.encoding["type"] == "json"
88
+ and "lima" in stream.info["format"]
89
+ ):
90
+ if name.split(":")[-2] in lima_names:
91
+ streams[name.split(":")[-2]] = stream
92
+
93
+ elif name.split(":")[-1] in counter_names:
94
+ streams[name.split(":")[-1]] = stream
95
+
96
+ nnames = len(lima_names) + len(counter_names)
97
+ nstreams = len(streams)
98
+ if nnames != nstreams:
99
+ logger.warning("asked for %d names but got %s streams", nnames, nstreams)
100
+ return dict()
101
+
102
+ return streams
103
+
104
+
105
+ def iter_bliss_scan_data_from_memory_slice(
106
+ scan_key: str,
107
+ lima_names: List[str],
108
+ counter_names: List[str],
109
+ slice_range: Optional[Tuple[int, int]] = None,
110
+ retry_timeout: Optional[float] = None,
111
+ retry_period: Optional[float] = None,
112
+ yield_timeout: Optional[float] = None,
113
+ max_slicing_size: Optional[float] = None,
114
+ verbose: Optional[bool] = False,
115
+ ):
116
+ """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)
117
+
118
+ :param str scan_key: key of the Bliss scan (e.g. "esrf:scan:XXXX")
119
+ :param list lima_names: names of lima detectors
120
+ :param list counter_names: names of non-lima detectors (you need to provide at least one)
121
+ :param tuple slice_range: two elements which define the limits of the iteration along the scan. If None, it iterates along the whole scan
122
+ :param float retry_timeout: timeout when it cannot access the data for `retry_timeout` seconds
123
+ :param float retry_period: interval in seconds between data access retries
124
+ :param float yield_timeout: timeout to stop slicing the stream and yield the buffered data
125
+ :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
126
+ :yields dict: data
127
+ """
128
+ streams = _get_streams(scan_key, lima_names, counter_names)
129
+ if not streams:
130
+ return
131
+
132
+ if slice_range is None:
133
+ slice_range = (0, INFINITY)
134
+
135
+ if yield_timeout is None:
136
+ yield_timeout = 0.01
137
+
138
+ buffers_count = Counter({counter: slice_range[0] for counter in streams.keys()})
139
+
140
+ # Read and yield continuously
141
+ stream_on = True
142
+
143
+ incoming_buffers = {stream_name: [] for stream_name in streams.keys()}
144
+ non_yielded_buffers = {stream_name: [] for stream_name in streams.keys()}
145
+
146
+ restart_buffer = time.perf_counter()
147
+ while stream_on:
148
+
149
+ # While loop will stop unless one single stream is successfully sliced
150
+ stream_on = False
151
+
152
+ for stream_name, stream in streams.items():
153
+ try:
154
+ # Stop condition for limited slices
155
+ if (
156
+ slice_range[1] is not INFINITY
157
+ and buffers_count[stream_name] >= slice_range[1]
158
+ ):
159
+ continue
160
+
161
+ # Test first index, (slicing between limits do not fall into Error)
162
+ _ = stream[buffers_count[stream_name]]
163
+ if max_slicing_size is None:
164
+ stream_data = stream[buffers_count[stream_name] : slice_range[1]]
165
+ else:
166
+ stream_data = stream[
167
+ buffers_count[stream_name] : min(
168
+ slice_range[1],
169
+ buffers_count[stream_name] + max_slicing_size,
170
+ )
171
+ ]
172
+ incoming_buffers[stream_name] = stream_data
173
+ buffers_count[stream_name] += len(stream_data)
174
+ stream_on = True
175
+
176
+ except IndexNotYetThereError:
177
+ stream_on = True
178
+ except IndexWontBeThereError:
179
+ pass
180
+ except IndexNoMoreThereError:
181
+ pass
182
+ except EndOfStream:
183
+ pass
184
+ except RuntimeError:
185
+ pass
186
+
187
+ for stream_name in incoming_buffers.keys():
188
+ if len(incoming_buffers[stream_name]) > 0:
189
+ if len(non_yielded_buffers[stream_name]) == 0:
190
+ non_yielded_buffers[stream_name] = numpy.array(
191
+ incoming_buffers[stream_name]
192
+ )
193
+ else:
194
+ non_yielded_buffers[stream_name] = numpy.concatenate(
195
+ (
196
+ non_yielded_buffers[stream_name],
197
+ incoming_buffers[stream_name],
198
+ )
199
+ )
200
+ incoming_buffers[stream_name] = []
201
+
202
+ if not stream_on or ((time.perf_counter() - restart_buffer) > yield_timeout):
203
+
204
+ frames_to_yield = min(
205
+ [len(value) for value in non_yielded_buffers.values()]
206
+ )
207
+
208
+ if frames_to_yield > 0:
209
+ if verbose:
210
+ for stream_name, stream_buffer in non_yielded_buffers.items():
211
+ print(
212
+ f"After slicing the stream: {stream_name} buffer contains {len(stream_buffer)} items"
213
+ )
214
+
215
+ # Yield point by point
216
+ for index in range(frames_to_yield):
217
+ yield {
218
+ stream_name: stream_buffer[index]
219
+ for stream_name, stream_buffer in non_yielded_buffers.items()
220
+ }
221
+
222
+ # Save the non-yielded points for the next iteration
223
+ for stream_name in non_yielded_buffers.keys():
224
+ non_yielded_buffers[stream_name] = non_yielded_buffers[stream_name][
225
+ frames_to_yield:
226
+ ]
227
+
228
+ if verbose:
229
+ for stream_name, stream_buffer in non_yielded_buffers.items():
230
+ print(
231
+ f"After yielding: {stream_name} buffer contains {len(stream_buffer)} non-yielded items"
232
+ )
233
+
234
+ restart_buffer = time.perf_counter()
@@ -0,0 +1,43 @@
1
+ import functools
2
+ from typing import Iterator
3
+ from collections.abc import Generator
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
@@ -57,17 +57,21 @@ class _DatasetWriterBase:
57
57
  class DatasetWriter(_DatasetWriterBase):
58
58
  """Append arrays of the same shape to a new HDF5 dataset in a sequential manner.
59
59
 
60
+ Instead of creating a dataset with the :code:`h5py` API
61
+
60
62
  .. code-block::python
61
63
 
62
- with DatasetWriter(parent, "mydataset") as writer:
63
- writer.add_point([1,2,3])
64
- writer.add_points([[4,5,6], [7,8,9]])
64
+ h5group["mydataset"] = [[1,2,3], [4,5,6], [7,8,9]]
65
65
 
66
- This is equivalent to
66
+ it can be done like this
67
67
 
68
68
  .. code-block::python
69
69
 
70
- parent["mydataset"] = [[1,2,3], [4,5,6], [7,8,9]]
70
+ with DatasetWriter(h5group, "mydataset") as writer:
71
+ writer.add_point([1,2,3])
72
+ writer.add_points([[4,5,6], [7,8,9]])
73
+
74
+ Chunk size determination, chunk-aligned writing, compression and flushing is handled.
71
75
  """
72
76
 
73
77
  def __init__(
@@ -168,21 +172,25 @@ class StackDatasetWriter(_DatasetWriterBase):
168
172
  in a sequential manner per item. So each item of the HDF5 dataset is a
169
173
  stack to which we can append data in a sequential manner.
170
174
 
175
+ Instead of creating a dataset with the :code:`h5py` API
176
+
171
177
  .. code-block::python
172
178
 
173
- with DatasetWriter(parent, "mydataset") as writer:
174
- writer.add_point([1,2,3], 0)
175
- writer.add_point([10,11,12], 1)
176
- writer.add_points([[4,5,6], [7,8,9]], 0)
177
- writer.add_points([[13,14,15], [16,17,18]], 1)
179
+ stack0 = [[1,2,3], [4,5,6], [7,8,9]]
180
+ stack1 = [[10,11,12], [13,14,15], [16,17,18]]
181
+ h5group["mydataset"] = [stack0, stack1]
178
182
 
179
- This is equivalent to
183
+ it can be done like this
180
184
 
181
185
  .. code-block::python
182
186
 
183
- stack0 = [[1,2,3], [4,5,6], [7,8,9]]
184
- stack1 = [[10,11,12], [13,14,15], [16,17,18]]
185
- parent["mydataset"] = [stack0, stack1]
187
+ with StackDatasetWriter(h5group, "mydataset") as writer:
188
+ writer.add_point([1,2,3], 0)
189
+ writer.add_point([10,11,12], 1)
190
+ writer.add_points([[13,14,15], [16,17,18]], 1)
191
+ writer.add_points([[4,5,6], [7,8,9]], 0)
192
+
193
+ Chunk size determination, chunk-aligned writing, compression and flushing is handled.
186
194
  """
187
195
 
188
196
  def __init__(
@@ -0,0 +1,61 @@
1
+ from ..data import contextiterator
2
+
3
+
4
+ def test_context_iterator():
5
+
6
+ exited = False
7
+
8
+ @contextiterator.contextiterator
9
+ def iterator():
10
+ nonlocal exited
11
+ exited = False
12
+ try:
13
+ yield from [1, 2, 3]
14
+ finally:
15
+ exited = True
16
+
17
+ # Use like a normal iterator
18
+ it = iterator()
19
+ assert next(it) == 1
20
+ assert not exited
21
+
22
+ # Use like a context iterator
23
+ with iterator() as it:
24
+ assert next(it) == 1
25
+ assert not exited
26
+ assert exited
27
+
28
+
29
+ def test_context_generator():
30
+
31
+ @contextiterator.contextiterator
32
+ def generator():
33
+ nonlocal exited
34
+ exited = False
35
+ try:
36
+ while True:
37
+ x = yield
38
+ yield x * 2
39
+ finally:
40
+ exited = True
41
+
42
+ # Use like a normal generator
43
+ exited = False
44
+ gen = generator()
45
+ assert next(gen) is None # run up to first yield
46
+ assert not exited
47
+ assert gen.send(10) == 20
48
+ assert next(gen) is None
49
+ assert gen.send(15) == 30
50
+ assert not exited
51
+
52
+ # Use like a context iterator
53
+ exited = False
54
+ with generator() as gen:
55
+ assert next(gen) is None
56
+ assert not exited
57
+ assert gen.send(10) == 20
58
+ assert next(gen) is None
59
+ assert gen.send(15) == 30
60
+ assert not exited
61
+ assert exited
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ewoksdata
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Ewoks tasks for data access
5
5
  Home-page: https://gitlab.esrf.fr/workflow/ewoksapps/ewoksdata/
6
6
  Author: ESRF
@@ -18,9 +18,12 @@ License-File: LICENSE.md
18
18
  Requires-Dist: h5py>=2.8
19
19
  Requires-Dist: silx>=1.1.0
20
20
  Requires-Dist: hdf5plugin
21
+ Requires-Dist: blissdata>=0.3.4
21
22
  Requires-Dist: blissdata<1.0.1; python_version < "3.8"
22
23
  Requires-Dist: blissdata; python_version >= "3.8"
23
24
  Requires-Dist: redis
25
+ Requires-Dist: packaging
26
+ Requires-Dist: importlib_metadata; python_version < "3.8"
24
27
  Provides-Extra: online
25
28
  Requires-Dist: blissdata[tango]; extra == "online"
26
29
  Provides-Extra: test
@@ -18,14 +18,14 @@ src/ewoksdata/data/utils.py
18
18
  src/ewoksdata/data/blissdata/__init__.py
19
19
  src/ewoksdata/data/blissdata/blissdatav0.py
20
20
  src/ewoksdata/data/blissdata/blissdatav1.py
21
- src/ewoksdata/data/blissdata/blissdatavid31.py
22
- src/ewoksdata/data/blissdata/exceptions.py
21
+ src/ewoksdata/data/blissdata/blissdatav2.py
23
22
  src/ewoksdata/data/hdf5/__init__.py
24
23
  src/ewoksdata/data/hdf5/config.py
25
24
  src/ewoksdata/data/hdf5/dataset_writer.py
26
25
  src/ewoksdata/data/hdf5/types.py
27
26
  src/ewoksdata/tests/__init__.py
28
27
  src/ewoksdata/tests/conftest.py
28
+ src/ewoksdata/tests/test_contextiterator.py
29
29
  src/ewoksdata/tests/test_data_bliss.py
30
30
  src/ewoksdata/tests/test_data_hdf5.py
31
31
  src/ewoksdata/tests/test_data_nexus.py
@@ -1,10 +1,13 @@
1
1
  h5py>=2.8
2
2
  silx>=1.1.0
3
3
  hdf5plugin
4
+ blissdata>=0.3.4
4
5
  redis
6
+ packaging
5
7
 
6
8
  [:python_version < "3.8"]
7
9
  blissdata<1.0.1
10
+ importlib_metadata
8
11
 
9
12
  [:python_version >= "3.8"]
10
13
  blissdata
@@ -1 +0,0 @@
1
- __version__ = "0.5.0"
@@ -1,36 +0,0 @@
1
- from .exceptions import VersionError
2
-
3
- try:
4
- # blissdata >=1
5
- from .blissdatav1 import iter_bliss_scan_data_from_memory
6
- from .blissdatav1 import last_lima_image
7
- from blissdata.h5api import dynamic_hdf5
8
- except VersionError as exc:
9
- try:
10
- # blissdata >0.3.3,<1 (unreleased, branch id31_2.0)
11
- from .blissdatavid31 import iter_bliss_scan_data_from_memory
12
- from .blissdatavid31 import last_lima_image
13
- from blissdata.h5api import dynamic_hdf5
14
- except VersionError:
15
- try:
16
- # blissdata <=0.3.3
17
- from .blissdatav0 import iter_bliss_scan_data_from_memory
18
- from .blissdatav0 import last_lima_image
19
- from blissdata.h5api import dynamic_hdf5
20
- except VersionError: # noqa F841
21
- _EXC = exc
22
-
23
- def _reraise(*_, **kw):
24
- raise _EXC
25
-
26
- class BlissDataVersionNotSupported:
27
- def __getattr__(self, *_):
28
- return _reraise
29
-
30
- dynamic_hdf5 = BlissDataVersionNotSupported()
31
-
32
- def iter_bliss_scan_data_from_memory(*_, **kw):
33
- raise _EXC
34
-
35
- def last_lima_image(*_, **kw):
36
- raise _EXC
@@ -1,91 +0,0 @@
1
- from numbers import Number
2
- from collections import Counter
3
- from typing import List, Optional
4
-
5
- from numpy.typing import ArrayLike
6
-
7
- from .exceptions import VersionError
8
-
9
- try:
10
- from blissdata.beacon.data import BeaconData
11
- from blissdata.redis_engine.store import DataStore
12
- from blissdata.redis_engine.scan import Scan
13
- from blissdata.redis_engine.scan import ScanState
14
- from blissdata.redis_engine.stream import StreamingClient
15
- from blissdata.redis_engine.exceptions import EndOfStream
16
- from blissdata.redis_engine.exceptions import IndexNoMoreThereError
17
- from blissdata.lima.client import lima_client_factory
18
- except ImportError as e:
19
- raise VersionError(str(e)) from e
20
-
21
-
22
- def iter_bliss_scan_data_from_memory(
23
- db_name: str,
24
- lima_names: List[str],
25
- counter_names: List[str],
26
- retry_timeout: Optional[Number] = None,
27
- retry_period: Optional[Number] = None,
28
- ):
29
- data_store = _get_data_store()
30
- scan = data_store.load_scan(db_name, scan_cls=Scan)
31
- buffers = {name: list() for name in lima_names + counter_names}
32
-
33
- while scan.state < ScanState.PREPARED:
34
- scan.update()
35
-
36
- lima_streams = dict()
37
- lima_clients = dict()
38
- counter_streams = dict()
39
- for name, stream in scan.streams.items():
40
- if stream.encoding["type"] == "json" and "lima" in stream.info["format"]:
41
- if name.split(":")[-2] in lima_names:
42
- lima_streams[name] = stream
43
- lima_clients[name] = lima_client_factory(data_store, stream.info)
44
- elif name.split(":")[-1] in counter_names:
45
- counter_streams[name] = stream
46
-
47
- client = StreamingClient({**lima_streams, **counter_streams})
48
- lima_buffer_count = Counter()
49
-
50
- while True:
51
- try:
52
- output = client.read()
53
- except EndOfStream:
54
- break
55
- for stream, (_, payload) in output.items():
56
- name_parts = stream.name.split(":")
57
- if stream.name in lima_streams:
58
- # payload is a sequence of JSON statuses
59
- ctr_name = name_parts[-2]
60
- last_status = payload[-1]
61
- lima_client = lima_clients[stream.name]
62
- lima_client.update(**last_status)
63
- n_already_read = lima_buffer_count[ctr_name]
64
- try:
65
- data = lima_client[n_already_read:]
66
- except IndexNoMoreThereError:
67
- continue
68
- buffers[ctr_name].extend(data)
69
- lima_buffer_count[ctr_name] += len(data)
70
- else:
71
- # payload is a sequence of data points (0D, 1D, 2D)
72
- ctr_name = name_parts[-1]
73
- buffers[ctr_name].extend(payload)
74
-
75
- nyield = min(len(v) for v in buffers.values())
76
- if nyield:
77
- for i in range(nyield):
78
- yield {name: values[i] for name, values in buffers.items()}
79
- buffers = {name: values[nyield:] for name, values in buffers.items()}
80
-
81
-
82
- def last_lima_image(channel_info: dict) -> ArrayLike:
83
- """Get last lima image from memory"""
84
- data_store = _get_data_store()
85
- lima_client = lima_client_factory(data_store, channel_info)
86
- return lima_client.get_last_live_image().array
87
-
88
-
89
- def _get_data_store() -> None:
90
- redis_url = BeaconData().get_redis_data_db()
91
- return DataStore(redis_url)
@@ -1,105 +0,0 @@
1
- from numbers import Number
2
- from collections import Counter
3
- from typing import List, Optional
4
-
5
- from numpy.typing import ArrayLike
6
-
7
- from .exceptions import VersionError
8
-
9
- try:
10
- from blissdata.redis_engine.scan import Scan
11
- from blissdata.redis_engine.stream import StreamingClient
12
- from blissdata.redis_engine.models import ScanState
13
- from blissdata.redis_engine.exceptions import EndOfStream
14
- from blissdata.lima.client import lima_client_factory
15
- from blissdata.beacon.data import BeaconData
16
- from blissdata import redis_engine
17
- except ImportError as e:
18
- raise VersionError(str(e)) from e
19
-
20
-
21
- def iter_bliss_scan_data_from_memory(
22
- db_name: str,
23
- lima_names: List[str],
24
- counter_names: List[str],
25
- retry_timeout: Optional[Number] = None,
26
- retry_period: Optional[Number] = None,
27
- ):
28
- _ensure_redis()
29
- scan = Scan.load(db_name)
30
- buffers = {name: list() for name in lima_names + counter_names}
31
-
32
- while scan.state < ScanState.PREPARED:
33
- scan.update()
34
- if scan.state is ScanState.ABORTED:
35
- return
36
-
37
- lima_streams = dict()
38
- lima_clients = dict()
39
- lima_buffer_count = dict()
40
- counter_streams = dict()
41
- for name, stream in scan.streams.items():
42
- if stream.encoding["type"] == "json" and "lima" in stream.info["format"]:
43
- if name.split(":")[-2] in lima_names:
44
- lima_streams[name] = stream
45
- lima_clients[name] = lima_client_factory(stream.info)
46
- lima_buffer_count[name] = 0
47
- elif name.split(":")[-1] in counter_names:
48
- counter_streams[name] = stream
49
-
50
- client = StreamingClient({**lima_streams, **counter_streams})
51
- lima_buffer_count = Counter()
52
-
53
- while True:
54
- try:
55
- output = client.read()
56
- except EndOfStream:
57
- break
58
- for stream, (_, payload) in output.items():
59
- name_parts = stream.name.split(":")
60
- if stream.name in lima_streams:
61
- # payload is a sequence of JSON statuses
62
- ctr_name = name_parts[-2]
63
- lima_client = lima_clients[stream.name]
64
- lima_client.update(**payload[-1])
65
- n_already_read = lima_buffer_count[ctr_name]
66
- data = lima_client[n_already_read:]
67
- buffers[ctr_name].extend(data)
68
- lima_buffer_count[ctr_name] += len(data)
69
- else:
70
- # payload is a sequence of data points (0D, 1D, 2D)
71
- ctr_name = name_parts[-1]
72
- buffers[ctr_name].extend(payload)
73
-
74
- nyield = min(len(v) for v in buffers.values())
75
- if nyield:
76
- for i in range(nyield):
77
- yield {name: values[i] for name, values in buffers.items()}
78
- buffers = {name: values[nyield:] for name, values in buffers.items()}
79
-
80
-
81
- def last_lima_image(channel_info: dict) -> ArrayLike:
82
- """Get last lima image from memory"""
83
- _ensure_redis()
84
- lima_client = lima_client_factory(channel_info)
85
- return lima_client.get_last_live_image().array
86
-
87
-
88
- def _ensure_redis() -> None:
89
- url = BeaconData().get_redis_data_db()
90
- current_url = _current_redis_url()
91
- if current_url:
92
- if url == current_url:
93
- return
94
- raise RuntimeError("The Redis URL has changed. Restart the Nexus writer.")
95
- redis_engine.set_redis_url(url)
96
-
97
-
98
- def _current_redis_url() -> Optional[str]:
99
- if redis_engine._redis is None:
100
- return
101
- kwargs = redis_engine._redis.connection_pool.connection_kwargs
102
- if "path" in kwargs:
103
- return f"unix://{kwargs['path']}"
104
- else:
105
- return f"redis://{kwargs['host']}:{kwargs['port']}"
@@ -1,4 +0,0 @@
1
- class VersionError(ImportError):
2
- """Import attempt failed from a library for which we support multiple versions with different import API's."""
3
-
4
- pass
@@ -1,35 +0,0 @@
1
- from collections.abc import Generator
2
-
3
-
4
- class ContextIterator(Generator):
5
- def __init__(self, it):
6
- self._it = it
7
-
8
- def __iter__(self):
9
- return self
10
-
11
- def __next__(self):
12
- return next(self._it)
13
-
14
- def __enter__(self):
15
- return self
16
-
17
- def __exit__(self, *_):
18
- self._it.close()
19
-
20
- def send(self):
21
- return next(self._it)
22
-
23
- def throw(self):
24
- raise StopIteration
25
-
26
-
27
- def contextiterator(iterator):
28
- """Decorator that allows to use an iterator as a context manager.
29
- This ensures that the iterator is closed when exiting the context manager.
30
- """
31
-
32
- def wrapper(*args, **kw):
33
- return ContextIterator(iterator(*args, **kw))
34
-
35
- return wrapper
File without changes
File without changes
File without changes
File without changes