Skip to content

zarr_metadata.v3.chunk_grid

zarr_metadata.v3.chunk_grid

Zarr v3 chunk grid metadata types.

Each chunk grid lives in its own submodule:

  • regular -- core v3 spec
  • rectilinear -- zarr-extensions

The <X>ChunkGridMetadata aliases re-exported here are the canonical type for each grid's permitted JSON shapes. For the underlying <X>ChunkGridObject, <X>ChunkGridConfiguration, etc., import directly from the leaf submodule.

See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-grids

zarr_metadata.v3.chunk_grid.regular

Regular chunk grid (Zarr v3 core spec).

See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#regular-grids

REGULAR_CHUNK_GRID_NAME module-attribute

REGULAR_CHUNK_GRID_NAME: Final = 'regular'

The name field value of the regular chunk grid.

RegularChunkGridMetadata module-attribute

RegularChunkGridMetadata = RegularChunkGridObject

Permitted JSON shape for regular chunk grid metadata.

chunk_shape is required and has no default, so only the object form is valid; the short-hand-name form is not permitted by the spec for this grid. https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/core/index.rst#L528-L537 ("must be an object with the names name and configuration") https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/core/index.rst#L1562-L1564

RegularChunkGridName module-attribute

RegularChunkGridName = Literal['regular']

Literal type of the name field of the regular chunk grid.

__all__ module-attribute

__all__ = [
    "REGULAR_CHUNK_GRID_NAME",
    "RegularChunkGrid",
    "RegularChunkGridConfiguration",
    "RegularChunkGridMetadata",
    "RegularChunkGridName",
    "RegularChunkGridObject",
]

RegularChunkGrid dataclass

Bases: ChunkGridEntity

The regular chunk grid, coerced from its metadata.

Source code in src/zarr_metadata/v3/chunk_grid/regular.py
@dataclass(frozen=True)
class RegularChunkGrid(ChunkGridEntity):
    """The `regular` chunk grid, coerced from its metadata."""

    chunk_shape: tuple[int, ...] = ()

    identifier: ClassVar[str] = REGULAR_CHUNK_GRID_NAME

    configuration_required: ClassVar[bool] = True
    member_types: ClassVar[MemberTypes] = {"chunk_shape": (True, sequence_of(is_int))}

    def problems(self) -> tuple[ValidationProblem, ...]:
        """Every chunk extent must be at least one element.

        A chunk of zero elements along an axis covers nothing, so no
        finite number of them tiles the axis; a negative one is
        meaningless. Whether there is one extent *per array dimension* is
        a question for the document, and the rules layer asks it.
        """
        return tuple(
            ValidationProblem(
                ("chunk_shape", position),
                f"expected a positive chunk extent, got {extent}",
                "invalid_value",
            )
            for position, extent in enumerate(self.chunk_shape)
            if extent < 1
        )

    def shape_problems(self, array_shape: object) -> tuple[ValidationProblem, ...]:
        """A regular grid must chunk every array dimension."""
        if not isinstance(array_shape, (list, tuple)):
            return ()
        extents = tuple(cast("Sequence[object]", array_shape))
        if len(self.chunk_shape) == len(extents):
            return ()
        return problem(
            ("chunk_shape",),
            f"chunk_shape has {len(self.chunk_shape)} entries but shape has "
            f"{len(extents)} dimensions",
            "invalid_value",
        )

    def grid(self, array_shape: object) -> ChunkGrid:
        """One extent per axis, the same for every chunk on that axis."""
        return ChunkGrid.regular(self.chunk_shape)

    def to_json(self) -> RegularChunkGridObject:
        return cast("RegularChunkGridObject", super().to_json())

chunk_shape class-attribute instance-attribute

chunk_shape: tuple[int, ...] = ()

configuration_required class-attribute

configuration_required: bool = True

Whether the bare-name spelling says too little for this entity.

The spec permits a bare name "if no configuration metadata is required", so this is true exactly when some member is required.

identifier class-attribute

The name this entity is registered under.

Usually the name the metadata carries. The raw-bytes data types are the exception: every r<N> spelling is one family, so the family gets an invented identifier that no real name can collide with.

member_types class-attribute

member_types: MemberTypes = {
    "chunk_shape": (True, sequence_of(is_int))
}

The configuration members, and the type each one takes.

The same keys as the configuration TypedDict, which is the same as the constructor signature; tests/v3/test_entities.py holds the three together.

must_understand class-attribute instance-attribute

must_understand: bool = field(default=True, kw_only=True)

name property

name: str

The name this entity carries, as a document would write it.

Usually the identifier. They differ for the raw-bytes family, whose identifier is invented and belongs in no message a reader sees -- so anything user-facing wants this, and anything looking something up wants identifier.

required_class_vars class-attribute

required_class_vars: tuple[str, ...] = ('identifier',)

Every class variable a concrete entity of this kind must declare.

__init__

__init__(
    chunk_shape: tuple[int, ...] = (),
    *,
    must_understand: bool = True,
) -> None

__init_subclass__

__init_subclass__(
    *, base: bool = False, **kwargs: object
) -> None

Refuse a subclass that forgot to say what it is.

identifier and the per-kind class variables carry no default, so a subclass omitting one type-checks cleanly and then raises AttributeError from whichever method is reached first. Saying so here makes it an import-time error in the extension's own module.

base=True for a class that exists to add a class variable rather than to be an entity -- CodecEntity, IntegerDataType.

Source code in src/zarr_metadata/v3/_entity.py
def __init_subclass__(cls, *, base: bool = False, **kwargs: object) -> None:
    """Refuse a subclass that forgot to say what it is.

    `identifier` and the per-kind class variables carry no default,
    so a subclass omitting one type-checks cleanly and then raises
    `AttributeError` from whichever method is reached first. Saying so
    here makes it an import-time error in the extension's own module.

    `base=True` for a class that exists to add a class variable
    rather than to be an entity -- `CodecEntity`, `IntegerDataType`.
    """
    super().__init_subclass__(**kwargs)
    if base:
        return
    missing = [name for name in cls.required_class_vars if not hasattr(cls, name)]
    if len(missing) != 0:
        msg = f"{cls.__name__} does not declare {', '.join(missing)}"
        raise TypeError(msg)
    # An optional member defaults to UNSET or `configuration` emits it
    # for every instance, so the bare-name spelling becomes
    # unreachable and a document gains a member it never wrote.
    invented = [
        key
        for key, (required, _) in cls.member_types.items()
        if not required and getattr(cls, key, UNSET) is not UNSET
    ]
    if len(invented) != 0:
        msg = (
            f"{cls.__name__} gives the optional member(s) "
            f"{', '.join(invented)} a default other than UNSET"
        )
        raise TypeError(msg)

accepts classmethod

accepts(name: str) -> bool

Whether name denotes this entity.

Constant for all but the raw-bytes family, where one class covers every r<N>.

Source code in src/zarr_metadata/v3/_entity.py
@classmethod
def accepts(cls, name: str) -> bool:
    """Whether `name` denotes this entity.

    Constant for all but the raw-bytes family, where one class covers
    every `r<N>`.
    """
    return name == cls.identifier

canonical

canonical() -> Self

This entity in the simplest form that means the same thing.

A transformation, asked for by canonicalize_array_metadata_v3 and by nothing else. to_json does not apply it, because writing a document back is not the same as asking for it to be rewritten: a reader that reads and writes should not change bytes it was not asked to change.

Default: entities are already canonical. Override where two spellings of a member mean the same -- a rectilinear dimension's run-length encoding, a typesize that noshuffle ignores -- and where a contained entity has its own canonical form.

Source code in src/zarr_metadata/v3/_entity.py
def canonical(self) -> Self:
    """This entity in the simplest form that means the same thing.

    A *transformation*, asked for by `canonicalize_array_metadata_v3`
    and by nothing else. `to_json` does not apply it, because writing
    a document back is not the same as asking for it to be rewritten:
    a reader that reads and writes should not change bytes it was not
    asked to change.

    Default: entities are already canonical. Override where two
    spellings of a member mean the same -- a rectilinear dimension's
    run-length encoding, a `typesize` that `noshuffle` ignores -- and
    where a contained entity has its own canonical form.
    """
    return self

coerce classmethod

coerce(value: object, context: Context) -> Coerced[Self]

value as this entity, or the reasons it is not one.

context is the scope this reading is happening in; most entities have no use for it and ignore it.

Source code in src/zarr_metadata/v3/_entity.py
@classmethod
def coerce(cls, value: object, context: Context) -> Coerced[Self]:
    """`value` as this entity, or the reasons it is not one.

    `context` is the scope this reading is happening in; most entities
    have no use for it and ignore it.
    """
    name, configuration, must_understand = named_configuration(value)
    if name is None or not cls.accepts(name):
        return None, problem((), f"expected the {cls.identifier!r} entity")
    if configuration is None:
        if cls.configuration_required:
            return None, problem(
                ("configuration",),
                f"{cls.identifier!r} requires a configuration",
                "missing_key",
            )
        configuration = cast("Mapping[str, object]", {})
    members, found, unreadable = coerce_members(configuration, cls.member_types)
    if len(unreadable) != 0:
        # The entity cannot be built, but the members that *did* read
        # can still be judged -- one bad member should not hide the
        # value problems of the ones beside it. Anything the partial
        # reading says about an unreadable member is its default
        # talking, so those are dropped.
        partial = cls(must_understand=must_understand, **members)  # type: ignore[arg-type]
        found = (
            *found,
            # `within`, because a partial reading reports relative to
            # the configuration and `coerce`'s caller does not insert
            # that segment -- `coerce_members` problems already carry it.
            *within(
                (),
                [
                    entry
                    for entry in partial.problems()
                    if entry.loc[:1] not in {(key,) for key in unreadable}
                ],
            ),
        )
        return None, found
    return cls(must_understand=must_understand, **members), found  # type: ignore[arg-type]

configuration

configuration() -> dict[str, object]

This entity's configuration, as the document would write it.

Faithful to every member the entity holds: to_json is serialization, not canonicalization, so nothing is simplified here. Override only to render a member that is not already JSON, such as a contained entity.

Absent optional members are left out, which is what makes the bare-name spelling reachable. Absence is UNSET, never None: this package holds None to mean a JSON null the document actually wrote, and scale_offset is a real case where null and absent are different documents.

Source code in src/zarr_metadata/v3/_entity.py
def configuration(self) -> dict[str, object]:
    """This entity's configuration, as the document would write it.

    Faithful to every member the entity holds: `to_json` is
    serialization, not canonicalization, so nothing is simplified
    here. Override only to render a member that is not already JSON,
    such as a contained entity.

    Absent optional members are left out, which is what makes the
    bare-name spelling reachable. Absence is `UNSET`, never `None`:
    this package holds `None` to mean a JSON `null` the document
    actually wrote, and `scale_offset` is a real case where `null`
    and absent are different documents.
    """
    return {
        key: value
        for key in type(self).member_types
        if (value := getattr(self, key)) is not UNSET
    }

grid

grid(array_shape: object) -> ChunkGrid

One extent per axis, the same for every chunk on that axis.

Source code in src/zarr_metadata/v3/chunk_grid/regular.py
def grid(self, array_shape: object) -> ChunkGrid:
    """One extent per axis, the same for every chunk on that axis."""
    return ChunkGrid.regular(self.chunk_shape)

problems

problems() -> tuple[ValidationProblem, ...]

Every chunk extent must be at least one element.

A chunk of zero elements along an axis covers nothing, so no finite number of them tiles the axis; a negative one is meaningless. Whether there is one extent per array dimension is a question for the document, and the rules layer asks it.

Source code in src/zarr_metadata/v3/chunk_grid/regular.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every chunk extent must be at least one element.

    A chunk of zero elements along an axis covers nothing, so no
    finite number of them tiles the axis; a negative one is
    meaningless. Whether there is one extent *per array dimension* is
    a question for the document, and the rules layer asks it.
    """
    return tuple(
        ValidationProblem(
            ("chunk_shape", position),
            f"expected a positive chunk extent, got {extent}",
            "invalid_value",
        )
        for position, extent in enumerate(self.chunk_shape)
        if extent < 1
    )

shape_problems

shape_problems(
    array_shape: object,
) -> tuple[ValidationProblem, ...]

A regular grid must chunk every array dimension.

Source code in src/zarr_metadata/v3/chunk_grid/regular.py
def shape_problems(self, array_shape: object) -> tuple[ValidationProblem, ...]:
    """A regular grid must chunk every array dimension."""
    if not isinstance(array_shape, (list, tuple)):
        return ()
    extents = tuple(cast("Sequence[object]", array_shape))
    if len(self.chunk_shape) == len(extents):
        return ()
    return problem(
        ("chunk_shape",),
        f"chunk_shape has {len(self.chunk_shape)} entries but shape has "
        f"{len(extents)} dimensions",
        "invalid_value",
    )

to_json

This entity as a document would write it.

Faithful to every member: read a document, write it back, and the members come out as they went in. Ask canonical first if you want the simplest equivalent spelling.

What is not preserved is the envelope's spelling, because the entity does not model it: a bare name, {"name": x}, and {"name": x, "configuration": {}} all mean the same and all read to the same entity, so all three write back as the bare name. must_understand is omitted when true, which is its default; an explicit false is kept, because that one says something.

Subclasses narrow the return type to their own object TypedDict, which is the JSON form this dataclass models.

Source code in src/zarr_metadata/v3/chunk_grid/regular.py
def to_json(self) -> RegularChunkGridObject:
    return cast("RegularChunkGridObject", super().to_json())

RegularChunkGridConfiguration

Bases: TypedDict

Configuration for the regular chunk grid.

Source code in src/zarr_metadata/v3/chunk_grid/regular.py
class RegularChunkGridConfiguration(TypedDict, closed=True):
    """Configuration for the regular chunk grid."""

    chunk_shape: tuple[int, ...]

chunk_shape instance-attribute

chunk_shape: tuple[int, ...]

RegularChunkGridObject

Bases: TypedDict

Regular chunk grid metadata in object form.

Source code in src/zarr_metadata/v3/chunk_grid/regular.py
class RegularChunkGridObject(TypedDict, closed=True):
    """Regular chunk grid metadata in object form."""

    name: RegularChunkGridName
    configuration: RegularChunkGridConfiguration
    must_understand: NotRequired[bool]

configuration instance-attribute

must_understand instance-attribute

must_understand: NotRequired[bool]

name instance-attribute

zarr_metadata.v3.chunk_grid.rectilinear

Rectilinear chunk grid (zarr-extensions).

See https://github.com/zarr-developers/zarr-extensions/blob/4da7b37a84f76e660902f6d3de3eaef0e0febae6/chunk-grids/rectilinear/README.md

RECTILINEAR_CHUNK_GRID_KIND module-attribute

RECTILINEAR_CHUNK_GRID_KIND: Final = ('inline',)

The kind values the rectilinear grid defines.

Only inline so far: the extents are written into the metadata. The member exists so a later kind can put them somewhere else.

RECTILINEAR_CHUNK_GRID_NAME module-attribute

RECTILINEAR_CHUNK_GRID_NAME: Final = 'rectilinear'

The name field value of the rectilinear chunk grid.

RectilinearChunkGridMetadata module-attribute

RectilinearChunkGridMetadata = RectilinearChunkGridObject

Permitted JSON shape for rectilinear chunk grid metadata.

kind and chunk_shapes are required, so only the object form is valid; the short-hand-name form is not permitted by the spec for this grid. https://github.com/zarr-developers/zarr-extensions/blob/4da7b37a84f76e660902f6d3de3eaef0e0febae6/chunk-grids/rectilinear/README.md#L59-L62 https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/core/index.rst#L1562-L1564

RectilinearChunkGridName module-attribute

RectilinearChunkGridName = Literal['rectilinear']

Literal type of the name field of the rectilinear chunk grid.

RectilinearDimSpec module-attribute

RectilinearDimSpec = int | tuple[int | tuple[int, int], ...]

JSON shape for one dimension's rectilinear spec.

Either a bare integer (uniform shorthand for a regular dimension within a rectilinear grid), or a tuple of integers and/or [value, count] RLE pairs.

__all__ module-attribute

__all__ = [
    "RECTILINEAR_CHUNK_GRID_KIND",
    "RECTILINEAR_CHUNK_GRID_NAME",
    "RectilinearChunkGrid",
    "RectilinearChunkGridConfiguration",
    "RectilinearChunkGridMetadata",
    "RectilinearChunkGridName",
    "RectilinearChunkGridObject",
    "RectilinearDimSpec",
    "canonical_chunk_shapes",
    "canonical_dim_spec",
]

RectilinearChunkGrid dataclass

Bases: ChunkGridEntity

The rectilinear chunk grid, coerced from its metadata.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
@dataclass(frozen=True)
class RectilinearChunkGrid(ChunkGridEntity):
    """The `rectilinear` chunk grid, coerced from its metadata."""

    kind: Literal["inline"] = "inline"
    chunk_shapes: tuple[RectilinearDimSpec, ...] = ()

    identifier: ClassVar[str] = RECTILINEAR_CHUNK_GRID_NAME

    configuration_required: ClassVar[bool] = True
    member_types: ClassVar[MemberTypes] = {
        "kind": (True, one_of(RECTILINEAR_CHUNK_GRID_KIND)),
        "chunk_shapes": (True, _is_dim_specs),
    }

    def problems(self) -> tuple[ValidationProblem, ...]:
        """Every chunk extent, bare or run-length encoded, must be positive.

        A run's count must be positive too: a run of zero chunks is a way
        of writing nothing at all, and the empty spelling already exists.
        """
        found: list[ValidationProblem] = []
        for dim, spec in enumerate(self.chunk_shapes):
            loc: tuple[str | int, ...] = ("chunk_shapes", dim)
            if isinstance(spec, int):
                if spec < 1:
                    found.extend(
                        problem(
                            loc, f"expected a positive chunk extent, got {spec}", "invalid_value"
                        )
                    )
                continue
            for position, item in enumerate(spec):
                if isinstance(item, int):
                    if item < 1:
                        found.extend(
                            problem(
                                (*loc, position),
                                f"expected a positive chunk extent, got {item}",
                                "invalid_value",
                            )
                        )
                elif item[0] < 1 or item[1] < 1:
                    found.extend(
                        problem(
                            (*loc, position),
                            f"expected a positive [size, count] pair, got {item!r}",
                            "invalid_value",
                        )
                    )
        return tuple(found)

    def shape_problems(self, array_shape: object) -> tuple[ValidationProblem, ...]:
        """One spec per dimension, and explicit specs must cover it.

        A bare integer is uniform shorthand, so it covers whatever the
        dimension turns out to be and imposes no sum; an explicit list
        names every chunk, so the names have to add up.
        """
        if not isinstance(array_shape, (list, tuple)):
            return ()
        extents = tuple(cast("Sequence[object]", array_shape))
        if len(self.chunk_shapes) != len(extents):
            return problem(
                ("chunk_shapes",),
                f"chunk_shapes has {len(self.chunk_shapes)} entries but shape has "
                f"{len(extents)} dimensions",
                "invalid_value",
            )
        found: list[ValidationProblem] = []
        for dim, (spec, extent) in enumerate(zip(self.chunk_shapes, extents, strict=True)):
            if isinstance(spec, int) or not is_integer(extent):
                continue
            total = _covered_extent(spec)
            if total is not None and total < extent:
                found.extend(
                    problem(
                        ("chunk_shapes", dim),
                        f"chunk sizes sum to {total} but must cover shape[{dim}] extent {extent}",
                        "invalid_value",
                    )
                )
        return tuple(found)

    def grid(self, array_shape: object) -> ChunkGrid:
        """The distinct lengths each axis's chunks take.

        Plural per axis, which is the point of a rectilinear grid: an
        axis of `[30, 34]` gives `{30, 34}`, and anything asking about
        divisibility has to hold for both.
        """
        return ChunkGrid.derived(tuple(_axis_lengths(spec) for spec in self.chunk_shapes))

    def canonical(self) -> Self:
        """Run-length encoded, which is the spelling that does not grow.

        Two dimension specs listing the same extents describe the same
        grid, and the encoded one stays the same size as the array grows.
        """
        return replace(self, chunk_shapes=canonical_chunk_shapes(self.chunk_shapes))

    def to_json(self) -> RectilinearChunkGridObject:
        return cast("RectilinearChunkGridObject", super().to_json())

chunk_shapes class-attribute instance-attribute

chunk_shapes: tuple[RectilinearDimSpec, ...] = ()

configuration_required class-attribute

configuration_required: bool = True

Whether the bare-name spelling says too little for this entity.

The spec permits a bare name "if no configuration metadata is required", so this is true exactly when some member is required.

identifier class-attribute

The name this entity is registered under.

Usually the name the metadata carries. The raw-bytes data types are the exception: every r<N> spelling is one family, so the family gets an invented identifier that no real name can collide with.

kind class-attribute instance-attribute

kind: Literal['inline'] = 'inline'

member_types class-attribute

member_types: MemberTypes = {
    "kind": (True, one_of(RECTILINEAR_CHUNK_GRID_KIND)),
    "chunk_shapes": (True, _is_dim_specs),
}

The configuration members, and the type each one takes.

The same keys as the configuration TypedDict, which is the same as the constructor signature; tests/v3/test_entities.py holds the three together.

must_understand class-attribute instance-attribute

must_understand: bool = field(default=True, kw_only=True)

name property

name: str

The name this entity carries, as a document would write it.

Usually the identifier. They differ for the raw-bytes family, whose identifier is invented and belongs in no message a reader sees -- so anything user-facing wants this, and anything looking something up wants identifier.

required_class_vars class-attribute

required_class_vars: tuple[str, ...] = ('identifier',)

Every class variable a concrete entity of this kind must declare.

__init__

__init__(
    kind: Literal["inline"] = "inline",
    chunk_shapes: tuple[RectilinearDimSpec, ...] = (),
    *,
    must_understand: bool = True,
) -> None

__init_subclass__

__init_subclass__(
    *, base: bool = False, **kwargs: object
) -> None

Refuse a subclass that forgot to say what it is.

identifier and the per-kind class variables carry no default, so a subclass omitting one type-checks cleanly and then raises AttributeError from whichever method is reached first. Saying so here makes it an import-time error in the extension's own module.

base=True for a class that exists to add a class variable rather than to be an entity -- CodecEntity, IntegerDataType.

Source code in src/zarr_metadata/v3/_entity.py
def __init_subclass__(cls, *, base: bool = False, **kwargs: object) -> None:
    """Refuse a subclass that forgot to say what it is.

    `identifier` and the per-kind class variables carry no default,
    so a subclass omitting one type-checks cleanly and then raises
    `AttributeError` from whichever method is reached first. Saying so
    here makes it an import-time error in the extension's own module.

    `base=True` for a class that exists to add a class variable
    rather than to be an entity -- `CodecEntity`, `IntegerDataType`.
    """
    super().__init_subclass__(**kwargs)
    if base:
        return
    missing = [name for name in cls.required_class_vars if not hasattr(cls, name)]
    if len(missing) != 0:
        msg = f"{cls.__name__} does not declare {', '.join(missing)}"
        raise TypeError(msg)
    # An optional member defaults to UNSET or `configuration` emits it
    # for every instance, so the bare-name spelling becomes
    # unreachable and a document gains a member it never wrote.
    invented = [
        key
        for key, (required, _) in cls.member_types.items()
        if not required and getattr(cls, key, UNSET) is not UNSET
    ]
    if len(invented) != 0:
        msg = (
            f"{cls.__name__} gives the optional member(s) "
            f"{', '.join(invented)} a default other than UNSET"
        )
        raise TypeError(msg)

accepts classmethod

accepts(name: str) -> bool

Whether name denotes this entity.

Constant for all but the raw-bytes family, where one class covers every r<N>.

Source code in src/zarr_metadata/v3/_entity.py
@classmethod
def accepts(cls, name: str) -> bool:
    """Whether `name` denotes this entity.

    Constant for all but the raw-bytes family, where one class covers
    every `r<N>`.
    """
    return name == cls.identifier

canonical

canonical() -> Self

Run-length encoded, which is the spelling that does not grow.

Two dimension specs listing the same extents describe the same grid, and the encoded one stays the same size as the array grows.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
def canonical(self) -> Self:
    """Run-length encoded, which is the spelling that does not grow.

    Two dimension specs listing the same extents describe the same
    grid, and the encoded one stays the same size as the array grows.
    """
    return replace(self, chunk_shapes=canonical_chunk_shapes(self.chunk_shapes))

coerce classmethod

coerce(value: object, context: Context) -> Coerced[Self]

value as this entity, or the reasons it is not one.

context is the scope this reading is happening in; most entities have no use for it and ignore it.

Source code in src/zarr_metadata/v3/_entity.py
@classmethod
def coerce(cls, value: object, context: Context) -> Coerced[Self]:
    """`value` as this entity, or the reasons it is not one.

    `context` is the scope this reading is happening in; most entities
    have no use for it and ignore it.
    """
    name, configuration, must_understand = named_configuration(value)
    if name is None or not cls.accepts(name):
        return None, problem((), f"expected the {cls.identifier!r} entity")
    if configuration is None:
        if cls.configuration_required:
            return None, problem(
                ("configuration",),
                f"{cls.identifier!r} requires a configuration",
                "missing_key",
            )
        configuration = cast("Mapping[str, object]", {})
    members, found, unreadable = coerce_members(configuration, cls.member_types)
    if len(unreadable) != 0:
        # The entity cannot be built, but the members that *did* read
        # can still be judged -- one bad member should not hide the
        # value problems of the ones beside it. Anything the partial
        # reading says about an unreadable member is its default
        # talking, so those are dropped.
        partial = cls(must_understand=must_understand, **members)  # type: ignore[arg-type]
        found = (
            *found,
            # `within`, because a partial reading reports relative to
            # the configuration and `coerce`'s caller does not insert
            # that segment -- `coerce_members` problems already carry it.
            *within(
                (),
                [
                    entry
                    for entry in partial.problems()
                    if entry.loc[:1] not in {(key,) for key in unreadable}
                ],
            ),
        )
        return None, found
    return cls(must_understand=must_understand, **members), found  # type: ignore[arg-type]

configuration

configuration() -> dict[str, object]

This entity's configuration, as the document would write it.

Faithful to every member the entity holds: to_json is serialization, not canonicalization, so nothing is simplified here. Override only to render a member that is not already JSON, such as a contained entity.

Absent optional members are left out, which is what makes the bare-name spelling reachable. Absence is UNSET, never None: this package holds None to mean a JSON null the document actually wrote, and scale_offset is a real case where null and absent are different documents.

Source code in src/zarr_metadata/v3/_entity.py
def configuration(self) -> dict[str, object]:
    """This entity's configuration, as the document would write it.

    Faithful to every member the entity holds: `to_json` is
    serialization, not canonicalization, so nothing is simplified
    here. Override only to render a member that is not already JSON,
    such as a contained entity.

    Absent optional members are left out, which is what makes the
    bare-name spelling reachable. Absence is `UNSET`, never `None`:
    this package holds `None` to mean a JSON `null` the document
    actually wrote, and `scale_offset` is a real case where `null`
    and absent are different documents.
    """
    return {
        key: value
        for key in type(self).member_types
        if (value := getattr(self, key)) is not UNSET
    }

grid

grid(array_shape: object) -> ChunkGrid

The distinct lengths each axis's chunks take.

Plural per axis, which is the point of a rectilinear grid: an axis of [30, 34] gives {30, 34}, and anything asking about divisibility has to hold for both.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
def grid(self, array_shape: object) -> ChunkGrid:
    """The distinct lengths each axis's chunks take.

    Plural per axis, which is the point of a rectilinear grid: an
    axis of `[30, 34]` gives `{30, 34}`, and anything asking about
    divisibility has to hold for both.
    """
    return ChunkGrid.derived(tuple(_axis_lengths(spec) for spec in self.chunk_shapes))

problems

problems() -> tuple[ValidationProblem, ...]

Every chunk extent, bare or run-length encoded, must be positive.

A run's count must be positive too: a run of zero chunks is a way of writing nothing at all, and the empty spelling already exists.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every chunk extent, bare or run-length encoded, must be positive.

    A run's count must be positive too: a run of zero chunks is a way
    of writing nothing at all, and the empty spelling already exists.
    """
    found: list[ValidationProblem] = []
    for dim, spec in enumerate(self.chunk_shapes):
        loc: tuple[str | int, ...] = ("chunk_shapes", dim)
        if isinstance(spec, int):
            if spec < 1:
                found.extend(
                    problem(
                        loc, f"expected a positive chunk extent, got {spec}", "invalid_value"
                    )
                )
            continue
        for position, item in enumerate(spec):
            if isinstance(item, int):
                if item < 1:
                    found.extend(
                        problem(
                            (*loc, position),
                            f"expected a positive chunk extent, got {item}",
                            "invalid_value",
                        )
                    )
            elif item[0] < 1 or item[1] < 1:
                found.extend(
                    problem(
                        (*loc, position),
                        f"expected a positive [size, count] pair, got {item!r}",
                        "invalid_value",
                    )
                )
    return tuple(found)

shape_problems

shape_problems(
    array_shape: object,
) -> tuple[ValidationProblem, ...]

One spec per dimension, and explicit specs must cover it.

A bare integer is uniform shorthand, so it covers whatever the dimension turns out to be and imposes no sum; an explicit list names every chunk, so the names have to add up.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
def shape_problems(self, array_shape: object) -> tuple[ValidationProblem, ...]:
    """One spec per dimension, and explicit specs must cover it.

    A bare integer is uniform shorthand, so it covers whatever the
    dimension turns out to be and imposes no sum; an explicit list
    names every chunk, so the names have to add up.
    """
    if not isinstance(array_shape, (list, tuple)):
        return ()
    extents = tuple(cast("Sequence[object]", array_shape))
    if len(self.chunk_shapes) != len(extents):
        return problem(
            ("chunk_shapes",),
            f"chunk_shapes has {len(self.chunk_shapes)} entries but shape has "
            f"{len(extents)} dimensions",
            "invalid_value",
        )
    found: list[ValidationProblem] = []
    for dim, (spec, extent) in enumerate(zip(self.chunk_shapes, extents, strict=True)):
        if isinstance(spec, int) or not is_integer(extent):
            continue
        total = _covered_extent(spec)
        if total is not None and total < extent:
            found.extend(
                problem(
                    ("chunk_shapes", dim),
                    f"chunk sizes sum to {total} but must cover shape[{dim}] extent {extent}",
                    "invalid_value",
                )
            )
    return tuple(found)

to_json

This entity as a document would write it.

Faithful to every member: read a document, write it back, and the members come out as they went in. Ask canonical first if you want the simplest equivalent spelling.

What is not preserved is the envelope's spelling, because the entity does not model it: a bare name, {"name": x}, and {"name": x, "configuration": {}} all mean the same and all read to the same entity, so all three write back as the bare name. must_understand is omitted when true, which is its default; an explicit false is kept, because that one says something.

Subclasses narrow the return type to their own object TypedDict, which is the JSON form this dataclass models.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
def to_json(self) -> RectilinearChunkGridObject:
    return cast("RectilinearChunkGridObject", super().to_json())

RectilinearChunkGridConfiguration

Bases: TypedDict

Configuration for the rectilinear chunk grid.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
class RectilinearChunkGridConfiguration(TypedDict, closed=True):
    """Configuration for the rectilinear chunk grid."""

    kind: Literal["inline"]
    chunk_shapes: tuple[RectilinearDimSpec, ...]

chunk_shapes instance-attribute

chunk_shapes: tuple[RectilinearDimSpec, ...]

kind instance-attribute

kind: Literal['inline']

RectilinearChunkGridObject

Bases: TypedDict

Rectilinear chunk grid metadata in object form.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
class RectilinearChunkGridObject(TypedDict, closed=True):
    """Rectilinear chunk grid metadata in object form."""

    name: RectilinearChunkGridName
    configuration: RectilinearChunkGridConfiguration
    must_understand: NotRequired[bool]

configuration instance-attribute

must_understand instance-attribute

must_understand: NotRequired[bool]

name instance-attribute

canonical_chunk_shapes

canonical_chunk_shapes(
    chunk_shapes: tuple[RectilinearDimSpec, ...],
) -> tuple[RectilinearDimSpec, ...]

Every dimension's chunk sizes in their simplest equivalent form.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
def canonical_chunk_shapes(
    chunk_shapes: tuple[RectilinearDimSpec, ...],
) -> tuple[RectilinearDimSpec, ...]:
    """Every dimension's chunk sizes in their simplest equivalent form."""
    return tuple(canonical_dim_spec(spec) for spec in chunk_shapes)

canonical_dim_spec

canonical_dim_spec(
    spec: RectilinearDimSpec,
) -> RectilinearDimSpec

One dimension's chunk sizes in their simplest equivalent form.

Runs of equal sizes collapse to [size, count] pairs, because that is the spelling that does not grow with the number of chunks: a million equal chunks is two numbers, not a million. A run of one stays a bare size, and [size, 1] collapses to one, since a pair says nothing extra there. Adjacent spellings of the same size merge, which is what makes this idempotent: [[32, 2], 32] and [32, [32, 2]] both become [[32, 3]].

A dimension-level bare integer is left alone. It is a step that repeats until it covers the extent, so it is not equivalent to any fixed list — expanding it would pin a grid that currently adapts, and the two would diverge the moment the array were resized. For the same reason a one-element list is never collapsed to a bare integer: [32] declares exactly one chunk and 32 declares as many as it takes.

Assumes a spec the shape validator has already accepted.

Source code in src/zarr_metadata/v3/chunk_grid/rectilinear.py
def canonical_dim_spec(spec: RectilinearDimSpec) -> RectilinearDimSpec:
    """One dimension's chunk sizes in their simplest equivalent form.

    Runs of equal sizes collapse to `[size, count]` pairs, because that is
    the spelling that does not grow with the number of chunks: a million
    equal chunks is two numbers, not a million. A run of one stays a bare
    size, and `[size, 1]` collapses to one, since a pair says nothing extra
    there. Adjacent spellings of the same size merge, which is what makes
    this idempotent: `[[32, 2], 32]` and `[32, [32, 2]]` both become
    `[[32, 3]]`.

    A dimension-level bare integer is left alone. It is a *step* that
    repeats until it covers the extent, so it is not equivalent to any
    fixed list — expanding it would pin a grid that currently adapts, and
    the two would diverge the moment the array were resized. For the same
    reason a one-element list is never collapsed to a bare integer:
    `[32]` declares exactly one chunk and `32` declares as many as it takes.

    Assumes a spec the shape validator has already accepted.
    """
    if not isinstance(spec, tuple):
        return spec
    runs: list[tuple[int, int]] = []
    for entry in spec:
        size, count = entry if isinstance(entry, tuple) else (entry, 1)
        if len(runs) != 0 and runs[-1][0] == size:
            runs[-1] = (size, runs[-1][1] + count)
        else:
            runs.append((size, count))
    return tuple(size if count == 1 else (size, count) for size, count in runs)