Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/zarr-metadata/changes/349.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Each extension's metadata is a definition: a value holding the name the
metadata carries, the TypedDict its configuration is -- the one
declaration of that JSON -- and plain functions over that TypedDict for
the rules. `zarr_metadata.v3.definition` is the door, and its module
docstring the guide.

Reading a field is three steps, each usable on its own by a caller
holding nothing but JSON. `check(value, SomeTypedDict)`, from
`zarr_metadata.typed_json`, type-checks JSON against a TypedDict and needs
nothing else; `definition.judge(configuration)` is the check, each nested
field's envelope judged, and then the rules; `resolve(field, CodecDefinition, scope)`
reads a whole field in a scope -- its envelope judged, its name related to
a definition, its configuration judged, each nested field read the same
way -- and returns what the scope made of it, `Resolved`, with every
problem located. A name nothing in scope claims is `out_of_scope` and left
unjudged. A field is read as one of the five kinds, with or without type
arguments, and `configuration_of(resolved, GZIP_CODEC)` is its
configuration typed as that definition's TypedDict.

The TypedDict reads as the typing spec defines it -- `total`, `Required`,
`NotRequired`, `closed` and `extra_items` mean what they mean to a type
checker, whether or not its module postpones annotations -- and a
configuration comes back holding what its TypedDict admits and nothing
else: a key a closed TypedDict does not declare is reported, as
`unknown_key`, and left out, so a rule never meets one. A definition
refuses a TypedDict that says nothing of the keys it does not declare,
and a member typed `ZarrV3MetadataFieldJSON`, which would check as plain
JSON; a member holding another metadata field is typed with the field
alias of its kind, `CodecField`.

`gzip`, `bytes`, `crc32c` and the regular chunk grid are defined
(`GZIP_CODEC`, `BYTES_CODEC`, `CRC32C_CODEC`, `REGULAR_CHUNK_GRID`), and
`CORE` and `CORE_AND_EXTENSIONS` are the scopes that hold them. A third
party writes a TypedDict, a rule function and a definition, and extends a
scope with it; a definition checks itself when built, and nothing happens
at class creation.
4 changes: 4 additions & 0 deletions packages/zarr-metadata/docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ The package is organized to mirror the structure of the Zarr specifications:
documents, with subpackages for [chunk grids](v3/chunk_grid.md),
[chunk key encodings](v3/chunk_key_encoding.md), [codecs](v3/codec.md),
and [data types](v3/data_type.md)
- [`zarr_metadata.v3.definition`](v3/definition.md) β€” each extension's
metadata as a definition: the TypedDict its configuration is, and the
rules on it; check JSON against a TypedDict, judge a configuration, or
read a whole field in a scope. Its module docstring is the guide

The document types, models, and spec vocabulary β€” including the store keys β€”
are re-exported at the top level, so
Expand Down
8 changes: 8 additions & 0 deletions packages/zarr-metadata/docs/api/v3/definition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: definition
---

::: zarr_metadata.v3.definition
options:
inherited_members: false
show_if_no_docstring: false
1 change: 1 addition & 0 deletions packages/zarr-metadata/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ nav:
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr_metadata.v3.chunk_key_encoding</code>': api/v3/chunk_key_encoding.md
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr_metadata.v3.codec</code>': api/v3/codec.md
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr_metadata.v3.data_type</code>': api/v3/data_type.md
- '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code> <code>zarr_metadata.v3.definition</code>': api/v3/definition.md
- Release notes: release-notes.md
# This site is a Read the Docs subproject of zarr-python; give readers a way
# back to the parent docs, which list every companion package.
Expand Down
8 changes: 5 additions & 3 deletions packages/zarr-metadata/src/zarr_metadata/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,14 @@
is_array_metadata_v3,
is_group_metadata_v2,
is_group_metadata_v3,
is_metadata_field_v3,
parse_array_metadata_v2,
parse_array_metadata_v3,
parse_group_metadata_v2,
parse_group_metadata_v3,
parse_metadata_field_v3,
validate_array_metadata_v2,
validate_array_metadata_v3,
validate_group_metadata_v2,
validate_group_metadata_v3,
validate_metadata_field_v3,
)

# Store keys are facts about the on-disk specs, so they are defined in the
Expand All @@ -81,6 +78,11 @@
ZARR_V2_GROUP_METADATA_STORE_KEY,
ZarrV2GroupMetadataStoreKey,
)
from zarr_metadata.v3._common import (
is_metadata_field_v3,
parse_metadata_field_v3,
validate_metadata_field_v3,
)
from zarr_metadata.v3.array import (
ZARR_V3_ARRAY_METADATA_STORE_KEY,
ZarrV3ArrayMetadataStoreKey,
Expand Down
2 changes: 1 addition & 1 deletion packages/zarr-metadata/src/zarr_metadata/model/_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
load_store_json,
parse_array_metadata_v2,
parse_array_metadata_v3,
parse_metadata_field_v3,
)
from zarr_metadata.v2.array import ZARR_V2_ARRAY_METADATA_STORE_KEY
from zarr_metadata.v2.attributes import ZARR_V2_ATTRIBUTES_STORE_KEY
from zarr_metadata.v3._common import parse_metadata_field_v3
from zarr_metadata.v3.array import ZARR_V3_ARRAY_METADATA_STORE_KEY

if TYPE_CHECKING:
Expand Down
85 changes: 3 additions & 82 deletions packages/zarr-metadata/src/zarr_metadata/model/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
from zarr_metadata._json import prefixed as _prefix
from zarr_metadata.v2.array import ZarrV2ArrayMetadataJSON
from zarr_metadata.v2.group import ZarrV2GroupMetadataJSON
from zarr_metadata.v3._common import ZarrV3MetadataFieldJSON
from zarr_metadata.v3._common import (
validate_metadata_field_v3,
)
from zarr_metadata.v3.array import ZarrV3ArrayMetadataJSON
from zarr_metadata.v3.group import ZarrV3GroupMetadataJSON

Expand Down Expand Up @@ -144,87 +146,6 @@ def _validate_other_members(
return tuple(problems)


def validate_metadata_field_v3(
value: object, *, allow_must_understand_false: bool = True
) -> tuple[ValidationProblem, ...]:
"""Return every reason `value` is not a v3 metadata field.

A metadata field is a bare name string or a mapping containing `name` and
optional `configuration` and `must_understand` members.
"""
if isinstance(value, str):
return ()
if not isinstance(value, Mapping):
return (
ValidationProblem(
(),
"expected a metadata field (string or extension object)",
"invalid_type",
),
)
field = cast("Mapping[object, object]", value)
problems: list[ValidationProblem] = []
allowed_keys = frozenset({"name", "configuration", "must_understand"})
for key in field:
if not isinstance(key, str):
problems.append(
ValidationProblem((), f"non-string metadata field key {key!r}", "invalid_type")
)
elif key not in allowed_keys:
problems.append(
ValidationProblem((key,), "unexpected metadata field member", "invalid_value")
)
if not isinstance(field.get("name"), str):
problems.append(ValidationProblem(("name",), "expected a string name", "invalid_type"))
if "configuration" in field:
configuration = field["configuration"]
if not isinstance(configuration, Mapping):
problems.append(
ValidationProblem(("configuration",), "expected a mapping", "invalid_type")
)
elif not all(isinstance(k, str) for k in cast("Mapping[object, object]", configuration)):
problems.append(
ValidationProblem(("configuration",), "expected string keys", "invalid_type")
)
else:
for key, item in cast("Mapping[str, object]", configuration).items():
problems.extend(_prefix("configuration", _prefix(key, validate_json(item))))
if "must_understand" in field:
must_understand = field["must_understand"]
if not isinstance(must_understand, bool):
problems.append(
ValidationProblem(("must_understand",), "expected a boolean", "invalid_type")
)
elif not allow_must_understand_false and not must_understand:
problems.append(
ValidationProblem(
("must_understand",),
"false is not supported at this extension point",
"invalid_value",
)
)
return tuple(problems)


def is_metadata_field_v3(value: object) -> TypeGuard[ZarrV3MetadataFieldJSON]:
"""Whether `value` is a v3 metadata field: a bare name or a named config."""
if isinstance(value, str):
return True
if not isinstance(value, dict):
return False
field = cast("dict[object, object]", value)
return _is_canonical_json(field) and not validate_metadata_field_v3(field)


def parse_metadata_field_v3(value: object) -> ZarrV3MetadataFieldJSON:
"""Return `value` narrowed to `ZarrV3MetadataFieldJSON`, or raise `MetadataValidationError`."""
normalized = arrays_to_tuples(value)
problems = validate_metadata_field_v3(normalized)
if len(problems) != 0:
raise MetadataValidationError(problems)
return cast(ZarrV3MetadataFieldJSON, normalized)


def _is_array(value: object) -> TypeGuard[Sequence[object]]:
"""Whether `value` reads as a JSON array: a sequence that is not a string or bytes.

Expand Down
104 changes: 100 additions & 4 deletions packages/zarr-metadata/src/zarr_metadata/v3/_common.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
"""Internal cross-cutting aliases for Zarr v3 metadata.
"""The v3 metadata field: its JSON, and the validators that judge one on its own.

This module is private (underscore-prefixed) and exists to avoid circular
imports between leaf modules and sub-package `__init__.py` re-exports.
Public consumers should import `ZarrV3MetadataFieldJSON` from `zarr_metadata.v3`.
Private, and below both readers of a field: the model, which judges the
fields of a document, and the definitions, which read a field's
configuration. Public consumers import `ZarrV3MetadataFieldJSON` from
`zarr_metadata.v3`, and the validators from `zarr_metadata.model`.
"""

from collections.abc import Mapping
from typing import TypeGuard, cast

from zarr_metadata._common import ZarrV3NamedConfigJSON
from zarr_metadata._json import (
MetadataValidationError,
ValidationProblem,
arrays_to_tuples,
is_canonical_json,
prefixed,
validate_json,
)

ZarrV3MetadataFieldJSON = str | ZarrV3NamedConfigJSON
"""The JSON shape of any v3 metadata extension-point entry: either a bare
Expand All @@ -18,6 +30,90 @@
"""


def validate_metadata_field_v3(
value: object, *, allow_must_understand_false: bool = True
) -> tuple[ValidationProblem, ...]:
"""Return every reason `value` is not a v3 metadata field.

A metadata field is a bare name string or a mapping containing `name` and
optional `configuration` and `must_understand` members.
"""
if isinstance(value, str):
return ()
if not isinstance(value, Mapping):
return (
ValidationProblem(
(),
"expected a metadata field (string or extension object)",
"invalid_type",
),
)
field = cast("Mapping[object, object]", value)
problems: list[ValidationProblem] = []
allowed_keys = frozenset({"name", "configuration", "must_understand"})
for key in field:
if not isinstance(key, str):
problems.append(
ValidationProblem((), f"non-string metadata field key {key!r}", "invalid_type")
)
elif key not in allowed_keys:
problems.append(
ValidationProblem((key,), "unexpected metadata field member", "invalid_value")
)
if not isinstance(field.get("name"), str):
problems.append(ValidationProblem(("name",), "expected a string name", "invalid_type"))
if "configuration" in field:
configuration = field["configuration"]
if not isinstance(configuration, Mapping):
problems.append(
ValidationProblem(("configuration",), "expected a mapping", "invalid_type")
)
elif not all(isinstance(k, str) for k in cast("Mapping[object, object]", configuration)):
problems.append(
ValidationProblem(("configuration",), "expected string keys", "invalid_type")
)
else:
for key, item in cast("Mapping[str, object]", configuration).items():
problems.extend(prefixed("configuration", prefixed(key, validate_json(item))))
if "must_understand" in field:
must_understand = field["must_understand"]
if not isinstance(must_understand, bool):
problems.append(
ValidationProblem(("must_understand",), "expected a boolean", "invalid_type")
)
elif not allow_must_understand_false and not must_understand:
problems.append(
ValidationProblem(
("must_understand",),
"false is not supported at this extension point",
"invalid_value",
)
)
return tuple(problems)


def is_metadata_field_v3(value: object) -> TypeGuard[ZarrV3MetadataFieldJSON]:
"""Whether `value` is a v3 metadata field: a bare name or a named config."""
if isinstance(value, str):
return True
if not isinstance(value, dict):
return False
field = cast("dict[object, object]", value)
return is_canonical_json(field) and not validate_metadata_field_v3(field)


def parse_metadata_field_v3(value: object) -> ZarrV3MetadataFieldJSON:
"""Return `value` narrowed to `ZarrV3MetadataFieldJSON`, or raise `MetadataValidationError`."""
normalized = arrays_to_tuples(value)
problems = validate_metadata_field_v3(normalized)
if len(problems) != 0:
raise MetadataValidationError(problems)
return cast(ZarrV3MetadataFieldJSON, normalized)


__all__ = [
"ZarrV3MetadataFieldJSON",
"is_metadata_field_v3",
"parse_metadata_field_v3",
"validate_metadata_field_v3",
]
Loading
Loading