Commit 015732f
feat(zarr-metadata): Zarr metadata model layer (#4119)
* chore(deps): bump the actions group across 1 directory with 8 updates (#176)
Bumps the actions group with 8 updates in the / directory:
| Package | From | To |
| --- | --- | --- |
| [prefix-dev/setup-pixi](https://github.com/prefix-dev/setup-pixi) | `0.9.5` | `0.9.6` |
| [codecov/codecov-action](https://github.com/codecov/codecov-action) | `6.0.0` | `6.0.1` |
| [github/issue-metrics](https://github.com/github/issue-metrics) | `4.2.2` | `4.2.7` |
| [j178/prek-action](https://github.com/j178/prek-action) | `2.0.3` | `2.0.4` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `7.0.0` | `8.0.1` |
| [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.13.0` | `1.14.0` |
| [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.5.3` | `0.5.6` |
Updates `prefix-dev/setup-pixi` from 0.9.5 to 0.9.6
- [Release notes](https://github.com/prefix-dev/setup-pixi/releases)
- [Commits](prefix-dev/setup-pixi@1b2de7f...5185adf)
Updates `codecov/codecov-action` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](codecov/codecov-action@57e3a13...e79a696)
Updates `github/issue-metrics` from 4.2.2 to 4.2.7
- [Release notes](https://github.com/github/issue-metrics/releases)
- [Commits](github-community-projects/issue-metrics@c9e9838...1e38d5e)
Updates `j178/prek-action` from 2.0.3 to 2.0.4
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](j178/prek-action@6ad8027...bdca6f1)
Updates `actions/upload-artifact` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v7...043fb46)
Updates `actions/download-artifact` from 7.0.0 to 8.0.1
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@v7...3e5f45b)
Updates `pypa/gh-action-pypi-publish` from 1.13.0 to 1.14.0
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](pypa/gh-action-pypi-publish@v1.13.0...cef2210)
Updates `zizmorcore/zizmor-action` from 0.5.3 to 0.5.6
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](zizmorcore/zizmor-action@b1d7e1f...5f14fd0)
---
updated-dependencies:
- dependency-name: prefix-dev/setup-pixi
dependency-version: 0.9.6
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions
- dependency-name: codecov/codecov-action
dependency-version: 6.0.1
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions
- dependency-name: github/issue-metrics
dependency-version: 4.2.7
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions
- dependency-name: j178/prek-action
dependency-version: 2.0.4
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions
- dependency-name: actions/upload-artifact
dependency-version: 7.0.1
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions
- dependency-name: actions/download-artifact
dependency-version: 8.0.1
dependency-type: direct:production
update-type: version-update:semver-major
dependency-group: actions
- dependency-name: pypa/gh-action-pypi-publish
dependency-version: 1.14.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: actions
- dependency-name: zizmorcore/zizmor-action
dependency-version: 0.5.6
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(zarr-metadata): add model._validation — structural validators
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): add model._array — array metadata models
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): port array model test suite
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): add group metadata models
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): add consolidated metadata model tests
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): export model layer from package front door
Assisted-by: ClaudeCode:claude-fable-5
* docs(zarr-metadata): changelog entry for the model layer
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): harden model validation and error reporting
Findings from an API-ergonomics exercise (a fresh agent consuming
defective metadata documents):
- ValidationProblem gains a machine-readable kind (missing_key /
invalid_type / invalid_value / invalid_json), ending message
string-matching in consumers.
- The v2 array validator now enforces what its types declare (dtype,
order, compressor, filters, dimension_separator), and all four
document validators check the fixed zarr_format / node_type literals.
- All ingestion failures surface as MetadataValidationError: missing
store keys and undecodable bytes in from_key_value (previously
KeyError / JSONDecodeError) and constructor invariants (previously
bare ValueError).
- ZarrMetadataV3 is renamed NamedConfigModelV3: it models a name +
configuration pair, and the old name read as a whole-document type.
- Discoverability: the validate_*/is_*/parse_* contract is documented
on zarr_metadata.model itself; update() documents that it does not
re-validate; the v2 to_json/to_key_value attributes split is
documented on both.
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): annotate metadata fields with role alias MetadataFieldModelV3
Model fields and consumer signatures should convey the logical meaning
of the type (a metadata-document field), not the form it takes when
JSON-serialized (a named configuration). MetadataFieldModelV3 is today
exactly NamedConfigModelV3; if a future spec revision adds a field form
that cannot normalize to name + configuration, the alias widens to a
union and annotation sites do not move. Mirrors the raw-layer split
between NamedConfigV3 (shape) and MetadataV3 (field union).
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): assert required-key coverage via the typed constant
test_v3_to_json_includes_required_fields hand-enumerated keys with
chained asserts, restating what ARRAY_METADATA_REQUIRED_KEYS_V3 already
defines. Now: one coverage assert driven by the constant (tracks the
TypedDict automatically) and one whole-document equality for the values.
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): single whole-document comparison for v3 to_json
The subset assert against ARRAY_METADATA_REQUIRED_KEYS_V3 was redundant:
equality with a literal that spells out the full document already covers
every required key. One dict, one assert.
Assisted-by: ClaudeCode:claude-fable-5
* fix(zarr-metadata): close validation holes found by adversarial review
Invalid documents that previously passed validation:
- shape/chunks containing JSON booleans (bool is an int subclass in
Python but not an integer in a metadata document) or negative values
- dimension_names whose length does not match shape
- attributes and configuration values that are not JSON-serializable —
now checked recursively like fill_value, so an int-keyed dict cannot
be silently rewritten by json.dumps on round-trip and a set() cannot
escape as a TypeError from to_key_value
- consolidated_metadata envelopes: the group validator now deep-validates
the envelope and its entries via the shared
validate_consolidated_metadata_v3, which ConsolidatedMetadataModelV3
.from_json also uses, so is_group_metadata_v3 never vouches for a
document the model constructor would reject
Three pre-existing test fixtures paired dimension_names=('x',) with the
default scalar shape () and were themselves spec-invalid; they now use a
matching 1-d shape.
Deliberately unchanged, pending a design decision: unknown extension
fields with must_understand: true still pass (which layer owns the
spec's refusal duty), and empty v2 dtype records / empty codec names
still pass (domain territory).
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): expose must_understand_fields on the v3 models
The v3 core spec: 'An implementation MUST fail to open Zarr groups or
arrays if any metadata fields are present which (a) the implementation
does not recognize and (b) are not explicitly set to
"must_understand": false' — and fields are implicitly must-understand
unless waived.
The model layer cannot discharge this itself: recognition is
reader-specific (consolidated_metadata is itself an extension field one
reader understands and another does not), and a document carrying a
must-understand extension is still a valid document. So the models
partition by obligation: must_understand_fields is the subset of
extra_fields not explicitly waived, and a compliant reader fails to
open when must_understand_fields.keys() - recognized is non-empty.
The design spec pins that duty on the part-2 resolve layer, matching
what zarr-python's parse_extra_fields enforces today.
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): executable example of pydantic integration
Delegate wholesale rather than letting pydantic introspect the dataclass:
InstanceOf (is-instance core schema) + BeforeValidator(from_json) +
PlainSerializer(to_json, return_type=dict). Field-by-field validation is
impossible anyway (the models' annotation-only imports live behind
TYPE_CHECKING, so pydantic raises class-not-fully-defined) and would
diverge from the library's structural validation via coercion if it
weren't. MetadataValidationError subclasses ValueError, so failed parses
surface as pydantic ValidationError with the loc-annotated messages.
pydantic is already in the package's test dependency group.
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): document pydantic's native dataclass path and why not
Correcting the previous commit's too-strong claim: pydantic CAN
introspect the model dataclass — TypeAdapter(...).rebuild() with the
TYPE_CHECKING-only names supplied as _types_namespace resolves the
schema, and __post_init__ invariants still run. A new test exercises
that path and pins why it is not the recommended integration: it
validates the model shape, not the document (bare-string data_type
rejected — no from_json normalization), and pydantic's lax coercion
silently re-opens holes the library validators close (shape=[True, -5]
coerces to (1, -5); a wrong dimension_names count passes).
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): engine-backed pydantic BaseModel example (pydantic-zarr pattern)
For consumers that want a first-class BaseModel — JSON schema generation
and generics for typed attributes, as in pydantic-zarr's ArraySpec — the
example adds a third pattern: pydantic-native fields as the user-facing
surface, with the library as the engine. A mode='before' validator
canonicalizes every input via from_json(...).to_json(), so structural
validation and normalization run before pydantic parses fields (the
[True, -5] coercion divergence cannot occur), and to_metadata_model /
to_document bridge both ways through the document form. One translation
noted at the bridge: the document spells 'no dimension names' as key
absence, the pydantic side as None.
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): pin that a null dimension_names field is invalid
Spec: 'If specified, must be an array of strings or null objects...
If dimension_names is not specified, all dimensions are unnamed.' The
null object is a permitted element (an unnamed dimension), never the
field value; key absence is the only spelling of 'not specified'. Pins
the validator's existing rejection so it is not later 'fixed' to accept
null-as-absence, and documents that in-memory None maps to key absence
on serialization.
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): optional pydantic integration as zarr_metadata.pydantic
Gamed out three shapes with prototypes before choosing:
- dunders on the core classes (works, verified pydantic 2.0-2.13, but
puts a framework protocol in the dependency-free layer);
- pydantic-aware SUBCLASSES in a namespace (rejected on empirical
failures: identity split breaks equality, core instances are rejected
by subclass-typed fields, and nested construction produces core-class
children unless every cross-reference is overridden);
- Annotated field types over the CORE classes in an opt-in module
(chosen): instances are the core classes so interop is free, pydantic
imports eagerly at the module (loud failure when absent), core stays
framework-free, and pydantic-protocol risk is quarantined to one
clearly-labeled module.
The module exports one field type per model. Validation delegates to
from_json (structural validation and normalization cannot be bypassed by
pydantic coercion), instances pass through, serialization emits the
canonical document, and WithJsonSchema describes the accepted document
form so model_json_schema works. Tests cover all seven field types,
core-instance interop, error quality, JSON schema, roundtrip, and that
importing zarr_metadata does not import pydantic.
Assisted-by: ClaudeCode:claude-fable-5
* fix(zarr-metadata): create_default derives the chunk grid from shape
create_default(shape=(100, 100)) silently kept the scalar default's 0-d
chunk grid (chunk_shape: ()), producing a structurally-valid but
semantically inconsistent document — a footgun for every test fixture
built on it. When shape is overridden and the grid is not, the default
is now one regular chunk covering the array (v3 chunk_shape == shape,
v2 chunks == shape); an explicit chunk_grid/chunks override still wins.
update() stays a dumb dataclasses.replace, per its documented contract.
One existing whole-document test literal carried exactly this
inconsistency (shape (10,) with chunk_shape ()) and was updated.
Assisted-by: ClaudeCode:claude-fable-5
* test(zarr-metadata): pin zero-length-dimension case of the derived chunk grid
The spec's constraint is conditional ('non-zero when the corresponding
dimensions of the arrays have non-zero length'), so chunk_shape == shape
is sound for every shape, including empty dimensions.
Assisted-by: ClaudeCode:claude-fable-5
* docs(zarr-metadata): document that create_default's derivation is one-way
Overriding shape without a grid derives the grid; the reverse does not
hold. A user-supplied chunk_grid is an extension point taken verbatim —
deriving shape from it would require interpreting grid configurations,
which the model layer never does and cannot do for unrecognized grid
names. Pinned by test so the asymmetry reads as a decision, not an
oversight; the v2 model documents the same one-way rule for chunks for
cross-version consistency.
Assisted-by: ClaudeCode:claude-fable-5
* refactor(zarr-metadata): eliminate every type-ignore comment
Audited all 24 (15 src, 9 tests); each was either obsolete, replaceable
by a sound cast, or avoidable by better-typed code:
- Two fill_value arg-type ignores were factually obsolete: their
justifying comment said 'fill_value: object in upstream TypedDict',
but 0.3.0 narrowed it to JSONValue.
- Eight pre-existing call-arg/reportInvalidTypeForm ignores on the PEP
728 TypedDicts and the recursive JSONValue alias were mypy-dialect
suppressions that the checker of record (pyright strict with
enableExperimentalFeatures) never needed; mypy has never checked this
package.
- The two extra_fields comprehensions are a genuine checker limitation
(a key filter cannot narrow a PEP 728 TypedDict's item-value union),
now expressed as casts whose comments state the soundness claim
instead of suppressing the diagnostic.
- pydantic.py's generic coercer factory takes the parse callable
explicitly instead of calling from_json through type[_M].
- NamedConfigModelV3.from_json casts the validated configuration (sound
since configuration values are now deep-validated as JSON).
- Tests: _build_v2/_build_v3 gained real Unpack[...Partial] signatures;
raw-document pydantic inputs go through model_validate (the idiomatic
entry point for untyped data) instead of ignoring constructor
signatures; the frozen-dataclass test uses setattr for its
intentional runtime error.
src and tests/model now carry zero type-ignore comments.
Assisted-by: ClaudeCode:claude-fable-5
* fix(zarr-metadata): absent v2 dimension_separator means '.', not '/'
roborev job 426 (branch review) found that ArrayMetadataModelV2
normalized an ABSENT dimension_separator key to '/', inherited verbatim
from the zng prototype. The v2 convention's default is '.': a consumer
deriving chunk keys from the model against a real-world v2 array
written with the default separator would have looked for '0/0' instead
of '0.0'. No test caught it because every fixture started from
create_default(), which always carries an explicit separator.
Absence is normalized to an explicit '.' -- a semantics-preserving
spelling normalization consistent with the model's existing canonical
forms (bare-string metadata fields, missing configuration). The field
is deliberately NOT modeled as Optional: the document grammar has no
null spelling for this key, and a None in the model invites writing
'dimension_separator': null into documents. Pinned by three tests,
including explicit-null rejection.
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): UNSET sentinel for absent optional document keys
Establishes the models' None/absence invariant: None in a model always
corresponds to a JSON null in the document (a v2 compressor/filters
value, an unnamed dimension inside dimension_names), and UNSET always
means the key is absent. The two are never interchangeable.
Applied to the two fields that used None as an absence marker:
dimension_names (ArrayMetadataModelV3) and consolidated_metadata
(GroupMetadataModelV3). For dimension_names this also preserves a
semantic distinction d-v-b identified: an absent field ("there are no
dimension names") and an explicit all-null array ("every dimension has
a name, which is null") are different documents; both spellings now
round-trip faithfully and compare unequal.
Normalizing absence to the all-null form was considered and rejected:
the spellings' interpretations coincide but interpretation-equivalence
is the resolve layer's business, and collapsing document-level
distinctions on that basis is the layer violation this package exists
to avoid. Verified that current zarr-python never writes
"consolidated_metadata": null (GroupMetadata.to_dict pops the key), so
None there was purely an absence marker, not a document spelling.
UnsetType is a single-member enum (identity-checkable, repr "UNSET",
deliberately truthy so `if not x` cannot silently treat it as absent);
UNSET and UnsetType are exported from zarr_metadata.model and the
package front door.
Assisted-by: ClaudeCode:claude-fable-5
* fix(zarr-metadata): accept and preserve the wild consolidated_metadata null
Historical zarr-python versions wrote "consolidated_metadata": null into
group documents for groups without consolidated metadata, so real stores
contain the spelling; the validator was rejecting those documents
("expected a mapping"). Per the None/UNSET invariant, the field is now
honestly three-state: UNSET (key absent), None (the document's literal
null, preserved on round-trip), or a ConsolidatedMetadataModelV3.
Interpreting null as absence is the consumer's call, not a document
rewrite by this layer.
Also records an implementation constraint on the sentinel itself:
typing_extensions.Sentinel (PEP 661) is the intended spelling, but
pyright 1.1.411 degrades a Sentinel to Unknown in dataclass FIELD
annotations (function signatures work), verified by probe both with and
without enableExperimentalFeatures. Using it would reintroduce
suppressions at every use site under the strict gate, so UNSET stays a
single-member enum, with the Sentinel switch documented in
_sentinel.py for when pyright catches up.
Assisted-by: ClaudeCode:claude-fable-5
* fix(zarr-metadata): repair consolidated_metadata null to absence, not preserve it
d-v-b: the bugged spelling should not be preserved or honored. The
three-state field reverts to two states (model | UNSET): a document
carrying "consolidated_metadata": null — written by a historical
zarr-python bug — remains readable (the validator accepts it so real
stores open), but the spelling gets no model representation: it is read
as absence and never written back. This is the one deliberate exception
to faithful round-tripping, pinned as such: from_json(null_doc) equals
from_json(absent_doc), and to_json omits the key.
Assisted-by: ClaudeCode:claude-fable-5
* docs(zarr-metadata): pin the Sentinel blocker to pyright regression #11115
Investigated: the Unknown-degradation of typing_extensions.Sentinel is a
confirmed upstream pyright regression, not by-design. Introduced in
1.1.405 (verified: 1.1.404 is clean on the same probe, 1.1.411 fails),
affects reads of any class-body attribute annotation (dataclass or
plain class), does not affect function signatures or module variables,
and Final on the sentinel does not help. Tracked as
microsoft/pyright#11115 (open, bug+regression); #11467 closed as its
duplicate. The enum sentinel stays until the fix lands.
Assisted-by: ClaudeCode:claude-fable-5
* docs(zarr-metadata): sentinel switch is blocked by mypy too, not just pyright
Pinning a working pyright (<= 1.1.404) in CI was considered and does
not suffice: the pin controls one of four checker surfaces. Contributor
IDEs (Pylance bundles current pyright) and downstream consumers'
pyright read the py.typed inline annotations with their own versions,
and decisively, mypy 2.1.0 has no PEP 661 support at all — a sentinel
in type position is a hard [valid-type] error, which would degrade
these fields to Any for mypy consumers, including zarr-python itself.
The enum is currently the only spelling with exact types on every
surface; switch when pyright#11115 is fixed AND mypy implements PEP 661.
Assisted-by: ClaudeCode:claude-fable-5
* docs(zarr-metadata): PEP 661 is Final (Python 3.15), not a draft
Corrects the sentinel implementation note: PEP 661 was accepted
2026-04-23 and ships as stdlib sentinel in Python 3.15. The two checker
gaps blocking the Sentinel spelling (pyright regression #11115, mypy
not yet implementing the PEP) are therefore temporary gaps against a
Final standard, and the enum is a stopgap with a defined end state.
Assisted-by: ClaudeCode:claude-fable-5
* docs(zarr-metadata): ty fully supports typed sentinels; pyright/mypy are the laggards
ty 0.0.56 types the Sentinel spelling perfectly in dataclass fields:
exact T | UNSET unions, both-direction is/is-not narrowing, and
wrong-typed constructor arguments rejected (verified with reveal_type,
so it is real inference, not silent Any). The checker matrix for
sentinel-in-type-position is therefore ty full / pyright regressed
(#11115) / mypy not implemented — recorded so the switch decision has
current calibration.
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): adopt the PEP 661 sentinel for UNSET
d-v-b's call: PEP 661 is Final, ty already types the sentinel spelling
exactly, mypy support is in review (python/mypy#21647) and treated as
imminent, and pyright has a known-good version — so use the standard
sentinel today rather than carrying the enum stopgap.
- UNSET is now typing_extensions.Sentinel("UNSET"), used directly in
type expressions (tuple[str | None, ...] | UNSET); the UnsetType
companion enum is gone from the API.
- typing_extensions floor bumped to 4.14 (where Sentinel arrived).
- CI pins pyright==1.1.404, the last version before the class-attribute
sentinel regression (microsoft/pyright#11115); pyproject documents the
same pin for local runs. 0 errors on the pin; ty checks the sentinel
fields clean (its 2 remaining diagnostics are its incomplete PEP 728
extra_items write support, unrelated).
- Known short-term cost, accepted deliberately: mypy-checked consumers
need cast/type-ignore at narrowing sites until mypy#21647 merges, and
contributors' Pylance may show phantom Unknowns until the pyright fix
ships. Recorded in _sentinel.py and the changelog.
- The pydantic native-introspection test reverts to documenting that
introspection is unsupported (pydantic 2.13 cannot schema a Sentinel);
the delegation patterns are unaffected.
Assisted-by: ClaudeCode:claude-fable-5
* fix(zarr-metadata): .zattrs presence is part of the store, not an artifact
Resolves the last flagged round-trip question from the initial port:
to_key_value on the v2 models always emitted a .zattrs key, so a store
that never had one gained a file on round-trip. Per d-v-b's ruling,
attributes on ArrayMetadataModelV2/GroupMetadataModelV2 is now
`dict[str, JSONValue] | UNSET`: UNSET means no .zattrs file (and no
attributes key in the merged document form) and emits nothing, while
any dict — including an explicit empty {} — means the file exists and
is emitted. The two spellings stay distinct through round-trips, per
the None/UNSET invariant; create_default defaults to UNSET (a fresh
minimal node has no .zattrs).
Assisted-by: ClaudeCode:claude-fable-5
* fix(zarr-metadata): require typing_extensions>=4.16 so UNSET pickles by reference
Models hold the UNSET sentinel as field values (dimension_names,
attributes), so any object graph containing a model must survive pickling
and deep-copying. A sentinel's contract is identity — state-based pickling
would produce impostor objects that fail every `is UNSET` check — which is
why typing_extensions <= 4.15 refused to pickle sentinels at all.
typing_extensions 4.16 implements Sentinel.__reduce__ as pickling by
reference (a lookup of the sentinel's name on its defining module), the same
mechanism enum members use, so the singleton identity survives the round
trip. Bump the floor and pin the behavior with tests: identity across
pickle/copy/deepcopy, models holding UNSET round-tripping, and a guard that
a non-importable sentinel still fails loudly rather than pickling by state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(zarr-metadata): format-version-first naming; dataclasses primary, JSON suffix for documents
Applies the naming decisions from the PR discussion: ZarrV2/ZarrV3 moves to
the front of every type name so a format version cannot be misread as a
class revision, and the model dataclasses take the bare entity names
(ZarrV3ArrayMetadata, ZarrV3GroupMetadata, ZarrV3ConsolidatedMetadata,
ZarrV3NamedConfig, role alias ZarrV3MetadataField) while the TypedDict
document forms carry a JSON suffix (ZarrV3ArrayMetadataJSON, ...,
ZarrV3MetadataFieldJSON, ZarrV3NamedConfigJSON).
The zarr_metadata.pydantic field types take the bare entity names, matching
the model classes they validate into; the module now references the model
module qualified to keep those names free. Raw-layer names released in 0.3.0
are renamed without aliases (pre-1.0), documented in changes/4119.removal.md.
Validation problem messages name documents in plain English instead of type
names. snake_case function names (validate_array_metadata_v3, ...) and
SCREAMING_SNAKE constants are deliberately untouched: the revision ambiguity
the rename fixes does not arise for them, and renaming them is a separate
decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(zarr-metadata): harden model validation
Assisted-by: Codex:gpt-5
* docs(zarr-metadata): define v3 conformance boundary
Assisted-by: Codex:gpt-5
* fix(zarr-metadata): preserve v3 extension obligations
Assisted-by: Codex:gpt-5
* fix(zarr-metadata): enforce v3 core extension rules
Assisted-by: Codex:gpt-5
* fix(zarr-metadata): align v3 additional field types
Assisted-by: Codex:gpt-5
* chore(zarr-metadata): finalize v3 model conformance
Assisted-by: Codex:gpt-5
* docs: remove llm docs
* chore(zarr-metadata): correct changelog PR number
Assisted-by: Codex:gpt-5
* fix(metadata): enforce canonical document boundaries
Assisted-by: Codex:GPT-5
* docs(metadata): record review fix design
Assisted-by: Codex:gpt-5
* fix(metadata): align validation and schemas
Assisted-by: Codex:gpt-5
* fix(metadata): tighten validation boundaries
Assisted-by: Codex:gpt-5
* fix(metadata): repair package CI
Assisted-by: Codex:gpt-5
* chore: remove stray design notes from docs
Assisted-by: ClaudeCode:claude-fable-5
* fix(zarr-metadata): deep-copy mutable state in to_json output
to_json previously returned documents holding direct references to the
model's internal dicts (attributes, named-config configurations, extra
fields, v2 compressor/filters, consolidated entries), so mutating a
serialized document silently mutated the frozen model. Every value that
can hold a mutable container is now deep-copied on the way out, with
parametrized tests proving mutation independence for all seven models.
Assisted-by: ClaudeCode:claude-fable-5
* docs(zarr-metadata): describe the widened package scope
The README tagline, intro, and scope section (and the PyPI description)
still presented the package as type definitions only. Restructure them
around the two layers plus optional integration, extend the contribution
scope to models and structural validation, and state the runtime-behavior
boundary explicitly.
Assisted-by: ClaudeCode:claude-fable-5
* style(zarr-metadata): move None to the end of the JSONValue union
The unpinned ruff in CI now enforces RUF036.
Assisted-by: ClaudeCode:claude-fable-5
* feat(zarr-metadata): export store-key aliases and type to_key_value with them
The six store-key Literal aliases were private and unused: only reachable
via underscore modules, and absent from every signature. Export them from
zarr_metadata.model beside their constants (matching the package's
name/constant pairing everywhere else), and key each to_key_value return
mapping by them so the store keys a model can emit are visible in its
signature. from_key_value keeps Mapping[str, bytes] input on purpose — it
accepts whole store mappings. A pair test guards export and value drift,
and the removal note now states the version-placement and JSON-suffix
conventions explicitly.
Assisted-by: ClaudeCode:claude-fable-5
* refactor(zarr-metadata): make every public type name parse against a naming grammar
Three grammars now cover the public surface, enforced by a conformance
test that walks every public module's __all__:
- core document/model names: ZarrV{2,3} + entity + optional role suffix
(JSON / JSONPartial / Partial / StoreKey)
- extension-entity names: registered entity + exactly one role suffix
(CodecMetadata, DataTypeName, FillValue, ...)
- a closed standalone-vocabulary allowlist for role-less scalar and
diagnostic types, with a staleness guard
The three .z-file document types were the only names that fit no grammar
and are renamed: ZArrayMetadata -> ZarrV2ZArrayJSON, ZGroupMetadata ->
ZarrV2ZGroupJSON, ZAttrsMetadata -> ZarrV2ZAttrsJSON. The leading V2 of
V2ChunkKeyEncodingMetadata is that encoding's registered entity name, not
a format version; its module docstring now says so.
Assisted-by: ClaudeCode:claude-fable-5
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent cb93ef8 commit 015732f
47 files changed
Lines changed: 5940 additions & 299 deletions
File tree
- .github/workflows
- packages/zarr-metadata
- changes
- src/zarr_metadata
- model
- v2
- v3
- chunk_key_encoding
- codec
- data_type
- tests
- model
- v2
- array
- consolidated
- group
- v3
- array
- consolidated
- group
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
86 | 89 | | |
87 | 90 | | |
88 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
12 | 21 | | |
13 | 22 | | |
14 | 23 | | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
19 | 27 | | |
20 | 28 | | |
21 | 29 | | |
22 | | - | |
23 | | - | |
| 30 | + | |
24 | 31 | | |
25 | 32 | | |
26 | 33 | | |
27 | 34 | | |
28 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
29 | 47 | | |
30 | 48 | | |
31 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
32 | 54 | | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
37 | 62 | | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
42 | 70 | | |
43 | 71 | | |
44 | 72 | | |
45 | 73 | | |
46 | 74 | | |
47 | 75 | | |
48 | | - | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
49 | 84 | | |
50 | 85 | | |
51 | 86 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
36 | 40 | | |
37 | 41 | | |
38 | 42 | | |
| |||
43 | 47 | | |
44 | 48 | | |
45 | 49 | | |
46 | | - | |
| 50 | + | |
47 | 51 | | |
48 | 52 | | |
49 | 53 | | |
| |||
67 | 71 | | |
68 | 72 | | |
69 | 73 | | |
70 | | - | |
71 | | - | |
72 | | - | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
73 | 77 | | |
74 | 78 | | |
75 | 79 | | |
| |||
82 | 86 | | |
83 | 87 | | |
84 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
85 | 92 | | |
86 | 93 | | |
87 | 94 | | |
| |||
0 commit comments