From 8017d6b357a72a3b9ac553daa88f5ab013768f50 Mon Sep 17 00:00:00 2001 From: Kenneth Li Date: Wed, 30 Jul 2025 12:28:37 -0400 Subject: [PATCH 1/5] obstore delete_dir --- src/zarr/storage/_obstore.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/zarr/storage/_obstore.py b/src/zarr/storage/_obstore.py index e1469a991e..39699f6662 100644 --- a/src/zarr/storage/_obstore.py +++ b/src/zarr/storage/_obstore.py @@ -13,6 +13,7 @@ Store, SuffixByteRequest, ) +from zarr.core.common import concurrent_map from zarr.core.config import config if TYPE_CHECKING: @@ -196,6 +197,15 @@ async def delete(self, key: str) -> None: with contextlib.suppress(FileNotFoundError): await obs.delete_async(self.store, key) + async def delete_dir(self, prefix: str) -> None: + # docstring inherited + self._check_writable() + if prefix != "" and not prefix.endswith("/"): + prefix += "/" + + keys = [(k,) async for k in self.list_prefix(prefix)] + await concurrent_map(keys, self.delete, limit=config.get("async.concurrency")) + @property def supports_partial_writes(self) -> bool: # docstring inherited From 9ebbd5806f84c1333de3b4dc6f00e4eb70dc34aa Mon Sep 17 00:00:00 2001 From: Kenneth Li Date: Wed, 30 Jul 2025 12:34:09 -0400 Subject: [PATCH 2/5] add cl --- changes/3310.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/3310.feature.rst diff --git a/changes/3310.feature.rst b/changes/3310.feature.rst new file mode 100644 index 0000000000..b21d3219fc --- /dev/null +++ b/changes/3310.feature.rst @@ -0,0 +1 @@ +Add obstore implementation of delete_dir. From 146297c8ed74e7757f87036cf5d1e370473c825e Mon Sep 17 00:00:00 2001 From: Kenneth Li Date: Wed, 30 Jul 2025 12:55:52 -0400 Subject: [PATCH 3/5] add a delete test --- tests/test_store/test_object.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index d8b89e56b7..73681d54d4 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -90,6 +90,17 @@ async def test_store_getsize_prefix(self, store: ObjectStore) -> None: total_size = await store.getsize_prefix("c") assert total_size == len(buf) * 2 + async def test_store_delete(self, store: ObjectStore) -> None: + assert store.supports_deletes + buf = cpu.Buffer.from_bytes(b"\x01\x02\x03\x04") + await store.set("foo/1", buf) + await store.set("foo/2", buf) + await store.delete("foo/1") + assert not await store.exists("foo/1") + assert await store.exists("foo/2") + await store.delete_dir("foo") # FileNotFoundErrors are suppressed + assert not await store.exists("foo/2") + @pytest.mark.slow_hypothesis def test_zarr_hierarchy(): From 2e99a4d4cfe07dcd6316e263904d528b7d323491 Mon Sep 17 00:00:00 2001 From: Kenneth Li Date: Thu, 31 Jul 2025 11:01:45 -0400 Subject: [PATCH 4/5] use obstore list and collect_async --- src/zarr/storage/_obstore.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/zarr/storage/_obstore.py b/src/zarr/storage/_obstore.py index 39699f6662..a311442555 100644 --- a/src/zarr/storage/_obstore.py +++ b/src/zarr/storage/_obstore.py @@ -199,11 +199,14 @@ async def delete(self, key: str) -> None: async def delete_dir(self, prefix: str) -> None: # docstring inherited + import obstore as obs + self._check_writable() if prefix != "" and not prefix.endswith("/"): prefix += "/" - keys = [(k,) async for k in self.list_prefix(prefix)] + metas = await obs.list(self.store, prefix).collect_async() + keys = [(m["path"],) for m in metas] await concurrent_map(keys, self.delete, limit=config.get("async.concurrency")) @property From 0735f523f4e18860257dea167e2ab811da59db5c Mon Sep 17 00:00:00 2001 From: Kenneth Li Date: Sat, 6 Sep 2025 14:47:48 -0400 Subject: [PATCH 5/5] delete test --- tests/test_store/test_object.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index 73681d54d4..d8b89e56b7 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -90,17 +90,6 @@ async def test_store_getsize_prefix(self, store: ObjectStore) -> None: total_size = await store.getsize_prefix("c") assert total_size == len(buf) * 2 - async def test_store_delete(self, store: ObjectStore) -> None: - assert store.supports_deletes - buf = cpu.Buffer.from_bytes(b"\x01\x02\x03\x04") - await store.set("foo/1", buf) - await store.set("foo/2", buf) - await store.delete("foo/1") - assert not await store.exists("foo/1") - assert await store.exists("foo/2") - await store.delete_dir("foo") # FileNotFoundErrors are suppressed - assert not await store.exists("foo/2") - @pytest.mark.slow_hypothesis def test_zarr_hierarchy():