Skip to content

Commit 5706098

Browse files
committed
Merge branch 'main' into zarr-optional
2 parents 78e99c3 + ef242d7 commit 5706098

27 files changed

+776
-213
lines changed

docs/changelog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### New Features
66

7-
- [ome_zarr_models.v04.ImageLabel][] now supports being created from a Zarr array backed by an un-listable store.
7+
- All models now support being created from Zarr groups in remote HTTP stores, and more generally from any groups stored in any unlistable store.
88

99
## 1.0
1010

docs/cli.md

Lines changed: 385 additions & 0 deletions
Large diffs are not rendered by default.

mkdocs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ validation:
7878

7979
nav:
8080
- Home: index.md
81-
- Tutorial: tutorial.py
81+
- Command line: cli.md
82+
- Python tutorial: tutorial.py
8283
- How do I...?: how-to.md
8384
- API reference:
8485
- api/index.md

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ ignore = [
8787
"D205", # 1 blank line required between summary line and description
8888
"D400", # First line should end with a period
8989
"D100", # Missing docstring in public module
90+
"D102", # Missing docstring in public method
9091
"D104", # Missing docstring in public package
9192
"TC001", # Move application import `...` into a type-checking block. This messes with nested pydantic models
9293

src/ome_zarr_models/_utils.py

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,178 @@
66

77
from collections import Counter
88
from dataclasses import MISSING, fields, is_dataclass
9-
from typing import TYPE_CHECKING, TypeVar
9+
from typing import TYPE_CHECKING, Any, TypeVar
1010

1111
import pydantic
12+
import pydantic_zarr.v2
13+
import pydantic_zarr.v3
1214
from pydantic import create_model
1315

1416
if TYPE_CHECKING:
1517
from collections.abc import Hashable, Iterable
1618

1719
from zarr.abc.store import Store
1820

19-
T = TypeVar("T")
21+
from ome_zarr_models.base import BaseAttrsv2, BaseAttrsv3
22+
from ome_zarr_models.common.validation import (
23+
check_array_path,
24+
check_group_path,
25+
)
26+
27+
if TYPE_CHECKING:
28+
from collections.abc import Hashable, Iterable
29+
30+
import zarr
31+
from zarr.abc.store import Store
32+
33+
from ome_zarr_models._v06.base import BaseGroupv06
34+
from ome_zarr_models.v04.base import BaseGroupv04
35+
from ome_zarr_models.v05.base import BaseGroupv05
36+
37+
TBaseGroupv2 = TypeVar("TBaseGroupv2", bound="BaseGroupv04[Any]")
38+
TAttrsv2 = TypeVar("TAttrsv2", bound=BaseAttrsv2)
39+
40+
41+
def _from_zarr_v2(
42+
group: zarr.Group,
43+
group_cls: type[TBaseGroupv2],
44+
attrs_cls: type[TAttrsv2],
45+
) -> TBaseGroupv2:
46+
"""
47+
Create a GroupSpec from a potentially unlistable Zarr group.
48+
49+
This uses methods on the attribute class to get required and optional
50+
paths to ararys and groups, and then manually constructs the GroupSpec
51+
from those paths.
52+
53+
Parameters
54+
----------
55+
group :
56+
Zarr group to create GroupSpec from.
57+
group_cls :
58+
Class of the Group to return.
59+
attrs_cls :
60+
Attributes class.
61+
"""
62+
# on unlistable storage backends, the members of this group will be {}
63+
group_spec_in: pydantic_zarr.v2.AnyGroupSpec
64+
group_spec_in = pydantic_zarr.v2.GroupSpec.from_zarr(group, depth=0)
65+
attributes = attrs_cls.model_validate(group_spec_in.attributes)
66+
67+
members_tree_flat: dict[
68+
str, pydantic_zarr.v2.AnyGroupSpec | pydantic_zarr.v2.AnyArraySpec
69+
] = {}
70+
71+
# Required array paths
72+
for array_path in attrs_cls.get_array_paths(attributes):
73+
array_spec = check_array_path(group, array_path, expected_zarr_version=2)
74+
members_tree_flat["/" + array_path] = array_spec
75+
76+
# Optional array paths
77+
for array_path in attrs_cls.get_optional_array_paths(attributes):
78+
try:
79+
array_spec = check_array_path(group, array_path, expected_zarr_version=2)
80+
except ValueError:
81+
continue
82+
members_tree_flat["/" + array_path] = array_spec
83+
84+
# Required group paths
85+
required_groups = attrs_cls.get_group_paths(attributes)
86+
for group_path in required_groups:
87+
check_group_path(group, group_path, expected_zarr_version=2)
88+
group_flat = required_groups[group_path].from_zarr(group[group_path]).to_flat() # type: ignore[arg-type]
89+
for path in group_flat:
90+
members_tree_flat["/" + group_path + path] = group_flat[path]
91+
92+
# Optional group paths
93+
optional_groups = attrs_cls.get_optional_group_paths(attributes)
94+
for group_path in optional_groups:
95+
try:
96+
check_group_path(group, group_path, expected_zarr_version=2)
97+
except FileNotFoundError:
98+
continue
99+
group_flat = optional_groups[group_path].from_zarr(group[group_path]).to_flat() # type: ignore[arg-type]
100+
for path in group_flat:
101+
members_tree_flat["/" + group_path + path] = group_flat[path]
102+
103+
members_normalized: pydantic_zarr.v2.AnyGroupSpec = (
104+
pydantic_zarr.v2.GroupSpec.from_flat(members_tree_flat)
105+
)
106+
return group_cls(members=members_normalized.members, attributes=attributes)
107+
108+
109+
TBaseGroupv3 = TypeVar("TBaseGroupv3", bound="BaseGroupv05[Any] | BaseGroupv06[Any]")
110+
TAttrsv3 = TypeVar("TAttrsv3", bound=BaseAttrsv3)
111+
112+
113+
def _from_zarr_v3(
114+
group: zarr.Group,
115+
group_cls: type[TBaseGroupv3],
116+
attrs_cls: type[TAttrsv3],
117+
) -> TBaseGroupv3:
118+
"""
119+
Create a GroupSpec from a potentially unlistable Zarr group.
120+
121+
This uses methods on the attribute class to get required and optional
122+
paths to ararys and groups, and then manually constructs the GroupSpec
123+
from those paths.
124+
125+
Parameters
126+
----------
127+
group :
128+
Zarr group to create GroupSpec from.
129+
group_cls :
130+
Class of the Group to return.
131+
attrs_cls :
132+
Attributes class.
133+
"""
134+
# on unlistable storage backends, the members of this group will be {}
135+
group_spec_in: pydantic_zarr.v3.AnyGroupSpec
136+
group_spec_in = pydantic_zarr.v3.GroupSpec.from_zarr(group, depth=0)
137+
ome_attributes = attrs_cls.model_validate(group.attrs.asdict()["ome"])
138+
139+
members_tree_flat: dict[
140+
str, pydantic_zarr.v3.AnyGroupSpec | pydantic_zarr.v3.AnyArraySpec
141+
] = {}
142+
143+
# Required array paths
144+
for array_path in attrs_cls.get_array_paths(ome_attributes):
145+
array_spec = check_array_path(group, array_path, expected_zarr_version=3)
146+
members_tree_flat["/" + array_path] = array_spec
147+
148+
# Optional array paths
149+
for array_path in attrs_cls.get_optional_array_paths(ome_attributes):
150+
try:
151+
array_spec = check_array_path(group, array_path, expected_zarr_version=3)
152+
except ValueError:
153+
continue
154+
members_tree_flat["/" + array_path] = array_spec
155+
156+
# Required group paths
157+
required_groups = attrs_cls.get_group_paths(ome_attributes)
158+
for group_path in required_groups:
159+
check_group_path(group, group_path, expected_zarr_version=3)
160+
group_flat = required_groups[group_path].from_zarr(group[group_path]).to_flat() # type: ignore[arg-type]
161+
for path in group_flat:
162+
members_tree_flat["/" + group_path + path] = group_flat[path]
163+
164+
# Optional group paths
165+
optional_groups = attrs_cls.get_optional_group_paths(ome_attributes)
166+
for group_path in optional_groups:
167+
try:
168+
check_group_path(group, group_path, expected_zarr_version=3)
169+
except FileNotFoundError:
170+
continue
171+
group_flat = optional_groups[group_path].from_zarr(group[group_path]).to_flat() # type: ignore[arg-type]
172+
for path in group_flat:
173+
members_tree_flat["/" + group_path + path] = group_flat[path]
174+
175+
members_normalized: pydantic_zarr.v3.AnyGroupSpec = (
176+
pydantic_zarr.v3.GroupSpec.from_flat(members_tree_flat)
177+
)
178+
return group_cls( # type: ignore[return-value]
179+
members=members_normalized.members, attributes=group_spec_in.attributes
180+
)
20181

21182

22183
def get_store_path(store: Store) -> str:

src/ome_zarr_models/_v06/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
import pydantic_zarr.v3
77
from pydantic import BaseModel
88

9-
from ome_zarr_models.base import BaseAttrs, BaseGroup
9+
from ome_zarr_models.base import BaseAttrsv3, BaseGroup
1010

1111
if TYPE_CHECKING:
1212
import zarr
1313

1414

15-
class BaseOMEAttrs(BaseAttrs):
15+
class BaseOMEAttrs(BaseAttrsv3):
1616
"""
17-
Base class for attributes under an OME-Zarr group.
17+
Base class for OME-Zarr 0.6 attributes.
1818
"""
1919

2020
version: Literal["0.6"]

src/ome_zarr_models/_v06/hcs.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
from collections.abc import Generator, Mapping
2-
from typing import Self
2+
from typing import TYPE_CHECKING, Self
33

44
# Import needed for pydantic type resolution
55
import pydantic_zarr # noqa: F401
6+
import zarr
67
from pydantic import model_validator
78
from pydantic_zarr.v3 import GroupSpec
89

10+
from ome_zarr_models._utils import _from_zarr_v3
911
from ome_zarr_models._v06.base import BaseGroupv06, BaseOMEAttrs
1012
from ome_zarr_models._v06.plate import Plate
1113
from ome_zarr_models._v06.well import Well
1214
from ome_zarr_models.common.well import WellGroupNotFoundError
1315

16+
if TYPE_CHECKING:
17+
from pydantic_zarr.v3 import AnyGroupSpec
18+
1419
__all__ = ["HCS", "HCSAttrs"]
1520

1621

@@ -21,12 +26,38 @@ class HCSAttrs(BaseOMEAttrs):
2126

2227
plate: Plate
2328

29+
def get_optional_group_paths(self) -> dict[str, type[Well]]: # type: ignore[override]
30+
return {well.path: Well for well in self.plate.wells}
31+
2432

2533
class HCS(BaseGroupv06[HCSAttrs]):
2634
"""
2735
An OME-Zarr high content screening (HCS) dataset.
2836
"""
2937

38+
@classmethod
39+
def from_zarr(cls, group: zarr.Group) -> Self: # type: ignore[override]
40+
"""
41+
Create an OME-Zarr image model from a `zarr.Group`.
42+
43+
Parameters
44+
----------
45+
group : zarr.Group
46+
A Zarr group that has valid OME-Zarr image metadata.
47+
"""
48+
hcs = _from_zarr_v3(group, cls, HCSAttrs)
49+
# Traverse all the Well groups, which themselves contain Image groups
50+
hcs_flat = hcs.to_flat()
51+
for well in hcs.ome_attributes.plate.wells:
52+
if well.path in group:
53+
well_group = group[well.path]
54+
well_group_flat = Well.from_zarr(well_group).to_flat() # type: ignore[arg-type]
55+
for path in well_group_flat:
56+
hcs_flat["/" + well.path + path] = well_group_flat[path]
57+
58+
hcs_unflat: AnyGroupSpec = GroupSpec.from_flat(hcs_flat)
59+
return cls(attributes=hcs_unflat.attributes, members=hcs_unflat.members)
60+
3061
@model_validator(mode="after")
3162
def _check_valid_acquisitions(self) -> Self:
3263
"""

src/ome_zarr_models/_v06/image.py

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any, Self
3+
from collections.abc import Sequence
4+
from typing import TYPE_CHECKING, Self
45

56
# Import needed for pydantic type resolution
67
import pydantic_zarr # noqa: F401
78
from pydantic import Field, JsonValue, model_validator
89
from pydantic_zarr.v3 import AnyArraySpec, AnyGroupSpec, GroupSpec
910

11+
from ome_zarr_models._utils import _from_zarr_v3
1012
from ome_zarr_models._v06.axes import Axis
1113
from ome_zarr_models._v06.base import BaseGroupv06, BaseOMEAttrs, BaseZarrAttrs
1214
from ome_zarr_models._v06.labels import Labels
1315
from ome_zarr_models._v06.multiscales import Dataset, Multiscale
1416
from ome_zarr_models.common.coordinate_transformations import _build_transforms
15-
from ome_zarr_models.common.validation import check_array_path
1617

1718
if TYPE_CHECKING:
1819
from collections.abc import Sequence
@@ -34,6 +35,16 @@ class ImageAttrs(BaseOMEAttrs):
3435
min_length=1,
3536
)
3637

38+
def get_array_paths(self) -> list[str]:
39+
paths = []
40+
for multiscale in self.multiscales:
41+
for dataset in multiscale.datasets:
42+
paths.append(dataset.path)
43+
return paths
44+
45+
def get_optional_group_paths(self) -> dict[str, type[Labels]]: # type: ignore[override]
46+
return {"labels": Labels}
47+
3748

3849
class Image(BaseGroupv06[ImageAttrs]):
3950
"""
@@ -50,39 +61,7 @@ def from_zarr(cls, group: zarr.Group) -> Self: # type: ignore[override]
5061
group : zarr.Group
5162
A Zarr group that has valid OME-Zarr image metadata.
5263
"""
53-
try:
54-
import zarr
55-
import zarr.errors
56-
except ImportError as e:
57-
raise ImportError("zarr is required to use this function") from e
58-
59-
# on unlistable storage backends, the members of this group will be {}
60-
group_spec: GroupSpec[dict[str, Any], Any] = GroupSpec.from_zarr(group, depth=0)
61-
62-
if "ome" not in group_spec.attributes:
63-
raise RuntimeError(f"Did not find 'ome' key in {group} attributes")
64-
multi_meta = ImageAttrs.model_validate(group_spec.attributes["ome"])
65-
members_tree_flat: dict[str, AnyGroupSpec | AnyArraySpec] = {}
66-
for multiscale in multi_meta.multiscales:
67-
for dataset in multiscale.datasets:
68-
array_spec = check_array_path(
69-
group, dataset.path, expected_zarr_version=3
70-
)
71-
members_tree_flat["/" + dataset.path] = array_spec
72-
73-
try:
74-
labels_group = zarr.open_group(store=group.store_path / "labels", mode="r")
75-
labels = Labels.from_zarr(labels_group)
76-
# members_tree_flat["/labels"] = labels
77-
labels_flat = labels.to_flat()
78-
for path in labels_flat:
79-
members_tree_flat[f"/labels{path}"] = labels_flat[path]
80-
81-
except zarr.errors.GroupNotFoundError:
82-
pass
83-
84-
members_normalized: AnyGroupSpec = GroupSpec.from_flat(members_tree_flat)
85-
return cls(attributes=group_spec.attributes, members=members_normalized.members)
64+
return _from_zarr_v3(group, cls, ImageAttrs)
8665

8766
@classmethod
8867
def new(

0 commit comments

Comments
 (0)