-
Notifications
You must be signed in to change notification settings - Fork 24
fix(py): resolve cross-file JSON Schema references during validation #659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
thewtex
merged 3 commits into
fideus-labs:main
from
vboussot:fix/py-issue-647-schema-registry
Aug 21, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,51 +1,129 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC | ||
| # SPDX-License-Identifier: MIT | ||
| import json | ||
| from pathlib import Path | ||
| from functools import cache | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from importlib_resources import files as file_resources | ||
| from packaging import version as packaging_version | ||
|
|
||
| if TYPE_CHECKING: | ||
| from importlib_resources.abc import Traversable | ||
| from referencing import Registry | ||
|
|
||
| NGFF_URI = "https://ngff.openmicroscopy.org" | ||
|
|
||
|
|
||
| @cache | ||
| def _bundled_versions() -> frozenset: | ||
| """The versions that have a bundled ``spec/<version>/schemas`` tree.""" | ||
| spec = file_resources("ngff_zarr").joinpath("spec") | ||
| return frozenset( | ||
| entry.name | ||
| for entry in spec.iterdir() | ||
| if entry.is_dir() and entry.joinpath("schemas").is_dir() | ||
| ) | ||
|
|
||
|
|
||
| def _schemas_dir(version: str) -> "Traversable": | ||
| """Locate the bundled ``schemas`` directory that holds ``version``. | ||
|
|
||
| A pre-release shares the tree of the release it leads to: the bundled 0.6 | ||
| schemas carry the upstream ``0.6.dev4`` tag, so the ``"0.6.dev4"`` string a | ||
| 0.6 store records on disk resolves to ``spec/0.6`` just as ``"0.6"`` does. | ||
|
|
||
| The version is matched against the bundled directory names rather than | ||
| joined onto the path as given, because it reaches here straight from a | ||
| store's own metadata. | ||
| """ | ||
| available = _bundled_versions() | ||
| name = str(version) | ||
| if name not in available: | ||
| try: | ||
| parsed = packaging_version.parse(name) | ||
| except packaging_version.InvalidVersion: | ||
| name = "" | ||
| else: | ||
| # Only a pre-release leads to a release. A post-release or a local | ||
| # version shares a base version with one without being it. | ||
| name = parsed.base_version if parsed.is_prerelease else "" | ||
| if name not in available: | ||
| raise ValueError( | ||
| f"No JSON Schema is bundled for OME-Zarr version {version!r}. " | ||
| f"Bundled versions: {', '.join(sorted(available))}." | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| return file_resources("ngff_zarr").joinpath("spec").joinpath(name, "schemas") | ||
|
|
||
|
|
||
| def load_schema( | ||
| version: str = "0.4", model: str = "image", strict: bool = False | ||
| ) -> dict: | ||
| strict_str = "" | ||
| if strict: | ||
| strict_str = "strict_" | ||
| schema = ( | ||
| file_resources("ngff_zarr") | ||
| .joinpath( | ||
| Path("spec") | ||
| / Path(version) | ||
| / Path("schemas") | ||
| / f"{strict_str}{model}.schema" | ||
| ) | ||
| .read_text() | ||
| ) | ||
| schema = _schemas_dir(version).joinpath(f"{strict_str}{model}.schema").read_text() | ||
| return json.loads(schema) | ||
|
|
||
|
|
||
| @cache | ||
| def _schema_registry(version: str) -> "Registry": | ||
| """Register every bundled schema for ``version`` under its own ``$id``. | ||
|
|
||
| From 0.6 the spec splits axes, coordinate systems and coordinate | ||
| transformations into their own files, which ``image.schema`` reaches by | ||
| absolute ``$id`` URL. Validation is offline, so nothing dereferences those | ||
| URLs: each sibling file has to be in the registry for the references to | ||
| resolve. The pre-0.6 schemas keep the same layout, where the only | ||
| cross-file references are ``strict_*`` wrappers around their base schema. | ||
| """ | ||
| from referencing import Registry, Resource | ||
| from referencing.jsonschema import DRAFT202012 | ||
|
|
||
| resources = [] | ||
| for entry in _schemas_dir(version).iterdir(): | ||
| if not entry.name.endswith(".schema"): | ||
| continue | ||
| contents = json.loads(entry.read_text()) | ||
| # Some bundled schemas omit ``$schema``; they all predate 2020-12 draft | ||
| # divergences, so the draft the validator runs is the right default. | ||
| resource = Resource.from_contents(contents, default_specification=DRAFT202012) | ||
| resources.append((contents.get("$id", NGFF_URI), resource)) | ||
| return Registry().with_resources(resources) | ||
|
|
||
|
|
||
| def validate( | ||
| ngff_dict: dict, version: str = "0.4", model: str = "image", strict: bool = False | ||
| ): | ||
| ) -> None: | ||
| """Validate OME-Zarr metadata against its bundled JSON Schema. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| ngff_dict: | ||
| The parsed group attributes to check. | ||
| version: | ||
| The OME-Zarr version whose bundled schemas to validate against. A | ||
| pre-release resolves to the tree of the release it leads to. | ||
| model: | ||
| The schema to validate against: ``image``, ``label``, ``plate`` or | ||
| ``well``. | ||
| strict: | ||
| Validate against the ``strict_*`` variant of ``model``. | ||
|
|
||
| Raises | ||
| ------ | ||
| ImportError | ||
| When the optional ``[validate]`` extra is not installed. | ||
| ValueError | ||
| When no schema tree is bundled for ``version``. | ||
| jsonschema.ValidationError | ||
| When ``ngff_dict`` does not satisfy the schema. | ||
| """ | ||
| try: | ||
| from jsonschema import Draft202012Validator | ||
| from referencing import Registry, Resource | ||
| except ImportError: | ||
| raise ImportError( | ||
| "jsonschema is required to validate NGFF metadata - install the ngff-zarr[validate] extra" | ||
| ) | ||
| schema = load_schema(version=version, model=model, strict=strict) | ||
| registry = Registry().with_resource( | ||
| NGFF_URI, resource=Resource.from_contents(schema) | ||
| ) | ||
| if packaging_version.parse(version) >= packaging_version.parse("0.5"): | ||
| version_schema = load_schema(version=version, model="_version") | ||
| registry = registry.with_resource( | ||
| NGFF_URI, resource=Resource.from_contents(version_schema) | ||
| ) | ||
| validator = Draft202012Validator(schema, registry=registry) | ||
| validator = Draft202012Validator(schema, registry=_schema_registry(version)) | ||
| validator.validate(ngff_dict) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.