Skip to content

Commit 7f588a6

Browse files
authored
Support unlistable stores in v05 and v06 (#284)
* Move _to_zarr helper utility * Create BaseAttrsv2 * Rename typevar * Create BaseAttrsv3 * Add path methods to BaseAttrsv3 * Rename typevar * Add _from_zarr_v3 * Use from_zarrv3 with v05 well * Import modules instead of classes * Use from_zarr for v05 image * Support unlistable stores for v05 HCS * Support unlistable stores in v06 * Update changelog. * Remove errant print * Fix typing errors
1 parent 074d272 commit 7f588a6

File tree

25 files changed

+385
-206
lines changed

25 files changed

+385
-206
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

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ ignore = [
7777
"D205", # 1 blank line required between summary line and description
7878
"D400", # First line should end with a period
7979
"D100", # Missing docstring in public module
80+
"D102", # Missing docstring in public method
8081
"D104", # Missing docstring in public package
8182
"TC001", # Move application import `...` into a type-checking block. This messes with nested pydantic models
8283

src/ome_zarr_models/_utils.py

Lines changed: 165 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,177 @@
22
Private utilities.
33
"""
44

5+
from __future__ import annotations
6+
57
from collections import Counter
6-
from collections.abc import Hashable, Iterable
78
from dataclasses import MISSING, fields, is_dataclass
8-
from typing import TypeVar
9+
from typing import TYPE_CHECKING, Any, TypeVar
910

1011
import pydantic
12+
import pydantic_zarr.v2
13+
import pydantic_zarr.v3
1114
from pydantic import create_model
12-
from zarr.abc.store import Store
1315

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

16177

17178
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: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Sequence
2-
from typing import Any, Self
2+
from typing import Self
33

44
# Import needed for pydantic type resolution
55
import pydantic_zarr # noqa: F401
@@ -8,12 +8,12 @@
88
from pydantic import Field, JsonValue, model_validator
99
from pydantic_zarr.v3 import AnyArraySpec, AnyGroupSpec, GroupSpec
1010

11+
from ome_zarr_models._utils import _from_zarr_v3
1112
from ome_zarr_models._v06.axes import Axis
1213
from ome_zarr_models._v06.base import BaseGroupv06, BaseOMEAttrs, BaseZarrAttrs
1314
from ome_zarr_models._v06.labels import Labels
1415
from ome_zarr_models._v06.multiscales import Dataset, Multiscale
1516
from ome_zarr_models.common.coordinate_transformations import _build_transforms
16-
from ome_zarr_models.common.validation import check_array_path
1717

1818
__all__ = ["Image", "ImageAttrs"]
1919

@@ -29,6 +29,16 @@ class ImageAttrs(BaseOMEAttrs):
2929
min_length=1,
3030
)
3131

32+
def get_array_paths(self) -> list[str]:
33+
paths = []
34+
for multiscale in self.multiscales:
35+
for dataset in multiscale.datasets:
36+
paths.append(dataset.path)
37+
return paths
38+
39+
def get_optional_group_paths(self) -> dict[str, type[Labels]]: # type: ignore[override]
40+
return {"labels": Labels}
41+
3242

3343
class Image(BaseGroupv06[ImageAttrs]):
3444
"""
@@ -45,33 +55,7 @@ def from_zarr(cls, group: zarr.Group) -> Self: # type: ignore[override]
4555
group : zarr.Group
4656
A Zarr group that has valid OME-Zarr image metadata.
4757
"""
48-
# on unlistable storage backends, the members of this group will be {}
49-
group_spec: GroupSpec[dict[str, Any], Any] = GroupSpec.from_zarr(group, depth=0)
50-
51-
if "ome" not in group_spec.attributes:
52-
raise RuntimeError(f"Did not find 'ome' key in {group} attributes")
53-
multi_meta = ImageAttrs.model_validate(group_spec.attributes["ome"])
54-
members_tree_flat: dict[str, AnyGroupSpec | AnyArraySpec] = {}
55-
for multiscale in multi_meta.multiscales:
56-
for dataset in multiscale.datasets:
57-
array_spec = check_array_path(
58-
group, dataset.path, expected_zarr_version=3
59-
)
60-
members_tree_flat["/" + dataset.path] = array_spec
61-
62-
try:
63-
labels_group = zarr.open_group(store=group.store_path / "labels", mode="r")
64-
labels = Labels.from_zarr(labels_group)
65-
# members_tree_flat["/labels"] = labels
66-
labels_flat = labels.to_flat()
67-
for path in labels_flat:
68-
members_tree_flat[f"/labels{path}"] = labels_flat[path]
69-
70-
except zarr.errors.GroupNotFoundError:
71-
pass
72-
73-
members_normalized: AnyGroupSpec = GroupSpec.from_flat(members_tree_flat)
74-
return cls(attributes=group_spec.attributes, members=members_normalized.members)
58+
return _from_zarr_v3(group, cls, ImageAttrs)
7559

7660
@classmethod
7761
def new(

src/ome_zarr_models/_v06/well.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Import needed for pydantic type resolution
2-
import pydantic_zarr # noqa: F401
2+
from typing import Self
33

4+
import zarr
5+
6+
from ome_zarr_models._utils import _from_zarr_v3
47
from ome_zarr_models._v06.base import BaseGroupv06, BaseOMEAttrs
8+
from ome_zarr_models._v06.image import Image
59
from ome_zarr_models._v06.well_types import WellMeta
610

711
__all__ = ["Well", "WellAttrs"]
@@ -14,8 +18,23 @@ class WellAttrs(BaseOMEAttrs):
1418

1519
well: WellMeta
1620

21+
def get_optional_group_paths(self) -> dict[str, type[Image]]: # type: ignore[override]
22+
return {im.path: Image for im in self.well.images}
23+
1724

1825
class Well(BaseGroupv06[WellAttrs]):
1926
"""
2027
An OME-Zarr well dataset.
2128
"""
29+
30+
@classmethod
31+
def from_zarr(cls, group: zarr.Group) -> Self: # type: ignore[override]
32+
"""
33+
Create an OME-Zarr well model from a `zarr.Group`.
34+
35+
Parameters
36+
----------
37+
group : zarr.Group
38+
A Zarr group that has valid OME-Zarr well metadata.
39+
"""
40+
return _from_zarr_v3(group, cls, WellAttrs)

0 commit comments

Comments
 (0)