Skip to content

zarr_metadata.v3.entity

zarr_metadata.v3.entity

The extension layer: what an entity is, and what is in scope.

Every Zarr v3 extension point -- codecs, data types, chunk grids, chunk key encodings, storage transformers -- is modelled as a class that answers for itself. This module is the public door to that layer, for two kinds of caller.

Reading metadata. ArrayDocumentV3.from_json is the fail-fast front door: one call, and either every extension point is read or a single MetadataValidationError carries every reason it is not. The entities it yields know things the document does not spell out -- what a data type's scalars are, which position a codec occupies, what a grid divides an array into. A name the scope does not model is not a failure: it arrives as an Opaque marked out_of_scope, for the reader to resolve elsewhere.

from zarr_metadata.v3.entity import ArrayDocumentV3, CodecEntity

array = ArrayDocumentV3.from_json(json.loads(raw))   # or raises
array.parts.grid.rank
for codec in array.codecs:
    if isinstance(codec, CodecEntity):
        codec.kind                  # 'array_bytes'
    else:
        codec.json, codec.reason    # 'out_of_scope': resolve it yourself

Writing an extension. Subclass CodecEntity, DataTypeEntity, ChunkGridEntity or MetadataEntity, declare identifier and member_types, and put it in a Context:

@dataclass(frozen=True)
class AcmeLz4Codec(CodecEntity):
    acceleration: int = 1

    identifier: ClassVar[str] = "acme.lz4"
    kind: ClassVar[CodecKind] = "bytes_bytes"
    member_types: ClassVar[MemberTypes] = {"acceleration": (False, is_int)}

SCOPE = Context({**CORE_AND_EXTENSIONS.entities,
                 "codecs": {**CORE_AND_EXTENSIONS.entities["codecs"],
                            AcmeLz4Codec.identifier: AcmeLz4Codec}})

validate_array_metadata_v3(document, context=SCOPE)

A name in no scope is not rejected -- that is what extension openness means -- so registering yours is how you get it judged rather than waved through.

One known friction, under mypy only. An entity's to_json returns its own object TypedDict, and mypy does not accept that where a ZarrV3MetadataFieldJSON is wanted: it reads every TypedDict as Mapping[str, object], never as the Mapping[str, JSONValue] the envelope declares. So putting to_json() output straight into a codecs list needs a cast under mypy. Pyright accepts it.

That conversion is sound here, which is why pyright is the one that is right. The rule mypy is applying exists because an ordinary TypedDict may carry extra items of types it never declared, so the union of the declared value types does not bound what is in the mapping. Every TypedDict in this package is closed (PEP 728), which forbids exactly that, and pyright implements PEP 728. Mypy does not yet -- see python/mypy#8994 and python/mypy#18439.

The type therefore stays as it is. Widening configuration to Mapping[str, object] or Mapping[str, Any] would satisfy mypy by making the annotation say something false: a configuration's values are JSON, and that is worth more than one checker's cast.

CHUNK_GRID module-attribute

CHUNK_GRID: Final = 'chunk_grid'

CHUNK_KEY_ENCODING module-attribute

CHUNK_KEY_ENCODING: Final = 'chunk_key_encoding'

CODECS module-attribute

CODECS: Final = 'codecs'

CORE module-attribute

CORE: Final = Context(
    {
        CODECS: _CORE_CODECS,
        DATA_TYPE: _CORE_DATA_TYPES,
        CHUNK_GRID: _CORE_CHUNK_GRIDS,
        CHUNK_KEY_ENCODING: _CORE_CHUNK_KEY_ENCODINGS,
    }
)

Only what the Zarr v3 specification defines.

CORE_AND_EXTENSIONS module-attribute

CORE_AND_EXTENSIONS: Final = Context(
    {
        CODECS: {**_CORE_CODECS, **_EXTENSION_CODECS},
        DATA_TYPE: {
            **_CORE_DATA_TYPES,
            **_EXTENSION_DATA_TYPES,
        },
        CHUNK_GRID: {
            **_CORE_CHUNK_GRIDS,
            **_EXTENSION_CHUNK_GRIDS,
        },
        CHUNK_KEY_ENCODING: {**_CORE_CHUNK_KEY_ENCODINGS},
    }
)

What the specification defines, plus what zarr-extensions registers.

CodecKind module-attribute

CodecKind = Literal[
    "array_array", "array_bytes", "bytes_bytes"
]

The three pipeline positions the v3 spec sorts codecs into.

Here rather than in zarr_metadata.v3.codec.kind because each codec declares its own kind, and that module imports every codec to build the tuples it will no longer need once they all do.

Coerced module-attribute

Coerced: TypeAlias = tuple[
    EntityT | None, tuple[ValidationProblem, ...]
]

The entity if it could be built, and every problem found.

One direction holds: no entity means at least one problem. The converse does not -- a survivable problem (an unknown key, an optional member of the wrong type) comes back with the entity, because the entity is still readable and saying so is more useful than refusing.

So test entity is None to decide whether to go on reading, and test the problems to decide the verdict. They are different questions.

DATA_TYPE module-attribute

DATA_TYPE: Final = 'data_type'

ExtensionPointField module-attribute

ExtensionPointField = Literal[
    "data_type",
    "chunk_grid",
    "chunk_key_encoding",
    "codecs",
    "storage_transformers",
]

The v3 array metadata fields whose values name an extension.

Here rather than in _extension_points because an entity that contains other entities has to say which point it is reading them at, and _extension_points also folds r<N> names -- which means importing the data types, which import this.

Extents module-attribute

Extents: TypeAlias = 'tuple[frozenset[int] | None, ...]'

One entry per dimension: the lengths that dimension's chunks take.

A singleton is a uniform axis. None is an axis whose lengths this package cannot determine — distinct from an empty set, which would claim the axis has no chunks at all.

FLOAT_SPECIALS module-attribute

FLOAT_SPECIALS: Final = ('NaN', 'Infinity', '-Infinity')

The three non-finite floats the spec spells as strings.

Loc module-attribute

Loc: TypeAlias = 'tuple[str | int, ...]'

MemberTypes module-attribute

MemberTypes: TypeAlias = (
    "Mapping[str, tuple[bool, TypeCheck]]"
)

Per configuration member: whether it is required, and its type check.

STORAGE_TRANSFORMERS module-attribute

STORAGE_TRANSFORMERS: Final = 'storage_transformers'

StorageClass module-attribute

StorageClass = Literal[
    "single_byte", "multi_byte", "variable_length"
]

How one scalar of a data type occupies bytes.

single_byte and multi_byte are both fixed-size; they differ only in whether a byte order applies, which is what the bytes codec's endian member is about.

TypeCheck module-attribute

TypeCheck: TypeAlias = (
    "Callable[[object, Loc], tuple[ValidationProblem, ...]]"
)

Whether one value has the type a member declares, and where if not.

UNKNOWN_GRID module-attribute

UNKNOWN_GRID: ChunkGrid = ChunkGrid(None, None)

A grid nothing is known about — not even how many dimensions it has.

__all__ module-attribute

__all__ = [
    "CHUNK_GRID",
    "CHUNK_KEY_ENCODING",
    "CODECS",
    "CORE",
    "CORE_AND_EXTENSIONS",
    "DATA_TYPE",
    "FLOAT_SPECIALS",
    "STORAGE_TRANSFORMERS",
    "UNKNOWN_GRID",
    "ArrayDocumentV3",
    "ArrayParts",
    "ChunkGrid",
    "ChunkGridEntity",
    "CodecEntity",
    "CodecKind",
    "Coerced",
    "ComplexDataType",
    "Context",
    "DataTypeEntity",
    "ExtensionPointField",
    "Extents",
    "FloatDataType",
    "IntegerDataType",
    "Loc",
    "MemberTypes",
    "MetadataEntity",
    "NumpyTimeDataType",
    "Opaque",
    "StorageClass",
    "TypeCheck",
    "array_problems_v3",
    "as_sequence",
    "byte_values",
    "chain_problems",
    "coerce_members",
    "is_bool",
    "is_int",
    "is_integer",
    "is_json_value",
    "is_str",
    "named_configuration",
    "one_of",
    "order_problems",
    "problem",
    "read_array_v3",
    "sequence_of",
    "shard_index_grid",
    "within",
]

ArrayDocumentV3 dataclass

A v3 array document with its extension points read as entities.

A field that could not be read holds an Opaque, which carries the JSON the document wrote and says whether the name was out of scope -- an extension this reader does not model, which is not an error -- or claimed and refused. Both are narrowable: every field is an exhaustive two-case union.

Source code in src/zarr_metadata/v3/_document.py
@dataclass(frozen=True, slots=True)
class ArrayDocumentV3:
    """A v3 array document with its extension points read as entities.

    A field that could not be read holds an `Opaque`, which carries the
    JSON the document wrote and says whether the name was out of scope --
    an extension this reader does not model, which is not an error -- or
    claimed and refused. Both are narrowable: every field is an exhaustive
    two-case union.
    """

    document: Mapping[str, object]
    data_type: DataTypeEntity | Opaque
    chunk_grid: ChunkGridEntity | Opaque
    chunk_key_encoding: MetadataEntity | Opaque
    codecs: tuple[CodecEntity | Opaque, ...]
    storage_transformers: tuple[MetadataEntity | Opaque, ...]

    def problems(self) -> tuple[ValidationProblem, ...]:
        """Every semantic problem this document has, once it has been read.

        The type-space problems are `read_array_v3`'s, because they are
        the reasons some of this is `Opaque` rather than an entity.
        """
        return (
            *_entity_problems(self),
            *_fill_value_problems(self),
            *_grid_problems(self),
            *_dimension_names_problems(self),
            *chain_problems(self.codecs, self.parts, ("codecs",)),
        )

    @classmethod
    def from_json(cls, value: object, *, context: Context = CORE_AND_EXTENSIONS) -> ArrayDocumentV3:
        """A v3 array document read into entities, or raise.

        The reader's front door, and the one entry point that fails fast:
        one call, and either every extension point is read or a single
        `MetadataValidationError` carries every reason it is not --
        structural and semantic together. Use `validate_array_metadata_v3`
        instead when you want the problems as data.

        A name this `context` does not model is *not* a failure. It comes
        back as an `Opaque` marked `out_of_scope`, because a document may
        legitimately use an extension this reader does not know, and
        refusing it would make openness unimplementable. What fails is
        metadata that is wrong, not metadata that is unfamiliar.
        """
        normalized = arrays_to_tuples(value)
        problems = validate_array_metadata_v3_structure(normalized)
        if isinstance(normalized, Mapping) and len(problems) == 0:
            document = cast("Mapping[str, object]", normalized)
            array, found = read_array_v3(document, context)
            problems = (*found, *array.problems())
            if len(problems) == 0:
                return array
        if len(problems) == 0:  # pragma: no cover - a non-mapping always has problems
            problems = (ValidationProblem((), "expected a v3 array document", "invalid_type"),)
        raise MetadataValidationError(problems)

    @property
    def parts(self) -> ArrayParts:
        """The array the codec pipeline is handed."""
        shape = self.document.get("shape")
        # A grid out of scope still divides an array of some rank, and the
        # shape is what pins it -- which is enough to catch a shard whose
        # inner chunk has the wrong number of dimensions.
        grid = (
            self.chunk_grid.grid(shape)
            if isinstance(self.chunk_grid, ChunkGridEntity)
            else ChunkGrid.unreadable(shape)
        )
        return ArrayParts(
            grid, self.data_type if isinstance(self.data_type, DataTypeEntity) else None
        )

chunk_grid instance-attribute

chunk_grid: ChunkGridEntity | Opaque

chunk_key_encoding instance-attribute

chunk_key_encoding: MetadataEntity | Opaque

codecs instance-attribute

codecs: tuple[CodecEntity | Opaque, ...]

data_type instance-attribute

data_type: DataTypeEntity | Opaque

document instance-attribute

document: Mapping[str, object]

parts property

parts: ArrayParts

The array the codec pipeline is handed.

storage_transformers instance-attribute

storage_transformers: tuple[MetadataEntity | Opaque, ...]

__init__

__init__(
    document: Mapping[str, object],
    data_type: DataTypeEntity | Opaque,
    chunk_grid: ChunkGridEntity | Opaque,
    chunk_key_encoding: MetadataEntity | Opaque,
    codecs: tuple[CodecEntity | Opaque, ...],
    storage_transformers: tuple[
        MetadataEntity | Opaque, ...
    ],
) -> None

from_json classmethod

from_json(
    value: object, *, context: Context = CORE_AND_EXTENSIONS
) -> ArrayDocumentV3

A v3 array document read into entities, or raise.

The reader's front door, and the one entry point that fails fast: one call, and either every extension point is read or a single MetadataValidationError carries every reason it is not -- structural and semantic together. Use validate_array_metadata_v3 instead when you want the problems as data.

A name this context does not model is not a failure. It comes back as an Opaque marked out_of_scope, because a document may legitimately use an extension this reader does not know, and refusing it would make openness unimplementable. What fails is metadata that is wrong, not metadata that is unfamiliar.

Source code in src/zarr_metadata/v3/_document.py
@classmethod
def from_json(cls, value: object, *, context: Context = CORE_AND_EXTENSIONS) -> ArrayDocumentV3:
    """A v3 array document read into entities, or raise.

    The reader's front door, and the one entry point that fails fast:
    one call, and either every extension point is read or a single
    `MetadataValidationError` carries every reason it is not --
    structural and semantic together. Use `validate_array_metadata_v3`
    instead when you want the problems as data.

    A name this `context` does not model is *not* a failure. It comes
    back as an `Opaque` marked `out_of_scope`, because a document may
    legitimately use an extension this reader does not know, and
    refusing it would make openness unimplementable. What fails is
    metadata that is wrong, not metadata that is unfamiliar.
    """
    normalized = arrays_to_tuples(value)
    problems = validate_array_metadata_v3_structure(normalized)
    if isinstance(normalized, Mapping) and len(problems) == 0:
        document = cast("Mapping[str, object]", normalized)
        array, found = read_array_v3(document, context)
        problems = (*found, *array.problems())
        if len(problems) == 0:
            return array
    if len(problems) == 0:  # pragma: no cover - a non-mapping always has problems
        problems = (ValidationProblem((), "expected a v3 array document", "invalid_type"),)
    raise MetadataValidationError(problems)

problems

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

Every semantic problem this document has, once it has been read.

The type-space problems are read_array_v3's, because they are the reasons some of this is Opaque rather than an entity.

Source code in src/zarr_metadata/v3/_document.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every semantic problem this document has, once it has been read.

    The type-space problems are `read_array_v3`'s, because they are
    the reasons some of this is `Opaque` rather than an entity.
    """
    return (
        *_entity_problems(self),
        *_fill_value_problems(self),
        *_grid_problems(self),
        *_dimension_names_problems(self),
        *chain_problems(self.codecs, self.parts, ("codecs",)),
    )

ArrayParts dataclass

Every part of an array a codec will be handed, and their type.

The parts an array is divided into, not the fields of its metadata. Plural deliberately: one pipeline encodes every chunk, so a rule about it quantifies over all of them — a shard's inner chunk shape must divide every chunk, which under a rectilinear grid is several different lengths.

data_type is the coerced data type, so a rule asks it what it is rather than comparing names, and it is None where the element type is undetermined while the array itself is not. That happens inside a shard: the inner grid is the sharding codec's own chunk_shape whatever reached it, so an unreadable codec upstream costs the type and not the parts. None in place of the whole value means something else again — that there is no array here at all, past the array->bytes boundary or beyond a codec that could have changed anything.

Source code in src/zarr_metadata/v3/_parts.py
@dataclass(frozen=True, slots=True)
class ArrayParts:
    """Every part of an array a codec will be handed, and their type.

    The parts an array is divided into, not the fields of its metadata.
    Plural deliberately: one pipeline encodes every chunk, so a rule about
    it quantifies over all of them — a shard's inner chunk shape must
    divide *every* chunk, which under a rectilinear grid is several
    different lengths.

    `data_type` is the coerced data type, so a rule asks it what it is
    rather than comparing names, and it is `None` where the element type
    is undetermined
    while the array itself is not. That happens inside a shard: the inner
    grid is the sharding codec's own `chunk_shape` whatever reached it, so
    an unreadable codec upstream costs the type and not the parts. `None`
    in place of the whole value means something else again — that there is
    no array here at all, past the array->bytes boundary or beyond a codec
    that could have changed anything.
    """

    grid: ChunkGrid
    data_type: DataTypeEntity | None

    def with_grid(self, grid: ChunkGrid) -> ArrayParts:
        return replace(self, grid=grid)

    def with_data_type(self, data_type: DataTypeEntity | None) -> ArrayParts:
        return replace(self, data_type=data_type)

data_type instance-attribute

data_type: DataTypeEntity | None

grid instance-attribute

grid: ChunkGrid

__init__

__init__(
    grid: ChunkGrid, data_type: DataTypeEntity | None
) -> None

with_data_type

with_data_type(
    data_type: DataTypeEntity | None,
) -> ArrayParts
Source code in src/zarr_metadata/v3/_parts.py
def with_data_type(self, data_type: DataTypeEntity | None) -> ArrayParts:
    return replace(self, data_type=data_type)

with_grid

with_grid(grid: ChunkGrid) -> ArrayParts
Source code in src/zarr_metadata/v3/_parts.py
def with_grid(self, grid: ChunkGrid) -> ArrayParts:
    return replace(self, grid=grid)

ChunkGrid dataclass

The division of an array into the parts a codec pipeline encodes.

Nothing here is the metadata: a grid entity keeps its own, and what reaches a codec is the division, not the spelling of it. A derived grid -- the regular one a sharding codec imposes, or a transposed one -- has no metadata to keep anyway.

Source code in src/zarr_metadata/v3/_parts.py
@dataclass(frozen=True, slots=True)
class ChunkGrid:
    """The division of an array into the parts a codec pipeline encodes.

    Nothing here is the metadata: a grid entity keeps its own, and what
    reaches a codec is the division, not the spelling of it. A derived
    grid -- the regular one a sharding codec imposes, or a transposed one
    -- has no metadata to keep anyway.
    """

    rank: int | None
    extents: Extents | None

    @classmethod
    def unreadable(cls, array_shape: object) -> ChunkGrid:
        """A grid nothing is known about but the rank the array pins.

        Every third-party grid, and every modelled one whose own metadata
        could not be read: the array still has a rank, and a rule about
        rank is still answerable.
        """
        rank = _rank_of(array_shape)
        return cls(rank, None if rank is None else (None,) * rank)

    @classmethod
    def derived(cls, extents: Extents) -> ChunkGrid:
        """A grid this package computed rather than read from a document."""
        return cls(len(extents), extents)

    @classmethod
    def regular(cls, lengths: Sequence[object]) -> ChunkGrid:
        """The regular grid a sharding codec's `chunk_shape` imposes."""
        return cls.derived(_uniform(lengths))

    def permuted(self, order: Sequence[int]) -> ChunkGrid:
        """This grid with its dimensions reordered by `order`.

        A transposed grid is still a grid — permuting a regular one gives
        a regular one — but it is no longer the grid the document wrote,
        so the metadata does not survive the trip.

        Declines on anything that is not a permutation of this grid's rank.
        The caller checks that too and reports it, but an order is only
        shape-validated as a tuple of integers, so this must not be the
        thing that decides whether a validator raises `IndexError`.
        """
        if self.extents is None or sorted(order) != list(range(len(self.extents))):
            return ChunkGrid(self.rank, None)
        return ChunkGrid.derived(tuple(self.extents[axis] for axis in order))

    def axis(self, dimension: int) -> frozenset[int] | None:
        """The lengths `dimension`'s chunks take, or None if undetermined."""
        if self.extents is None or dimension >= len(self.extents):
            return None
        return self.extents[dimension]

extents instance-attribute

extents: Extents | None

rank instance-attribute

rank: int | None

__init__

__init__(rank: int | None, extents: Extents | None) -> None

axis

axis(dimension: int) -> frozenset[int] | None

The lengths dimension's chunks take, or None if undetermined.

Source code in src/zarr_metadata/v3/_parts.py
def axis(self, dimension: int) -> frozenset[int] | None:
    """The lengths `dimension`'s chunks take, or None if undetermined."""
    if self.extents is None or dimension >= len(self.extents):
        return None
    return self.extents[dimension]

derived classmethod

derived(extents: Extents) -> ChunkGrid

A grid this package computed rather than read from a document.

Source code in src/zarr_metadata/v3/_parts.py
@classmethod
def derived(cls, extents: Extents) -> ChunkGrid:
    """A grid this package computed rather than read from a document."""
    return cls(len(extents), extents)

permuted

permuted(order: Sequence[int]) -> ChunkGrid

This grid with its dimensions reordered by order.

A transposed grid is still a grid — permuting a regular one gives a regular one — but it is no longer the grid the document wrote, so the metadata does not survive the trip.

Declines on anything that is not a permutation of this grid's rank. The caller checks that too and reports it, but an order is only shape-validated as a tuple of integers, so this must not be the thing that decides whether a validator raises IndexError.

Source code in src/zarr_metadata/v3/_parts.py
def permuted(self, order: Sequence[int]) -> ChunkGrid:
    """This grid with its dimensions reordered by `order`.

    A transposed grid is still a grid — permuting a regular one gives
    a regular one — but it is no longer the grid the document wrote,
    so the metadata does not survive the trip.

    Declines on anything that is not a permutation of this grid's rank.
    The caller checks that too and reports it, but an order is only
    shape-validated as a tuple of integers, so this must not be the
    thing that decides whether a validator raises `IndexError`.
    """
    if self.extents is None or sorted(order) != list(range(len(self.extents))):
        return ChunkGrid(self.rank, None)
    return ChunkGrid.derived(tuple(self.extents[axis] for axis in order))

regular classmethod

regular(lengths: Sequence[object]) -> ChunkGrid

The regular grid a sharding codec's chunk_shape imposes.

Source code in src/zarr_metadata/v3/_parts.py
@classmethod
def regular(cls, lengths: Sequence[object]) -> ChunkGrid:
    """The regular grid a sharding codec's `chunk_shape` imposes."""
    return cls.derived(_uniform(lengths))

unreadable classmethod

unreadable(array_shape: object) -> ChunkGrid

A grid nothing is known about but the rank the array pins.

Every third-party grid, and every modelled one whose own metadata could not be read: the array still has a rank, and a rule about rank is still answerable.

Source code in src/zarr_metadata/v3/_parts.py
@classmethod
def unreadable(cls, array_shape: object) -> ChunkGrid:
    """A grid nothing is known about but the rank the array pins.

    Every third-party grid, and every modelled one whose own metadata
    could not be read: the array still has a rank, and a rule about
    rank is still answerable.
    """
    rank = _rank_of(array_shape)
    return cls(rank, None if rank is None else (None,) * rank)

ChunkGridEntity dataclass

Bases: MetadataEntity

An entity that divides an array into the parts a pipeline encodes.

Source code in src/zarr_metadata/v3/_entity.py
@dataclass(frozen=True)
class ChunkGridEntity(MetadataEntity, base=True):
    """An entity that divides an array into the parts a pipeline encodes."""

    def shape_problems(self, array_shape: object) -> tuple[ValidationProblem, ...]:
        """Why this grid does not divide an array of `array_shape`.

        Locations are relative to the grid's `configuration`. Default:
        nothing, for a grid this package reads but has no such rule for.
        """
        return ()

    def grid(self, array_shape: object) -> ChunkGrid:
        """What this grid divides an array of `array_shape` into.

        The array shape is a parameter because neither determines a grid
        alone: a grid whose own metadata cannot be read still has the
        array's rank, and rank is enough for several rules.
        """
        return ChunkGrid.unreadable(array_shape)

configuration_required class-attribute

configuration_required: bool = False

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

identifier: str

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 = MappingProxyType({})

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__(*, 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

What this grid divides an array of array_shape into.

The array shape is a parameter because neither determines a grid alone: a grid whose own metadata cannot be read still has the array's rank, and rank is enough for several rules.

Source code in src/zarr_metadata/v3/_entity.py
def grid(self, array_shape: object) -> ChunkGrid:
    """What this grid divides an array of `array_shape` into.

    The array shape is a parameter because neither determines a grid
    alone: a grid whose own metadata cannot be read still has the
    array's rank, and rank is enough for several rules.
    """
    return ChunkGrid.unreadable(array_shape)

problems

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

Every value of this entity the spec disallows.

Locations are relative to the entity's configuration. Default: an entity whose type admits only valid values has nothing to add.

Source code in src/zarr_metadata/v3/_entity.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every value of this entity the spec disallows.

    Locations are relative to the entity's `configuration`. Default:
    an entity whose type admits only valid values has nothing to add.
    """
    return ()

shape_problems

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

Why this grid does not divide an array of array_shape.

Locations are relative to the grid's configuration. Default: nothing, for a grid this package reads but has no such rule for.

Source code in src/zarr_metadata/v3/_entity.py
def shape_problems(self, array_shape: object) -> tuple[ValidationProblem, ...]:
    """Why this grid does not divide an array of `array_shape`.

    Locations are relative to the grid's `configuration`. Default:
    nothing, for a grid this package reads but has no such rule for.
    """
    return ()

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/_entity.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    """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.
    """
    configuration = self.configuration()
    if len(configuration) == 0 and self.must_understand:
        return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
    entry: dict[str, object] = {"name": type(self).identifier}
    if len(configuration) != 0:
        entry["configuration"] = configuration
    if not self.must_understand:
        entry["must_understand"] = False
    return cast("ZarrV3MetadataFieldJSON", entry)

CodecEntity dataclass

Bases: MetadataEntity

An entity that occupies a position in the codec pipeline.

Source code in src/zarr_metadata/v3/_entity.py
@dataclass(frozen=True)
class CodecEntity(MetadataEntity, base=True):
    """An entity that occupies a position in the codec pipeline."""

    kind: ClassVar[CodecKind]
    required_class_vars: ClassVar[tuple[str, ...]] = ("identifier", "kind")

    variable_size: ClassVar[bool] = False
    """Whether this codec's output size depends on the bytes it is given.

    A compressor's does, so a shard index encoded with one has no size
    derivable from metadata alone, and the shard cannot be read.
    """

    def incoming_problems(self, incoming: ArrayParts | None) -> tuple[ValidationProblem, ...]:
        """Why this codec cannot be applied to the array that reaches it.

        `incoming` is None once the chain can no longer say what reaches
        here, and the default answer to that is nothing: declining beats
        guessing. Locations are relative to this codec's `configuration`,
        as `problems`' are; an empty one lands on the codec itself.
        """
        return ()

    def transition(self, incoming: ArrayParts) -> ArrayParts | None:
        """What the next codec in the chain sees, or None if undeterminable.

        Only an array-to-array codec has anything to say: the two later
        kinds end shape propagation by construction, one by consuming the
        array and the other by never having had it.

        The default is None, so a modelled codec that forgets to say how
        it transforms the array stops propagation rather than silently
        claiming to leave it alone. Failing closed here costs a judgment;
        failing open would invent one.
        """
        return None

configuration_required class-attribute

configuration_required: bool = False

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

identifier: str

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

kind: CodecKind

member_types class-attribute

member_types: MemberTypes = MappingProxyType({})

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",
    "kind",
)

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

variable_size class-attribute

variable_size: bool = False

Whether this codec's output size depends on the bytes it is given.

A compressor's does, so a shard index encoded with one has no size derivable from metadata alone, and the shard cannot be read.

__init__

__init__(*, 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
    }

incoming_problems

incoming_problems(
    incoming: ArrayParts | None,
) -> tuple[ValidationProblem, ...]

Why this codec cannot be applied to the array that reaches it.

incoming is None once the chain can no longer say what reaches here, and the default answer to that is nothing: declining beats guessing. Locations are relative to this codec's configuration, as problems' are; an empty one lands on the codec itself.

Source code in src/zarr_metadata/v3/_entity.py
def incoming_problems(self, incoming: ArrayParts | None) -> tuple[ValidationProblem, ...]:
    """Why this codec cannot be applied to the array that reaches it.

    `incoming` is None once the chain can no longer say what reaches
    here, and the default answer to that is nothing: declining beats
    guessing. Locations are relative to this codec's `configuration`,
    as `problems`' are; an empty one lands on the codec itself.
    """
    return ()

problems

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

Every value of this entity the spec disallows.

Locations are relative to the entity's configuration. Default: an entity whose type admits only valid values has nothing to add.

Source code in src/zarr_metadata/v3/_entity.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every value of this entity the spec disallows.

    Locations are relative to the entity's `configuration`. Default:
    an entity whose type admits only valid values has nothing to add.
    """
    return ()

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/_entity.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    """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.
    """
    configuration = self.configuration()
    if len(configuration) == 0 and self.must_understand:
        return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
    entry: dict[str, object] = {"name": type(self).identifier}
    if len(configuration) != 0:
        entry["configuration"] = configuration
    if not self.must_understand:
        entry["must_understand"] = False
    return cast("ZarrV3MetadataFieldJSON", entry)

transition

transition(incoming: ArrayParts) -> ArrayParts | None

What the next codec in the chain sees, or None if undeterminable.

Only an array-to-array codec has anything to say: the two later kinds end shape propagation by construction, one by consuming the array and the other by never having had it.

The default is None, so a modelled codec that forgets to say how it transforms the array stops propagation rather than silently claiming to leave it alone. Failing closed here costs a judgment; failing open would invent one.

Source code in src/zarr_metadata/v3/_entity.py
def transition(self, incoming: ArrayParts) -> ArrayParts | None:
    """What the next codec in the chain sees, or None if undeterminable.

    Only an array-to-array codec has anything to say: the two later
    kinds end shape propagation by construction, one by consuming the
    array and the other by never having had it.

    The default is None, so a modelled codec that forgets to say how
    it transforms the array stops propagation rather than silently
    claiming to leave it alone. Failing closed here costs a judgment;
    failing open would invent one.
    """
    return None

ComplexDataType dataclass

Bases: DataTypeEntity

A complex number: a [real, imag] pair of the component float type.

Source code in src/zarr_metadata/v3/data_type/_families.py
@dataclass(frozen=True)
class ComplexDataType(DataTypeEntity, base=True):
    """A complex number: a `[real, imag]` pair of the component float type."""

    scalar_storage: ClassVar[StorageClass] = "multi_byte"
    twos_complement: ClassVar[bool] = False
    component: ClassVar[type[FloatDataType]]

    def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
        pair = as_sequence(value)
        if pair is None or len(pair) != 2:
            return problem(loc, f"expected a [real, imag] pair, got {value!r}", "invalid_value")
        component = type(self).component()
        return tuple(
            ValidationProblem(found.loc, f"invalid component: {found.message}", found.kind)
            for index, part in enumerate(pair)
            for found in component.fill_value_problems(part, (*loc, index))
        )

component class-attribute

component: type[FloatDataType]

configuration_required class-attribute

configuration_required: bool = False

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

identifier: str

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 = MappingProxyType({})

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",
    "scalar_storage",
    "twos_complement",
)

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

scalar_storage class-attribute

scalar_storage: StorageClass = 'multi_byte'

twos_complement class-attribute

twos_complement: bool = False

Whether this type's scalars are two's complement integers.

Asked by cast_value, whose out_of_range: "wrap" is defined only for such a target. Required rather than defaulted: a data type added later must decide, because either default would answer for it silently -- and getting it wrong in one direction accepts a cast the spec does not define.

__init__

__init__(*, 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
    }

fill_value_problems

fill_value_problems(
    value: object, loc: Loc = ()
) -> tuple[ValidationProblem, ...]

Why value is not a fill value of this type, if it is not.

Default: nothing. A data type this package does not model accepts whatever its extension says it does, and guessing would reject valid documents.

Source code in src/zarr_metadata/v3/data_type/_families.py
def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
    pair = as_sequence(value)
    if pair is None or len(pair) != 2:
        return problem(loc, f"expected a [real, imag] pair, got {value!r}", "invalid_value")
    component = type(self).component()
    return tuple(
        ValidationProblem(found.loc, f"invalid component: {found.message}", found.kind)
        for index, part in enumerate(pair)
        for found in component.fill_value_problems(part, (*loc, index))
    )

problems

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

Every value of this entity the spec disallows.

Locations are relative to the entity's configuration. Default: an entity whose type admits only valid values has nothing to add.

Source code in src/zarr_metadata/v3/_entity.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every value of this entity the spec disallows.

    Locations are relative to the entity's `configuration`. Default:
    an entity whose type admits only valid values has nothing to add.
    """
    return ()

storage_class

storage_class() -> StorageClass | None

How one scalar occupies bytes, or None if undetermined.

None only for a composite whose parts are not all in scope: an answer would be a guess, and the rules that ask decline instead.

Source code in src/zarr_metadata/v3/_entity.py
def storage_class(self) -> StorageClass | None:
    """How one scalar occupies bytes, or None if undetermined.

    None only for a composite whose parts are not all in scope: an
    answer would be a guess, and the rules that ask decline instead.
    """
    return type(self).scalar_storage

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/_entity.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    """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.
    """
    configuration = self.configuration()
    if len(configuration) == 0 and self.must_understand:
        return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
    entry: dict[str, object] = {"name": type(self).identifier}
    if len(configuration) != 0:
        entry["configuration"] = configuration
    if not self.must_understand:
        entry["must_understand"] = False
    return cast("ZarrV3MetadataFieldJSON", entry)

Context dataclass

The entities in scope while metadata is being read.

Passed to every coerce, and most entities ignore it: a gzip codec is a gzip codec whatever else is in scope. The ones that do not ignore it hold other entities inside their own configuration -- a struct data type holds field data types, a sharding_indexed codec holds two codec pipelines -- and cannot read those without knowing what is in scope inside them.

A scope is not a property of the entities, it is a choice the reader makes: judging against the specification alone, or against the specification plus what zarr-extensions registers.

Source code in src/zarr_metadata/v3/_registry.py
@dataclass(frozen=True, slots=True)
class Context:
    """The entities in scope while metadata is being read.

    Passed to every `coerce`, and most entities ignore it: a `gzip` codec
    is a `gzip` codec whatever else is in scope. The ones that do not
    ignore it hold other entities inside their own configuration -- a
    `struct` data type holds field data types, a `sharding_indexed` codec
    holds two codec pipelines -- and cannot read those without knowing
    what is in scope inside them.

    A scope is not a property of the entities, it is a choice the reader
    makes: judging against the specification alone, or against the
    specification plus what `zarr-extensions` registers.
    """

    entities: Mapping[ExtensionPointField, Mapping[str, type[MetadataEntity]]]

    def __post_init__(self) -> None:
        """Refuse a table whose key an entity would not answer to.

        `resolve` finds a candidate by key and then asks the entity
        whether the name is really one of its own, so a key that is not
        the entity's `identifier` can never resolve. If the two disagree
        -- a typo, or a rename that missed one of the two places the name
        is written -- registration appears to succeed, validation runs,
        and the verdict is clean. Indistinguishable from extension
        openness, and the easiest way to ship a broken extension.

        The key is the identifier, not a name a document writes: the
        raw-bytes family registers under an invented one that `accepts`
        deliberately refuses.
        """
        for field, table in self.entities.items():
            for key, entity in table.items():
                if key != entity.identifier:
                    msg = (
                        f"{entity.__name__} is registered at {field!r} under {key!r} "
                        f"but its identifier is {entity.identifier!r}"
                    )
                    raise ValueError(msg)

    def resolve(self, field: ExtensionPointField, name: str) -> type[MetadataEntity] | None:
        """The entity `name` denotes at `field`, or None if out of scope.

        Out of scope is not an error: an unknown name may be an extension
        this reader does not model, and openness means leaving it unjudged.

        The entity has the last word, via `accepts`. Folding is what finds
        a candidate -- every `r<N>` spelling is tabled under one invented
        identifier -- and the candidate is what says whether the name is
        really one of its own. Otherwise the identifier itself would be a
        name a document could write.
        """
        entity = self.entities.get(field, {}).get(canonical_name(field, name))
        if entity is None or not entity.accepts(name):
            return None
        return entity

    @overload
    def coerce(
        self,
        field: Literal["data_type"],
        value: object,
        loc: Loc = (),
        *,
        envelope_judged: bool = False,
    ) -> tuple[DataTypeEntity | Opaque, tuple[ValidationProblem, ...]]: ...

    @overload
    def coerce(
        self,
        field: Literal["codecs"],
        value: object,
        loc: Loc = (),
        *,
        envelope_judged: bool = False,
    ) -> tuple[CodecEntity | Opaque, tuple[ValidationProblem, ...]]: ...

    @overload
    def coerce(
        self,
        field: Literal["chunk_grid"],
        value: object,
        loc: Loc = (),
        *,
        envelope_judged: bool = False,
    ) -> tuple[ChunkGridEntity | Opaque, tuple[ValidationProblem, ...]]: ...

    @overload
    def coerce(
        self,
        field: ExtensionPointField,
        value: object,
        loc: Loc = (),
        *,
        envelope_judged: bool = False,
    ) -> tuple[MetadataEntity | Opaque, tuple[ValidationProblem, ...]]: ...

    def coerce(
        self,
        field: ExtensionPointField,
        value: object,
        loc: Loc = (),
        *,
        envelope_judged: bool = False,
    ) -> tuple[MetadataEntity | Opaque, tuple[ValidationProblem, ...]]:
        """One nested entity, read in this scope.

        The primitive the containing entities are built from: a `struct`
        data type reads its fields with it, a `sharding_indexed` codec its
        two pipelines. Returns the entity when its name is in scope, and
        the value untouched when it is not -- an unmodelled extension is
        left unjudged, which is what makes the format open.

        `loc` prefixes the problems, so they point at where in the
        containing configuration the entity sat.

        A metadata field is a metadata field wherever it appears, so the
        envelope gets the same structural judgment here that the model
        layer gives a top-level one -- an extra member, a `configuration`
        that is not an object, a `must_understand` that is not a boolean.
        `envelope_judged` says that judgment has already happened, which
        it has for the fields of a document the model layer accepted.
        """
        problems: list[ValidationProblem] = []
        if not envelope_judged:
            problems.extend(
                ValidationProblem((*loc, *found.loc), found.message, found.kind)
                for found in validate_metadata_field_v3(value)
            )
        name, _, _ = named_configuration(value)
        if name is None:
            return Opaque(value, "invalid"), (
                *problems,
                ValidationProblem(loc, f"expected a metadata field, got {value!r}", "invalid_type"),
            )
        entity_type = self.resolve(field, name)
        if entity_type is None:
            return Opaque(value, "out_of_scope"), tuple(problems)
        entity, found = entity_type.coerce(value, self)
        problems.extend(
            ValidationProblem((*loc, *entry.loc), entry.message, entry.kind) for entry in found
        )
        if entity is None:
            return Opaque(value, "invalid"), tuple(problems)
        return entity, tuple(problems)

entities instance-attribute

__init__

__init__(
    entities: Mapping[
        ExtensionPointField,
        Mapping[str, type[MetadataEntity]],
    ],
) -> None

__post_init__

__post_init__() -> None

Refuse a table whose key an entity would not answer to.

resolve finds a candidate by key and then asks the entity whether the name is really one of its own, so a key that is not the entity's identifier can never resolve. If the two disagree -- a typo, or a rename that missed one of the two places the name is written -- registration appears to succeed, validation runs, and the verdict is clean. Indistinguishable from extension openness, and the easiest way to ship a broken extension.

The key is the identifier, not a name a document writes: the raw-bytes family registers under an invented one that accepts deliberately refuses.

Source code in src/zarr_metadata/v3/_registry.py
def __post_init__(self) -> None:
    """Refuse a table whose key an entity would not answer to.

    `resolve` finds a candidate by key and then asks the entity
    whether the name is really one of its own, so a key that is not
    the entity's `identifier` can never resolve. If the two disagree
    -- a typo, or a rename that missed one of the two places the name
    is written -- registration appears to succeed, validation runs,
    and the verdict is clean. Indistinguishable from extension
    openness, and the easiest way to ship a broken extension.

    The key is the identifier, not a name a document writes: the
    raw-bytes family registers under an invented one that `accepts`
    deliberately refuses.
    """
    for field, table in self.entities.items():
        for key, entity in table.items():
            if key != entity.identifier:
                msg = (
                    f"{entity.__name__} is registered at {field!r} under {key!r} "
                    f"but its identifier is {entity.identifier!r}"
                )
                raise ValueError(msg)

coerce

coerce(
    field: Literal["data_type"],
    value: object,
    loc: Loc = (),
    *,
    envelope_judged: bool = False,
) -> tuple[
    DataTypeEntity | Opaque, tuple[ValidationProblem, ...]
]
coerce(
    field: Literal["codecs"],
    value: object,
    loc: Loc = (),
    *,
    envelope_judged: bool = False,
) -> tuple[
    CodecEntity | Opaque, tuple[ValidationProblem, ...]
]
coerce(
    field: Literal["chunk_grid"],
    value: object,
    loc: Loc = (),
    *,
    envelope_judged: bool = False,
) -> tuple[
    ChunkGridEntity | Opaque, tuple[ValidationProblem, ...]
]
coerce(
    field: ExtensionPointField,
    value: object,
    loc: Loc = (),
    *,
    envelope_judged: bool = False,
) -> tuple[
    MetadataEntity | Opaque, tuple[ValidationProblem, ...]
]
coerce(
    field: ExtensionPointField,
    value: object,
    loc: Loc = (),
    *,
    envelope_judged: bool = False,
) -> tuple[
    MetadataEntity | Opaque, tuple[ValidationProblem, ...]
]

One nested entity, read in this scope.

The primitive the containing entities are built from: a struct data type reads its fields with it, a sharding_indexed codec its two pipelines. Returns the entity when its name is in scope, and the value untouched when it is not -- an unmodelled extension is left unjudged, which is what makes the format open.

loc prefixes the problems, so they point at where in the containing configuration the entity sat.

A metadata field is a metadata field wherever it appears, so the envelope gets the same structural judgment here that the model layer gives a top-level one -- an extra member, a configuration that is not an object, a must_understand that is not a boolean. envelope_judged says that judgment has already happened, which it has for the fields of a document the model layer accepted.

Source code in src/zarr_metadata/v3/_registry.py
def coerce(
    self,
    field: ExtensionPointField,
    value: object,
    loc: Loc = (),
    *,
    envelope_judged: bool = False,
) -> tuple[MetadataEntity | Opaque, tuple[ValidationProblem, ...]]:
    """One nested entity, read in this scope.

    The primitive the containing entities are built from: a `struct`
    data type reads its fields with it, a `sharding_indexed` codec its
    two pipelines. Returns the entity when its name is in scope, and
    the value untouched when it is not -- an unmodelled extension is
    left unjudged, which is what makes the format open.

    `loc` prefixes the problems, so they point at where in the
    containing configuration the entity sat.

    A metadata field is a metadata field wherever it appears, so the
    envelope gets the same structural judgment here that the model
    layer gives a top-level one -- an extra member, a `configuration`
    that is not an object, a `must_understand` that is not a boolean.
    `envelope_judged` says that judgment has already happened, which
    it has for the fields of a document the model layer accepted.
    """
    problems: list[ValidationProblem] = []
    if not envelope_judged:
        problems.extend(
            ValidationProblem((*loc, *found.loc), found.message, found.kind)
            for found in validate_metadata_field_v3(value)
        )
    name, _, _ = named_configuration(value)
    if name is None:
        return Opaque(value, "invalid"), (
            *problems,
            ValidationProblem(loc, f"expected a metadata field, got {value!r}", "invalid_type"),
        )
    entity_type = self.resolve(field, name)
    if entity_type is None:
        return Opaque(value, "out_of_scope"), tuple(problems)
    entity, found = entity_type.coerce(value, self)
    problems.extend(
        ValidationProblem((*loc, *entry.loc), entry.message, entry.kind) for entry in found
    )
    if entity is None:
        return Opaque(value, "invalid"), tuple(problems)
    return entity, tuple(problems)

resolve

resolve(
    field: ExtensionPointField, name: str
) -> type[MetadataEntity] | None

The entity name denotes at field, or None if out of scope.

Out of scope is not an error: an unknown name may be an extension this reader does not model, and openness means leaving it unjudged.

The entity has the last word, via accepts. Folding is what finds a candidate -- every r<N> spelling is tabled under one invented identifier -- and the candidate is what says whether the name is really one of its own. Otherwise the identifier itself would be a name a document could write.

Source code in src/zarr_metadata/v3/_registry.py
def resolve(self, field: ExtensionPointField, name: str) -> type[MetadataEntity] | None:
    """The entity `name` denotes at `field`, or None if out of scope.

    Out of scope is not an error: an unknown name may be an extension
    this reader does not model, and openness means leaving it unjudged.

    The entity has the last word, via `accepts`. Folding is what finds
    a candidate -- every `r<N>` spelling is tabled under one invented
    identifier -- and the candidate is what says whether the name is
    really one of its own. Otherwise the identifier itself would be a
    name a document could write.
    """
    entity = self.entities.get(field, {}).get(canonical_name(field, name))
    if entity is None or not entity.accepts(name):
        return None
    return entity

DataTypeEntity dataclass

Bases: MetadataEntity

An entity that says how the array's scalars are stored.

Only data types answer that, and every rule that turns on it -- a bytes codec is pointless before a single-byte type, a struct field cannot be variable-length -- asks a data type rather than consulting a table of names.

Source code in src/zarr_metadata/v3/_entity.py
@dataclass(frozen=True)
class DataTypeEntity(MetadataEntity, base=True):
    """An entity that says how the array's scalars are stored.

    Only data types answer that, and every rule that turns on it -- a
    `bytes` codec is pointless before a single-byte type, a struct field
    cannot be variable-length -- asks a data type rather than consulting
    a table of names.
    """

    scalar_storage: ClassVar[StorageClass]

    twos_complement: ClassVar[bool]
    """Whether this type's scalars are two's complement integers.

    Asked by `cast_value`, whose `out_of_range: "wrap"` is defined only
    for such a target. Required rather than defaulted: a data type added
    later must decide, because either default would answer for it
    silently -- and getting it wrong in one direction accepts a cast the
    spec does not define.
    """

    required_class_vars: ClassVar[tuple[str, ...]] = (
        "identifier",
        "scalar_storage",
        "twos_complement",
    )

    def storage_class(self) -> StorageClass | None:
        """How one scalar occupies bytes, or None if undetermined.

        None only for a composite whose parts are not all in scope: an
        answer would be a guess, and the rules that ask decline instead.
        """
        return type(self).scalar_storage

    def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
        """Why `value` is not a fill value of this type, if it is not.

        Default: nothing. A data type this package does not model accepts
        whatever its extension says it does, and guessing would reject
        valid documents.
        """
        return ()

configuration_required class-attribute

configuration_required: bool = False

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

identifier: str

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 = MappingProxyType({})

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",
    "scalar_storage",
    "twos_complement",
)

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

scalar_storage class-attribute

scalar_storage: StorageClass

twos_complement class-attribute

twos_complement: bool

Whether this type's scalars are two's complement integers.

Asked by cast_value, whose out_of_range: "wrap" is defined only for such a target. Required rather than defaulted: a data type added later must decide, because either default would answer for it silently -- and getting it wrong in one direction accepts a cast the spec does not define.

__init__

__init__(*, 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
    }

fill_value_problems

fill_value_problems(
    value: object, loc: Loc = ()
) -> tuple[ValidationProblem, ...]

Why value is not a fill value of this type, if it is not.

Default: nothing. A data type this package does not model accepts whatever its extension says it does, and guessing would reject valid documents.

Source code in src/zarr_metadata/v3/_entity.py
def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
    """Why `value` is not a fill value of this type, if it is not.

    Default: nothing. A data type this package does not model accepts
    whatever its extension says it does, and guessing would reject
    valid documents.
    """
    return ()

problems

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

Every value of this entity the spec disallows.

Locations are relative to the entity's configuration. Default: an entity whose type admits only valid values has nothing to add.

Source code in src/zarr_metadata/v3/_entity.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every value of this entity the spec disallows.

    Locations are relative to the entity's `configuration`. Default:
    an entity whose type admits only valid values has nothing to add.
    """
    return ()

storage_class

storage_class() -> StorageClass | None

How one scalar occupies bytes, or None if undetermined.

None only for a composite whose parts are not all in scope: an answer would be a guess, and the rules that ask decline instead.

Source code in src/zarr_metadata/v3/_entity.py
def storage_class(self) -> StorageClass | None:
    """How one scalar occupies bytes, or None if undetermined.

    None only for a composite whose parts are not all in scope: an
    answer would be a guess, and the rules that ask decline instead.
    """
    return type(self).scalar_storage

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/_entity.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    """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.
    """
    configuration = self.configuration()
    if len(configuration) == 0 and self.must_understand:
        return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
    entry: dict[str, object] = {"name": type(self).identifier}
    if len(configuration) != 0:
        entry["configuration"] = configuration
    if not self.must_understand:
        entry["must_understand"] = False
    return cast("ZarrV3MetadataFieldJSON", entry)

FloatDataType dataclass

Bases: DataTypeEntity

A binary float. A fill value may be a number, a named non-finite, or hex.

Source code in src/zarr_metadata/v3/data_type/_families.py
@dataclass(frozen=True)
class FloatDataType(DataTypeEntity, base=True):
    """A binary float. A fill value may be a number, a named non-finite, or hex."""

    scalar_storage: ClassVar[StorageClass] = "multi_byte"
    twos_complement: ClassVar[bool] = False
    hex_parser: ClassVar[Callable[[str], object]]

    largest: ClassVar[float | None]
    """The largest finite magnitude this width holds, or None for float64.

    None because a Python float *is* a float64, so no literal that reaches
    here can exceed it.
    """

    def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
        if is_integer(value) or isinstance(value, float):
            largest = type(self).largest
            if largest is not None and abs(value) > largest:
                return problem(
                    loc,
                    f"expected a {type(self).identifier} value, got {value!r}",
                    "invalid_value",
                )
            return ()
        if not isinstance(value, str):
            return problem(loc, f"expected a number or string, got {value!r}", "invalid_value")
        if value in FLOAT_SPECIALS:
            return ()
        try:
            type(self).hex_parser(value)
        except ValueError:
            return problem(
                loc,
                f"expected a number, one of 'NaN'/'Infinity'/'-Infinity', or a "
                f"{type(self).identifier} hex string, got {value!r}",
                "invalid_value",
            )
        return ()

configuration_required class-attribute

configuration_required: bool = False

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.

hex_parser class-attribute

hex_parser: Callable[[str], object]

identifier class-attribute

identifier: str

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.

largest class-attribute

largest: float | None

The largest finite magnitude this width holds, or None for float64.

None because a Python float is a float64, so no literal that reaches here can exceed it.

member_types class-attribute

member_types: MemberTypes = MappingProxyType({})

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",
    "scalar_storage",
    "twos_complement",
)

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

scalar_storage class-attribute

scalar_storage: StorageClass = 'multi_byte'

twos_complement class-attribute

twos_complement: bool = False

Whether this type's scalars are two's complement integers.

Asked by cast_value, whose out_of_range: "wrap" is defined only for such a target. Required rather than defaulted: a data type added later must decide, because either default would answer for it silently -- and getting it wrong in one direction accepts a cast the spec does not define.

__init__

__init__(*, 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
    }

fill_value_problems

fill_value_problems(
    value: object, loc: Loc = ()
) -> tuple[ValidationProblem, ...]

Why value is not a fill value of this type, if it is not.

Default: nothing. A data type this package does not model accepts whatever its extension says it does, and guessing would reject valid documents.

Source code in src/zarr_metadata/v3/data_type/_families.py
def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
    if is_integer(value) or isinstance(value, float):
        largest = type(self).largest
        if largest is not None and abs(value) > largest:
            return problem(
                loc,
                f"expected a {type(self).identifier} value, got {value!r}",
                "invalid_value",
            )
        return ()
    if not isinstance(value, str):
        return problem(loc, f"expected a number or string, got {value!r}", "invalid_value")
    if value in FLOAT_SPECIALS:
        return ()
    try:
        type(self).hex_parser(value)
    except ValueError:
        return problem(
            loc,
            f"expected a number, one of 'NaN'/'Infinity'/'-Infinity', or a "
            f"{type(self).identifier} hex string, got {value!r}",
            "invalid_value",
        )
    return ()

problems

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

Every value of this entity the spec disallows.

Locations are relative to the entity's configuration. Default: an entity whose type admits only valid values has nothing to add.

Source code in src/zarr_metadata/v3/_entity.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every value of this entity the spec disallows.

    Locations are relative to the entity's `configuration`. Default:
    an entity whose type admits only valid values has nothing to add.
    """
    return ()

storage_class

storage_class() -> StorageClass | None

How one scalar occupies bytes, or None if undetermined.

None only for a composite whose parts are not all in scope: an answer would be a guess, and the rules that ask decline instead.

Source code in src/zarr_metadata/v3/_entity.py
def storage_class(self) -> StorageClass | None:
    """How one scalar occupies bytes, or None if undetermined.

    None only for a composite whose parts are not all in scope: an
    answer would be a guess, and the rules that ask decline instead.
    """
    return type(self).scalar_storage

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/_entity.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    """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.
    """
    configuration = self.configuration()
    if len(configuration) == 0 and self.must_understand:
        return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
    entry: dict[str, object] = {"name": type(self).identifier}
    if len(configuration) != 0:
        entry["configuration"] = configuration
    if not self.must_understand:
        entry["must_understand"] = False
    return cast("ZarrV3MetadataFieldJSON", entry)

IntegerDataType dataclass

Bases: DataTypeEntity

A fixed-width integer. The width is the whole difference.

Source code in src/zarr_metadata/v3/data_type/_families.py
@dataclass(frozen=True)
class IntegerDataType(DataTypeEntity, base=True):
    """A fixed-width integer. The width is the whole difference."""

    bounds: ClassVar[tuple[int, int]]
    twos_complement: ClassVar[bool] = True

    def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
        low, high = type(self).bounds
        if not is_integer(value):
            return problem(loc, f"expected an integer, got {value!r}", "invalid_value")
        if not low <= value <= high:
            return problem(
                loc, f"expected an integer in [{low}, {high}], got {value!r}", "invalid_value"
            )
        return ()

bounds class-attribute

bounds: tuple[int, int]

configuration_required class-attribute

configuration_required: bool = False

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

identifier: str

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 = MappingProxyType({})

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",
    "scalar_storage",
    "twos_complement",
)

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

scalar_storage class-attribute

scalar_storage: StorageClass

twos_complement class-attribute

twos_complement: bool = True

Whether this type's scalars are two's complement integers.

Asked by cast_value, whose out_of_range: "wrap" is defined only for such a target. Required rather than defaulted: a data type added later must decide, because either default would answer for it silently -- and getting it wrong in one direction accepts a cast the spec does not define.

__init__

__init__(*, 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
    }

fill_value_problems

fill_value_problems(
    value: object, loc: Loc = ()
) -> tuple[ValidationProblem, ...]

Why value is not a fill value of this type, if it is not.

Default: nothing. A data type this package does not model accepts whatever its extension says it does, and guessing would reject valid documents.

Source code in src/zarr_metadata/v3/data_type/_families.py
def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
    low, high = type(self).bounds
    if not is_integer(value):
        return problem(loc, f"expected an integer, got {value!r}", "invalid_value")
    if not low <= value <= high:
        return problem(
            loc, f"expected an integer in [{low}, {high}], got {value!r}", "invalid_value"
        )
    return ()

problems

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

Every value of this entity the spec disallows.

Locations are relative to the entity's configuration. Default: an entity whose type admits only valid values has nothing to add.

Source code in src/zarr_metadata/v3/_entity.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every value of this entity the spec disallows.

    Locations are relative to the entity's `configuration`. Default:
    an entity whose type admits only valid values has nothing to add.
    """
    return ()

storage_class

storage_class() -> StorageClass | None

How one scalar occupies bytes, or None if undetermined.

None only for a composite whose parts are not all in scope: an answer would be a guess, and the rules that ask decline instead.

Source code in src/zarr_metadata/v3/_entity.py
def storage_class(self) -> StorageClass | None:
    """How one scalar occupies bytes, or None if undetermined.

    None only for a composite whose parts are not all in scope: an
    answer would be a guess, and the rules that ask decline instead.
    """
    return type(self).scalar_storage

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/_entity.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    """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.
    """
    configuration = self.configuration()
    if len(configuration) == 0 and self.must_understand:
        return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
    entry: dict[str, object] = {"name": type(self).identifier}
    if len(configuration) != 0:
        entry["configuration"] = configuration
    if not self.must_understand:
        entry["must_understand"] = False
    return cast("ZarrV3MetadataFieldJSON", entry)

MetadataEntity dataclass

One named entity, coerced from its metadata.

Subclasses add their configuration members as fields, which is what makes them well-typed by construction: an instance exists only if coerce accepted the metadata that produced it. An optional member is typed | None with a default of None, so absence is representable and a canonical spelling can leave it out.

Frozen, so an entity of hashable members is hashable. One holding a value out of scope is not, because that value is the JSON the document wrote and a JSON object is a dict -- the same way any frozen dataclass holding a list is unhashable. It cannot be an immutable mapping instead: MappingProxyType is unhashable too, and anything else stops json.dumps from serializing what to_json returns.

Most subclasses declare member_types and nothing else: the default coerce and to_json are written once here against that table. The ones that override are the ones with something particular to say -- a configuration containing other entities, a name that is a family rather than a constant, a member another member renders meaningless.

Source code in src/zarr_metadata/v3/_entity.py
@dataclass(frozen=True)
class MetadataEntity:
    """One named entity, coerced from its metadata.

    Subclasses add their configuration members as fields, which is what
    makes them well-typed by construction: an instance exists only if
    `coerce` accepted the metadata that produced it. An optional member is
    typed `| None` with a default of `None`, so absence is representable
    and a canonical spelling can leave it out.

    Frozen, so an entity of hashable members is hashable. One holding a
    value out of scope is not, because that value is the JSON the document
    wrote and a JSON object is a `dict` -- the same way any frozen
    dataclass holding a list is unhashable. It cannot be an immutable
    mapping instead: `MappingProxyType` is unhashable too, and anything
    else stops `json.dumps` from serializing what `to_json` returns.

    Most subclasses declare `member_types` and nothing else: the default
    `coerce` and `to_json` are written once here against that table. The
    ones that override are the ones with something particular to say --
    a configuration containing other entities, a name that is a family
    rather than a constant, a member another member renders meaningless.
    """

    # Keyword-only: it is the envelope's member, not the configuration's,
    # and it would otherwise take the first positional slot of every
    # entity -- so `RawBytesDataType("r16")` would set this instead of
    # the field it reads as.
    must_understand: bool = field(default=True, kw_only=True)

    identifier: ClassVar[str]
    """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: ClassVar[MemberTypes] = MappingProxyType({})
    """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.
    """

    configuration_required: ClassVar[bool] = False
    """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.
    """

    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)

    required_class_vars: ClassVar[tuple[str, ...]] = ("identifier",)
    """Every class variable a concrete entity of this kind must declare."""

    @property
    def name(self) -> 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`.
        """
        return type(self).identifier

    @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

    @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]

    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

    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
        }

    def problems(self) -> tuple[ValidationProblem, ...]:
        """Every value of this entity the spec disallows.

        Locations are relative to the entity's `configuration`. Default:
        an entity whose type admits only valid values has nothing to add.
        """
        return ()

    def to_json(self) -> ZarrV3MetadataFieldJSON:
        """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.
        """
        configuration = self.configuration()
        if len(configuration) == 0 and self.must_understand:
            return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
        entry: dict[str, object] = {"name": type(self).identifier}
        if len(configuration) != 0:
            entry["configuration"] = configuration
        if not self.must_understand:
            entry["must_understand"] = False
        return cast("ZarrV3MetadataFieldJSON", entry)

configuration_required class-attribute

configuration_required: bool = False

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

identifier: str

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 = MappingProxyType({})

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__(*, 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
    }

problems

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

Every value of this entity the spec disallows.

Locations are relative to the entity's configuration. Default: an entity whose type admits only valid values has nothing to add.

Source code in src/zarr_metadata/v3/_entity.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every value of this entity the spec disallows.

    Locations are relative to the entity's `configuration`. Default:
    an entity whose type admits only valid values has nothing to add.
    """
    return ()

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/_entity.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    """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.
    """
    configuration = self.configuration()
    if len(configuration) == 0 and self.must_understand:
        return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
    entry: dict[str, object] = {"name": type(self).identifier}
    if len(configuration) != 0:
        entry["configuration"] = configuration
    if not self.must_understand:
        entry["must_understand"] = False
    return cast("ZarrV3MetadataFieldJSON", entry)

NumpyTimeDataType dataclass

Bases: DataTypeEntity

A numpy time scalar: a signed 64-bit count of units, or NaT.

Source code in src/zarr_metadata/v3/data_type/_families.py
@dataclass(frozen=True)
class NumpyTimeDataType(DataTypeEntity, base=True):
    """A numpy time scalar: a signed 64-bit count of units, or `NaT`."""

    scalar_storage: ClassVar[StorageClass] = "multi_byte"
    # Stored as a signed integer, but it denotes an instant or a
    # duration; wrapping one is not a defined cast.
    twos_complement: ClassVar[bool] = False

    def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
        if value == "NaT":
            return ()
        if not is_integer(value):
            return problem(
                loc, f"expected a signed 64-bit integer or 'NaT', got {value!r}", "invalid_value"
            )
        if not -(2**63) <= value <= 2**63 - 1:
            return problem(loc, f"expected a signed 64-bit integer, got {value!r}", "invalid_value")
        return ()

configuration_required class-attribute

configuration_required: bool = False

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

identifier: str

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 = MappingProxyType({})

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",
    "scalar_storage",
    "twos_complement",
)

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

scalar_storage class-attribute

scalar_storage: StorageClass = 'multi_byte'

twos_complement class-attribute

twos_complement: bool = False

Whether this type's scalars are two's complement integers.

Asked by cast_value, whose out_of_range: "wrap" is defined only for such a target. Required rather than defaulted: a data type added later must decide, because either default would answer for it silently -- and getting it wrong in one direction accepts a cast the spec does not define.

__init__

__init__(*, 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
    }

fill_value_problems

fill_value_problems(
    value: object, loc: Loc = ()
) -> tuple[ValidationProblem, ...]

Why value is not a fill value of this type, if it is not.

Default: nothing. A data type this package does not model accepts whatever its extension says it does, and guessing would reject valid documents.

Source code in src/zarr_metadata/v3/data_type/_families.py
def fill_value_problems(self, value: object, loc: Loc = ()) -> tuple[ValidationProblem, ...]:
    if value == "NaT":
        return ()
    if not is_integer(value):
        return problem(
            loc, f"expected a signed 64-bit integer or 'NaT', got {value!r}", "invalid_value"
        )
    if not -(2**63) <= value <= 2**63 - 1:
        return problem(loc, f"expected a signed 64-bit integer, got {value!r}", "invalid_value")
    return ()

problems

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

Every value of this entity the spec disallows.

Locations are relative to the entity's configuration. Default: an entity whose type admits only valid values has nothing to add.

Source code in src/zarr_metadata/v3/_entity.py
def problems(self) -> tuple[ValidationProblem, ...]:
    """Every value of this entity the spec disallows.

    Locations are relative to the entity's `configuration`. Default:
    an entity whose type admits only valid values has nothing to add.
    """
    return ()

storage_class

storage_class() -> StorageClass | None

How one scalar occupies bytes, or None if undetermined.

None only for a composite whose parts are not all in scope: an answer would be a guess, and the rules that ask decline instead.

Source code in src/zarr_metadata/v3/_entity.py
def storage_class(self) -> StorageClass | None:
    """How one scalar occupies bytes, or None if undetermined.

    None only for a composite whose parts are not all in scope: an
    answer would be a guess, and the rules that ask decline instead.
    """
    return type(self).scalar_storage

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/_entity.py
def to_json(self) -> ZarrV3MetadataFieldJSON:
    """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.
    """
    configuration = self.configuration()
    if len(configuration) == 0 and self.must_understand:
        return cast("ZarrV3MetadataFieldJSON", type(self).identifier)
    entry: dict[str, object] = {"name": type(self).identifier}
    if len(configuration) != 0:
        entry["configuration"] = configuration
    if not self.must_understand:
        entry["must_understand"] = False
    return cast("ZarrV3MetadataFieldJSON", entry)

Opaque dataclass

A metadata field this reading did not turn into an entity.

Carrying the JSON rather than dropping it is what makes the result a real union: CodecEntity | Opaque is exhaustive and narrows, where CodecEntity | object is just object and narrows to nothing.

reason is the distinction a reader needs and could not otherwise make. out_of_scope is a name no entity in this Context claims -- an extension this reader does not model, which is not an error and is the reader's cue to resolve it elsewhere. invalid is a name that was claimed and then refused; the reasons are in the problems reported alongside.

Source code in src/zarr_metadata/v3/_entity.py
@dataclass(frozen=True, slots=True)
class Opaque:
    """A metadata field this reading did not turn into an entity.

    Carrying the JSON rather than dropping it is what makes the result a
    real union: `CodecEntity | Opaque` is exhaustive and narrows, where
    `CodecEntity | object` is just `object` and narrows to nothing.

    `reason` is the distinction a reader needs and could not otherwise
    make. `out_of_scope` is a name no entity in this `Context` claims --
    an extension this reader does not model, which is not an error and is
    the reader's cue to resolve it elsewhere. `invalid` is a name that
    *was* claimed and then refused; the reasons are in the problems
    reported alongside.
    """

    json: object
    reason: Literal["out_of_scope", "invalid"]

json instance-attribute

json: object

reason instance-attribute

reason: Literal['out_of_scope', 'invalid']

__init__

__init__(
    json: object, reason: Literal["out_of_scope", "invalid"]
) -> None

array_problems_v3

array_problems_v3(
    document: Mapping[str, object], context: Context
) -> tuple[ValidationProblem, ...]

Every semantic problem in document, read in context.

Expects a document the model layer has already accepted, so every member is present and typed as its TypedDict declares.

Source code in src/zarr_metadata/v3/_document.py
def array_problems_v3(
    document: Mapping[str, object], context: Context
) -> tuple[ValidationProblem, ...]:
    """Every semantic problem in `document`, read in `context`.

    Expects a document the model layer has already accepted, so every
    member is present and typed as its TypedDict declares.
    """
    array, problems = read_array_v3(document, context)
    return (*problems, *array.problems())

as_sequence

as_sequence(value: object) -> tuple[object, ...] | None

value as a tuple if it is a JSON array, else None.

A string is a sequence in Python and never a JSON array, so it is excluded.

Source code in src/zarr_metadata/v3/data_type/_families.py
def as_sequence(value: object) -> tuple[object, ...] | None:
    """`value` as a tuple if it is a JSON array, else None.

    A string is a sequence in Python and never a JSON array, so it is
    excluded.
    """
    if isinstance(value, str) or not isinstance(value, Sequence):
        return None
    return tuple(value)  # type: ignore[arg-type]

byte_values

byte_values(
    value: object, expected: int | None, loc: Loc
) -> tuple[ValidationProblem, ...]

An array of expected integers in [0, 255], or any length if None.

Source code in src/zarr_metadata/v3/data_type/_families.py
def byte_values(value: object, expected: int | None, loc: Loc) -> tuple[ValidationProblem, ...]:
    """An array of `expected` integers in [0, 255], or any length if None."""
    items = as_sequence(value)
    if items is None:
        return problem(loc, f"expected an array of byte values, got {value!r}", "invalid_value")
    if expected is not None and len(items) != expected:
        return problem(loc, f"expected {expected} byte values, got {len(items)}", "invalid_value")
    return tuple(
        found
        for index, item in enumerate(items)
        if not (is_integer(item) and 0 <= item <= 255)
        for found in problem(
            (*loc, index), f"expected integers in [0, 255], got {item!r}", "invalid_value"
        )
    )

chain_problems

chain_problems(
    codecs: Sequence[object],
    start: ArrayParts | None,
    loc: Loc,
) -> tuple[ValidationProblem, ...]

Every problem this pipeline has, ordering and per-codec alike.

start is what the first codec receives: the document's own array, or a shard's inner chunk, or its index.

Source code in src/zarr_metadata/v3/_chain.py
def chain_problems(
    codecs: Sequence[object], start: ArrayParts | None, loc: Loc
) -> tuple[ValidationProblem, ...]:
    """Every problem this pipeline has, ordering and per-codec alike.

    `start` is what the first codec receives: the document's own array, or
    a shard's inner chunk, or its index.
    """
    problems = list(order_problems(codecs, loc))
    incoming = start
    for index, codec in enumerate(codecs):
        if not isinstance(codec, CodecEntity):
            # Out of scope: unjudged, and everything after it is too.
            incoming = None
            continue
        problems.extend(within((*loc, index), codec.incoming_problems(incoming)))
        incoming = (
            None
            if incoming is None or type(codec).kind != "array_array"
            else codec.transition(incoming)
        )
    return tuple(problems)

coerce_members

coerce_members(
    configuration: Mapping[str, object], types: MemberTypes
) -> tuple[
    dict[str, object],
    tuple[ValidationProblem, ...],
    frozenset[str],
]

The members types declares, taken from configuration.

Returns what was accepted, every problem found, and the names of the required members that could not be read. Three kinds of problem, and they differ in that last part:

  • a key the entity does not declare says the value carries something extra, not that it is wrong;
  • an optional member of the wrong type leaves that member absent, and everything else about the entity is still readable -- a bad index_location says nothing about whether a shard's pipelines are well formed, and silencing them would lose a real judgment;
  • a required member missing or of the wrong type does stop it. There is no honest reading of a blosc whose level is a string.
Source code in src/zarr_metadata/v3/_entity.py
def coerce_members(
    configuration: Mapping[str, object], types: MemberTypes
) -> tuple[dict[str, object], tuple[ValidationProblem, ...], frozenset[str]]:
    """The members `types` declares, taken from `configuration`.

    Returns what was accepted, every problem found, and the names of the
    required members that could not be read. Three kinds of problem, and
    they differ in that last part:

    - a key the entity does not declare says the value carries something
      extra, not that it is wrong;
    - an *optional* member of the wrong type leaves that member absent,
      and everything else about the entity is still readable -- a bad
      `index_location` says nothing about whether a shard's pipelines
      are well formed, and silencing them would lose a real judgment;
    - a *required* member missing or of the wrong type does stop it.
      There is no honest reading of a `blosc` whose level is a string.
    """
    problems: list[ValidationProblem] = []
    members: dict[str, object] = {}
    unreadable: set[str] = set()
    for key in configuration:
        if key not in types:
            problems.extend(
                problem(("configuration", key), f"unexpected key {key!r}", "unknown_key")
            )
    for key, (required, check) in types.items():
        if key not in configuration:
            if required:
                problems.extend(
                    problem(("configuration", key), f"missing required key {key!r}", "missing_key")
                )
                unreadable.add(key)
            continue
        # Normalized before the check, so a check only ever sees the tuples
        # the TypedDicts declare -- never the lists raw JSON arrives as.
        value = _as_tuples(configuration[key])
        found = check(value, ("configuration", key))
        problems.extend(found)
        # An unknown key says the value carries something extra, not that
        # it is the wrong type -- so the member is still readable, and
        # dropping it here would make `to_json` lose what was written.
        if all(entry.kind == "unknown_key" for entry in found):
            members[key] = value
        elif required:
            unreadable.add(key)
    return members, tuple(problems), frozenset(unreadable)

is_bool

is_bool(
    value: object, loc: Loc
) -> tuple[ValidationProblem, ...]
Source code in src/zarr_metadata/v3/_entity.py
def is_bool(value: object, loc: Loc) -> tuple[ValidationProblem, ...]:
    if not isinstance(value, bool):
        return problem(loc, f"expected a boolean, got {value!r}")
    return ()

is_int

is_int(
    value: object, loc: Loc
) -> tuple[ValidationProblem, ...]

An integer, and not a bool -- JSON true is not the integer 1.

Source code in src/zarr_metadata/v3/_entity.py
def is_int(value: object, loc: Loc) -> tuple[ValidationProblem, ...]:
    """An integer, and not a bool -- JSON `true` is not the integer 1."""
    if not is_integer(value):
        return problem(loc, f"expected an integer, got {value!r}")
    return ()

is_integer

is_integer(value: object) -> TypeIs[int]

A JSON integer: an int, and not a bool.

True is an int in Python and true is not a number in JSON, so the two have to be told apart everywhere a number is expected.

Source code in src/zarr_metadata/v3/_entity.py
def is_integer(value: object) -> TypeIs[int]:
    """A JSON integer: an `int`, and not a `bool`.

    `True` is an `int` in Python and `true` is not a number in JSON, so
    the two have to be told apart everywhere a number is expected.
    """
    return not isinstance(value, bool) and isinstance(value, int)

is_json_value

is_json_value(
    value: object, loc: Loc
) -> tuple[ValidationProblem, ...]

Any JSON value at all -- the widest type a member can declare.

Source code in src/zarr_metadata/v3/_entity.py
def is_json_value(value: object, loc: Loc) -> tuple[ValidationProblem, ...]:
    """Any JSON value at all -- the widest type a member can declare."""
    if not is_json(value):
        return problem(loc, f"expected a JSON value, got {value!r}")
    return ()

is_str

is_str(
    value: object, loc: Loc
) -> tuple[ValidationProblem, ...]
Source code in src/zarr_metadata/v3/_entity.py
def is_str(value: object, loc: Loc) -> tuple[ValidationProblem, ...]:
    if not isinstance(value, str):
        return problem(loc, f"expected a string, got {value!r}")
    return ()

named_configuration

named_configuration(
    value: object,
) -> tuple[str | None, Mapping[str, object] | None, bool]

Split metadata into (name, configuration, must_understand).

The shared shape every entity arrives in: a bare name, or an object carrying one. A None name means the value is not a metadata field at all; a None configuration means the bare spelling was used.

Source code in src/zarr_metadata/v3/_entity.py
def named_configuration(
    value: object,
) -> tuple[str | None, Mapping[str, object] | None, bool]:
    """Split metadata into `(name, configuration, must_understand)`.

    The shared shape every entity arrives in: a bare name, or an object
    carrying one. A `None` name means the value is not a metadata field at
    all; a `None` configuration means the bare spelling was used.
    """
    if isinstance(value, str):
        return value, None, True
    if not isinstance(value, _Mapping):
        return None, None, True
    entry = cast("Mapping[str, object]", value)
    name = entry.get("name")
    if not isinstance(name, str):
        return None, None, True
    configuration = entry.get("configuration")
    must_understand = entry.get("must_understand", True)
    return (
        name,
        cast("Mapping[str, object]", configuration)
        if isinstance(configuration, _Mapping)
        else None,
        must_understand if isinstance(must_understand, bool) else True,
    )

one_of

one_of(allowed: tuple[str, ...]) -> TypeCheck

A member whose type is a closed set of names.

Source code in src/zarr_metadata/v3/_entity.py
def one_of(allowed: tuple[str, ...]) -> TypeCheck:
    """A member whose type is a closed set of names."""

    def check(value: object, loc: Loc) -> tuple[ValidationProblem, ...]:
        if value not in allowed:
            return problem(loc, f"expected one of {allowed!r}, got {value!r}", "invalid_value")
        return ()

    return check

order_problems

order_problems(
    codecs: Sequence[object], loc: Loc
) -> tuple[ValidationProblem, ...]

Whether the pipeline is shaped the way the spec orders it.

A codec out of scope is skipped: it imposes no ordering constraint, and it makes the exactly-one-array->bytes count inconclusive, because it might be the pipeline's own array->bytes stage. So that count is only checked when every codec is in scope.

Source code in src/zarr_metadata/v3/_chain.py
def order_problems(codecs: Sequence[object], loc: Loc) -> tuple[ValidationProblem, ...]:
    """Whether the pipeline is shaped the way the spec orders it.

    A codec out of scope is skipped: it imposes no ordering constraint,
    and it makes the exactly-one-`array->bytes` count inconclusive,
    because it might be the pipeline's own `array->bytes` stage. So that
    count is only checked when every codec is in scope.
    """
    problems: list[ValidationProblem] = []
    latest = -1
    array_bytes = 0
    for index, codec in enumerate(codecs):
        if not isinstance(codec, CodecEntity):
            continue
        kind = type(codec).kind
        rank = _KIND_RANK[kind]
        if rank < latest:
            problems.append(
                ValidationProblem(
                    (*loc, index),
                    f"{kind.replace('_', '->')} codec {_label(codec)} may not "
                    "follow a later-stage codec in the pipeline",
                    "invalid_value",
                )
            )
        latest = max(latest, rank)
        if kind == "array_bytes":
            array_bytes += 1
            if array_bytes > 1:
                problems.append(
                    ValidationProblem(
                        (*loc, index),
                        f"extra array->bytes codec {_label(codec)}: a pipeline has exactly one",
                        "invalid_value",
                    )
                )
    if array_bytes == 0 and all(isinstance(codec, CodecEntity) for codec in codecs):
        problems.append(
            ValidationProblem(loc, "codec pipeline has no array->bytes codec", "invalid_value")
        )
    return tuple(problems)

problem

problem(
    loc: Loc,
    message: str,
    kind: ProblemKind = "invalid_type",
) -> tuple[ValidationProblem, ...]

One problem, as the tuple every check returns.

Source code in src/zarr_metadata/v3/_entity.py
def problem(
    loc: Loc, message: str, kind: ProblemKind = "invalid_type"
) -> tuple[ValidationProblem, ...]:
    """One problem, as the tuple every check returns."""
    return (ValidationProblem(loc, message, kind),)

read_array_v3

read_array_v3(
    document: Mapping[str, object], context: Context
) -> tuple[ArrayDocumentV3, tuple[ValidationProblem, ...]]

document's extension points, read in context.

Type-space only: what comes back is well-typed by construction, and the problems are the reasons some of it is not an entity.

Source code in src/zarr_metadata/v3/_document.py
def read_array_v3(
    document: Mapping[str, object], context: Context
) -> tuple[ArrayDocumentV3, tuple[ValidationProblem, ...]]:
    """`document`'s extension points, read in `context`.

    Type-space only: what comes back is well-typed by construction, and
    the problems are the reasons some of it is not an entity.
    """
    read: dict[str, MetadataEntity | Opaque] = {}
    problems: list[ValidationProblem] = []
    for field, key in _SINGLE_FIELDS:
        value = document.get(key)
        if value is None:
            read[key] = Opaque(None, "invalid")
            continue
        entity, found = context.coerce(field, value, (key,), envelope_judged=True)
        read[key] = entity
        problems.extend(found)
    sequences: dict[str, tuple[MetadataEntity | Opaque, ...]] = {}
    for field, key in _SEQUENCE_FIELDS:
        read_entries: list[MetadataEntity | Opaque] = []
        entries = document.get(key)
        if isinstance(entries, (list, tuple)):
            for index, entry in enumerate(cast("Sequence[object]", entries)):
                entity, found = context.coerce(field, entry, (key, index), envelope_judged=True)
                read_entries.append(entity)
                problems.extend(found)
        sequences[key] = tuple(read_entries)
    return (
        ArrayDocumentV3(
            document=document,
            data_type=cast("DataTypeEntity | Opaque", read["data_type"]),
            chunk_grid=cast("ChunkGridEntity | Opaque", read["chunk_grid"]),
            chunk_key_encoding=read["chunk_key_encoding"],
            codecs=cast("tuple[CodecEntity | Opaque, ...]", sequences["codecs"]),
            storage_transformers=sequences["storage_transformers"],
        ),
        tuple(problems),
    )

sequence_of

sequence_of(element: TypeCheck) -> TypeCheck

A member whose type is a sequence, checked element by element.

Source code in src/zarr_metadata/v3/_entity.py
def sequence_of(element: TypeCheck) -> TypeCheck:
    """A member whose type is a sequence, checked element by element."""

    def check(value: object, loc: Loc) -> tuple[ValidationProblem, ...]:
        if not isinstance(value, (list, tuple)):
            return problem(loc, f"expected a sequence, got {value!r}")
        elements: tuple[object, ...] = tuple(cast("list[object] | tuple[object, ...]", value))
        return tuple(
            found for index, entry in enumerate(elements) for found in element(entry, (*loc, index))
        )

    return check

shard_index_grid

shard_index_grid(
    shard: ChunkGrid, inner: Sequence[object]
) -> ChunkGrid

The grid of a shard's index array.

The spec derives it from the two shapes around it: "The index is an array with 64-bit unsigned integers with a shape that matches the chunks per shard tuple with an appended dimension of size 2." The index is one array rather than a divided one, so each axis holds a single length — except that under a rectilinear grid the shard itself varies, so the chunk count varies with it and the axis holds every value it takes.

Source code in src/zarr_metadata/v3/_parts.py
def shard_index_grid(shard: ChunkGrid, inner: Sequence[object]) -> ChunkGrid:
    """The grid of a shard's index array.

    The spec derives it from the two shapes around it: "The index is an
    array with 64-bit unsigned integers with a shape that matches the
    chunks per shard tuple with an appended dimension of size 2." The
    index is one array rather than a divided one, so each axis holds a
    single length — except that under a rectilinear grid the shard itself
    varies, so the chunk count varies with it and the axis holds every
    value it takes.
    """
    inner_extents = _uniform(inner)
    trailing: frozenset[int] | None = frozenset({2})
    if shard.extents is None or len(shard.extents) != len(inner_extents):
        return ChunkGrid.derived((*(None,) * len(inner_extents), trailing))
    counts: list[frozenset[int] | None] = []
    for lengths, divisor in zip(shard.extents, inner_extents, strict=True):
        if lengths is None or divisor is None:
            counts.append(None)
            continue
        step = next(iter(divisor))
        quotients = {length // step for length in lengths if length % step == 0}
        counts.append(frozenset(quotients) if len(quotients) == len(lengths) else None)
    return ChunkGrid.derived((*counts, trailing))

within

within(
    prefix: Loc, problems: Sequence[ValidationProblem]
) -> tuple[ValidationProblem, ...]

One entity's problems, located in the document that holds it.

An entity reports relative to its own configuration, so that is what goes between the field and the member. A problem with an empty location is about the entity itself -- a malformed r<N> name, a codec that cannot encode what reaches it -- and lands on the field.

Source code in src/zarr_metadata/v3/_entity.py
def within(prefix: Loc, problems: Sequence[ValidationProblem]) -> tuple[ValidationProblem, ...]:
    """One entity's problems, located in the document that holds it.

    An entity reports relative to its own `configuration`, so that is what
    goes between the field and the member. A problem with an empty
    location is about the entity itself -- a malformed `r<N>` name, a
    codec that cannot encode what reaches it -- and lands on the field.
    """
    return tuple(
        ValidationProblem(
            (*prefix, *(("configuration", *found.loc) if len(found.loc) != 0 else ())),
            found.message,
            found.kind,
        )
        for found in problems
    )