Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/polystore/zarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

DEFAULT_PLATE_NAME = os.getenv("POLYSTORE_PLATE_NAME", "Polystore_Plate")
DISK_PASSTHROUGH_EXTENSIONS = (".json", ".csv", ".txt", ".roi.zip", ".zip")
ZARR_METADATA_FILENAMES = frozenset({"zarr.json", ".zgroup", ".zarray", ".zattrs", ".zmetadata"})


def _get_attr(attrs: Any, key: str):
Expand Down Expand Up @@ -129,6 +130,26 @@ def output_format(self):
def _is_disk_passthrough_path(path: str | Path) -> bool:
return str(path).endswith(DISK_PASSTHROUGH_EXTENSIONS)

@classmethod
def _disk_passthrough_files(cls, directory: str | Path, *, recursive: bool) -> list[Path]:
"""Files this backend saved on disk inside ``directory``, never its own metadata."""
root = Path(directory)
if not root.is_dir():
return []
entries = root.rglob("*") if recursive else root.iterdir()
return sorted(
entry
for entry in entries
if entry.name not in ZARR_METADATA_FILENAMES
and cls._is_disk_passthrough_path(entry)
and entry.is_file()
)

@staticmethod
def _declares_array_store(directory: str | Path) -> bool:
root = Path(directory)
return (root / "zarr.json").is_file() or (root / ".zgroup").is_file()

def supports_file_path(self, path: str | Path) -> bool:
return self._is_disk_passthrough_path(path) or super().supports_file_path(path)

Expand Down Expand Up @@ -722,6 +743,12 @@ def _matches_filters(name: str) -> bool:
return any(name.lower().endswith(ext.lower()) for ext in extensions)
return True

# Disk-passthrough saves are this backend's files too: list them beside the arrays.
saved_on_disk = self._disk_passthrough_files(directory, recursive=recursive)
result.extend(path for path in saved_on_disk if _matches_filters(path.name))
if saved_on_disk and not self._declares_array_store(directory):
return result

try:
# Open zarr group and traverse OME-ZARR structure
location = OmeZarrLocation(store, mode="r")
Expand Down Expand Up @@ -764,9 +791,7 @@ def delete(self, path: str | Path) -> None:
IsADirectoryError: If path is a non-empty group
StorageResolutionError: For unexpected failures
"""
# Passthrough to disk backend for text files (JSON, CSV, TXT)
path_str = str(path)
if path_str.endswith((".json", ".csv", ".txt")):
if self._is_disk_passthrough_path(path):
from .backend_registry import get_backend_instance

disk_backend = get_backend_instance(Backend.DISK.value)
Expand Down Expand Up @@ -953,6 +978,10 @@ def is_dir(self, path: str | Path) -> bool:
NotADirectoryError: If resolved target is not a group
StorageResolutionError: For symlink cycles or other failures
"""
if not self._declares_array_store(path) and self._disk_passthrough_files(
path, recursive=False
):
return True
if not isinstance(self._resolved_node(path), zarr.Group):
raise NotADirectoryError(f"Zarr path is an array (file): {path}")
return True
Expand Down
38 changes: 38 additions & 0 deletions tests/test_zarr_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,44 @@ def test_zarr_declared_passthrough_support_batch_save_and_native_readback(tmp_pa
assert backend.physical_source_path(path, base_path=tmp_path) == path


def test_zarr_lists_and_reports_directories_of_its_passthrough_saves(tmp_path):
"""A result directory holding only passthrough saves is this backend's directory."""
backend = ZarrStorageBackend()
results = tmp_path / "results"
paths = [results / "A01_details.csv", results / "A01_rois.roi.zip"]
for path in paths:
backend.save("payload" if path.suffix == ".csv" else b"zip", path)
assert all(backend.exists(path) for path in paths)
assert backend.is_dir(results)
assert backend.list_files(results) == sorted(paths)
assert backend.list_files(results, extensions={".csv"}) == [paths[0]]
assert backend.list_files(results, extensions={".tif"}) == []


@pytest.mark.parametrize("name", ["table.csv", "rois.roi.zip", "bundle.zip"])
def test_zarr_deletes_every_passthrough_save_it_wrote(tmp_path, name):
backend = ZarrStorageBackend()
path = tmp_path / "results" / name
backend.save("payload" if name.endswith(".csv") else b"zip", path)
backend.delete(path)
assert not path.exists()
assert not backend.exists(path)


def test_zarr_lists_passthrough_saves_beside_arrays_without_store_metadata(tmp_path):
backend = ZarrStorageBackend()
store = tmp_path / "images"
array_path = store / "A01_s1.tif"
text_path = store / "positions.json"
backend.save(np.zeros((2, 3), dtype=np.uint16), array_path)
backend.save("[]", text_path)
assert backend.is_dir(store)
listed = backend.list_files(store)
assert text_path in listed
assert not any(path.name in zarr_module.ZARR_METADATA_FILENAMES for path in listed)
assert backend.list_files(store, extensions={".json"}) == [text_path]


@pytest.mark.parametrize("reverse", [False, True])
def test_zarr_mixed_passthrough_array_batch_rejects_before_any_writes(tmp_path, reverse):
backend = ZarrStorageBackend()
Expand Down
Loading