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
25 changes: 23 additions & 2 deletions python/zarrista/_array.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class Array:
) -> Array:
"""Use the provided metadata to open a new array at `path` in `store`.

This does **not** write the metadata to the store.
This does **not** write the metadata to the store; use
[`Array.store_metadata`][zarrista.Array.store_metadata] for that.
"""
@property
def attrs(self) -> dict[str, JSONValue]:
Expand Down Expand Up @@ -177,6 +178,15 @@ class Array:
responsible for ensuring they match the array's codec pipeline; invalid
bytes produce a chunk that cannot be decoded.
"""
def store_metadata(self) -> None:
"""Write the array's metadata to the store.

This is the write counterpart to
[`Array.from_metadata`][zarrista.Array.from_metadata], which only constructs an
in-memory array, without writing to the store.

Any existing metadata at the array's path is overwritten.
"""
def compact_chunk(
self,
chunk_indices: list[int],
Expand Down Expand Up @@ -263,7 +273,9 @@ class AsyncArray:
) -> AsyncArray:
"""Use the provided metadata to open a new array at `path` in `store`.

This does **not** write the metadata to the store.
This does **not** write the metadata to the store; use
[`AsyncArray.store_metadata`][zarrista.AsyncArray.store_metadata] for
that.
"""
@property
def attrs(self) -> dict[str, JSONValue]:
Expand Down Expand Up @@ -400,6 +412,15 @@ class AsyncArray:
responsible for ensuring they match the array's codec pipeline; invalid
bytes produce a chunk that cannot be decoded.
"""
async def store_metadata(self) -> None:
"""Write the array's metadata to the store.

This is the write counterpart to
[`AsyncArray.from_metadata`][zarrista.AsyncArray.from_metadata], which only
constructs an in-memory array, without writing to the store.

Any existing metadata at the array's path is overwritten.
"""
async def compact_chunk(
self,
chunk_indices: list[int],
Expand Down
25 changes: 22 additions & 3 deletions python/zarrista/_builder.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,27 @@ class ArrayBuilder:
) -> ArrayBuilder:
"""Return a new builder with the inner (subchunk) shape, enabling sharding."""
def create(self, store: FilesystemStore | MemoryStore, path: str) -> Array:
"""Build the array in `store` at `path` and return it."""
"""Build the array in `store` at `path` and return it.

This **does** write to the store: the array's metadata is stored at
`path`, overwriting any metadata already there. Use
[`ArrayBuilder.create_metadata`][zarrista.ArrayBuilder.create_metadata]
to build metadata without touching a store.
"""
async def create_async(self, store: AsyncStore, path: str) -> AsyncArray:
"""Build the array in an async `store` at `path` and return it."""
"""Build the array in an async `store` at `path` and return it.

This **does** write to the store: the array's metadata is stored at
`path`, overwriting any metadata already there. Use
[`ArrayBuilder.create_metadata`][zarrista.ArrayBuilder.create_metadata]
to build metadata without touching a store.
"""
def create_metadata(self) -> ArrayMetadataV3:
"""Build the array's Zarr v3 metadata without touching a store."""
"""Build the array's Zarr v3 metadata without touching a store.

Nothing is written. Pass the result to
[`Array.from_metadata`][zarrista.Array.from_metadata] to open an
in-memory array, or use
[`ArrayBuilder.create`][zarrista.ArrayBuilder.create] to build and write
in one step.
"""
12 changes: 12 additions & 0 deletions src/array/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,16 @@ impl PyAsyncArray {
Ok(())
})
}

/// Write the array metadata to the store.
fn store_metadata<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
future_into_py(py, async move {
inner
.async_store_metadata()
.await
.map_err(ZarristaError::from)?;
Ok(())
})
}
}
6 changes: 6 additions & 0 deletions src/array/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,10 @@ impl PyArray {
}
Ok(())
}

/// Write the array metadata to the store.
fn store_metadata(&self) -> ZarristaResult<()> {
self.inner.store_metadata()?;
Ok(())
}
}
82 changes: 82 additions & 0 deletions tests/array/test_store_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""`store_metadata()` writes the array's in-memory metadata to the store."""

from pathlib import Path
from typing import TYPE_CHECKING

import pytest
from obstore.store import LocalStore

from zarrista import Array, AsyncArray
from zarrista.exceptions import ZarristaError
from zarrista.store import FilesystemStore, MemoryStore

if TYPE_CHECKING:
from zarr_metadata import ArrayMetadataV3


def _metadata(data_type: str = "int16") -> "ArrayMetadataV3":
return {
"zarr_format": 3,
"attributes": {},
"chunk_grid": {"name": "regular", "configuration": {"chunk_shape": (2, 2)}},
"data_type": data_type,
"chunk_key_encoding": {"name": "default", "configuration": {"separator": "/"}},
"fill_value": 0,
"node_type": "array",
"shape": (4, 4),
"codecs": ({"name": "bytes"},),
}


def test_from_metadata_does_not_write() -> None:
store = MemoryStore()
Array.from_metadata(_metadata(), store)

# Nothing was written, so there is no array to open.
with pytest.raises(ZarristaError):
Array.open(store)


def test_store_metadata_makes_array_openable() -> None:
store = MemoryStore()
array = Array.from_metadata(_metadata(), store)
array.store_metadata()

reopened = Array.open(store)
assert reopened.shape == [4, 4]
assert reopened.dtype.name == "int16"


def test_store_metadata_overwrites(tmp_path: Path) -> None:
store = FilesystemStore(tmp_path)
Array.from_metadata(_metadata("int16"), store).store_metadata()
Array.from_metadata(_metadata("float64"), store).store_metadata()

assert Array.open(FilesystemStore(tmp_path)).dtype.name == "float64"


def test_read_only_store_metadata_raises() -> None:
store = MemoryStore()
read_only = Array.from_metadata(_metadata(), store).read_only()
with pytest.raises(ZarristaError, match="read only"):
read_only.store_metadata()


# --- async --------------------------------------------------------------------


async def test_async_store_metadata_makes_array_openable(tmp_path: Path) -> None:
store = LocalStore(str(tmp_path))
array = AsyncArray.from_metadata(_metadata(), store)
await array.store_metadata()

reopened = await AsyncArray.open_async(LocalStore(str(tmp_path)))
assert reopened.shape == [4, 4]
assert reopened.dtype.name == "int16"


async def test_async_read_only_store_metadata_raises(tmp_path: Path) -> None:
store = LocalStore(str(tmp_path))
read_only = AsyncArray.from_metadata(_metadata(), store).read_only()
with pytest.raises(ZarristaError, match="read only"):
await read_only.store_metadata()
18 changes: 18 additions & 0 deletions tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from zarrista import (
Array,
ArrayBuilder,
ChunkGrid,
ChunkKeyEncoding,
Expand Down Expand Up @@ -65,6 +66,23 @@ def test_create_returns_configured_array():
assert array.dimension_names == ["y", "x"]


def test_create_writes_metadata():
"""`create` writes metadata, so the array can be reopened from the store."""
store = MemoryStore()
_builder().create(store, "/a")

assert Array.open(store, "/a").shape == [8, 8]


def test_create_overwrites_existing_metadata():
"""`create` overwrites metadata already present at `path`."""
store = MemoryStore()
_builder().create(store, "/a")
_builder().shape([16, 16]).create(store, "/a")

assert Array.open(store, "/a").shape == [16, 16]


def test_dimension_names_can_be_cleared():
meta = (
_builder().dimension_names(["y", "x"]).dimension_names(None).create_metadata()
Expand Down
Loading