ewoksdata 1.3.0__tar.gz → 2.0.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 (35) hide show
  1. {ewoksdata-1.3.0/src/ewoksdata.egg-info → ewoksdata-2.0.0}/PKG-INFO +2 -1
  2. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/pyproject.toml +18 -4
  3. ewoksdata-2.0.0/src/ewoksdata/data/bliss.py +286 -0
  4. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/blissdata/__init__.py +7 -1
  5. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/blissdata/blissdatav0.py +21 -7
  6. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/blissdata/blissdatav1.py +28 -15
  7. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/blissdata/blissdatav2.py +136 -43
  8. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/hdf5/config.py +5 -3
  9. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/hdf5/dataset_writer.py +3 -2
  10. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/hdf5/types.py +3 -1
  11. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/nexus.py +6 -1
  12. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/url.py +3 -1
  13. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/utils.py +1 -0
  14. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/data/bliss_scans.py +4 -3
  15. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/test_data_bliss.py +9 -2
  16. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/test_dataset_writer.py +1 -2
  17. {ewoksdata-1.3.0 → ewoksdata-2.0.0/src/ewoksdata.egg-info}/PKG-INFO +2 -1
  18. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata.egg-info/requires.txt +1 -0
  19. ewoksdata-1.3.0/src/ewoksdata/data/bliss.py +0 -148
  20. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/LICENSE.md +0 -0
  21. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/README.md +0 -0
  22. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/setup.cfg +0 -0
  23. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/__init__.py +0 -0
  24. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/__init__.py +0 -0
  25. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/contextiterator.py +1 -1
  26. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/data/hdf5/__init__.py +0 -0
  27. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/__init__.py +0 -0
  28. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/conftest.py +0 -0
  29. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/data/__init__.py +0 -0
  30. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/test_contextiterator.py +0 -0
  31. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/test_data_hdf5.py +0 -0
  32. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata/tests/test_data_nexus.py +1 -1
  33. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata.egg-info/SOURCES.txt +0 -0
  34. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
  35. {ewoksdata-1.3.0 → ewoksdata-2.0.0}/src/ewoksdata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ewoksdata
3
- Version: 1.3.0
3
+ Version: 2.0.0
4
4
  Summary: Ewoks tasks for data access
5
5
  Author-email: ESRF <dau-pydev@esrf.fr>
6
6
  License: # MIT License
@@ -53,6 +53,7 @@ Provides-Extra: dev
53
53
  Requires-Dist: ewoksdata[test]; extra == "dev"
54
54
  Requires-Dist: black>=25; extra == "dev"
55
55
  Requires-Dist: flake8>=4; extra == "dev"
56
+ Requires-Dist: isort; extra == "dev"
56
57
  Provides-Extra: doc
57
58
  Requires-Dist: ewoksdata[test]; extra == "doc"
58
59
  Requires-Dist: sphinx>=4.5; extra == "doc"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ewoksdata"
7
- version = "1.3.0"
7
+ version = "2.0.0"
8
8
  authors = [{ name = "ESRF", email = "dau-pydev@esrf.fr" }]
9
9
  description = "Ewoks tasks for data access"
10
10
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -35,9 +35,19 @@ Issues = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksdata/issues"
35
35
  Changelog = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksdata/-/blob/main/CHANGELOG.md"
36
36
 
37
37
  [project.optional-dependencies]
38
- online = ["blissdata[tango]"]
39
- test = ["ewoksdata[online]", "pytest >=7"]
40
- dev = ["ewoksdata[test]", "black >=25", "flake8 >=4"]
38
+ online = [
39
+ "blissdata[tango]"
40
+ ]
41
+ test = [
42
+ "ewoksdata[online]",
43
+ "pytest >=7",
44
+ ]
45
+ dev = [
46
+ "ewoksdata[test]",
47
+ "black >=25",
48
+ "flake8 >=4",
49
+ "isort",
50
+ ]
41
51
  doc = [
42
52
  "ewoksdata[test]",
43
53
  "sphinx >=4.5",
@@ -53,3 +63,7 @@ where = ["src"]
53
63
 
54
64
  [tool.coverage.run]
55
65
  omit = ['*/tests/*']
66
+
67
+ [tool.isort]
68
+ profile = "black"
69
+ force_single_line = "True"
@@ -0,0 +1,286 @@
1
+ """Utilities for reading Bliss scan data from HDF5/Nexus files
2
+ or from memory (Redis or device servers).
3
+ """
4
+
5
+ import logging
6
+ import sys
7
+ from numbers import Integral
8
+ from numbers import Number
9
+ from typing import Any
10
+ from typing import Dict
11
+ from typing import Iterator
12
+ from typing import List
13
+ from typing import Optional
14
+ from typing import Sequence
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ import h5py
19
+ import hdf5plugin # noqa: F401 - needed for transparent compression handling
20
+ import numpy
21
+ from numpy.typing import ArrayLike
22
+ from silx.io import h5py_utils
23
+ from silx.io.url import DataUrl
24
+ from silx.io.utils import get_data as silx_get_data
25
+ from silx.utils import retry as retrymod
26
+
27
+ from . import hdf5
28
+ from . import nexus
29
+ from . import url
30
+ from .blissdata import BUFFER_IN_MEMORY_DEFAULT
31
+ from .blissdata import dynamic_hdf5
32
+ from .blissdata import iter_bliss_scan_data_from_memory
33
+ from .blissdata import iter_bliss_scan_data_from_memory_slice # noqa: F401
34
+ from .blissdata import last_lima_image # noqa: F401
35
+ from .contextiterator import contextiterator
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def get_data(
41
+ data: Union[str, DataUrl, ArrayLike, Number],
42
+ **hdf5_options: Any,
43
+ ) -> Union[numpy.ndarray, Number, Sequence]:
44
+ """
45
+ Return a NumPy array or a scalar from *data*.
46
+
47
+ :param data: Either a data-URL, an array-like object or a scalar number.
48
+ :param **hdf5_options: Additional options forwarded to :class:`silx.io.h5py_utils.File`.
49
+ :return: The requested data.
50
+ """
51
+ if isinstance(data, (str, DataUrl)):
52
+ data_url = url.as_dataurl(data)
53
+ filename, h5path, idx = url.h5dataset_url_parse(data_url)
54
+
55
+ if h5py.is_hdf5(filename):
56
+ return _get_hdf5_data(filename, h5path, idx=idx, **hdf5_options)
57
+
58
+ if not data_url.scheme():
59
+ for scheme in ("fabio", "silx"):
60
+ if sys.platform == "win32":
61
+ new_data_url = url.as_dataurl(f"{scheme}:///{data}")
62
+ else:
63
+ new_data_url = url.as_dataurl(f"{scheme}://{data}")
64
+ if new_data_url.is_valid():
65
+ data_url = new_data_url
66
+ break
67
+
68
+ return silx_get_data(data_url)
69
+
70
+ if isinstance(data, (Sequence, Number, numpy.ndarray)):
71
+ return data
72
+
73
+ raise TypeError(f"Unsupported data type: {type(data)!r}")
74
+
75
+
76
+ def get_image(
77
+ data: Union[str, DataUrl, ArrayLike, Number], **hdf5_options: Any
78
+ ) -> numpy.ndarray:
79
+ """
80
+ Return a NumPy array or a scalar from *data*.
81
+
82
+ :param data: Either a data-URL, an array-like object or a scalar number.
83
+ :param retry_timeout: Timeout for the retry mechanism.
84
+ :param retry_period: Interval between retries.
85
+ :param **hdf5_options: Additional options forwarded to :class:`silx.io.h5py_utils.File`.
86
+ :return: A 2-D image.
87
+ """
88
+ data = get_data(data, **hdf5_options)
89
+ return numpy.atleast_2d(numpy.squeeze(data))
90
+
91
+
92
+ @h5py_utils.retry()
93
+ def _get_hdf5_data(
94
+ filename: str,
95
+ h5path: str,
96
+ idx: Optional[Tuple[int, ...]] = None,
97
+ **hdf5_options: Any,
98
+ ) -> numpy.ndarray:
99
+ """
100
+ Read a slice of a dataset inside an HDF5 / Nexus file.
101
+
102
+ :param filename: Path to the HDF5 / Nexus file.
103
+ :param h5path: Internal path to the dataset (e.g. ``/entry/data``).
104
+ :param idx: Optional index or slice tuple used to read a subset of the dataset.
105
+ :param **hdf5_options: Additional options forwarded to :class:`silx.io.h5py_utils.File`.
106
+ :return: The requested slice.
107
+ """
108
+ with hdf5.h5context(filename, h5path, **hdf5_options) as dset:
109
+ if _is_bliss_file(dset):
110
+ # ``end_time`` is added by the Bliss writer when the scan is
111
+ # finished. If it is missing we wait for the file to be
112
+ # completed.
113
+ if "end_time" not in nexus.get_nxentry(dset):
114
+ raise retrymod.RetryError("Bliss file not yet complete")
115
+ if idx is None:
116
+ idx = tuple()
117
+ return dset[idx]
118
+
119
+
120
+ @contextiterator
121
+ def iter_bliss_scan_data_from_hdf5(
122
+ filename: str,
123
+ scan_nr: Integral,
124
+ subscan: Optional[Integral] = None,
125
+ lima_names: Optional[List[str]] = None,
126
+ counter_names: Optional[List[str]] = None,
127
+ retry_timeout: Optional[float] = None,
128
+ retry_period: Optional[float] = None,
129
+ **hdf5_options: Any,
130
+ ) -> Iterator[Dict[str, Any]]:
131
+ """
132
+ Iterate over the data of a single Bliss scan stored in an HDF5 file.
133
+
134
+ :param filename: Bliss dataset filename (HDF5 / Nexus).
135
+ :param scan_nr: Scan number inside the file.
136
+ :param lima_names: List of LIMA detector names.
137
+ :param counter_names: List of non-LIMA counter names.
138
+ :param subscan: Sub-scan number (e.g. ``10.2`` -> ``scan_nr=10`` and ``subscan=2``).
139
+ :param retry_timeout: Timeout for the retry mechanism.
140
+ :param retry_period: Interval between retries.
141
+ :param **hdf5_options: Additional options forwarded to :class:`blissdata.h5api.dynamic_hdf5.File`.
142
+ :yields: Mapping ``detector_name -> value`` for the each scan point.
143
+ """
144
+ if not subscan:
145
+ subscan = 1
146
+ lima_names = lima_names or []
147
+ counter_names = counter_names or []
148
+
149
+ with dynamic_hdf5.File(
150
+ filename,
151
+ lima_names=lima_names,
152
+ retry_timeout=retry_timeout,
153
+ retry_period=retry_period,
154
+ **hdf5_options,
155
+ ) as root:
156
+ scan = root[f"{scan_nr}.{subscan}"]
157
+ # assert _is_bliss_file(scan), "Not a Bliss dataset file"
158
+ measurement = scan["measurement"]
159
+ instrument = scan["instrument"]
160
+
161
+ datasets: Dict[str, Any] = {name: measurement[name] for name in counter_names}
162
+ for name in lima_names:
163
+ datasets[name] = instrument[f"{name}/data"]
164
+
165
+ names = list(datasets.keys())
166
+ for values in zip(*datasets.values()):
167
+ yield dict(zip(names, values))
168
+
169
+
170
+ @contextiterator
171
+ def iter_bliss_data(
172
+ filename: str,
173
+ scan_nr: Integral,
174
+ subscan: Optional[Integral] = None,
175
+ scan_key: Optional[str] = None,
176
+ lima_names: Optional[List[str]] = None,
177
+ counter_names: Optional[List[str]] = None,
178
+ start_index: Optional[int] = None,
179
+ retry_timeout: Optional[float] = None,
180
+ retry_period: Optional[float] = None,
181
+ buffer_in_memory: bool = BUFFER_IN_MEMORY_DEFAULT,
182
+ **options: Any,
183
+ ) -> Iterator[Tuple[int, Dict[str, Any]]]:
184
+ """
185
+ Iterate over a Bliss scan and also return the scan-point index.
186
+
187
+ :param filename: Bliss dataset filename.
188
+ :param scan_nr: Scan number inside the dataset.
189
+ :param subscan: Sub-scan number (e.g. ``10.2`` -> ``scan_nr=10`` and ``subscan=2``).
190
+ :param scan_key: Identifier of a scan for blissdata. Iterate scan data from blissdata-cache.
191
+ :param lima_names: List of LIMA detector names.
192
+ :param counter_names: List of non-LIMA counter names.
193
+ :param start_index: First point to yield (default ``0`` -> all points).
194
+ :param retry_timeout: Timeout for the retry mechanism.
195
+ :param retry_period: Interval between retries.
196
+ :param buffer_in_memory: Blissdata buffering in memory. Only applies when `scan_key` is provided.
197
+ Use only when you have enough memory or the data processing can keep up with the scan.
198
+ :param **hdf5_options: Additional options forwarded to :class:`blissdata.h5api.dynamic_hdf5.File`.
199
+ :yields: ``(point_index, data_dict)`` where ``point_index`` is the zero-based
200
+ scan-point number and ``data_dict`` maps detector names to values.
201
+ """
202
+ start_index = 0 if start_index is None else start_index
203
+
204
+ for idx, data in enumerate(
205
+ iter_bliss_scan_data(
206
+ filename,
207
+ scan_nr,
208
+ subscan=subscan,
209
+ scan_key=scan_key,
210
+ lima_names=lima_names,
211
+ counter_names=counter_names,
212
+ retry_timeout=retry_timeout,
213
+ retry_period=retry_period,
214
+ buffer_in_memory=buffer_in_memory,
215
+ **options,
216
+ )
217
+ ):
218
+ if idx >= start_index:
219
+ yield idx, data
220
+
221
+
222
+ @contextiterator
223
+ def iter_bliss_scan_data(
224
+ filename: str,
225
+ scan_nr: Integral,
226
+ subscan: Optional[Integral] = None,
227
+ scan_key: Optional[str] = None,
228
+ lima_names: Optional[List[str]] = None,
229
+ counter_names: Optional[List[str]] = None,
230
+ retry_timeout: Optional[float] = None,
231
+ retry_period: Optional[float] = None,
232
+ buffer_in_memory: bool = BUFFER_IN_MEMORY_DEFAULT,
233
+ **hdf5_options: Any,
234
+ ) -> Iterator[Dict[str, Any]]:
235
+ """
236
+ Yield the data of a Bliss scan either from an HDF5 file or from memory (Redis or device servers).
237
+
238
+ :param filename: Path to the HDF5 / Nexus file (ignored when *scan_key* is supplied).
239
+ :param scan_nr: Scan number inside the file.
240
+ :param subscan: Sub-scan number (e.g. ``10.2`` -> ``scan_nr=10`` and ``subscan=2``).
241
+ :param scan_key: Identifier of a scan for blissdata. Iterate scan data from blissdata-cache.
242
+ :param lima_names: List of LIMA detector names.
243
+ :param counter_names: List of non-LIMA counter names.
244
+ :param retry_timeout: Timeout for the retry mechanism.
245
+ :param retry_period: Interval between retries.
246
+ :param buffer_in_memory: Blissdata buffering in memory. Only applies when `scan_key` is provided.
247
+ Use only when you have enough memory or the data processing can keep up with the scan.
248
+ :param **hdf5_options: Additional options forwarded to :class:`blissdata.h5api.dynamic_hdf5.File`.
249
+ :yields: Mapping ``detector_name -> value`` for each scan point.
250
+ """
251
+ if scan_key:
252
+ yield from iter_bliss_scan_data_from_memory(
253
+ scan_key,
254
+ lima_names=lima_names,
255
+ counter_names=counter_names,
256
+ retry_timeout=retry_timeout,
257
+ retry_period=retry_period,
258
+ buffer_in_memory=buffer_in_memory,
259
+ )
260
+ else:
261
+ yield from iter_bliss_scan_data_from_hdf5(
262
+ filename,
263
+ scan_nr,
264
+ lima_names=lima_names,
265
+ counter_names=counter_names,
266
+ subscan=subscan,
267
+ retry_timeout=retry_timeout,
268
+ retry_period=retry_period,
269
+ **hdf5_options,
270
+ )
271
+
272
+
273
+ def _is_bliss_file(h5item: Union[h5py.Dataset, h5py.Group]) -> bool:
274
+ """
275
+ Return ``True`` if *h5item* belongs to a Bliss-generated file.
276
+
277
+ :param h5item: HDF5 dataset or group.
278
+ :return: ``True`` when the file attributes indicate a Bliss creator/publisher.
279
+ """
280
+ attrs = h5item.file.attrs
281
+ creator = attrs.get("creator", "").lower()
282
+ publisher = attrs.get("publisher", "").lower()
283
+ return creator in _BLISS_PUBLISHERS or publisher in _BLISS_PUBLISHERS
284
+
285
+
286
+ _BLISS_PUBLISHERS = ("bliss", "blissdata")
@@ -1,6 +1,7 @@
1
- from packaging.version import Version
2
1
  from importlib.metadata import version
3
2
 
3
+ from packaging.version import Version
4
+
4
5
 
5
6
  def _not_supported_by_blissdata_version(*_, **kw):
6
7
  raise RuntimeError(f"Not supported by blissdata {_BLISSDATA_VERSION}")
@@ -14,12 +15,17 @@ if _BLISSDATA_VERSION >= Version("2.0.0rc1"):
14
15
  from .blissdatav2 import iter_bliss_scan_data_from_memory # noqa F401
15
16
  from .blissdatav2 import iter_bliss_scan_data_from_memory_slice # noqa F401
16
17
  from .blissdatav2 import last_lima_image # noqa F401
18
+
19
+ BUFFER_IN_MEMORY_DEFAULT = True
17
20
  elif _BLISSDATA_VERSION >= Version("1"):
18
21
  from .blissdatav1 import iter_bliss_scan_data_from_memory # noqa F401
19
22
  from .blissdatav1 import iter_bliss_scan_data_from_memory_slice # noqa F401
20
23
  from .blissdatav1 import last_lima_image # noqa F401
24
+
25
+ BUFFER_IN_MEMORY_DEFAULT = True
21
26
  else:
22
27
  from .blissdatav0 import iter_bliss_scan_data_from_memory # noqa F401
23
28
  from .blissdatav0 import last_lima_image # noqa F401
24
29
 
25
30
  iter_bliss_scan_data_from_memory_slice = _not_supported_by_blissdata_version
31
+ BUFFER_IN_MEMORY_DEFAULT = False
@@ -1,28 +1,42 @@
1
1
  import logging
2
2
  from numbers import Number
3
- from typing import List, Optional
3
+ from typing import Any
4
+ from typing import Dict
5
+ from typing import Generator
6
+ from typing import List
7
+ from typing import Optional
4
8
 
9
+ from blissdata.data.events.lima import ImageNotSaved
10
+ from blissdata.data.node import get_node
5
11
  from numpy.typing import ArrayLike
6
12
  from silx.utils import retry as retrymod
7
- from blissdata.data.node import get_node
8
- from blissdata.data.events.lima import ImageNotSaved
9
-
10
13
 
11
14
  logger = logging.getLogger(__name__)
12
15
 
13
16
 
14
17
  def iter_bliss_scan_data_from_memory(
15
18
  db_name: str,
16
- lima_names: List[str],
17
- counter_names: List[str],
19
+ lima_names: Optional[List[str]] = None,
20
+ counter_names: Optional[List[str]] = None,
18
21
  retry_timeout: Optional[Number] = None,
19
22
  retry_period: Optional[Number] = None,
20
- ):
23
+ buffer_in_memory: bool = True,
24
+ ) -> Generator[Dict[str, Any], None, None]:
25
+ if not buffer_in_memory:
26
+ raise NotImplementedError("Requires blissdata>=2.0.0rc1")
27
+ if lima_names is None:
28
+ lima_names = list()
29
+ if counter_names is None:
30
+ counter_names = list()
31
+
21
32
  scan_node = _get_node(
22
33
  db_name, "scan", retry_timeout=retry_timeout, retry_period=retry_period
23
34
  )
24
35
  indices = {name: 0 for name in lima_names + counter_names}
25
36
  buffers = {name: list() for name in lima_names + counter_names}
37
+ if not buffers:
38
+ return
39
+
26
40
  lima_acq_nb = dict()
27
41
  for event_type, node, event_data in scan_node.walk_events():
28
42
  if node.type == "lima":
@@ -1,24 +1,27 @@
1
+ import logging
1
2
  import sys
2
3
  import time
3
- import logging
4
- from numbers import Number
5
4
  from collections import Counter
6
- from typing import List, Optional, Tuple
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
7
12
 
8
13
  import numpy
9
- from numpy.typing import ArrayLike
10
14
  from blissdata.beacon.data import BeaconData
11
- from blissdata.redis_engine.store import DataStore
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
12
20
  from blissdata.redis_engine.scan import Scan
13
21
  from blissdata.redis_engine.scan import ScanState
22
+ from blissdata.redis_engine.store import DataStore
14
23
  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
24
+ from numpy.typing import ArrayLike
22
25
 
23
26
  try:
24
27
  from blissdata.stream import LimaStream
@@ -38,14 +41,24 @@ def _get_data_store() -> None:
38
41
 
39
42
  def iter_bliss_scan_data_from_memory(
40
43
  scan_key: str,
41
- lima_names: List[str],
42
- counter_names: List[str],
44
+ lima_names: Optional[List[str]] = None,
45
+ counter_names: Optional[List[str]] = None,
43
46
  retry_timeout: Optional[Number] = None,
44
47
  retry_period: Optional[Number] = None,
45
- ):
48
+ buffer_in_memory: bool = True,
49
+ ) -> Generator[Dict[str, Any], None, None]:
50
+ if not buffer_in_memory:
51
+ raise NotImplementedError("Requires blissdata>=2.0.0rc1")
52
+ if lima_names is None:
53
+ lima_names = list()
54
+ if counter_names is None:
55
+ counter_names = list()
56
+
46
57
  data_store = _get_data_store()
47
58
  scan = data_store.load_scan(scan_key, scan_cls=Scan)
48
59
  buffers = {name: list() for name in lima_names + counter_names}
60
+ if not buffers:
61
+ return
49
62
 
50
63
  while scan.state < ScanState.PREPARED:
51
64
  scan.update()
@@ -1,23 +1,36 @@
1
+ import logging
1
2
  import sys
2
3
  import time
3
- import logging
4
4
  from collections import Counter
5
- from typing import List, Optional, Tuple
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
6
13
 
7
14
  import numpy
8
- from numpy.typing import ArrayLike
9
-
10
15
  from blissdata.beacon.data import BeaconData
11
- from blissdata.redis_engine.store import DataStore
12
16
  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
-
17
+ from blissdata.redis_engine.store import DataStore
18
+ from numpy.typing import ArrayLike
19
+ from packaging.version import Version
20
+
21
+ _BLISSDATA_VERSION = Version(version("blissdata"))
22
+ if _BLISSDATA_VERSION >= Version("2.3.0"):
23
+ from blissdata.exceptions import EndOfStream
24
+ from blissdata.exceptions import IndexNoMoreThereError
25
+ from blissdata.exceptions import IndexNotYetThereError
26
+ from blissdata.exceptions import IndexWontBeThereError
27
+ from blissdata.streams import CursorGroup
28
+ else:
29
+ from blissdata.redis_engine.exceptions import EndOfStream
30
+ from blissdata.redis_engine.exceptions import IndexNoMoreThereError
31
+ from blissdata.redis_engine.exceptions import IndexNotYetThereError
32
+ from blissdata.redis_engine.exceptions import IndexWontBeThereError
33
+ from blissdata.streams.base import CursorGroup
21
34
 
22
35
  logger = logging.getLogger(__name__)
23
36
 
@@ -31,51 +44,131 @@ def _get_data_store() -> None:
31
44
 
32
45
  def iter_bliss_scan_data_from_memory(
33
46
  scan_key: str,
34
- lima_names: List[str],
35
- counter_names: List[str],
47
+ lima_names: Optional[List[str]] = None,
48
+ counter_names: Optional[List[str]] = None,
36
49
  retry_timeout: Optional[float] = None,
37
50
  retry_period: Optional[float] = None,
38
- ):
51
+ buffer_in_memory: bool = False,
52
+ ) -> Generator[Dict[str, Any], None, None]:
53
+ if lima_names is None:
54
+ lima_names = list()
55
+ if counter_names is None:
56
+ counter_names = list()
57
+
39
58
  streams = _get_streams(scan_key, lima_names, counter_names)
40
59
  if not streams:
41
60
  return
42
61
 
43
62
  cursor = CursorGroup(list(streams.values()))
44
63
  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()}
64
+ ctr_names = lima_names + counter_names
65
+ data_buffers = {ctr_name: _DataBuffer() for ctr_name in ctr_names}
66
+ stream_to_ctr_names = {
67
+ stream.name: ctr_name for ctr_name, stream in streams.items()
68
+ }
47
69
 
48
70
  while True:
49
71
  try:
50
72
  views = cursor.read(timeout=cursor_timeout)
51
73
  except EndOfStream:
74
+ # No more data - stop the generator
75
+ progress = [
76
+ f"{ctr_name}: {data_buffer.progress}"
77
+ for ctr_name, data_buffer in data_buffers.items()
78
+ ]
79
+ logger.info("Data progress on exit:\n %s", "\n".join(progress))
80
+ for ctr_name, data_buffer in data_buffers.items():
81
+ if data_buffer.length_left:
82
+ logger.warning(
83
+ "Data points for counter %r: %s",
84
+ ctr_name,
85
+ data_buffer.progress,
86
+ )
52
87
  break
53
88
 
54
- views = {ctr_names[stream.name]: view for stream, view in views.items()}
55
-
56
- nyield = min(
57
- len(buffer) + len(views.get(ctr_name, ()))
58
- for ctr_name, buffer in buffers.items()
59
- )
60
- view_positions = {ctr_name: 0 for ctr_name in buffers.keys()}
61
- for _ in range(nyield):
62
- data = {}
63
- for ctr_name, buffer in buffers.items():
64
- if buffer:
65
- data_point = buffer.pop(0)
66
- else:
67
- view = views[ctr_name]
68
- start = view_positions[ctr_name]
69
- data_points = view.get_data(start=start, stop=start + 1)
70
- data_point = data_points[0]
71
- view_positions[ctr_name] += 1
72
- data[ctr_name] = data_point
73
- yield data
74
-
75
- for ctr_name, view in views.items():
76
- start = view_positions[ctr_name]
77
- data_points = view.get_data(start=start, stop=None)
78
- buffers[ctr_name].extend(data_points)
89
+ for stream, view in views.items():
90
+ ctr_name = stream_to_ctr_names[stream.name]
91
+ data_buffers[ctr_name].append_view(view)
92
+
93
+ if buffer_in_memory:
94
+ for data_buffer in data_buffers.values():
95
+ data_buffer.buffer_in_memory()
96
+
97
+ while True:
98
+ try:
99
+ ctr_values = [
100
+ next(data_buffer) for data_buffer in data_buffers.values()
101
+ ]
102
+ except StopIteration:
103
+ # One of the buffers ran out of data
104
+ break
105
+
106
+ yield dict(zip(ctr_names, ctr_values))
107
+
108
+
109
+ class _DataBuffer(Iterator):
110
+ """Data iterator with view and data buffering for a single counter."""
111
+
112
+ def __init__(self):
113
+ self._views = []
114
+ self._view_lengths = []
115
+ self._first_view_index = 0
116
+ self._buffer = []
117
+ self._total_length = 0
118
+ self._total_yielded = 0
119
+
120
+ @property
121
+ def length_left(self) -> int:
122
+ return self._total_length - self._total_yielded
123
+
124
+ @property
125
+ def progress(self) -> str:
126
+ return f"{self._total_yielded}/{self._total_length}"
127
+
128
+ def buffer_in_memory(self) -> None:
129
+ """Buffer and clear all view objects."""
130
+ if not self._views:
131
+ return
132
+ starts = [0] * len(self._views)
133
+ starts[0] = self._first_view_index
134
+ for view, start in zip(self._views, starts):
135
+ self._buffer.extend(view.get_data(start=start, stop=None))
136
+ self._views.clear()
137
+ self._view_lengths.clear()
138
+ self._first_view_index = 0
139
+
140
+ def append_view(self, view) -> None:
141
+ """Append view object when not empty."""
142
+ view_length = len(view)
143
+ if view_length:
144
+ self._views.append(view)
145
+ self._view_lengths.append(view_length)
146
+ self._total_length += view_length
147
+
148
+ def __iter__(self) -> "_DataBuffer":
149
+ return self
150
+
151
+ def __next__(self):
152
+ """Return next scan data point for this counter."""
153
+ if self._buffer:
154
+ return self._buffer.pop(0)
155
+
156
+ if len(self._views) == 0:
157
+ raise StopIteration
158
+
159
+ view = self._views[0]
160
+ start = self._first_view_index
161
+
162
+ self._total_yielded += 1
163
+ self._first_view_index += 1
164
+
165
+ if self._first_view_index == self._view_lengths[0]:
166
+ self._views.pop(0)
167
+ self._view_lengths.pop(0)
168
+ self._first_view_index = 0
169
+
170
+ data = view.get_data(start=start, stop=start + 1)
171
+ return data[0]
79
172
 
80
173
 
81
174
  def last_lima_image(scan_key: str, lima_name: str) -> ArrayLike:
@@ -1,8 +1,8 @@
1
1
  """HDF5 configuration of optimal data storage (IO speed, compression, ...)"""
2
2
 
3
- from typing import Optional
4
- from numbers import Integral
5
3
  from collections.abc import Mapping
4
+ from numbers import Integral
5
+ from typing import Optional
6
6
 
7
7
  import numpy
8
8
  from numpy.typing import DTypeLike
@@ -12,7 +12,9 @@ try:
12
12
  except ImportError:
13
13
  hdf5plugin = None
14
14
 
15
- from .types import ShapeType, VarShapeType, VarH5pyShapeType
15
+ from .types import ShapeType
16
+ from .types import VarH5pyShapeType
17
+ from .types import VarShapeType
16
18
 
17
19
  DEFAULT_CHUNK_NBYTES = 1 << 20
18
20
  DEFAULT_COMPRESSION_LIMIT_NBYTES = 1 << 20
@@ -1,12 +1,13 @@
1
1
  import time
2
- from typing import Optional, List
2
+ from typing import List
3
+ from typing import Optional
3
4
 
4
5
  import h5py
5
6
  import numpy
6
7
  from numpy.typing import ArrayLike
7
8
 
8
- from .types import StrictPositiveIntegral
9
9
  from .config import guess_dataset_config
10
+ from .types import StrictPositiveIntegral
10
11
 
11
12
 
12
13
  class _DatasetWriterBase:
@@ -1,5 +1,7 @@
1
1
  from numbers import Integral
2
- from typing import Optional, Tuple, NewType
2
+ from typing import NewType
3
+ from typing import Optional
4
+ from typing import Tuple
3
5
 
4
6
  PositiveIntegral = NewType("PositiveIntegral", Integral) # >= 0
5
7
  StrictPositiveIntegral = NewType("StrictPositiveIntegral", Integral) # > 0
@@ -1,9 +1,14 @@
1
1
  import os
2
2
  from contextlib import contextmanager
3
- from typing import Union, Iterator, Optional, Tuple, Sequence
3
+ from typing import Iterator
4
+ from typing import Optional
5
+ from typing import Sequence
6
+ from typing import Tuple
7
+ from typing import Union
4
8
 
5
9
  import h5py
6
10
  from silx.io import h5py_utils
11
+
7
12
  from .url import DataUrl
8
13
  from .url import as_dataurl
9
14
 
@@ -1,5 +1,7 @@
1
1
  import sys
2
- from typing import Tuple, Union
2
+ from typing import Tuple
3
+ from typing import Union
4
+
3
5
  from silx.io.url import DataUrl
4
6
 
5
7
 
@@ -1,5 +1,6 @@
1
1
  from numbers import Number
2
2
  from typing import Mapping
3
+
3
4
  import numpy
4
5
 
5
6
 
@@ -1,10 +1,11 @@
1
1
  import os
2
2
  from datetime import datetime
3
3
  from pathlib import Path
4
- from typing import Optional, Tuple
5
- import numpy
6
- import h5py
4
+ from typing import Optional
5
+ from typing import Tuple
7
6
 
7
+ import h5py
8
+ import numpy
8
9
 
9
10
  __all__ = ["save_bliss_scan"]
10
11
 
@@ -1,7 +1,8 @@
1
- import numpy
2
1
  import h5py
3
- from fabio.edfimage import edfimage
2
+ import numpy
4
3
  import pytest
4
+ from fabio.edfimage import edfimage
5
+
5
6
  from ..data import bliss
6
7
 
7
8
 
@@ -19,11 +20,17 @@ def test_get_data_hdf5(tmpdir):
19
20
  img1 = numpy.random.uniform(((10, 12)))
20
21
  with h5py.File(filename, "w") as f:
21
22
  f["img"] = img1
23
+
22
24
  img2 = bliss.get_data(f"{filename}::/img")
23
25
  numpy.testing.assert_array_equal(img1, img2)
26
+
24
27
  img3 = bliss.get_data(f"{filename}?path=/img")
25
28
  numpy.testing.assert_array_equal(img1, img3)
26
29
 
30
+ filename_non_existing = str(tmpdir / "data_non_existing.h5")
31
+ with pytest.raises(IOError):
32
+ _ = bliss.get_data(f"{filename_non_existing}?path=/img")
33
+
27
34
 
28
35
  @pytest.mark.parametrize("lima_names", [(), ("p3",), ("p3", "p4")])
29
36
  @pytest.mark.parametrize("counter_names", [(), ("diode1",), ("diode1", "diode2")])
@@ -1,13 +1,12 @@
1
- import time
2
1
  import itertools
3
2
  import multiprocessing
3
+ import time
4
4
 
5
5
  import h5py
6
6
  import numpy
7
7
  import pytest
8
8
  from silx.io import h5py_utils
9
9
 
10
-
11
10
  from ..data.hdf5 import dataset_writer
12
11
 
13
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ewoksdata
3
- Version: 1.3.0
3
+ Version: 2.0.0
4
4
  Summary: Ewoks tasks for data access
5
5
  Author-email: ESRF <dau-pydev@esrf.fr>
6
6
  License: # MIT License
@@ -53,6 +53,7 @@ Provides-Extra: dev
53
53
  Requires-Dist: ewoksdata[test]; extra == "dev"
54
54
  Requires-Dist: black>=25; extra == "dev"
55
55
  Requires-Dist: flake8>=4; extra == "dev"
56
+ Requires-Dist: isort; extra == "dev"
56
57
  Provides-Extra: doc
57
58
  Requires-Dist: ewoksdata[test]; extra == "doc"
58
59
  Requires-Dist: sphinx>=4.5; extra == "doc"
@@ -14,6 +14,7 @@ redis-om!=1.0.3b0
14
14
  ewoksdata[test]
15
15
  black>=25
16
16
  flake8>=4
17
+ isort
17
18
 
18
19
  [doc]
19
20
  ewoksdata[test]
@@ -1,148 +0,0 @@
1
- import sys
2
- import logging
3
- from numbers import Integral, Number
4
- from typing import Iterator, List, Optional, Tuple, Sequence, Union
5
-
6
- import numpy
7
- import h5py
8
- import hdf5plugin # noqa F401
9
- from numpy.typing import ArrayLike
10
- from silx.io import h5py_utils
11
- from silx.utils import retry as retrymod
12
- from silx.io.utils import get_data as silx_get_data
13
-
14
- from .blissdata import iter_bliss_scan_data_from_memory # noqa F401
15
- from .blissdata import iter_bliss_scan_data_from_memory_slice # noqa F401
16
- from .blissdata import last_lima_image # noqa F401
17
- from .blissdata import dynamic_hdf5
18
-
19
- from .contextiterator import contextiterator
20
- from . import hdf5
21
- from . import nexus
22
- from . import url
23
-
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- def get_data(
29
- data: Union[str, ArrayLike, Number], **options
30
- ) -> Union[numpy.ndarray, Number]:
31
- if isinstance(data, str):
32
- data_url = url.as_dataurl(data)
33
- filename, h5path, idx = url.h5dataset_url_parse(data_url)
34
- if filename.endswith(".h5") or filename.endswith(".nx"):
35
- return _get_hdf5_data(filename, h5path, idx=idx, **options)
36
- if not data_url.scheme():
37
- if sys.platform == "win32":
38
- data_url = f"fabio:///{data}"
39
- else:
40
- data_url = f"fabio://{data}"
41
- return silx_get_data(data_url)
42
- elif isinstance(data, (Sequence, Number, numpy.ndarray)):
43
- return data
44
- else:
45
- raise TypeError(type(data))
46
-
47
-
48
- def get_image(*args, **kwargs) -> numpy.ndarray:
49
- data = get_data(*args, **kwargs)
50
- return numpy.atleast_2d(numpy.squeeze(data))
51
-
52
-
53
- @h5py_utils.retry()
54
- def _get_hdf5_data(filename: str, h5path: str, idx=None, **options) -> numpy.ndarray:
55
- with hdf5.h5context(filename, h5path, **options) as dset:
56
- if _is_bliss_file(dset):
57
- if "end_time" not in nexus.get_nxentry(dset):
58
- raise retrymod.RetryError
59
- if idx is None:
60
- idx = tuple()
61
- return dset[idx]
62
-
63
-
64
- @contextiterator
65
- def iter_bliss_scan_data(
66
- filename: str,
67
- scan_nr: Integral,
68
- lima_names: Optional[List[str]] = None,
69
- counter_names: Optional[List[str]] = None,
70
- subscan: Optional[Integral] = None,
71
- **options,
72
- ) -> Iterator[dict]:
73
- """Iterate over the data from one Bliss scan. The counters are assumed to have
74
- many data values as scan points.
75
-
76
- :param str filename: the Bliss dataset filename
77
- :param Integral filename: the scan number in the dataset
78
- :param list lima_names: names of lima detectors
79
- :param list counter_names: names of non-lima detectors (you need to provide at least one)
80
- :param Integral subscan: subscan number (for example "10.2" has `scan_nr=10` and `subscan=2`)
81
- :param Number retry_timeout: timeout when it cannot access the data for `retry_timeout` seconds
82
- :param Number retry_period: interval in seconds between data access retries
83
- :yields dict: data
84
- """
85
- if not subscan:
86
- subscan = 1
87
- if counter_names is None:
88
- counter_names = list()
89
- with dynamic_hdf5.File(filename, lima_names=lima_names, **options) as root:
90
- scan = root[f"{scan_nr}.{subscan}"]
91
- # assert _is_bliss_file(scan), "Not a Bliss dataset file"
92
- measurement = scan["measurement"]
93
- instrument = scan["instrument"]
94
- datasets = {name: measurement[name] for name in counter_names}
95
- for name in lima_names:
96
- datasets[name] = instrument[f"{name}/data"]
97
- names = list(datasets.keys())
98
- for values in zip(*datasets.values()):
99
- yield dict(zip(names, values))
100
-
101
-
102
- @contextiterator
103
- def iter_bliss_data(
104
- filename: str,
105
- scan_nr: Integral,
106
- lima_names: List[str],
107
- counter_names: List[str],
108
- subscan: Optional[Integral] = None,
109
- start_index: Optional[Integral] = None,
110
- **options,
111
- ) -> Iterator[Tuple[int, dict]]:
112
- """Iterate over the data from one Bliss scan. The counters are assumed to have
113
- many data values as scan points.
114
-
115
- :param str filename: the Bliss dataset filename
116
- :param Integral filename: the scan number in the dataset
117
- :param list lima_names: names of lima detectors
118
- :param list counter_names: names of non-lima detectors (you need to provide at least one)
119
- :param Integral subscan: subscan number (for example "10.2" has `scan_nr=10` and `subscan=2`)
120
- :param Number retry_timeout: timeout when it cannot access the data for `retry_timeout` seconds
121
- :param Number retry_period: interval in seconds between data access retries
122
- :param Integral start_index: start iterating from this scan point index
123
- :yields tuple: scan index, data
124
- """
125
- if start_index is None:
126
- start_index = 0
127
- for index, data in enumerate(
128
- iter_bliss_scan_data(
129
- filename,
130
- scan_nr,
131
- lima_names=lima_names,
132
- counter_names=counter_names,
133
- subscan=subscan,
134
- **options,
135
- )
136
- ):
137
- if index >= start_index:
138
- yield index, data
139
-
140
-
141
- def _is_bliss_file(h5item: Union[h5py.Dataset, h5py.Group]) -> bool:
142
- return (
143
- h5item.file.attrs.get("creator", "").lower() in _BLISS_PUBLISHERS
144
- or h5item.file.attrs.get("publisher", "").lower() in _BLISS_PUBLISHERS
145
- )
146
-
147
-
148
- _BLISS_PUBLISHERS = ("bliss", "blissdata")
File without changes
File without changes
File without changes
@@ -1,6 +1,6 @@
1
1
  import functools
2
- from typing import Iterator
3
2
  from collections.abc import Generator
3
+ from typing import Iterator
4
4
 
5
5
 
6
6
  class ContextIterator(Generator):
@@ -4,8 +4,8 @@ import pytest
4
4
  from silx.io import h5py_utils
5
5
  from silx.io.url import DataUrl
6
6
 
7
- from ..data import url
8
7
  from ..data import nexus
8
+ from ..data import url
9
9
 
10
10
 
11
11
  def test_create_nexus_group(tmp_path):