ewoksdata 2.2.1__tar.gz → 2.3.0rc2__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 (51) hide show
  1. {ewoksdata-2.2.1/src/ewoksdata.egg-info → ewoksdata-2.3.0rc2}/PKG-INFO +3 -4
  2. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/pyproject.toml +25 -7
  3. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/blissdata/__init__.py +3 -0
  4. ewoksdata-2.3.0rc2/src/ewoksdata/data/blissdata/_lima.py +447 -0
  5. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/blissdata/blissdatav1.py +0 -2
  6. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/blissdata/blissdatav2.py +0 -2
  7. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/config.py +4 -3
  8. ewoksdata-2.3.0rc2/src/ewoksdata/data/hdf5/dataset_writer/_base.py +112 -0
  9. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +74 -25
  10. ewoksdata-2.3.0rc2/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +238 -0
  11. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py +7 -12
  12. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +18 -22
  13. ewoksdata-2.3.0rc2/src/ewoksdata/data/hdf5/mirror.py +673 -0
  14. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/types.py +5 -3
  15. ewoksdata-2.3.0rc2/src/ewoksdata/tests/integration/__init__.py +0 -0
  16. ewoksdata-2.3.0rc2/src/ewoksdata/tests/integration/conftest.py +90 -0
  17. ewoksdata-2.3.0rc2/src/ewoksdata/tests/integration/test_blissdata.py +48 -0
  18. ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/__init__.py +0 -0
  19. ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/conftest.py +48 -0
  20. ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/data/bliss_scans.py +239 -0
  21. {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_contextiterator.py +1 -1
  22. {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_data_bliss.py +9 -9
  23. {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_data_hdf5.py +1 -1
  24. {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_data_nexus.py +2 -2
  25. {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_dataset_writer.py +206 -12
  26. ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/test_find_lima_scans.py +217 -0
  27. ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/test_mirror_hdf5.py +1035 -0
  28. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2/src/ewoksdata.egg-info}/PKG-INFO +3 -4
  29. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata.egg-info/SOURCES.txt +16 -8
  30. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata.egg-info/requires.txt +2 -3
  31. ewoksdata-2.2.1/src/ewoksdata/data/hdf5/dataset_writer/_base.py +0 -55
  32. ewoksdata-2.2.1/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +0 -158
  33. ewoksdata-2.2.1/src/ewoksdata/tests/conftest.py +0 -30
  34. ewoksdata-2.2.1/src/ewoksdata/tests/data/bliss_scans.py +0 -137
  35. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/LICENSE.md +0 -0
  36. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/README.md +0 -0
  37. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/setup.cfg +0 -0
  38. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/__init__.py +0 -0
  39. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/__init__.py +0 -0
  40. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/bliss.py +0 -0
  41. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/blissdata/blissdatav0.py +0 -0
  42. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/contextiterator.py +0 -0
  43. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/__init__.py +0 -0
  44. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/dataset_writer/__init__.py +0 -0
  45. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/nexus.py +0 -0
  46. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/url.py +0 -0
  47. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/utils.py +0 -0
  48. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/tests/__init__.py +0 -0
  49. {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/data/__init__.py +0 -0
  50. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
  51. {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/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: 2.2.1
3
+ Version: 2.3.0rc2
4
4
  Summary: Ewoks tasks for data access
5
5
  Author-email: ESRF <dau-pydev@esrf.fr>
6
6
  License: # MIT License
@@ -40,6 +40,7 @@ Requires-Dist: h5py>=2.8
40
40
  Requires-Dist: silx>=1.1.0
41
41
  Requires-Dist: hdf5plugin
42
42
  Requires-Dist: blissdata>=0.3.4
43
+ Requires-Dist: esrf-pathlib>=1.1.0
43
44
  Requires-Dist: redis
44
45
  Requires-Dist: redis!=5.3.0b1,!=5.3.0b3
45
46
  Requires-Dist: redis-om!=1.0.0b0; python_version < "3.9"
@@ -51,9 +52,7 @@ Requires-Dist: ewoksdata[online]; extra == "test"
51
52
  Requires-Dist: pytest>=7; extra == "test"
52
53
  Provides-Extra: dev
53
54
  Requires-Dist: ewoksdata[test]; extra == "dev"
54
- Requires-Dist: black>=25; extra == "dev"
55
- Requires-Dist: flake8>=4; extra == "dev"
56
- Requires-Dist: isort; extra == "dev"
55
+ Requires-Dist: ruff; extra == "dev"
57
56
  Provides-Extra: doc
58
57
  Requires-Dist: ewoksdata[test]; extra == "doc"
59
58
  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 = "2.2.1"
7
+ version = "2.3.0rc2"
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" }
@@ -21,6 +21,7 @@ dependencies = [
21
21
  "silx >=1.1.0",
22
22
  "hdf5plugin",
23
23
  "blissdata >=0.3.4",
24
+ "esrf-pathlib >=1.1.0",
24
25
  "redis", # missing dependency in blissdata<1
25
26
  "redis !=5.3.0b1,!=5.3.0b3",
26
27
  "redis-om !=1.0.0b0;python_version < '3.9'", # https://github.com/redis/redis-om-python/issues/693
@@ -44,9 +45,7 @@ test = [
44
45
  ]
45
46
  dev = [
46
47
  "ewoksdata[test]",
47
- "black >=25",
48
- "flake8 >=4",
49
- "isort",
48
+ "ruff",
50
49
  ]
51
50
  doc = [
52
51
  "ewoksdata[test]",
@@ -64,6 +63,25 @@ where = ["src"]
64
63
  [tool.coverage.run]
65
64
  omit = ['*/tests/*']
66
65
 
67
- [tool.isort]
68
- profile = "black"
69
- force_single_line = "True"
66
+ [tool.ruff]
67
+ line-length = 88
68
+
69
+ [tool.ruff.lint]
70
+ select = [
71
+ "E", # pycodestyle errors
72
+ "F", # pyflakes
73
+ "I", # isort
74
+ "S", # flake8-bandit
75
+ ]
76
+ ignore = [
77
+ "E501", # line too long (docstrings/comments are not wrapped by the formatter)
78
+ ]
79
+
80
+ [tool.ruff.lint.per-file-ignores]
81
+ "**/test_*.py" = ["S101"]
82
+ "**/tests/**/conftest.py" = ["S101"]
83
+ "ci/*.py" = ["S101"]
84
+
85
+ [tool.ruff.lint.isort]
86
+ force-single-line = true
87
+ known-first-party = ["ewoksdata"]
@@ -29,3 +29,6 @@ else:
29
29
 
30
30
  iter_bliss_scan_data_from_memory_slice = _not_supported_by_blissdata_version
31
31
  BUFFER_IN_MEMORY_DEFAULT = True
32
+
33
+ from ._lima import LimaScan # noqa F401
34
+ from ._lima import find_lima_scans # noqa F401
@@ -0,0 +1,447 @@
1
+ import logging
2
+ import re
3
+ from dataclasses import dataclass
4
+ from importlib.metadata import version
5
+ from pathlib import Path
6
+ from typing import Dict
7
+ from typing import List
8
+ from typing import Optional
9
+ from typing import Tuple
10
+ from typing import Union
11
+
12
+ import h5py
13
+ import hdf5plugin # noqa F401
14
+ import numpy
15
+ from blissdata.h5api.utils.lima import LimaDataset
16
+ from esrf_pathlib import ESRFPath
17
+ from packaging.version import Version
18
+ from silx.io import h5py_utils
19
+
20
+ _logger = logging.getLogger(__name__)
21
+
22
+ _FORBIDDEN_CREATORS = ["lima"]
23
+ _ALLOWED_CREATORS = ["bliss", "blissdata"]
24
+
25
+ _ESRF_TEMPLATE_PROPOSAL = "raw_proposal_file"
26
+ _ESRF_TEMPLATE_COLLECTION = "raw_collection_file"
27
+ _ESRF_TEMPLATE_DATASET = "raw_dataset_file"
28
+ _ALLOWED_ESRFPATH_TEMPLATES = [
29
+ _ESRF_TEMPLATE_PROPOSAL,
30
+ _ESRF_TEMPLATE_COLLECTION,
31
+ _ESRF_TEMPLATE_DATASET,
32
+ ]
33
+
34
+
35
+ @dataclass
36
+ class LimaScan:
37
+ """A single scan/detector entry with the information to access the data, to be used for ewoks tasks.
38
+
39
+ One instance is created by :func:`find_lima_scans` per scan/detector pair, and it holds
40
+ everything needed to read the frames, either through the Bliss master file (``url``) or
41
+ directly from the Lima data files (``lima_urls``, ``lima_url_template``).
42
+
43
+ :ivar filename: The dataset master file containing the scan, i.e. the file holding the
44
+ ``1.1``, ``2.1``, ``2.2``, ... groups. Normalized as an ESRF path (mount point stripped).
45
+ :ivar url: The url to access the data through the master file, in the format ``"filename::h5path"``,
46
+ the h5path being ``<scan_nb>.<subscan_nb>/measurement/<detector_name>``.
47
+ :ivar collection_name: The name of the collection/sample, taken from the file path.
48
+ :ivar dataset_name: The name of the dataset, taken from the file path.
49
+ :ivar scan_nb: The scan number, i.e. the part before the dot in the entry name.
50
+ :ivar subscan_nb: The subscan number, i.e. the part after the dot in the entry name (most of the time 1).
51
+ :ivar sample_id: The sample id parsed from the dataset name, ``None`` unless the dataset follows
52
+ the Daiquiri ``roi<sample_id>_<subsample_id>`` convention.
53
+ :ivar subsample_id: The subsample id parsed from the dataset name, ``None`` in the same conditions
54
+ as ``sample_id``.
55
+ :ivar detector_name: The name of the detector, as found in the measurement group.
56
+ :ivar data_shape: The shape of a single frame, i.e. the dataset shape without the frame dimension.
57
+ :ivar nb_frames: The number of frames in the scan, i.e. the first dimension of the dataset.
58
+ :ivar lima_urls: The urls of the Lima files holding the data, in the format ``"filename::h5path"``.
59
+ Missing files are skipped, so this list can be shorter than expected (or empty) for broken links.
60
+ :ivar lima_url_template: The template used to find the Lima files, ``None`` when they were found
61
+ through the virtual sources of the master file instead. See :func:`find_lima_scans`.
62
+ :ivar lima_url_template_args: The arguments to format ``lima_url_template``, ``None`` when there is
63
+ no template. See :func:`_get_lima_url_template_args`.
64
+ """
65
+
66
+ filename: str
67
+ url: str
68
+ collection_name: str
69
+ dataset_name: str
70
+ scan_nb: int
71
+ subscan_nb: int
72
+ sample_id: Optional[int]
73
+ subsample_id: Optional[int]
74
+ detector_name: str
75
+ data_shape: Tuple[int, ...]
76
+ nb_frames: int
77
+ lima_urls: List[str]
78
+ lima_url_template: Optional[str]
79
+ lima_url_template_args: Optional[dict]
80
+
81
+
82
+ def _normalize_esrf_path(
83
+ filename: Optional[Union[str, Path, ESRFPath]], assert_exists: bool = True
84
+ ) -> ESRFPath:
85
+ """
86
+ This method allows to use bliss files using VISA paths and also avoids
87
+ the full path problem between mounting points.
88
+ """
89
+ if filename is not None:
90
+ filename = ESRFPath(filename).resolve().strip_mount_point()
91
+ if assert_exists and not filename.is_file():
92
+ raise FileNotFoundError(f"{filename} does not exist")
93
+ return filename
94
+
95
+
96
+ def _get_lima_url_template_args(
97
+ scan_nb: int,
98
+ img_acq_device: str,
99
+ collection_name: str,
100
+ dataset_name: str,
101
+ scan_number_format: str = "%05d",
102
+ ) -> Dict[str, str]:
103
+ """
104
+ Lima url template args is a dictionary meant to format the lima_url_template string, once it is passed to blissdata.h5api.dynamic_hdf5.File.
105
+ For the blissdata API, it is useful during online data analysis from the .h5 file (only if the Lima files are already available, depending on number_frames_per_file)
106
+ For offline data analysis, it is useful in some cases:
107
+ - ID13 sometimes has used the Eiger hardware saving format (rare cases), hence the links from the master file are broken,
108
+ and the reading should be done through the dynamicfile API using lima_url_template and lima_url_template_args, to find the correct files and links to read the data.
109
+ For the rest of the cases, lima_url_template is not needed, as the master file has the data linked to the Lima files.
110
+ """
111
+ return {
112
+ "scan_nb": scan_number_format % int(scan_nb),
113
+ "img_acq_device": img_acq_device,
114
+ "collection_name": collection_name,
115
+ "dataset_name": dataset_name,
116
+ }
117
+
118
+
119
+ def _parse_daiquiri_roi(
120
+ dataset_name: str,
121
+ ) -> Tuple[Optional[int], Optional[int]]:
122
+ """Return the ``(sample_id, subsample_id)`` encoded in a Daiquiri dataset name
123
+ (e.g. ``roi122006_140844``), or ``(None, None)`` when the name does not follow that convention.
124
+ """
125
+ match_roi = re.match(r"^roi(\d+)_(\d+)$", dataset_name)
126
+ if match_roi:
127
+ sample_id, subsample_id = int(match_roi.group(1)), int(match_roi.group(2))
128
+ else:
129
+ sample_id, subsample_id = None, None
130
+ return sample_id, subsample_id
131
+
132
+
133
+ def _validate_bliss_file(h5_file: h5py.File) -> bool:
134
+ """Return True if the HDF5 root ``creator`` attribute is a 'bliss', 'blissdata', False otherwise."""
135
+ creator = h5_file.attrs.get("creator")
136
+ if isinstance(creator, bytes):
137
+ creator = creator.decode()
138
+ creator = creator.lower()
139
+ for forbidden in _FORBIDDEN_CREATORS:
140
+ if creator in forbidden:
141
+ _logger.error(f"File creator is '{creator}' which is not allowed.")
142
+ return False
143
+ for allowed in _ALLOWED_CREATORS:
144
+ if creator in allowed:
145
+ return True
146
+ _logger.error(
147
+ f"File creator is '{creator}' which is not recognized as a valid Bliss file creator."
148
+ )
149
+ return False
150
+
151
+
152
+ def _discover_2d_detectors(measurement_group: h5py.Group) -> List[str]:
153
+ """Return the names of the 2D (Lima) detectors (3D datasets) in a measurement group."""
154
+ # Is there a better way to search for Lima devices?
155
+ return [dset for dset in measurement_group if measurement_group[dset].ndim == 3]
156
+
157
+
158
+ def _get_lima_urls_from_virtual_sources(
159
+ dset: h5py.Dataset,
160
+ ) -> List[str]:
161
+ """Return the ``filename::h5path`` urls of the Lima files backing a virtual dataset.
162
+
163
+ The virtual sources of the Bliss master file are resolved relative to its directory.
164
+ Sources pointing to a missing file are skipped with a warning, so the returned list
165
+ may be shorter than the number of virtual sources (or empty for broken links).
166
+ """
167
+ urls_lima = []
168
+ for vs in dset.virtual_sources():
169
+ filename_lima = Path(dset.file.filename).parent / vs.file_name
170
+ if not filename_lima.is_file():
171
+ _logger.warning(
172
+ f"Lima file {filename_lima} does not exist. Virtual dataset may be broken."
173
+ )
174
+ continue
175
+ urls_lima.append(f"{str(_normalize_esrf_path(filename_lima))}::{vs.dset_name}")
176
+ return urls_lima
177
+
178
+
179
+ def _get_lima_urls_from_template(
180
+ filename_dataset: str,
181
+ lima_url_template: str,
182
+ scan_nb: int,
183
+ detector: str,
184
+ collection_name: str,
185
+ dataset_name: str,
186
+ scan_number_format: Optional[str] = "%05d",
187
+ ) -> List[str]:
188
+ """Return the ``filename::h5path`` urls of the Lima files found with ``lima_url_template``.
189
+
190
+ Alternative to :func:`_get_lima_urls_from_virtual_sources` for the cases where the links of
191
+ the master file cannot be used (e.g. Eiger hardware saving): the files are searched on disk
192
+ with the blissdata API instead. Files that do not exist are skipped with a warning.
193
+
194
+ :param filename_dataset: The dataset master file; its directory is where the Lima files are searched.
195
+ :param lima_url_template: The ``"<file template>::<h5path>"`` template, see :func:`find_lima_scans`.
196
+ :param scan_nb: The scan number, formatted with ``scan_number_format``.
197
+ :param detector: The detector name, i.e. the ``{img_acq_device}`` placeholder of the template.
198
+ :param collection_name: The collection name, i.e. the ``{collection_name}`` placeholder of the template.
199
+ :param dataset_name: The dataset name, i.e. the ``{dataset_name}`` placeholder of the template.
200
+ :param scan_number_format: The format used for the scan number (default ``"%05d"``).
201
+ """
202
+ _, h5path_data = lima_url_template.split("::")
203
+ lima_url_template_args = _get_lima_url_template_args(
204
+ scan_nb=scan_nb,
205
+ img_acq_device=detector,
206
+ collection_name=collection_name,
207
+ dataset_name=dataset_name,
208
+ scan_number_format=scan_number_format,
209
+ )
210
+ limadataset_params = {
211
+ "name": f"/{scan_nb}.1/instrument/{detector}/data", # this is dummy parameter
212
+ "dirname": str(Path(filename_dataset).parent),
213
+ "url_template": lima_url_template,
214
+ "url_template_args": lima_url_template_args,
215
+ }
216
+ if Version(version("blissdata")) < Version("1.1.0"):
217
+ for p in ("url_template", "url_template_args"):
218
+ limadataset_params.pop(p)
219
+
220
+ limadataset = LimaDataset(**limadataset_params)
221
+ limadataset.search()
222
+ urls_lima = []
223
+ for lima_file in limadataset._files:
224
+ if not Path(lima_file).is_file():
225
+ _logger.warning(
226
+ f"Lima file {lima_file} does not exist. Check the lima_url_template and lima_url_template_args, the Lima files may not be found."
227
+ )
228
+ continue
229
+ urls_lima.append(f"{_normalize_esrf_path(lima_file)}::{h5path_data}")
230
+ return urls_lima
231
+
232
+
233
+ def _validate_lima_urls(lima_urls: List[str]) -> None:
234
+ """Check that the data path is an accessible dataset in each Lima file."""
235
+ for lima_url in lima_urls:
236
+ filename_lima, h5path_lima = lima_url.split("::")
237
+ with h5py_utils.File(filename=filename_lima, mode="r") as f:
238
+ if h5path_lima not in f:
239
+ raise KeyError(
240
+ f"Validation of Lima files failed: {h5path_lima} not found in {filename_lima}"
241
+ )
242
+ if not isinstance(f[h5path_lima], h5py.Dataset):
243
+ raise TypeError(
244
+ f"Validation of Lima files failed: {h5path_lima} is not a dataset in {filename_lima}"
245
+ )
246
+
247
+
248
+ def _validate_virtual_datasets(
249
+ dataset: h5py.Dataset,
250
+ nb_frames_to_validate: int = 10,
251
+ ) -> None:
252
+ """Check that the virtual dataset is readable and not all zeros (broken links)."""
253
+ try:
254
+ dataset_from_bliss_file = dataset[0:nb_frames_to_validate]
255
+ except OSError:
256
+ raise OSError(
257
+ f"Validation of Virtual datasets failed: could not read data from the dataset {dataset.name}. This may be due to broken links to Lima files."
258
+ )
259
+ if numpy.array_equal(
260
+ dataset_from_bliss_file, numpy.zeros_like(dataset_from_bliss_file)
261
+ ):
262
+ raise ValueError(
263
+ f"Validation of Virtual datasets failed: data in {dataset.name} is all zeros, which points to a broken virtual dataset (broken links to Lima files)."
264
+ )
265
+
266
+
267
+ def _parse_scansubscan_entry(h5path_entry: str) -> Tuple[int, int]:
268
+ """Return the ``(scan_nb, subscan_nb)`` integers of a Bliss root entry name (e.g. ``"2.1"``)."""
269
+ pattern_scan_subscan_entry = r"^(\d+)\.(\d+)$"
270
+ match_scan_entry = re.match(pattern_scan_subscan_entry, h5path_entry)
271
+ scan_nb = int(match_scan_entry.group(1))
272
+ subscan_nb = int(match_scan_entry.group(2))
273
+ return scan_nb, subscan_nb
274
+
275
+
276
+ def _find_bliss_dataset_files(
277
+ *args: List[Union[str, Path, ESRFPath]],
278
+ ) -> List[ESRFPath]:
279
+ """Return the sorted, deduplicated dataset master files described by ``args``.
280
+
281
+ Both files and directories are accepted: a file is taken as-is, while a directory is expanded
282
+ into the ``*.h5`` files it directly contains (not recursive). Anything that is neither an
283
+ existing file nor an existing directory is ignored.
284
+
285
+ Each resulting filename is normalized to an :class:`ESRFPath` and expanded according to its
286
+ template: a proposal file yields all dataset files of all its collections, a collection file
287
+ yields all dataset files of that collection, and a dataset file yields itself. Filenames
288
+ matching none of :data:`ALLOWED_ESRFPATH_TEMPLATES` are skipped with an error log.
289
+ """
290
+ bliss_filenames = []
291
+ for item in args:
292
+ esrf_item = ESRFPath(item)
293
+ if esrf_item.is_file():
294
+ bliss_filenames.append(esrf_item)
295
+ elif esrf_item.is_dir():
296
+ bliss_filenames += sorted(esrf_item.glob("*.h5"))
297
+
298
+ filenames_datasets = []
299
+ for bliss_filename in bliss_filenames:
300
+ esrf_filename = _normalize_esrf_path(filename=bliss_filename)
301
+ if not esrf_filename:
302
+ continue
303
+
304
+ if esrf_filename.template_name not in _ALLOWED_ESRFPATH_TEMPLATES:
305
+ _logger.error(
306
+ f"{str(esrf_filename)} is a {esrf_filename.template_name} files are not allowed. Only {_ALLOWED_ESRFPATH_TEMPLATES}"
307
+ )
308
+ continue
309
+ if esrf_filename.template_name == _ESRF_TEMPLATE_PROPOSAL:
310
+ filenames_datasets += list(
311
+ esrf_filename.parent.replace_fields(collection="*", dataset="*").glob()
312
+ )
313
+ elif esrf_filename.template_name == _ESRF_TEMPLATE_COLLECTION:
314
+ filenames_datasets += list(
315
+ esrf_filename.parent.replace_fields(dataset="*").glob()
316
+ )
317
+ elif esrf_filename.template_name == _ESRF_TEMPLATE_DATASET:
318
+ filenames_datasets += [esrf_filename]
319
+ else:
320
+ _logger.debug("Unsupported HDF5 file name %r", esrf_filename)
321
+ return sorted(set(filenames_datasets))
322
+
323
+
324
+ def find_lima_scans(
325
+ bliss_filenames: List[str],
326
+ detectors: Optional[List[str]] = None,
327
+ scans: Optional[List[int]] = None,
328
+ subscans: Optional[List[int]] = None,
329
+ lima_url_template: Optional[str] = None,
330
+ scan_number_format: Optional[str] = "%05d",
331
+ validate_data_lima_files: bool = False,
332
+ validate_data_virtual_datasets: bool = False,
333
+ nb_frames_to_validate: int = 10,
334
+ ) -> List[LimaScan]:
335
+ """
336
+ Access the given Bliss .h5 files and find the scans matching the requested scan and subscan numbers and detectors.
337
+
338
+ :param bliss_filenames: A list of HDF5 filenames (e.g. ``path/to/file.h5``), written by NexusWriter,
339
+ and/or directories containing such files (e.g. ``path/to/dir``, expanded to the ``*.h5``
340
+ files it directly contains, not recursive).
341
+ A proposal, collection or dataset file is accepted; anything else is skipped with an error log.
342
+ :param detectors: A list of detector names to filter the scans (e.g. ``["detector1", "detector2"]``). If None, all 2D detectors are considered.
343
+ :param scans: A list of scan numbers to filter the scans (e.g. ``[1, 2, 3]``). If None, all scans are considered.
344
+ :param subscans: A list of subscan numbers to filter the scans (e.g. ``[1, 2]``). If None, all subscans are considered.
345
+ :param lima_url_template: A template to find the data files. To be used if the data files don't follow the Lima format (e.g. Eiger hardware saving)
346
+ the template must be a format string with the following placeholders:
347
+ - {dirname}: the directory of the dataset master file
348
+ - {scan_nb}: the scan number integer, which will be formatted according to the parameter scan_number_format
349
+ - {img_acq_device}: the name of the detector
350
+ - {collection_name}: the collection name
351
+ - {dataset_name}: the dataset name
352
+ example: "{dirname}/scan{scan_nb}/{img_acq_device}/{collection_name}_{dataset_name}_{scan_nb}_data_{{file_index}}.h5::/entry_0000/measurement/data"
353
+ :param scan_number_format: The format to use to find the scan number in the lima_url_template (default is "%05d" for 5 digits with leading zeros)
354
+ :param validate_data_lima_files: Whether to validate if the data of the Lima files are accesible
355
+ :param validate_data_virtual_datasets: Whether to validate if the data of the Virtual datasets are accesible and not broken (e.g. all zeros).
356
+ The number of data frames in memory is controlled by `nb_frames_to_validate`
357
+ :param nb_frames_to_validate: The number of frames to load in memory for the validation of virtual datasets.
358
+
359
+ :raises KeyError: When ``validate_data_lima_files`` is enabled and the data path is missing in a Lima file.
360
+ :raises TypeError: When ``validate_data_lima_files`` is enabled and the data path is not a dataset in a Lima file.
361
+ :raises OSError: When ``validate_data_virtual_datasets`` is enabled and a virtual dataset cannot be read (broken links).
362
+ :raises ValueError: When ``validate_data_virtual_datasets`` is enabled and a virtual dataset only contains zeros (broken links).
363
+
364
+ :return: One :class:`LimaScan` object per matching scan/detector pair (empty list when nothing
365
+ matches), see :class:`LimaScan` for the description of each field.
366
+ """
367
+ lima_scans = []
368
+ filenames_datasets = _find_bliss_dataset_files(*bliss_filenames)
369
+ for filename_dataset in filenames_datasets:
370
+ with h5py_utils.File(filename=filename_dataset, mode="r") as h5_bliss_root:
371
+ if not _validate_bliss_file(h5_bliss_root):
372
+ continue
373
+
374
+ for scan_entry_path in h5_bliss_root:
375
+ scan_nb, subscan_nb = _parse_scansubscan_entry(scan_entry_path)
376
+ if scans and scan_nb not in scans:
377
+ continue
378
+ if subscans and subscan_nb not in subscans:
379
+ continue
380
+ if "measurement" not in h5_bliss_root[scan_entry_path]:
381
+ continue
382
+
383
+ # Only for Daiquiri collected datasets
384
+ roi_id, subsample_id = _parse_daiquiri_roi(filename_dataset.dataset)
385
+
386
+ measurement_group = h5_bliss_root[f"{scan_entry_path}/measurement"]
387
+ if detectors:
388
+ detectors = [d for d in detectors if d in measurement_group]
389
+ else:
390
+ detectors = _discover_2d_detectors(measurement_group)
391
+
392
+ for detector in detectors:
393
+ # Data from virtual datasets
394
+ if validate_data_virtual_datasets:
395
+ _validate_virtual_datasets(
396
+ dataset=measurement_group[detector],
397
+ nb_frames_to_validate=nb_frames_to_validate,
398
+ )
399
+
400
+ # Search the lima files
401
+ if lima_url_template:
402
+ lima_urls = _get_lima_urls_from_template(
403
+ filename_dataset=str(filename_dataset),
404
+ lima_url_template=lima_url_template,
405
+ scan_nb=scan_nb,
406
+ detector=detector,
407
+ collection_name=filename_dataset.collection,
408
+ dataset_name=filename_dataset.dataset,
409
+ scan_number_format=scan_number_format,
410
+ )
411
+ else:
412
+ lima_urls = _get_lima_urls_from_virtual_sources(
413
+ measurement_group[detector]
414
+ )
415
+
416
+ if validate_data_lima_files:
417
+ _validate_lima_urls(lima_urls)
418
+
419
+ lima_scans.append(
420
+ LimaScan(
421
+ filename=str(filename_dataset),
422
+ url=f"{filename_dataset}::{scan_nb}.{subscan_nb}/measurement/{detector}",
423
+ collection_name=filename_dataset.collection,
424
+ dataset_name=filename_dataset.dataset,
425
+ scan_nb=scan_nb,
426
+ subscan_nb=subscan_nb,
427
+ sample_id=roi_id,
428
+ subsample_id=subsample_id,
429
+ detector_name=detector,
430
+ data_shape=measurement_group[detector].shape[1:],
431
+ nb_frames=measurement_group[detector].shape[0],
432
+ lima_urls=lima_urls,
433
+ lima_url_template=lima_url_template,
434
+ lima_url_template_args=(
435
+ _get_lima_url_template_args(
436
+ scan_nb=scan_nb,
437
+ img_acq_device=detector,
438
+ collection_name=filename_dataset.collection,
439
+ dataset_name=filename_dataset.dataset,
440
+ scan_number_format=scan_number_format,
441
+ )
442
+ if lima_url_template
443
+ else None
444
+ ),
445
+ )
446
+ )
447
+ return lima_scans
@@ -190,7 +190,6 @@ def iter_bliss_scan_data_from_memory_slice(
190
190
 
191
191
  restart_buffer = time.perf_counter()
192
192
  while stream_on:
193
-
194
193
  # While loop will stop unless one single stream is successfully sliced
195
194
  stream_on = False
196
195
 
@@ -245,7 +244,6 @@ def iter_bliss_scan_data_from_memory_slice(
245
244
  incoming_buffers[stream_name] = []
246
245
 
247
246
  if not stream_on or ((time.perf_counter() - restart_buffer) > yield_timeout):
248
-
249
247
  frames_to_yield = min(
250
248
  [len(value) for value in non_yielded_buffers.values()]
251
249
  )
@@ -271,7 +271,6 @@ def iter_bliss_scan_data_from_memory_slice(
271
271
 
272
272
  restart_buffer = time.perf_counter()
273
273
  while stream_on:
274
-
275
274
  # While loop will stop unless one single stream is successfully sliced
276
275
  stream_on = False
277
276
 
@@ -326,7 +325,6 @@ def iter_bliss_scan_data_from_memory_slice(
326
325
  incoming_buffers[stream_name] = []
327
326
 
328
327
  if not stream_on or ((time.perf_counter() - restart_buffer) > yield_timeout):
329
-
330
328
  frames_to_yield = min(
331
329
  [len(value) for value in non_yielded_buffers.values()]
332
330
  )
@@ -55,9 +55,10 @@ def guess_data_shape(
55
55
  detector_shape = tuple(n if n else DEFAULT_SCAN_DIM_SIZE for n in detector_shape)
56
56
  data_shape = scan_shape + detector_shape
57
57
  if max_shape:
58
- assert len(max_shape) == len(
59
- data_shape
60
- ), "HDF5 dataset shape must have the same dimensions as maxshape"
58
+ if len(max_shape) != len(data_shape):
59
+ raise ValueError(
60
+ "HDF5 dataset shape must have the same dimensions as maxshape"
61
+ )
61
62
  data_shape = tuple(
62
63
  n1 if not n2 else max(n1, n2) for n1, n2 in zip(data_shape, max_shape)
63
64
  )