For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a zarr_metadata.model subpackage — frozen-dataclass models that are canonical, lossless representations of Zarr v2/v3 metadata documents — by porting the prototyped array models from ~/dev/zng/zng_metadata and adding new group/consolidated models, so a later zarr-python PR can consume them.
Architecture: zarr_metadata.model sits on top of the existing raw TypedDicts (ArrayMetadataV3, GroupMetadataV3, ConsolidatedMetadataV3, ZGroupMetadata, ...). Every v3 extension point is a ZarrMetadataV3 (name + configuration) value. Models do structural validation only; no codec/dtype/grid interpretation. One PR against d-v-b/zarr-python main.
Tech Stack: Python ≥3.11, frozen slotted kw-only dataclasses, typing_extensions (TypedDict/TypeIs/Unpack/TypeAliasType), pytest, pyright strict.
- Package floor is
requires-python = ">=3.11": notype X = ...statements, no barefrom typing import TypeIs/Unpack— usetyping_extensions. - Reuse
JSONValuefromzarr_metadata._common(aTypeAliasType); do NOT port zng'stype JSON = ...alias. Recursive aliases must stayTypeAliasType(pydantic TypeAdapter fixture tests break otherwise). - JSON arrays are tuples:
_arrays_to_tuplesconverts on ingest; models hold tuples. fill_valueis held verbatim in JSON form (NaN as the string"NaN", etc.); models never interpret dtypes.- No falsy conditionals:
is None/ explicit tests, neverif not x:on possibly-falsy values. - Docstrings use plain single backticks (no Sphinx roles, no double backticks).
- Every new test function gets a docstring stating what behavior it verifies. Compact happy paths via parametrization; one named test per failure mode.
- Type
must_understandasbool, enforceFalseat runtime (__post_init__). - pyright strict is the checker for this package (
cd packages/zarr-metadata && uvx pyright src); root-repoprek run mypydoes not cover it. Ruff extends the root config withtarget-version = "py311". - Tests run as
cd packages/zarr-metadata && uv run --group test pytest tests. - Commits: conventional commits + trailer
Assisted-by: ClaudeCode:claude-fable-5. - Port editorially (capture spirit, not letter): apply the adaptation table exactly, but keep zng's structure/docstrings where they are already good.
- Port source:
/Users/d-v-b/dev/zng/zng_metadata/src/zng_metadata/_metadata.py(580 lines),/Users/d-v-b/dev/zng/zng_metadata/src/zng_metadata/__init__.py,/Users/d-v-b/dev/zng/zng_metadata/tests/test_metadata.py(1026 lines),/Users/d-v-b/dev/zng/zng_metadata/tests/_cases.py(42 lines). - Existing raw types:
packages/zarr-metadata/src/zarr_metadata/—_common.py(JSONValue,MetadataFieldV3area),v3/array.py(ArrayMetadataV3,ExtensionFieldV3),v3/group.py(GroupMetadataV3),v3/consolidated.py(ConsolidatedMetadataV3),v2/array.py(ZArrayMetadata,ArrayMetadataV2,CodecMetadataV2,DataTypeMetadataV2),v2/group.py(ZGroupMetadata,GroupMetadataV2),v2/consolidated.py(ConsolidatedMetadataV2),v2/attributes.py(ZAttrsMetadata).
| zng source | ported form |
|---|---|
from zng_metadata ... imports |
from zarr_metadata.model ... (absolute, matching package convention) |
type JSON = str | int | ... alias + JSON exports |
delete; use JSONValue from zarr_metadata._common everywhere (incl. TypeIs[JSONValue]) |
type ArrayMetadataStoreKeyV3 = Literal["zarr.json"] (and the other two type aliases) |
plain assignment: ArrayMetadataStoreKeyV3 = Literal["zarr.json"] |
from typing import ... TypeIs, Unpack, TypedDict |
TypeIs, Unpack, TypedDict from typing_extensions; the rest stays typing |
from zarr_metadata import (...) (zng imported the released package) |
same names, now in-tree: keep absolute from zarr_metadata import ... / from zarr_metadata.v2 import ... |
module name _metadata.py |
split: model/_validation.py + model/_array.py (boundaries specified in Tasks 1–2) |
packages/zarr-metadata/src/zarr_metadata/model/
__init__.py # public re-exports of everything below
_validation.py # ValidationProblem, MetadataValidationError, _prefix,
# validate/is/parse_json, validate/is/parse_metadata_field_v3,
# validate/is/parse_array_metadata_v3, validate/is/parse_array_metadata_v2,
# validate/is/parse_group_metadata_v3, validate/is/parse_group_metadata_v2,
# _is_int_sequence, _validate_attributes, _arrays_to_tuples,
# ARRAY_METADATA_*_KEYS_* and GROUP_METADATA_*_KEYS_* constants
_array.py # ZarrMetadataV3, ArrayMetadataModelV3(+Partial),
# ArrayMetadataModelV2(+Partial), store-key constants
_group.py # GroupMetadataModelV3(+Partial), GroupMetadataModelV2(+Partial),
# ConsolidatedMetadataModelV3, ConsolidatedMetadataModelV2,
# group/consolidated store-key constants
packages/zarr-metadata/tests/model/
__init__.py
_cases.py # ported Expect/ExpectFail helpers
test_array.py # ported test_metadata.py
test_group.py # new group/consolidated model tests
packages/zarr-metadata/changes/
+upstream_models.feature.md # towncrier fragment (rename to <PR#>.feature.md after PR opens if the checker requires it)
Files:
- Create:
packages/zarr-metadata/src/zarr_metadata/model/__init__.py - Create:
packages/zarr-metadata/src/zarr_metadata/model/_validation.py - Source:
/Users/d-v-b/dev/zng/zng_metadata/src/zng_metadata/_metadata.pylines 1–274 (imports throughparse_array_metadata_v2), plus_arrays_to_tuples(lines 286–292)
Interfaces:
-
Produces:
ValidationProblem(loc: tuple[str | int, ...], message: str);MetadataValidationError(problems: list[ValidationProblem]);validate_json(value: object) -> list[ValidationProblem];is_json(value: object) -> TypeIs[JSONValue];parse_json(value: object) -> JSONValue;validate_metadata_field_v3 / is_metadata_field_v3 / parse_metadata_field_v3;validate_array_metadata_v3 / is_array_metadata_v3 / parse_array_metadata_v3;validate_array_metadata_v2 / is_array_metadata_v2 / parse_array_metadata_v2;_arrays_to_tuples(obj: object) -> object;_prefix(loc_head, problems); constantsARRAY_METADATA_REQUIRED_KEYS_V3,ARRAY_METADATA_OPTIONAL_KEYS_V3,ARRAY_METADATA_STANDARD_KEYS_V3,ARRAY_METADATA_REQUIRED_KEYS_V2. -
Step 1: Create the module skeleton.
model/__init__.pystarts as a docstring only ("""In-memory models for Zarr metadata documents."""); exports are added in Task 6. -
Step 2: Port
_validation.py. Copy the listed line ranges from the zng source verbatim, then apply the Adaptation Table. The import header becomes:
"""Structural validation for Zarr metadata documents.
Validators check JSON structure (shapes, key presence, primitive kinds),
not domain validity. Each concept gets a `validate_*` (all problems),
`is_*` (TypeIs), and `parse_*` (narrow or raise) trio.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Final, cast
from typing_extensions import TypeIs
from zarr_metadata import ArrayMetadataV2, ArrayMetadataV3, MetadataFieldV3
from zarr_metadata._common import JSONValueAll JSON mentions become JSONValue (annotations, TypeIs[JSONValue], docstrings). _arrays_to_tuples moves here (both _array.py and _group.py will import it).
-
Step 3: Type-check. Run:
cd packages/zarr-metadata && uvx pyright src— expect 0 errors (iterate on strict-mode complaints; do not loosentypeCheckingMode). -
Step 4: Commit.
git add packages/zarr-metadata/src/zarr_metadata/model/
git commit -m "feat(zarr-metadata): add model._validation — structural validators
Assisted-by: ClaudeCode:claude-fable-5"Files:
- Create:
packages/zarr-metadata/src/zarr_metadata/model/_array.py - Source:
/Users/d-v-b/dev/zng/zng_metadata/src/zng_metadata/_metadata.pylines 276–580 (typestore-key aliases,ZarrMetadataV3,ArrayMetadataModelV3Partial,ArrayMetadataModelV3,ArrayMetadataModelV2Partial,ArrayMetadataModelV2)
Interfaces:
-
Consumes:
parse_metadata_field_v3,parse_array_metadata_v3,parse_array_metadata_v2,_arrays_to_tuples,ARRAY_METADATA_STANDARD_KEYS_V3fromzarr_metadata.model._validation. -
Produces:
ZarrMetadataV3(name: str, configuration: dict[str, object])withto_json() -> MetadataFieldV3,from_json(data: object) -> ZarrMetadataV3ArrayMetadataModelV3(fields:zarr_format=3,node_type="array"bothinit=False;shape: tuple[int, ...];fill_value: JSONValue;data_type: ZarrMetadataV3;chunk_grid: ZarrMetadataV3;codecs: tuple[ZarrMetadataV3, ...];chunk_key_encoding: ZarrMetadataV3;dimension_names: tuple[str | None, ...] | None;attributes: dict[str, object];storage_transformers: tuple[ZarrMetadataV3, ...];extra_fields: dict[str, ExtensionFieldV3]) withcreate_default(**overrides),update(**kwargs),to_json() -> ArrayMetadataV3,from_json(data: object),from_key_value(mapping: Mapping[str, bytes]),to_key_value(*, indent=None) -> Mapping[str, bytes]ArrayMetadataModelV2(fields:zarr_format=2init=False;shape;dtype: DataTypeMetadataV2;chunks;fill_value: JSONValue;order: ArrayOrderV2;compressor: CodecMetadataV2 | None;filters: tuple[CodecMetadataV2, ...] | None;dimension_separator: ArrayDimensionSeparatorV2 = "/";attributes) with the same method set;from_key_value/to_key_valuehandle the.zarray/.zattrssplitArrayMetadataModelV3Partial/ArrayMetadataModelV2PartialTypedDicts- Constants
ARRAY_METADATA_STORE_KEY_V3 = "zarr.json",ARRAY_METADATA_STORE_KEY_V2 = ".zarray",ATTRIBUTES_STORE_KEY_V2 = ".zattrs"
-
Step 1: Port
_array.py. Copy the line ranges, apply the Adaptation Table. Import header:
"""In-memory models for Zarr array metadata documents."""
from __future__ import annotations
import dataclasses
import json
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Final, Literal, cast
from typing_extensions import TypedDict, Unpack
from zarr_metadata import (
ArrayDimensionSeparatorV2,
ArrayMetadataV2,
ArrayMetadataV3,
ArrayOrderV2,
ExtensionFieldV3,
MetadataFieldV3,
)
from zarr_metadata._common import JSONValue
from zarr_metadata.model._validation import (
ARRAY_METADATA_STANDARD_KEYS_V3,
_arrays_to_tuples,
parse_array_metadata_v2,
parse_array_metadata_v3,
parse_metadata_field_v3,
)
from zarr_metadata.v2 import CodecMetadataV2, DataTypeMetadataV2Store-key aliases become plain assignments per the Adaptation Table. Preserve zng's docstrings, __post_init__ overlap check, and the # type: ignore[...] comments (convert mypy codes to plain # type: ignore[...] forms pyright accepts, or # pyright: ignore[...] where strict mode names a different rule).
-
Step 2: Type-check. Run:
cd packages/zarr-metadata && uvx pyright src— expect 0 errors. -
Step 3: Smoke round-trip. Run:
cd packages/zarr-metadata && uv run python -c "
from zarr_metadata.model._array import ArrayMetadataModelV3
m = ArrayMetadataModelV3.create_default(shape=(4,), attributes={'a': 1})
assert ArrayMetadataModelV3.from_json(m.to_json()) == m
print('roundtrip ok')"Expected: roundtrip ok
- Step 4: Commit.
git add packages/zarr-metadata/src/zarr_metadata/model/_array.py
git commit -m "feat(zarr-metadata): add model._array — array metadata models
Assisted-by: ClaudeCode:claude-fable-5"Files:
- Create:
packages/zarr-metadata/tests/model/__init__.py(empty) - Create:
packages/zarr-metadata/tests/model/_cases.py— port of/Users/d-v-b/dev/zng/zng_metadata/tests/_cases.py - Create:
packages/zarr-metadata/tests/model/test_array.py— port of/Users/d-v-b/dev/zng/zng_metadata/tests/test_metadata.py
Interfaces:
-
Consumes: everything from Task 1–2 via
from zarr_metadata.model import ...(public names) andfrom zarr_metadata.model._validation import _arrays_to_tuples, _prefix(private helpers, imported from their module directly). Case helpers viafrom tests.model._cases import Expect, ExpectFail(thetests/directory is already a package with__init__.py). -
Step 1: Port
_cases.pyverbatim (only change: none expected — it importsre,dataclasses,pytestonly). Keep theExpectFail.raises()regex-msg behavior exactly (msg is an optional regex; never passmsg=""). -
Step 2: Port
test_array.py. Apply the Adaptation Table to imports:from zng_metadata import (...)→from zarr_metadata.model import (...);JSON→JSONValue;from zng_metadata._metadata import _arrays_to_tuples, _prefix→from zarr_metadata.model._validation import _arrays_to_tuples, _prefix;from _cases import ...→from tests.model._cases import .... Note the ported public-name imports require Task 6's exports — until then import from the private modules (zarr_metadata.model._validation,zarr_metadata.model._array); flip tozarr_metadata.modelin Task 6. -
Step 3: Run the ported suite. Run:
cd packages/zarr-metadata && uv run --group test pytest tests/model -qExpected: all pass (zng suite is green upstream; failures indicate a porting error — fix the port, not the test, unless the failure is a genuine 3.11/typing_extensions incompatibility). -
Step 4: Run the whole package suite. Run:
cd packages/zarr-metadata && uv run --group test pytest tests -qExpected: all pass (existingtest_public_api.pyuntouched so far). -
Step 5: Commit.
git add packages/zarr-metadata/tests/model/
git commit -m "test(zarr-metadata): port array model test suite
Assisted-by: ClaudeCode:claude-fable-5"Files:
- Create:
packages/zarr-metadata/src/zarr_metadata/model/_group.py - Modify:
packages/zarr-metadata/src/zarr_metadata/model/_validation.py(add group validators + key constants) - Test:
packages/zarr-metadata/tests/model/test_group.py
Interfaces:
- Consumes:
ValidationProblem,_prefix,_validate_attributes,_arrays_to_tuples,MetadataValidationErrorfrom._validation;ArrayMetadataModelV3,ArrayMetadataModelV2from._array(for Task 5's consolidated models, defined in the same file). - Produces:
validate_group_metadata_v3 / is_group_metadata_v3 / parse_group_metadata_v3andvalidate_group_metadata_v2 / is_group_metadata_v2 / parse_group_metadata_v2in_validation.py, plusGROUP_METADATA_REQUIRED_KEYS_V3,GROUP_METADATA_OPTIONAL_KEYS_V3,GROUP_METADATA_STANDARD_KEYS_V3(built fromGroupMetadataV3.__required_keys__/__optional_keys__like the array constants)GroupMetadataModelV3(fields below) +GroupMetadataModelV3PartialGroupMetadataModelV2+GroupMetadataModelV2Partial- Constants
GROUP_METADATA_STORE_KEY_V3 = "zarr.json",GROUP_METADATA_STORE_KEY_V2 = ".zgroup"
Behavioral contract (mirror the array models' conventions exactly — same method names, same create_default/update idiom, same overlap __post_init__):
@dataclass(frozen=True, slots=True, kw_only=True)
class GroupMetadataModelV3:
zarr_format: Literal[3] = field(default=3, init=False)
node_type: Literal["group"] = field(default="group", init=False)
attributes: dict[str, JSONValue]
consolidated_metadata: ConsolidatedMetadataModelV3 | None
extra_fields: dict[str, ExtensionFieldV3]from_json:parse_group_metadata_v3(_arrays_to_tuples(data)); theconsolidated_metadatakey (a zarr-python convention stored as an extension field, already typed byzarr_metadata.v3.consolidated.ConsolidatedMetadataV3) is pulled out into the typed field; remaining non-standard keys land inextra_fields.__post_init__rejectsextra_fieldsoverlapping standard keys or"consolidated_metadata".to_json: emitszarr_format,node_type;attributesonly when non-empty;consolidated_metadataonly when notNone; then extra fields — matching the array model's omit-empty convention.from_key_value/to_key_value: singlezarr.jsonkey.create_default()returnsGroupMetadataModelV3(attributes={}, consolidated_metadata=None, extra_fields={})then.update(**overrides).
@dataclass(frozen=True, slots=True, kw_only=True)
class GroupMetadataModelV2:
zarr_format: Literal[2] = field(default=2, init=False)
attributes: dict[str, JSONValue]from_jsontakes the merged form (GroupMetadataV2TypedDict:zarr_format+ optionalattributes);from_key_valuemerges.zgroup+ optional.zattrs;to_key_valuesplits back (always emitting both keys, matchingArrayMetadataModelV2.to_key_value);to_jsonemitszarr_formatandattributesonly when non-empty.
validate_group_metadata_v3(value): mapping check; missing required keys (zarr_format, node_type) reported per-key; attributes via _validate_attributes; if consolidated_metadata present it must be a Mapping (problem at ("consolidated_metadata",) otherwise; deep validation of its entries belongs to the consolidated model in Task 5). Like the array validators, literal values are not checked — presence and shape only.
validate_group_metadata_v2(value): mapping check; missing zarr_format reported; attributes via _validate_attributes.
- Step 1: Write the failing tests in
tests/model/test_group.py. Docstring on every test. Cover, at minimum:
"""Tests for the group metadata models in `zarr_metadata.model`."""
import json
import pytest
from tests.model._cases import Expect, ExpectFail # same helpers as test_array
from zarr_metadata.model._group import (
GroupMetadataModelV2,
GroupMetadataModelV3,
)
from zarr_metadata.model._validation import (
MetadataValidationError,
parse_group_metadata_v2,
parse_group_metadata_v3,
)
def test_group_v3_roundtrip() -> None:
"""A v3 group document round-trips through the model unchanged."""
doc = {"zarr_format": 3, "node_type": "group", "attributes": {"a": (1, 2)}}
model = GroupMetadataModelV3.from_json(doc)
assert model.to_json() == doc
def test_group_v3_omits_empty_attributes() -> None:
"""to_json omits the attributes key when attributes is empty."""
model = GroupMetadataModelV3.create_default()
assert "attributes" not in model.to_json()
def test_group_v3_extra_fields_roundtrip() -> None:
"""Unknown top-level keys land in extra_fields and reappear in to_json."""
doc = {
"zarr_format": 3,
"node_type": "group",
"my_extension": {"name": "thing", "must_understand": False},
}
model = GroupMetadataModelV3.from_json(doc)
assert model.extra_fields == {"my_extension": {"name": "thing", "must_understand": False}}
assert model.to_json() == doc
def test_group_v3_extra_fields_overlap_rejected() -> None:
"""Constructing a v3 group model with extra_fields shadowing a standard key raises."""
with pytest.raises(ValueError, match="Extra fields"):
GroupMetadataModelV3(
attributes={},
consolidated_metadata=None,
extra_fields={"node_type": {"name": "x", "must_understand": False}},
)
def test_group_v3_missing_required_key() -> None:
"""parse_group_metadata_v3 reports each missing required key."""
with pytest.raises(MetadataValidationError, match="node_type"):
parse_group_metadata_v3({"zarr_format": 3})
def test_group_v3_key_value_roundtrip() -> None:
"""from_key_value(to_key_value()) is the identity for v3 groups."""
model = GroupMetadataModelV3.create_default(attributes={"a": 1})
assert GroupMetadataModelV3.from_key_value(model.to_key_value()) == model
def test_group_v2_key_value_split() -> None:
"""v2 to_key_value writes .zgroup and .zattrs; from_key_value merges them."""
model = GroupMetadataModelV2.create_default(attributes={"a": 1})
kv = model.to_key_value()
assert set(kv) == {".zgroup", ".zattrs"}
assert json.loads(kv[".zgroup"]) == {"zarr_format": 2}
assert GroupMetadataModelV2.from_key_value(kv) == model
def test_group_v2_from_key_value_without_zattrs() -> None:
"""A v2 group with no .zattrs file parses with empty attributes."""
model = GroupMetadataModelV2.from_key_value({".zgroup": b'{"zarr_format": 2}'})
assert model.attributes == {}
def test_group_v2_not_a_mapping() -> None:
"""parse_group_metadata_v2 rejects a non-mapping document."""
with pytest.raises(MetadataValidationError, match="expected a mapping"):
parse_group_metadata_v2([1, 2, 3])(Adjust Expect/ExpectFail usage to match the ported test_array.py idiom where parametrized; the inline forms above are acceptable.)
-
Step 2: Run to verify failure.
cd packages/zarr-metadata && uv run --group test pytest tests/model/test_group.py -q— expect ImportError (_groupdoes not exist). -
Step 3: Implement
_group.py(models per the contract above;GroupMetadataModelV3.consolidated_metadatatyped asConsolidatedMetadataModelV3 | Nonewith the class added in Task 5 — for this task, define a forward-declared placeholder is NOT allowed; instead implement Tasks 4+5's_group.pymodule in one pass if easier, keeping test commits separate) and the group validators + constants in_validation.py. -
Step 4: Run to verify pass.
cd packages/zarr-metadata && uv run --group test pytest tests/model -q— expect all pass. -
Step 5: Type-check.
cd packages/zarr-metadata && uvx pyright src— expect 0 errors. -
Step 6: Commit.
git add packages/zarr-metadata/src/zarr_metadata/model/ packages/zarr-metadata/tests/model/test_group.py
git commit -m "feat(zarr-metadata): add group metadata models
Assisted-by: ClaudeCode:claude-fable-5"Files:
- Modify:
packages/zarr-metadata/src/zarr_metadata/model/_group.py - Test:
packages/zarr-metadata/tests/model/test_group.py(append)
Interfaces:
- Produces:
ConsolidatedMetadataModelV3—kind: Literal["inline"](init=False),must_understand: bool = False(typed permissive,__post_init__enforcesis False),metadata: dict[str, ArrayMetadataModelV3 | GroupMetadataModelV3];from_jsondiscriminates entries on theirnode_typekey;to_jsonemits{kind, must_understand, metadata}with each entry'sto_json().ConsolidatedMetadataModelV2—zarr_consolidated_format: Literal[1](init=False),metadata: dict[str, JSONValue]holding the flat file-keyed map ("path/.zarray"→ raw doc) verbatim;from_jsonvalidates only the envelope (both keys present,metadataa string-keyed mapping) and tuple-izes;to_jsonis the inverse. Store-key constantCONSOLIDATED_METADATA_STORE_KEY_V2 = ".zmetadata".
Design note (records a spec refinement): the design spec says consolidated metadata holds thin child models. That holds for v3, where each entry is a complete zarr.json document. For v2, .zmetadata entries are file fragments (.zarray / .zgroup / .zattrs as separate keys); merging them into node models would lose file-level structure (e.g. which nodes had a .zattrs entry at all) and break byte-faithful round-tripping — the layer's first duty. So the v2 consolidated model stays flat and verbatim; interpreting entries into node models is consumer work (zarr-python's resolve layer, part 2). Record this refinement in the design spec when this task completes.
- Step 1: Write the failing tests (append to
test_group.py):
from zarr_metadata.model._group import (
ConsolidatedMetadataModelV2,
ConsolidatedMetadataModelV3,
)
from zarr_metadata.model._array import ArrayMetadataModelV3
def test_consolidated_v3_roundtrip() -> None:
"""A v3 group with inline consolidated metadata round-trips, with child
entries parsed into array/group models."""
child = ArrayMetadataModelV3.create_default(shape=(2,)).to_json()
doc = {
"zarr_format": 3,
"node_type": "group",
"consolidated_metadata": {
"kind": "inline",
"must_understand": False,
"metadata": {"a": child, "g": {"zarr_format": 3, "node_type": "group"}},
},
}
model = GroupMetadataModelV3.from_json(doc)
assert isinstance(model.consolidated_metadata, ConsolidatedMetadataModelV3)
assert isinstance(model.consolidated_metadata.metadata["a"], ArrayMetadataModelV3)
assert isinstance(model.consolidated_metadata.metadata["g"], GroupMetadataModelV3)
assert model.to_json() == doc
def test_consolidated_v3_must_understand_true_rejected() -> None:
"""ConsolidatedMetadataModelV3 enforces must_understand=False at runtime."""
with pytest.raises(ValueError, match="must_understand"):
ConsolidatedMetadataModelV3(must_understand=True, metadata={})
def test_consolidated_v2_verbatim_roundtrip() -> None:
"""The v2 .zmetadata model holds the flat file-keyed map verbatim,
including nodes that have no .zattrs entry."""
doc = {
"zarr_consolidated_format": 1,
"metadata": {
".zgroup": {"zarr_format": 2},
"a/.zarray": {
"zarr_format": 2, "shape": (2,), "chunks": (2,), "dtype": "|u1",
"fill_value": 0, "order": "C", "compressor": None, "filters": None,
},
},
}
model = ConsolidatedMetadataModelV2.from_json(doc)
assert model.to_json() == doc
def test_consolidated_v2_envelope_validation() -> None:
"""from_json rejects a .zmetadata document missing the metadata key."""
with pytest.raises(MetadataValidationError, match="metadata"):
ConsolidatedMetadataModelV2.from_json({"zarr_consolidated_format": 1})-
Step 2: Run to verify failure, Step 3: Implement, Step 4: Run to verify pass (
pytest tests/model -q), Step 5: pyright clean, Step 6: Update the design spec (docs/superpowers/specs/2026-07-04-thin-metadata-refactor-design.md"Groups and consolidated metadata" section — add the v2-verbatim refinement; re-push the gist withgh gist edit). -
Step 7: Commit.
git add packages/zarr-metadata/src/zarr_metadata/model/_group.py packages/zarr-metadata/tests/model/test_group.py
git commit -m "feat(zarr-metadata): add consolidated metadata models
Assisted-by: ClaudeCode:claude-fable-5"Files:
- Modify:
packages/zarr-metadata/src/zarr_metadata/model/__init__.py(re-export every public name from_validation,_array,_groupwith__all__, mirroring zng's__init__.pyplus the new group/consolidated/validator names) - Modify:
packages/zarr-metadata/src/zarr_metadata/__init__.py(top-level front door) - Modify:
packages/zarr-metadata/tests/test_public_api.py(EXPECTED list) - Modify:
packages/zarr-metadata/tests/model/test_array.py(flip private-module imports tozarr_metadata.modelpublic names, per Task 3 Step 2 note)
Interfaces:
-
Produces:
zarr_metadata.modelexporting all model/validator names; top-levelzarr_metadataadditionally exporting the class names only:ArrayMetadataModelV2,ArrayMetadataModelV2Partial,ArrayMetadataModelV3,ArrayMetadataModelV3Partial,GroupMetadataModelV2,GroupMetadataModelV2Partial,GroupMetadataModelV3,GroupMetadataModelV3Partial,ConsolidatedMetadataModelV2,ConsolidatedMetadataModelV3,ZarrMetadataV3,ValidationProblem,MetadataValidationError. Validator functions and key/store constants stay underzarr_metadata.model(front door promotes types; the function trio family is large and submodule-scoped). -
Step 1: Write
model/__init__.pyre-exports +__all__(RUF022 ordering is enforced by ruff — runprek run ruff --files ...or let the pre-commit fix it). -
Step 2: Add the 13 class names to the top-level
__init__.pyimport-and-__all__, and toEXPECTEDintests/test_public_api.py(grouping: TitleCase group — the test asserts SCREAMING/TitleCase/dunder grouping only). -
Step 3: Flip
test_array.pyimports to publicfrom zarr_metadata.model import (...)(keep_arrays_to_tuples/_prefixprivate imports as-is). -
Step 4: Run everything.
cd packages/zarr-metadata && uv run --group test pytest tests -q— all pass;uvx pyright src— 0 errors. -
Step 5: Commit.
git add packages/zarr-metadata/src/zarr_metadata/__init__.py packages/zarr-metadata/src/zarr_metadata/model/__init__.py packages/zarr-metadata/tests/
git commit -m "feat(zarr-metadata): export model layer from package front door
Assisted-by: ClaudeCode:claude-fable-5"Files:
-
Create:
packages/zarr-metadata/changes/+upstream_models.feature.md -
Step 1: Check the fragment-name rules:
uv run --no-sync python ci/check_changelog_entries.py packages/zarr-metadata/changes(run from repo root). If numeric names are required, defer naming to after the PR exists and use the PR number. -
Step 2: Fragment content:
Added `zarr_metadata.model`: frozen-dataclass models (`ArrayMetadataModelV2`,
`ArrayMetadataModelV3`, `GroupMetadataModelV2`, `GroupMetadataModelV3`,
`ConsolidatedMetadataModelV2`, `ConsolidatedMetadataModelV3`, `ZarrMetadataV3`)
that are canonical, lossless representations of Zarr metadata documents, plus
structural validators (`validate_*` / `is_*` / `parse_*`). Every v3 extension
point (data type, chunk grid, chunk key encoding, codecs, storage transformers)
is held as a name + configuration pair; nothing is interpreted.- Step 3: Lint.
prek run --all-filesfrom repo root (ruff formatting/RUF022 for the new files; mypy does not cover this package — pyright already ran). - Step 4: Create branch off
origin/main, cherry-pick/rebase the task commits onto it (work happens in this worktree's branch; final branch namezarr-metadata-model-layer), push toorigin(d-v-b/zarr-python). - Step 5: Open the PR against
d-v-b/zarr-pythonmain:- Title:
feat(zarr-metadata): add metadata model layer - Body starts with
:robot: _AI text below_ :robot:, links the design gist, summarizes the layer, and flags the two recorded follow-ups: (a) validators check key presence/shape but not literal values (zarr_format,node_type) — inherited from the zng prototype, flagged for reviewer judgment; (b)ArrayMetadataModelV2.to_key_valuealways emits.zattrseven when attributes are empty — fidelity question to settle before the zarr-python consumption PR. - End body with the standard Claude Code attribution line.
- Title:
- Step 6: Rename the changelog fragment to
<PR#>.feature.mdif Step 1 found numeric names are required; amend/push.
- This PR merges → cut
zarr_metadata-v0.4.0(release is the user's call; thezarr-metadata-release.ymlworkflow publishes from the tag). - Part 2 plan (separate document, written after this lands): zarr-python gains
zarr-metadata>=0.4.0, thefrom_metadata/to_metadatahooks,zarr.core.resolve, the metadata-class swap, consumer rewiring, and the old-machinery deletion — the clean break described in the design spec.
- Spec coverage: this plan implements the spec's "Metadata models" layer + "Groups and consolidated metadata" (with the v2-verbatim refinement recorded in Task 5); the runtime-resolution/AsyncArray/v2-builder sections are explicitly Part 2.
- Type consistency:
JSONValue(notJSON) throughout;ZarrMetadataV3name+configuration everywhere; group model field set matches between Task 4 contract and Task 5 tests. - No placeholders: ported code is specified by exact source path + line range + adaptation table; new code is given in full or by exact behavioral contract with complete test code.