diff --git a/doc/source/install.rst b/doc/source/install.rst index 899a350414..ce92e6daf8 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -265,6 +265,20 @@ It installs: * `h5pyd `_, the Python client for HSDS +blissdata support ++++++++++++++++++ + +The `blissdata` extra: + +.. code-block:: bash + + pip install silx[blissdata] + +adds support for accessing experimental data through `blissdata `_. + +It installs: + +* `blissdata `_ Build dependencies ++++++++++++++++++ diff --git a/pyproject.toml b/pyproject.toml index 68a645e364..a9e124d188 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,9 @@ full = [ 'silx[full_no_qt]', 'PySide6 >= 6.5', # PyQt6, PyQt5 # For silx.gui ] +blissdata = [ + 'blissdata >= 2.4.5' +] h5pyd = [ 'h5pyd >= 0.20.0' ] diff --git a/src/silx/gui/hdf5/Hdf5Formatter.py b/src/silx/gui/hdf5/Hdf5Formatter.py index d13f2f1edb..a9944b9f39 100644 --- a/src/silx/gui/hdf5/Hdf5Formatter.py +++ b/src/silx/gui/hdf5/Hdf5Formatter.py @@ -102,8 +102,8 @@ def humanReadableValue(self, dataset): numpy_object = dataset[()] text = self.__formatter.toString(numpy_object, dtype=dataset.dtype) else: - if dataset.size < 5 and dataset.compression is None: - numpy_object = dataset[0:5] + if dataset.size > 0 and dataset.size < 5 and dataset.compression is None: + numpy_object = dataset[0 : min(len(dataset), 5)] text = self.__formatter.toString(numpy_object, dtype=dataset.dtype) else: dimension = len(dataset.shape) diff --git a/src/silx/io/blissdatah5.py b/src/silx/io/blissdatah5.py new file mode 100644 index 0000000000..546241df42 --- /dev/null +++ b/src/silx/io/blissdatah5.py @@ -0,0 +1,90 @@ +import logging +from typing import Generator, Literal + +import numpy + +from . import commonh5 + +from blissdata.h5api import abstract as _abstract +from blissdata.h5api import redis_hdf5 as _redis_hdf5 + +_logger = logging.getLogger(__name__) + + +class BlissDataH5(commonh5.File): + def __init__( + self, + name: str, + mode: Literal["r"] | None = None, + attrs: dict | None = None, + ) -> None: + if mode not in ("r", None): + raise ValueError(f"Unsupported mode: {mode}") + + if attrs is None: + attrs = {} + + self.__file = _redis_hdf5.File(name) + + super().__init__(name, mode, attrs={**self.__file.attrs, **attrs}) + + for child in _children(self.__file): + self.add_node(child) + + _logger.warning( + "blissdata support is a preview feature: This may change or be removed without notice." + ) + + def close(self) -> None: + super().close() + self.__file.close() + self.__file = None + + +class BlissDataGroup(commonh5.LazyLoadableGroup): + def __init__( + self, + name: str, + group: _abstract.Group, + parent: BlissDataH5 | "BlissDataGroup" | None = None, + attrs: dict | None = None, + ) -> None: + super().__init__(name, parent, attrs) + self.__group = group + + def _create_child(self) -> None: + for child in _children(self.__group): + self.add_node(child) + + +class BlissDataDataset(commonh5.Dataset): + @property + def shape(self) -> tuple[int, ...]: + return self._get_data().shape + + @property + def size(self) -> int: + return self._get_data().size + + def __len__(self) -> int: + return len(self._get_data()) + + def __getitem__(self, item): + if isinstance(item, tuple) and len(item): + return self._get_data()[()][item] + return self._get_data()[item] + + @property + def value(self) -> numpy.ndarray: + return self._get_data()[()] + + +def _children(group: _abstract.Group) -> Generator[BlissDataDataset | BlissDataGroup]: + for name in group.keys(): + item = group[name] + if isinstance(item, _abstract.Group): + yield BlissDataGroup(name, item, parent=group, attrs=item.attrs) + elif isinstance(item, _abstract.Dataset): + yield BlissDataDataset(name, item, parent=group, attrs=item.attrs) + else: + _logger.warning(f"Cannot map child {name}: Ignored") diff --git a/src/silx/io/meson.build b/src/silx/io/meson.build index e3deb3e28d..beb9f16d55 100644 --- a/src/silx/io/meson.build +++ b/src/silx/io/meson.build @@ -4,6 +4,7 @@ subdir('specfile') py.install_sources([ '__init__.py', '_sliceh5.py', + 'blissdatah5.py', 'commonh5.py', 'configdict.py', 'convert.py', diff --git a/src/silx/io/test/test_url.py b/src/silx/io/test/test_url.py index ac6dfeac14..959ce4bf59 100644 --- a/src/silx/io/test/test_url.py +++ b/src/silx/io/test/test_url.py @@ -179,7 +179,7 @@ def test_unknown_scheme(): expected = (False, True, "foo", "/foo/foobar.h5", "/foo/bar", (5, 1)) assert_url(url, expected) assert ( - url.invalid_reason == "Invalid scheme. It can only be fabio, silx, http, https." + url.invalid_reason == "Invalid scheme. It can only be fabio, silx, http, https, blissdata." ) diff --git a/src/silx/io/url.py b/src/silx/io/url.py index 18af14146f..84403f6e96 100644 --- a/src/silx/io/url.py +++ b/src/silx/io/url.py @@ -132,7 +132,7 @@ class DataUrl: be false. """ - _SCHEMES = ("fabio", "silx", "http", "https") + _SCHEMES = ("fabio", "silx", "http", "https", "blissdata") def __init__( self, diff --git a/src/silx/io/utils.py b/src/silx/io/utils.py index a48d0bb398..fcb3311901 100644 --- a/src/silx/io/utils.py +++ b/src/silx/io/utils.py @@ -699,6 +699,14 @@ def open(filename): # pylint:disable=redefined-builtin h5_file = _open_local_file(url.file_path()) elif url.scheme() in ("http", "https"): return _open_url_with_h5pyd(filename) + elif url.scheme() == "blissdata": + try: + from .blissdatah5 import BlissDataH5 + except ImportError: + raise IOError( + f"blissdata support is not available, cannot open: {filename}" + ) + h5_file = BlissDataH5(url.file_path()) else: raise OSError(f"Unsupported URL scheme {url.scheme}: {filename}")