From 9493b7ffbaecb9e77ecb4c5fb2905a106b28ad9b Mon Sep 17 00:00:00 2001 From: Lawson Woods Date: Sat, 15 Jun 2024 21:15:49 -0700 Subject: [PATCH 01/13] initial commit --- kerchunk/hdf.py | 34 ++++++++++++++++++++++++++++++++-- kerchunk/tests/air_linked.nc | Bin 0 -> 75948 bytes kerchunk/tests/test_hdf.py | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 kerchunk/tests/air_linked.nc diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index 46c325c8..0339be4a 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -113,7 +113,7 @@ def __init__( self.error = error lggr.debug(f"HDF5 file URI: {self._uri}") - def translate(self): + def translate(self, preserve_linked_dsets=False): """Translate content of one HDF5 file into Zarr storage format. This method is the main entry point to execute the workflow, and @@ -121,6 +121,13 @@ def translate(self): No data is copied out of the HDF5 file. + Parameters + ---------- + preserve_linked_dsets : bool (optional, default False) + If True, translate HDF5 soft and hard links for each `h5py.Dataset` + into the reference structure. Requires h5py version 3.11.0 or later. + Will not translate external links or links to `h5py.Group` objects. + Returns ------- dict @@ -128,7 +135,17 @@ def translate(self): """ lggr.debug("Translation begins") self._transfer_attrs(self._h5f, self._zroot) + self._h5f.visititems(self._translator) + + if preserve_linked_dsets: + if h5py.__version__ < "3.11.0": + raise RuntimeError( + "preserve_links requires h5py 3.11.0 or later, " + f"found {h5py.__version__}" + ) + self._h5f.visititems_links(self._translator) + if self.spec < 1: return self.store elif isinstance(self.store, LazyReferenceMapper): @@ -247,10 +264,23 @@ def _decode_filters(self, h5obj: Union[h5py.Dataset, h5py.Group]): ) return filters - def _translator(self, name: str, h5obj: Union[h5py.Dataset, h5py.Group]): + def _translator( + self, + name: str, + h5obj: Union[ + h5py.Dataset, h5py.Group, h5py.SoftLink, h5py.HardLink, h5py.ExternalLink + ], + ): """Produce Zarr metadata for all groups and datasets in the HDF5 file.""" try: # method must not raise exception kwargs = {} + + if isinstance(h5obj, h5py.SoftLink) or isinstance(h5obj, h5py.HardLink): + h5obj = self._h5f[name] + if isinstance(h5obj, h5py.Group): + # continues iteration of visititems_links + return None + if isinstance(h5obj, h5py.Dataset): lggr.debug(f"HDF5 dataset: {h5obj.name}") lggr.debug(f"HDF5 compression: {h5obj.compression}") diff --git a/kerchunk/tests/air_linked.nc b/kerchunk/tests/air_linked.nc new file mode 100644 index 0000000000000000000000000000000000000000..c8d4136e1ecfe1a3265d9cff71c1c66b689adf03 GIT binary patch literal 75948 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 bQuOnc5m!m)>tA?)&S90Iz`vLB{D=MroykEN literal 0 HcmV?d00001 diff --git a/kerchunk/tests/test_hdf.py b/kerchunk/tests/test_hdf.py index a0cadce5..fa91c9bf 100644 --- a/kerchunk/tests/test_hdf.py +++ b/kerchunk/tests/test_hdf.py @@ -6,6 +6,7 @@ import pytest import xarray as xr import zarr +import h5py from kerchunk.hdf import SingleHdf5ToZarr from kerchunk.combine import MultiZarrToZarr, drop @@ -92,9 +93,11 @@ def test_multizarr(generate_mzz): assert set(ds) == set(expected) for name in ds: exp = { - k: (v.tolist() if v.size > 1 else v[0]) - if isinstance(v, np.ndarray) - else v + k: ( + (v.tolist() if v.size > 1 else v[0]) + if isinstance(v, np.ndarray) + else v + ) for k, v in expected[name].attrs.items() } assert dict(ds[name].attrs) == dict(exp) @@ -331,3 +334,27 @@ def test_inline_threshold(): fn, inline_threshold=1e9 ).translate() assert inline_0 != inline_1_million + + +@pytest.mark.skipif( + not h5py.__version__ >= "3.11.0", + reason="'h5py.Group.visititems_links' requires h5py 3.11.0 or later", +) +def test_translate_links(): + fn = osp.join(here, "air_linked.nc") + # choose a threshold that will give both inline and non-inline + # datasets for maximum test coverage + out = kerchunk.hdf.SingleHdf5ToZarr(fn, inline_threshold=50).translate( + preserve_linked_dsets=True + ) + fs = fsspec.filesystem("reference", fo=out) + z = zarr.open(fs.get_mapper()) + + # 1. Test the hard linked datasets were translated correctly + # 2. Test the soft linked datasets were translated correctly + for link in ("hard", "soft"): + for dset in ("lat", "time"): + assert z[f"{dset}_{link}"].shape == z[dset].shape + for key in z[f"{dset}_{link}"].attrs.keys(): + if key not in kerchunk.hdf._HIDDEN_ATTRS and key != "_ARRAY_DIMENSIONS": + assert z[f"{dset}_{link}"].attrs[key] == z[dset].attrs[key] From 5a48720f3cd017c53bca626f50a49fecca6c463d Mon Sep 17 00:00:00 2001 From: Lawson Woods Date: Sat, 15 Jun 2024 21:24:24 -0700 Subject: [PATCH 02/13] require instead of create group/dset --- kerchunk/hdf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index 0339be4a..0783ed5e 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -462,7 +462,7 @@ def _translator( ) # Create a Zarr array equivalent to this HDF5 dataset... - za = self._zroot.create_dataset( + za = self._zroot.require_dataset( h5obj.name, shape=h5obj.shape, dtype=dt or h5obj.dtype, @@ -510,7 +510,7 @@ def _translator( elif isinstance(h5obj, h5py.Group): lggr.debug(f"HDF5 group: {h5obj.name}") - zgrp = self._zroot.create_group(h5obj.name) + zgrp = self._zroot.require_group(h5obj.name) self._transfer_attrs(h5obj, zgrp) except Exception as e: import traceback From 2d73e5768860ac50bb70d743578593b2a8676245 Mon Sep 17 00:00:00 2001 From: Lawson Woods Date: Sat, 15 Jun 2024 21:28:24 -0700 Subject: [PATCH 03/13] spelling --- kerchunk/hdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index 0783ed5e..e578bae6 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -141,7 +141,7 @@ def translate(self, preserve_linked_dsets=False): if preserve_linked_dsets: if h5py.__version__ < "3.11.0": raise RuntimeError( - "preserve_links requires h5py 3.11.0 or later, " + "preserve_linked_dsets requires h5py 3.11.0 or later, " f"found {h5py.__version__}" ) self._h5f.visititems_links(self._translator) From a631bcb23c42e6f2d4c0853663a53c5979f0f0fd Mon Sep 17 00:00:00 2001 From: ljwoods2 Date: Tue, 25 Jun 2024 09:38:08 -0700 Subject: [PATCH 04/13] test dataset content rather than shape --- kerchunk/tests/test_hdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kerchunk/tests/test_hdf.py b/kerchunk/tests/test_hdf.py index fa91c9bf..1e0ec06b 100644 --- a/kerchunk/tests/test_hdf.py +++ b/kerchunk/tests/test_hdf.py @@ -354,7 +354,7 @@ def test_translate_links(): # 2. Test the soft linked datasets were translated correctly for link in ("hard", "soft"): for dset in ("lat", "time"): - assert z[f"{dset}_{link}"].shape == z[dset].shape + np.testing.assert_allclose(z[dset], z[f"{dset}_{link}"]) for key in z[f"{dset}_{link}"].attrs.keys(): if key not in kerchunk.hdf._HIDDEN_ATTRS and key != "_ARRAY_DIMENSIONS": assert z[f"{dset}_{link}"].attrs[key] == z[dset].attrs[key] From 89567e85d80ee6599f9252cf30c393eb1bcb7bc3 Mon Sep 17 00:00:00 2001 From: Lawson Woods Date: Sun, 30 Jun 2024 12:02:56 -0700 Subject: [PATCH 05/13] improved version compatibility check --- kerchunk/hdf.py | 12 ++++++---- kerchunk/tests/test_hdf.py | 45 ++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index e578bae6..3885060a 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -2,6 +2,7 @@ import io import logging from typing import Union, BinaryIO +from importlib.metadata import version import fsspec.core from fsspec.implementations.reference import LazyReferenceMapper @@ -139,10 +140,12 @@ def translate(self, preserve_linked_dsets=False): self._h5f.visititems(self._translator) if preserve_linked_dsets: - if h5py.__version__ < "3.11.0": + try: + h5py.Group.visititems_links + except AttributeError: raise RuntimeError( - "preserve_linked_dsets requires h5py 3.11.0 or later, " - f"found {h5py.__version__}" + "'preserve_linked_dsets' kwarg requires h5py 3.11.0 or later " + f"is installed, found {version("h5py")}" ) self._h5f.visititems_links(self._translator) @@ -275,10 +278,11 @@ def _translator( try: # method must not raise exception kwargs = {} - if isinstance(h5obj, h5py.SoftLink) or isinstance(h5obj, h5py.HardLink): + if isinstance(h5obj, (h5py.SoftLink, h5py.HardLink)): h5obj = self._h5f[name] if isinstance(h5obj, h5py.Group): # continues iteration of visititems_links + lggr.debug(f"Skipping translation of HDF5 linked group: '{h5obj.name}'") return None if isinstance(h5obj, h5py.Dataset): diff --git a/kerchunk/tests/test_hdf.py b/kerchunk/tests/test_hdf.py index 1e0ec06b..51f1d63e 100644 --- a/kerchunk/tests/test_hdf.py +++ b/kerchunk/tests/test_hdf.py @@ -25,7 +25,9 @@ def test_single(): m = fsspec.get_mapper( "reference://", fo=test_dict, remote_protocol="s3", remote_options=so ) - ds = xr.open_dataset(m, engine="zarr", backend_kwargs=dict(consolidated=False)) + ds = xr.open_dataset( + m, engine="zarr", backend_kwargs=dict(consolidated=False) + ) with fsspec.open(url, **so) as f: expected = xr.open_dataset(f, engine="h5netcdf") @@ -84,7 +86,9 @@ def test_multizarr(generate_mzz): m = fsspec.get_mapper( "reference://", fo=test_dict, remote_protocol="s3", remote_options=so ) - ds = xr.open_dataset(m, engine="zarr", backend_kwargs=dict(consolidated=False)) + ds = xr.open_dataset( + m, engine="zarr", backend_kwargs=dict(consolidated=False) + ) with fsspec.open_files(urls, **so) as fs: expts = [xr.open_dataset(f, engine="h5netcdf") for f in fs] @@ -132,9 +136,9 @@ def times_data(tmpdir): lon = xr.DataArray(np.linspace(-90, 90, 10), dims=["lon"], name="lon") time_attrs = {"axis": "T", "long_name": "time", "standard_name": "time"} time1 = xr.DataArray( - np.arange(-631108800000000000, -630158390000000000, 86400000000000).view( - "datetime64[ns]" - ), + np.arange( + -631108800000000000, -630158390000000000, 86400000000000 + ).view("datetime64[ns]"), dims=["time"], name="time", attrs=time_attrs, @@ -162,7 +166,9 @@ def test_times(times_data): "reference://", fo=test_dict, ) - result = xr.open_dataset(m, engine="zarr", backend_kwargs=dict(consolidated=False)) + result = xr.open_dataset( + m, engine="zarr", backend_kwargs=dict(consolidated=False) + ) expected = x1.to_dataset() xr.testing.assert_equal(result, expected) @@ -178,7 +184,9 @@ def test_times_str(times_data): "reference://", fo=test_dict, ) - result = xr.open_dataset(m, engine="zarr", backend_kwargs=dict(consolidated=False)) + result = xr.open_dataset( + m, engine="zarr", backend_kwargs=dict(consolidated=False) + ) expected = x1.to_dataset() xr.testing.assert_equal(result, expected) @@ -201,7 +209,9 @@ def test_string_embed(): def test_string_null(): fn = osp.join(here, "vlen.h5") - h = kerchunk.hdf.SingleHdf5ToZarr(fn, fn, vlen_encode="null", inline_threshold=0) + h = kerchunk.hdf.SingleHdf5ToZarr( + fn, fn, vlen_encode="null", inline_threshold=0 + ) out = h.translate() fs = fsspec.filesystem("reference", fo=out) z = zarr.open(fs.get_mapper()) @@ -240,7 +250,9 @@ def test_string_decode(): def test_compound_string_null(): fn = osp.join(here, "vlen2.h5") with open(fn, "rb") as f: - h = kerchunk.hdf.SingleHdf5ToZarr(f, fn, vlen_encode="null", inline_threshold=0) + h = kerchunk.hdf.SingleHdf5ToZarr( + f, fn, vlen_encode="null", inline_threshold=0 + ) out = h.translate() fs = fsspec.filesystem("reference", fo=out) z = zarr.open(fs.get_mapper()) @@ -336,8 +348,16 @@ def test_inline_threshold(): assert inline_0 != inline_1_million +def has_visititems_links(): + try: + h5py.Group.visititems_links + except AttributeError: + return False + return True + + @pytest.mark.skipif( - not h5py.__version__ >= "3.11.0", + not has_visititems_links(), reason="'h5py.Group.visititems_links' requires h5py 3.11.0 or later", ) def test_translate_links(): @@ -356,5 +376,8 @@ def test_translate_links(): for dset in ("lat", "time"): np.testing.assert_allclose(z[dset], z[f"{dset}_{link}"]) for key in z[f"{dset}_{link}"].attrs.keys(): - if key not in kerchunk.hdf._HIDDEN_ATTRS and key != "_ARRAY_DIMENSIONS": + if ( + key not in kerchunk.hdf._HIDDEN_ATTRS + and key != "_ARRAY_DIMENSIONS" + ): assert z[f"{dset}_{link}"].attrs[key] == z[dset].attrs[key] From 561c829a98d7adfe8a657dffba4d676b4f754804 Mon Sep 17 00:00:00 2001 From: Lawson Woods Date: Sun, 30 Jun 2024 12:08:36 -0700 Subject: [PATCH 06/13] undo black autoformat --- kerchunk/tests/test_hdf.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/kerchunk/tests/test_hdf.py b/kerchunk/tests/test_hdf.py index 51f1d63e..ae80c5bf 100644 --- a/kerchunk/tests/test_hdf.py +++ b/kerchunk/tests/test_hdf.py @@ -25,9 +25,7 @@ def test_single(): m = fsspec.get_mapper( "reference://", fo=test_dict, remote_protocol="s3", remote_options=so ) - ds = xr.open_dataset( - m, engine="zarr", backend_kwargs=dict(consolidated=False) - ) + ds = xr.open_dataset(m, engine="zarr", backend_kwargs=dict(consolidated=False)) with fsspec.open(url, **so) as f: expected = xr.open_dataset(f, engine="h5netcdf") @@ -86,9 +84,7 @@ def test_multizarr(generate_mzz): m = fsspec.get_mapper( "reference://", fo=test_dict, remote_protocol="s3", remote_options=so ) - ds = xr.open_dataset( - m, engine="zarr", backend_kwargs=dict(consolidated=False) - ) + ds = xr.open_dataset(m, engine="zarr", backend_kwargs=dict(consolidated=False)) with fsspec.open_files(urls, **so) as fs: expts = [xr.open_dataset(f, engine="h5netcdf") for f in fs] @@ -136,9 +132,9 @@ def times_data(tmpdir): lon = xr.DataArray(np.linspace(-90, 90, 10), dims=["lon"], name="lon") time_attrs = {"axis": "T", "long_name": "time", "standard_name": "time"} time1 = xr.DataArray( - np.arange( - -631108800000000000, -630158390000000000, 86400000000000 - ).view("datetime64[ns]"), + np.arange(-631108800000000000, -630158390000000000, 86400000000000).view( + "datetime64[ns]" + ), dims=["time"], name="time", attrs=time_attrs, @@ -166,9 +162,7 @@ def test_times(times_data): "reference://", fo=test_dict, ) - result = xr.open_dataset( - m, engine="zarr", backend_kwargs=dict(consolidated=False) - ) + result = xr.open_dataset(m, engine="zarr", backend_kwargs=dict(consolidated=False)) expected = x1.to_dataset() xr.testing.assert_equal(result, expected) @@ -184,9 +178,7 @@ def test_times_str(times_data): "reference://", fo=test_dict, ) - result = xr.open_dataset( - m, engine="zarr", backend_kwargs=dict(consolidated=False) - ) + result = xr.open_dataset(m, engine="zarr", backend_kwargs=dict(consolidated=False)) expected = x1.to_dataset() xr.testing.assert_equal(result, expected) @@ -209,9 +201,7 @@ def test_string_embed(): def test_string_null(): fn = osp.join(here, "vlen.h5") - h = kerchunk.hdf.SingleHdf5ToZarr( - fn, fn, vlen_encode="null", inline_threshold=0 - ) + h = kerchunk.hdf.SingleHdf5ToZarr(fn, fn, vlen_encode="null", inline_threshold=0) out = h.translate() fs = fsspec.filesystem("reference", fo=out) z = zarr.open(fs.get_mapper()) @@ -250,9 +240,7 @@ def test_string_decode(): def test_compound_string_null(): fn = osp.join(here, "vlen2.h5") with open(fn, "rb") as f: - h = kerchunk.hdf.SingleHdf5ToZarr( - f, fn, vlen_encode="null", inline_threshold=0 - ) + h = kerchunk.hdf.SingleHdf5ToZarr(f, fn, vlen_encode="null", inline_threshold=0) out = h.translate() fs = fsspec.filesystem("reference", fo=out) z = zarr.open(fs.get_mapper()) From de48c1cd3a577dd674851c42c097419035a2d2cf Mon Sep 17 00:00:00 2001 From: Lawson Woods Date: Sun, 30 Jun 2024 18:03:16 -0700 Subject: [PATCH 07/13] has_visititems_links suggestions --- kerchunk/hdf.py | 7 ++++--- kerchunk/tests/test_hdf.py | 10 +--------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index 3885060a..2ee8081d 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -140,9 +140,7 @@ def translate(self, preserve_linked_dsets=False): self._h5f.visititems(self._translator) if preserve_linked_dsets: - try: - h5py.Group.visititems_links - except AttributeError: + if not has_visititems_links(): raise RuntimeError( "'preserve_linked_dsets' kwarg requires h5py 3.11.0 or later " f"is installed, found {version("h5py")}" @@ -673,3 +671,6 @@ def _is_netcdf_datetime(dataset: h5py.Dataset): def _is_netcdf_variable(dataset: h5py.Dataset): return any("_Netcdf4" in _ for _ in dataset.attrs) + +def has_visititems_links(): + return hasattr(h5py.Group, "visititems_links") diff --git a/kerchunk/tests/test_hdf.py b/kerchunk/tests/test_hdf.py index ae80c5bf..b352dc99 100644 --- a/kerchunk/tests/test_hdf.py +++ b/kerchunk/tests/test_hdf.py @@ -8,7 +8,7 @@ import zarr import h5py -from kerchunk.hdf import SingleHdf5ToZarr +from kerchunk.hdf import SingleHdf5ToZarr, has_visititems_links from kerchunk.combine import MultiZarrToZarr, drop here = osp.dirname(__file__) @@ -336,14 +336,6 @@ def test_inline_threshold(): assert inline_0 != inline_1_million -def has_visititems_links(): - try: - h5py.Group.visititems_links - except AttributeError: - return False - return True - - @pytest.mark.skipif( not has_visititems_links(), reason="'h5py.Group.visititems_links' requires h5py 3.11.0 or later", From 5ebb0f4aa6efae73018627eb8f557db59abe5ae5 Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Sun, 30 Jun 2024 22:28:39 -0400 Subject: [PATCH 08/13] Update kerchunk/hdf.py --- kerchunk/hdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index 2ee8081d..9dd52744 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -143,7 +143,7 @@ def translate(self, preserve_linked_dsets=False): if not has_visititems_links(): raise RuntimeError( "'preserve_linked_dsets' kwarg requires h5py 3.11.0 or later " - f"is installed, found {version("h5py")}" + f"is installed, found {h5py.__version__)}" ) self._h5f.visititems_links(self._translator) From 3bccb5ac47f9411421bc8ac885cfa5d853422ea5 Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Sun, 30 Jun 2024 22:30:35 -0400 Subject: [PATCH 09/13] Update kerchunk/hdf.py --- kerchunk/hdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index 9dd52744..b1d06fd7 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -143,7 +143,7 @@ def translate(self, preserve_linked_dsets=False): if not has_visititems_links(): raise RuntimeError( "'preserve_linked_dsets' kwarg requires h5py 3.11.0 or later " - f"is installed, found {h5py.__version__)}" + f"is installed, found {h5py.__version__}" ) self._h5f.visititems_links(self._translator) From 6964b405456d52ff5c3c63dc03762f3e38e209a3 Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Sun, 30 Jun 2024 22:32:36 -0400 Subject: [PATCH 10/13] Update kerchunk/tests/test_hdf.py lint --- kerchunk/tests/test_hdf.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/kerchunk/tests/test_hdf.py b/kerchunk/tests/test_hdf.py index b352dc99..a20be2ad 100644 --- a/kerchunk/tests/test_hdf.py +++ b/kerchunk/tests/test_hdf.py @@ -356,8 +356,5 @@ def test_translate_links(): for dset in ("lat", "time"): np.testing.assert_allclose(z[dset], z[f"{dset}_{link}"]) for key in z[f"{dset}_{link}"].attrs.keys(): - if ( - key not in kerchunk.hdf._HIDDEN_ATTRS - and key != "_ARRAY_DIMENSIONS" - ): + if key not in kerchunk.hdf._HIDDEN_ATTRS and key != "_ARRAY_DIMENSIONS": assert z[f"{dset}_{link}"].attrs[key] == z[dset].attrs[key] From 6e48c41b06bbc4e97a7627a421bc41800873abbe Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Sun, 30 Jun 2024 22:41:58 -0400 Subject: [PATCH 11/13] Update kerchunk/hdf.py --- kerchunk/hdf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index b1d06fd7..bdab55a9 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -280,7 +280,9 @@ def _translator( h5obj = self._h5f[name] if isinstance(h5obj, h5py.Group): # continues iteration of visititems_links - lggr.debug(f"Skipping translation of HDF5 linked group: '{h5obj.name}'") + lggr.debug( + f"Skipping translation of HDF5 linked group: '{h5obj.name}'" + ) return None if isinstance(h5obj, h5py.Dataset): From 9a5499aaee71fc5bd1745649c0e75d1c7057c227 Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Sun, 30 Jun 2024 22:45:26 -0400 Subject: [PATCH 12/13] Update kerchunk/hdf.py --- kerchunk/hdf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index bdab55a9..8fad60ef 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -2,7 +2,6 @@ import io import logging from typing import Union, BinaryIO -from importlib.metadata import version import fsspec.core from fsspec.implementations.reference import LazyReferenceMapper From 3f7cacb6a7cfaec95f75fb23ac7fae141ea21963 Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Sun, 30 Jun 2024 22:45:58 -0400 Subject: [PATCH 13/13] Update kerchunk/hdf.py --- kerchunk/hdf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kerchunk/hdf.py b/kerchunk/hdf.py index 8fad60ef..b93a8937 100644 --- a/kerchunk/hdf.py +++ b/kerchunk/hdf.py @@ -673,5 +673,6 @@ def _is_netcdf_datetime(dataset: h5py.Dataset): def _is_netcdf_variable(dataset: h5py.Dataset): return any("_Netcdf4" in _ for _ in dataset.attrs) + def has_visititems_links(): return hasattr(h5py.Group, "visititems_links")