"""Remote files: `clawhdf5.File(url)` / `File.open_url(url, ...)` read over HTTP range requests (clawhdf5-remote's block cache), against a server in this process (conftest.RangeServer). Values are compared with h5py reading the same file locally; the rest checks what the server saw (only the blocks a read needs are fetched), the failure modes (no range support, a missing file, a file that changes, a server that goes away: errors, never wrong data), and that the GIL is released while a read waits on the network.""" import os import sys import threading import time import numpy as np import pytest import clawhdf5 from conftest import RangeServer def _write(h5py, path): rng = np.random.default_rng(7) with h5py.File(path, "w") as f: f.create_dataset("contig", data=rng.standard_normal((400, 300))) f.create_dataset( "chunked", data=rng.integers(0, 1000, size=(512, 512), dtype="= 2 def test_a_small_read_fetches_only_its_blocks(h5py, remote_file): """With 4 KiB blocks, opening and reading one chunk of a 1 MB chunked dataset costs a handful of requests and a few blocks, not the file.""" path, url, server = remote_file size = os.path.getsize(path) f = clawhdf5.File.open_url(url, block_size=4096) opened = server.requests() assert opened == 1, server.log ds = f["chunked"] got = ds[0:10, 0:10] with h5py.File(path, "r") as theirs: np.testing.assert_array_equal(got, theirs["chunked"][0:10, 0:10]) stats = f.remote_stats assert stats["bytes_fetched"] < size / 4, (stats, size) assert server.requests() - opened <= 12, server.log # A second read of the same region is served by the cache. before = server.requests() ds[0:10, 0:10] assert server.requests() == before assert f.remote_stats["hits"] > stats["hits"] assert clawhdf5.File(str(path), "r").remote_stats is None def test_server_without_range_support(h5py, tmp_path): """A server that ignores Range answers 200 with the whole file: that is an OSError by default, and a whole download when allowed.""" path = tmp_path / "remote.h5" _write(h5py, str(path)) server = RangeServer(tmp_path, ranges=False) try: url = server.url("remote.h5") with pytest.raises(OSError, match="range"): clawhdf5.File(url) with clawhdf5.File.open_url(url, allow_full_download=True) as ours, h5py.File(path, "r") as theirs: np.testing.assert_array_equal(ours["chunked"][...], theirs["chunked"][...]) np.testing.assert_array_equal(ours["contig"][5], theirs["contig"][5]) with pytest.raises(OSError): clawhdf5.File.open_url(url, allow_full_download=True, max_full_download=1000) finally: server.close() def test_errors_are_oserrors(remote_file): _, url, server = remote_file with pytest.raises(OSError, match="404"): clawhdf5.File(server.url("missing.h5")) with pytest.raises(ValueError, match="read-only"): clawhdf5.File(url, "r+") with pytest.raises(ValueError, match="read-only"): clawhdf5.File(url, "w") with pytest.raises(OSError, match="unsupported URL"): clawhdf5.File("nosuchscheme://x/y.h5") with pytest.raises(ValueError): clawhdf5.File.open_url(url, block_size=0) with pytest.raises(TypeError): clawhdf5.File.open_url(url, no_such_option=1) def test_object_store_urls_need_their_features(): """The default wheel has no S3/GCS/Azure clients (aws-lc-rs builds C): such a URL is an OSError naming the build feature.""" for url, feature in [("s3://bucket/k.h5", "s3"), ("gs://b/k.h5", "gcs"), ("az://c/k.h5", "azure")]: try: clawhdf5.File(url) except OSError as e: if "feature" in str(e): assert f"`{feature}`" in str(e), str(e) else: pytest.fail(f"{url} opened") def test_https_needs_the_https_feature(): """The default wheel has no TLS stack (rustls needs ring, which builds C): an https URL is an OSError that names the build feature.""" with pytest.raises(OSError) as e: clawhdf5.File("https://127.0.0.1:1/x.h5") msg = str(e.value) # Built with `--features https` the error is the refused connection. assert "https" in msg or "connect" in msg.lower() or "refused" in msg.lower(), msg def test_a_changed_file_is_an_error_not_mixed_data(h5py, remote_file): path, url, _ = remote_file f = clawhdf5.File.open_url(url, block_size=1024) first = f["grp/small"][...] # Rewrite the file with other values: new ETag, same name. time.sleep(0.01) with h5py.File(path, "w") as g: g.create_dataset("contig", data=np.zeros((400, 300))) with pytest.raises(OSError, match="changed"): f["contig"][...] np.testing.assert_array_equal(first, np.arange(10, dtype=" before assert took >= 0.2, took assert progress["n"] > 1000, progress # Held across a 0.2 s request, the spinner would stall that long. assert progress["worst"] < 0.1, (progress, took) def test_a_clawhdf5_written_file_reads_the_same_remotely(tmp_path, range_server): path = tmp_path / "ours.h5" data = np.arange(3000, dtype="