Skip to content

Commit f5e38eb

Browse files
authored
Be more forgiving on some broken metadata (#356)
* Add version metadata fixing for Plate * Automatically fix transforms in wrong order * Add changelog entry * Implement transform swapping for v05 * Fix method name * Add missing files * pre-commit fixes * Fixes * Fix doc import * Add TOC entry * Minor doc improvements
1 parent e91be2a commit f5e38eb

16 files changed

+651
-68
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ coverage.xml
1111

1212
# Where `mkdocs build` puts local website build
1313
site
14+
15+
.idea

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ repos:
5353
rev: v1.29.4
5454
hooks:
5555
- id: typos
56-
files: \.(py|md|rst|yaml|toml)
56+
files: \.(py|md|rst|toml)
5757
# empty to do not write fixes
5858
args: []
5959
exclude: pyproject.toml

docs/api/common/exceptions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Exceptions and warnings
2+
3+
::: ome_zarr_models.exceptions

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 1.4
4+
5+
### Fixing metadata
6+
7+
ome-zarr-models now has support for fixing common issues with OME-Zarr metadata when loading.
8+
See [the metadata fixes page](fixes.md) for more information.
9+
310
## 1.3
411

512
### Bug fixes

docs/fixes.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Metadata fixing
2+
3+
ome-zarr-models will attempt to fix metadata that is not technically compliant with the OME-Zarr specification, but can still be unambiguously interpreted and made compliant.
4+
In these cases a ValidationWarning will be emitted explaining the fix made.
5+
This provides a useful interface for software packages and users to read metadata with mistakes, but still access it from ome-zarr-models with those mistakes corrected.
6+
7+
ome-zarr-models will always write specification-compliant metadata.
8+
The `ome-zarr-models validate` command will always raise an error on invalid metadata, regardless of whether a fix is available or not.
9+
10+
If you have a suggestion for a fix we could add, please open an issue!
11+
12+
## Implemented fixes
13+
14+
- If the "version" field is not present in OME-Zarr 0.5 Plate metadata it is automatically set to `"0.5"`.
15+
- If the order of transforms is incorrect in OME-Zarr 0.4 images, they are automatically swapped to be in the correct order.

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ nav:
8282
- Command line: cli.md
8383
- Python tutorial: tutorial.py
8484
- How do I...?: how-to.md
85+
- Metadata fixes: fixes.md
8586
- API reference:
8687
- api/index.md
8788
- v05:
@@ -105,6 +106,7 @@ nav:
105106
- Shared:
106107
- Base objects: api/common/base.md
107108
- Validation: api/common/validation.md
109+
- Exceptions: api/common/exceptions.md
108110
- Well: api/common/well.md
109111
- Changelog: changelog.md
110112
- Contributing: contributing.md

src/ome_zarr_models/_cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import argparse
44
import sys
5+
import warnings
56
from typing import TYPE_CHECKING, Literal
67

78
from ome_zarr_models import __version__, open_ome_zarr
9+
from ome_zarr_models.exceptions import ValidationWarning
810

911
if TYPE_CHECKING:
1012
from zarr.storage import StoreLike
@@ -70,7 +72,8 @@ def validate(path: StoreLike, version: Literal["0.4", "0.5"] | None = None) -> N
7072
```
7173
"""
7274
try:
73-
open_ome_zarr(path, version=version)
75+
with warnings.catch_warnings(action="error", category=ValidationWarning):
76+
open_ome_zarr(path, version=version)
7477
except Exception as e:
7578
print(f"{e}\n")
7679
print(f"❌ Invalid OME-Zarr: {path}")

src/ome_zarr_models/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Custom exceptions and warnings used by `ome-zarr-models`
3+
"""
4+
5+
6+
class ValidationWarning(UserWarning):
7+
"""
8+
Warning emitted for OME-Zarr data that can be interpreted
9+
by `ome-zarr-models`, but is not strictly compliant with the specification.
10+
"""

src/ome_zarr_models/v04/multiscales.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import warnings
34
from collections import Counter
45
from typing import TYPE_CHECKING, Any, Literal, Self
56

@@ -19,6 +20,7 @@
1920
ValidTransform,
2021
VectorScale,
2122
VectorTransform,
23+
VectorTranslation,
2224
_build_transforms,
2325
_ndim,
2426
)
@@ -27,6 +29,7 @@
2729
check_ordered_scales,
2830
unique_items_validator,
2931
)
32+
from ome_zarr_models.exceptions import ValidationWarning
3033
from ome_zarr_models.v04.axes import Axes
3134

3235
if TYPE_CHECKING:
@@ -243,23 +246,40 @@ class Transforms(BaseModel):
243246
transforms = Transforms(transforms=transforms_obj).transforms
244247
check_length(transforms, valid_lengths=[1, 2], variable_name="transforms")
245248

246-
maybe_scale = transforms[0]
247-
if maybe_scale.type != "scale":
248-
msg = (
249-
"The first element of `coordinateTransformations` must be a scale "
250-
f"transform. Got {maybe_scale} instead."
251-
)
252-
raise ValueError(msg)
253-
if len(transforms) == 2:
254-
maybe_trans = transforms[1]
255-
if (maybe_trans.type) != "translation":
256-
msg = (
257-
"The second element of `coordinateTransformations` must be a "
258-
f"translation transform. Got {maybe_trans} instead."
249+
transform_types = tuple(t.type for t in transforms)
250+
if transform_types == ("scale",) or transform_types == ("scale", "translation"):
251+
return transforms_obj
252+
elif transform_types == ("translation", "scale"):
253+
if isinstance(transforms[0], VectorTranslation) and isinstance(
254+
transforms[1], VectorScale
255+
):
256+
# Can only do the swap if we know the vectors
257+
warnings.warn(
258+
"Translation and scale are in the wrong order "
259+
"(scale should come first). Swapping transforms.",
260+
ValidationWarning,
261+
stacklevel=2,
259262
)
260-
raise ValueError(msg)
263+
new_transforms = (
264+
transforms[1],
265+
VectorTranslation(
266+
type="translation",
267+
translation=[
268+
t * s
269+
for t, s in zip(
270+
transforms[0].translation,
271+
transforms[1].scale,
272+
strict=False,
273+
)
274+
],
275+
),
276+
)
277+
return new_transforms
261278

262-
return transforms_obj
279+
raise ValueError(
280+
"Expected a scale or (scale, translation) transform. "
281+
f"Got {transform_types} instead."
282+
)
263283

264284
@field_validator("coordinateTransformations", mode="after")
265285
@classmethod

src/ome_zarr_models/v05/multiscales.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import warnings
34
from collections import Counter
45
from typing import TYPE_CHECKING, Any, Self
56

@@ -19,6 +20,7 @@
1920
ValidTransform,
2021
VectorScale,
2122
VectorTransform,
23+
VectorTranslation,
2224
_build_transforms,
2325
_ndim,
2426
)
@@ -27,6 +29,7 @@
2729
check_ordered_scales,
2830
unique_items_validator,
2931
)
32+
from ome_zarr_models.exceptions import ValidationWarning
3033
from ome_zarr_models.v05.axes import Axes
3134

3235
if TYPE_CHECKING:
@@ -242,23 +245,40 @@ class Transforms(BaseModel):
242245
transforms = Transforms(transforms=transforms_obj).transforms
243246
check_length(transforms, valid_lengths=[1, 2], variable_name="transforms")
244247

245-
maybe_scale = transforms[0]
246-
if maybe_scale.type != "scale":
247-
msg = (
248-
"The first element of `coordinateTransformations` must be a scale "
249-
f"transform. Got {maybe_scale} instead."
250-
)
251-
raise ValueError(msg)
252-
if len(transforms) == 2:
253-
maybe_trans = transforms[1]
254-
if (maybe_trans.type) != "translation":
255-
msg = (
256-
"The second element of `coordinateTransformations` must be a "
257-
f"translation transform. Got {maybe_trans} instead."
248+
transform_types = tuple(t.type for t in transforms)
249+
if transform_types == ("scale",) or transform_types == ("scale", "translation"):
250+
return transforms_obj
251+
elif transform_types == ("translation", "scale"):
252+
if isinstance(transforms[0], VectorTranslation) and isinstance(
253+
transforms[1], VectorScale
254+
):
255+
# Can only do the swap if we know the vectors
256+
warnings.warn(
257+
"Translation and scale are in the wrong order "
258+
"(scale should come first). Swapping transforms.",
259+
ValidationWarning,
260+
stacklevel=2,
258261
)
259-
raise ValueError(msg)
262+
new_transforms = (
263+
transforms[1],
264+
VectorTranslation(
265+
type="translation",
266+
translation=[
267+
t * s
268+
for t, s in zip(
269+
transforms[0].translation,
270+
transforms[1].scale,
271+
strict=False,
272+
)
273+
],
274+
),
275+
)
276+
return new_transforms
260277

261-
return transforms_obj
278+
raise ValueError(
279+
"Expected a scale or (scale, translation) transform. "
280+
f"Got {transform_types} instead."
281+
)
262282

263283
@field_validator("coordinateTransformations", mode="after")
264284
@classmethod

0 commit comments

Comments
 (0)