ewoksdata 1.2.0__tar.gz → 1.3.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.2.0/src/ewoksdata.egg-info → ewoksdata-1.3.0}/PKG-INFO +2 -1
  2. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/pyproject.toml +2 -1
  3. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/bliss.py +7 -1
  4. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/blissdata/blissdatav2.py +25 -9
  5. ewoksdata-1.3.0/src/ewoksdata/data/nexus.py +182 -0
  6. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/url.py +8 -3
  7. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/test_data_nexus.py +17 -3
  8. {ewoksdata-1.2.0 → ewoksdata-1.3.0/src/ewoksdata.egg-info}/PKG-INFO +2 -1
  9. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata.egg-info/requires.txt +1 -0
  10. ewoksdata-1.2.0/src/ewoksdata/data/nexus.py +0 -111
  11. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/LICENSE.md +0 -0
  12. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/README.md +0 -0
  13. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/setup.cfg +0 -0
  14. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/__init__.py +0 -0
  15. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/__init__.py +0 -0
  16. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/blissdata/__init__.py +0 -0
  17. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/blissdata/blissdatav0.py +0 -0
  18. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/blissdata/blissdatav1.py +0 -0
  19. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/contextiterator.py +0 -0
  20. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/hdf5/__init__.py +0 -0
  21. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/hdf5/config.py +0 -0
  22. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/hdf5/dataset_writer.py +0 -0
  23. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/hdf5/types.py +0 -0
  24. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/data/utils.py +0 -0
  25. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/__init__.py +0 -0
  26. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/conftest.py +0 -0
  27. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/data/__init__.py +0 -0
  28. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/data/bliss_scans.py +0 -0
  29. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/test_contextiterator.py +0 -0
  30. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/test_data_bliss.py +0 -0
  31. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/test_data_hdf5.py +0 -0
  32. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata/tests/test_dataset_writer.py +0 -0
  33. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata.egg-info/SOURCES.txt +0 -0
  34. {ewoksdata-1.2.0 → ewoksdata-1.3.0}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
  35. {ewoksdata-1.2.0 → ewoksdata-1.3.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.2.0
3
+ Version: 1.3.0
4
4
  Summary: Ewoks tasks for data access
5
5
  Author-email: ESRF <dau-pydev@esrf.fr>
6
6
  License: # MIT License
@@ -43,6 +43,7 @@ Requires-Dist: blissdata>=0.3.4
43
43
  Requires-Dist: redis
44
44
  Requires-Dist: redis!=5.3.0b1,!=5.3.0b3
45
45
  Requires-Dist: redis-om!=1.0.0b0; python_version < "3.9"
46
+ Requires-Dist: redis-om!=1.0.3b0; python_version < "3.9"
46
47
  Provides-Extra: online
47
48
  Requires-Dist: blissdata[tango]; extra == "online"
48
49
  Provides-Extra: test
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ewoksdata"
7
- version = "1.2.0"
7
+ version = "1.3.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" }
@@ -24,6 +24,7 @@ dependencies = [
24
24
  "redis", # missing dependency in blissdata<1
25
25
  "redis !=5.3.0b1,!=5.3.0b3",
26
26
  "redis-om !=1.0.0b0;python_version < '3.9'", # https://github.com/redis/redis-om-python/issues/693
27
+ "redis-om !=1.0.3b0;python_version < '3.9'", # https://github.com/redis/redis-om-python/issues/706
27
28
  ]
28
29
 
29
30
  [project.urls]
@@ -139,4 +139,10 @@ def iter_bliss_data(
139
139
 
140
140
 
141
141
  def _is_bliss_file(h5item: Union[h5py.Dataset, h5py.Group]) -> bool:
142
- return h5item.file.attrs.get("creator", "").lower() == "bliss"
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")
@@ -51,15 +51,31 @@ def iter_bliss_scan_data_from_memory(
51
51
  except EndOfStream:
52
52
  break
53
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()}
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)
63
79
 
64
80
 
65
81
  def last_lima_image(scan_key: str, lima_name: str) -> ArrayLike:
@@ -0,0 +1,182 @@
1
+ import os
2
+ from contextlib import contextmanager
3
+ from typing import Union, Iterator, Optional, Tuple, Sequence
4
+
5
+ import h5py
6
+ from silx.io import h5py_utils
7
+ from .url import DataUrl
8
+ from .url import as_dataurl
9
+
10
+
11
+ def ensure_nxclass(group: h5py.Group) -> None:
12
+ """
13
+ Ensure the HDF5 group has an appropriate ``NX_class`` attribute.
14
+
15
+ The attribute is assigned based on the group's depth in the hierarchy:
16
+ * Root group (``/``): ``NXroot``
17
+ * First-level group: ``NXentry``
18
+ * Deeper groups: ``NXcollection``
19
+
20
+ :param group: The HDF5 group to update.
21
+ :type group: h5py.Group
22
+ """
23
+ if group.attrs.get("NX_class"):
24
+ return
25
+ groups = [s for s in group.name.split("/") if s]
26
+ n = len(groups)
27
+ if n == 0:
28
+ group.attrs["NX_class"] = "NXroot"
29
+ elif n == 1:
30
+ group.attrs["NX_class"] = "NXentry"
31
+ else:
32
+ group.attrs["NX_class"] = "NXcollection"
33
+
34
+
35
+ def select_default_plot(nxdata: h5py.Group) -> None:
36
+ """
37
+ Set the default NXdata group for visualization in the parent hierarchy.
38
+
39
+ The function walks up the group tree and sets the ``default`` attribute
40
+ in each parent to point to the given NXdata group.
41
+
42
+ :param nxdata: An HDF5 group representing an NXdata node.
43
+ :type nxdata: h5py.Group
44
+ """
45
+ parent = nxdata.parent
46
+ for name in nxdata.name.split("/")[::-1]:
47
+ if not name:
48
+ continue
49
+ parent.attrs["default"] = name
50
+ parent = parent.parent
51
+
52
+
53
+ def create_url(url: str, overwrite: bool = False, **open_options) -> DataUrl:
54
+ """
55
+ Create (or ensure the existence of) an HDF5 group specified by a URL.
56
+
57
+ :param url: The HDF5 URL (e.g. ``path/to/file.h5::/entry/data``).
58
+ :param overwrite: If True, overwrite the final group if it exists.
59
+ If False, reuse existing groups. Default is False.
60
+ :param open_options: Additional options passed to ``h5py.File``.
61
+ :return: A DataUrl object pointing to the created or existing group.
62
+ """
63
+ url = as_dataurl(url)
64
+ filename = url.file_path()
65
+ data_path = url.data_path() or "/"
66
+ dirname = os.path.dirname(filename)
67
+ if dirname:
68
+ os.makedirs(dirname, exist_ok=True)
69
+ _ = open_options.setdefault("mode", "a")
70
+ with h5py_utils.open_item(filename, "/", **open_options) as parent:
71
+ h5item, _ = _create_h5group(parent, data_path, overwrite=overwrite)
72
+ return as_dataurl(f"{filename}::{h5item.name}")
73
+
74
+
75
+ def get_nxentry(h5item: Union[h5py.Dataset, h5py.Group]) -> h5py.Group:
76
+ """
77
+ Return the top-level NXentry group containing the given HDF5 item.
78
+
79
+ :param h5item: An HDF5 dataset or group.
80
+ :return: The NXentry group containing ``h5item``.
81
+ :raises ValueError: If the item does not belong to an NXentry hierarchy.
82
+ """
83
+ parts = [s for s in h5item.name.split("/") if s]
84
+ if parts:
85
+ return h5item.file[parts[0]]
86
+ raise ValueError("HDF5 item must be part of an NXentry")
87
+
88
+
89
+ @contextmanager
90
+ def create_nexus_group(
91
+ url: Union[str, DataUrl],
92
+ retry_timeout=None,
93
+ retry_period=None,
94
+ default_levels: Optional[Sequence[str]] = None,
95
+ **open_options,
96
+ ) -> Iterator[Tuple[h5py.Group, bool]]:
97
+ """
98
+ Context manager to create or access an HDF5 group specified by a URL.
99
+
100
+ The data path inside HDF5 is ensured to have at least ``default_levels`` levels.
101
+
102
+ :param url: The HDF5 URL (e.g. ``path/to/file.h5::/entry/data``).
103
+ :param retry_timeout: Maximum time in seconds to retry opening the file.
104
+ :param retry_period: Time in seconds between retries.
105
+ :param default_levels: Minimum path hierarchy touse when missing. Default is ``["/", "results"]``.
106
+ :param open_options: Additional options passed to ``h5py.File``.
107
+ :yields: Tuple of (HDF5 group, already_existed flag).
108
+ """
109
+ url = as_dataurl(url)
110
+ filename = url.file_path()
111
+ itemname = url.data_path() or "/"
112
+ dirname = os.path.dirname(filename)
113
+ if dirname:
114
+ os.makedirs(dirname, exist_ok=True)
115
+ open_options.setdefault("mode", "a")
116
+ with h5py_utils.open_item(
117
+ filename,
118
+ "/",
119
+ retry_timeout=retry_timeout,
120
+ retry_period=retry_period,
121
+ **open_options,
122
+ ) as root:
123
+ yield _create_h5group(root, itemname, default_levels=default_levels)
124
+
125
+
126
+ def _create_h5group(
127
+ parent: h5py.Group,
128
+ data_path: str,
129
+ default_levels: Optional[Sequence[str]] = None,
130
+ overwrite: bool = False,
131
+ ) -> Tuple[h5py.Group, bool]:
132
+ """
133
+ Create or retrieve an HDF5 group under a given parent.
134
+
135
+ Ensures that intermediate groups exist, optionally enforcing a minimum
136
+ depth using ``default_levels``.
137
+
138
+ Missing ``NX_class`` attributes are automatically created on all levels.
139
+ The final group can be replaced if ``overwrite=True``.
140
+
141
+ :param parent: Parent group under which the path should be created.
142
+ :param data_path: Group path to create relative to ``parent``.
143
+ :param default_levels: Minimum hierarchy to enforce.
144
+ If ``data_path`` is shorter, extra levels are added.
145
+ :param overwrite: If True, delete and recreate the final group if it exists.
146
+ Default is False.
147
+ :return: Tuple of (HDF5 group, already_existed flag).
148
+ """
149
+ # Ensure the data path has at least N levels as defined by ``default_levels``
150
+ groups = [s for s in data_path.split("/") if s]
151
+ if default_levels:
152
+ default_levels = list(default_levels)
153
+ else:
154
+ default_levels = ["results"]
155
+ if len(groups) < len(default_levels):
156
+ groups += default_levels[len(groups) :]
157
+
158
+ # Ensure parent groups exist + ensure NX_class
159
+ create = False
160
+ ensure_nxclass(parent)
161
+ for group in groups[:-1]:
162
+ if group in parent:
163
+ parent = parent[group]
164
+ else:
165
+ parent = parent.create_group(group)
166
+ create = True
167
+ ensure_nxclass(parent)
168
+
169
+ # Final group: create, overwrite or confirm existence + ensure NX_class
170
+ group = groups[-1]
171
+ if group in parent:
172
+ if overwrite:
173
+ del parent[group]
174
+ parent = parent.create_group(group)
175
+ else:
176
+ parent = parent[group]
177
+ else:
178
+ parent = parent.create_group(group)
179
+ create = True
180
+ ensure_nxclass(parent)
181
+
182
+ return parent, not create
@@ -11,12 +11,17 @@ def as_dataurl(url: Union[str, DataUrl]) -> DataUrl:
11
11
  return url
12
12
 
13
13
 
14
- def h5dataset_url_parse(url: Union[str, DataUrl]) -> Tuple[str, str, Tuple]:
14
+ def as_h5url(url: Union[str, DataUrl]) -> DataUrl:
15
15
  url = as_dataurl(url)
16
+ if not url.data_path():
17
+ return DataUrl(f"{url.file_path()}::/")
18
+ return url
19
+
20
+
21
+ def h5dataset_url_parse(url: Union[str, DataUrl]) -> Tuple[str, str, Tuple]:
22
+ url = as_h5url(url)
16
23
  filename = str(url.file_path())
17
24
  h5path = url.data_path()
18
- if h5path is None:
19
- h5path = "/"
20
25
  idx = url.data_slice()
21
26
  if idx is None:
22
27
  idx = tuple()
@@ -1,8 +1,10 @@
1
1
  import time
2
2
 
3
+ import pytest
3
4
  from silx.io import h5py_utils
4
5
  from silx.io.url import DataUrl
5
6
 
7
+ from ..data import url
6
8
  from ..data import nexus
7
9
 
8
10
 
@@ -38,11 +40,23 @@ def test_create_nexus_group(tmp_path):
38
40
  assert h5item.name == "/c/b"
39
41
 
40
42
 
41
- def test_create_url(tmp_path):
42
- input_url = DataUrl(f"{tmp_path / 'file.h5'}::/1.1/measurement/integrated").path()
43
+ @pytest.mark.parametrize("as_string", [True, False])
44
+ @pytest.mark.parametrize("data_path", ["", "::/1.1/measurement/integrated"])
45
+ def test_create_url(tmp_path, data_path, as_string):
46
+ tmp_url = DataUrl(f"{tmp_path / 'file.h5'}{data_path}")
47
+
48
+ if data_path:
49
+ expected_url = url.as_dataurl(tmp_url.path())
50
+ else:
51
+ expected_url = url.as_dataurl(f"{tmp_url.path()}::/results")
52
+
53
+ if as_string:
54
+ input_url = tmp_url.path()
55
+ else:
56
+ input_url = tmp_url
43
57
 
44
58
  created_url = nexus.create_url(input_url)
45
- assert created_url == nexus.as_dataurl(input_url)
59
+ assert created_url == expected_url
46
60
 
47
61
  with h5py_utils.open_item(
48
62
  created_url.file_path(), created_url.data_path(), mode="a"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ewoksdata
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Ewoks tasks for data access
5
5
  Author-email: ESRF <dau-pydev@esrf.fr>
6
6
  License: # MIT License
@@ -43,6 +43,7 @@ Requires-Dist: blissdata>=0.3.4
43
43
  Requires-Dist: redis
44
44
  Requires-Dist: redis!=5.3.0b1,!=5.3.0b3
45
45
  Requires-Dist: redis-om!=1.0.0b0; python_version < "3.9"
46
+ Requires-Dist: redis-om!=1.0.3b0; python_version < "3.9"
46
47
  Provides-Extra: online
47
48
  Requires-Dist: blissdata[tango]; extra == "online"
48
49
  Provides-Extra: test
@@ -8,6 +8,7 @@ redis!=5.3.0b1,!=5.3.0b3
8
8
 
9
9
  [:python_version < "3.9"]
10
10
  redis-om!=1.0.0b0
11
+ redis-om!=1.0.3b0
11
12
 
12
13
  [dev]
13
14
  ewoksdata[test]
@@ -1,111 +0,0 @@
1
- import os
2
- from contextlib import contextmanager
3
- from typing import Union, Iterator, Optional, Tuple
4
-
5
- import h5py
6
- from silx.io import h5py_utils
7
- from .url import DataUrl
8
- from .url import as_dataurl
9
-
10
-
11
- def ensure_nxclass(group: h5py.Group) -> None:
12
- if group.attrs.get("NX_class"):
13
- return
14
- groups = [s for s in group.name.split("/") if s]
15
- n = len(groups)
16
- if n == 0:
17
- group.attrs["NX_class"] = "NXroot"
18
- elif n == 1:
19
- group.attrs["NX_class"] = "NXentry"
20
- else:
21
- group.attrs["NX_class"] = "NXcollection"
22
-
23
-
24
- def select_default_plot(nxdata: h5py.Group) -> None:
25
- parent = nxdata.parent
26
- for name in nxdata.name.split("/")[::-1]:
27
- if not name:
28
- continue
29
- parent.attrs["default"] = name
30
- parent = parent.parent
31
-
32
-
33
- def create_url(url: str, overwrite: bool = False, **open_options) -> DataUrl:
34
- url = as_dataurl(url)
35
- filename = url.file_path()
36
- dirname = os.path.dirname(filename)
37
- if dirname:
38
- os.makedirs(dirname, exist_ok=True)
39
- open_options.setdefault("mode", "a")
40
- with h5py_utils.open_item(filename, "/", **open_options) as parent:
41
- if overwrite and url.data_path() in parent:
42
- del parent[url.data_path()]
43
- h5item, _ = _create_h5group(parent, url.data_path())
44
- return as_dataurl(f"{filename}::{h5item.name}")
45
-
46
-
47
- def get_nxentry(h5item: Union[h5py.Dataset, h5py.Group]):
48
- parts = [s for s in h5item.name.split("/") if s]
49
- if parts:
50
- return h5item.file[parts[0]]
51
- raise ValueError("HDF5 item must be part of an NXentry")
52
-
53
-
54
- @contextmanager
55
- def create_nexus_group(
56
- url: Union[str, DataUrl],
57
- retry_timeout=None,
58
- retry_period=None,
59
- default_levels: Optional[Tuple[str]] = None,
60
- **open_options,
61
- ) -> Iterator[Tuple[h5py.Group, bool]]:
62
- """
63
- :yields: (h5group, already_existed)
64
- """
65
- url = as_dataurl(url)
66
- filename = url.file_path()
67
- itemname = url.data_path()
68
- if not itemname:
69
- itemname = "/"
70
- dirname = os.path.dirname(filename)
71
- if dirname:
72
- os.makedirs(dirname, exist_ok=True)
73
- open_options.setdefault("mode", "a")
74
- with h5py_utils.open_item(
75
- filename,
76
- "/",
77
- retry_timeout=retry_timeout,
78
- retry_period=retry_period,
79
- **open_options,
80
- ) as root:
81
- yield _create_h5group(root, itemname, default_levels=default_levels)
82
-
83
-
84
- def _create_h5group(
85
- parent, data_path: Optional[str], default_levels: Optional[Tuple[str]] = None
86
- ) -> Tuple[h5py.Group, bool]:
87
- """
88
- :yields: (h5group, already_existed)
89
- """
90
- if not data_path:
91
- data_path = ""
92
- groups = [""] + [s for s in data_path.split("/") if s]
93
- if default_levels:
94
- default_levels = [""] + list(default_levels)
95
- else:
96
- default_levels = ["", "results"]
97
- if len(groups) < len(default_levels):
98
- groups += default_levels[len(groups) :]
99
- data_path = "/".join(groups)
100
- groups[0] = "/"
101
-
102
- create = False
103
- ensure_nxclass(parent)
104
- for group in groups:
105
- if group in parent:
106
- parent = parent[group]
107
- else:
108
- parent = parent.create_group(group)
109
- create = True
110
- ensure_nxclass(parent)
111
- return parent, not create
File without changes
File without changes
File without changes