Skip to content

Commit f466006

Browse files
refactor: replace validators with precise type annotations per review
- spatial_dimensions: tuple[Literal['y'], Literal['x']] (removes validator) - spatial_bbox: tuple[float, float, float, float] (removes validator) - spatial_shape: tuple[int, int] (removes validator) - spatial_transform: tuple[float, ...] x6 (removes validator) - S1RtcOrbitGroupMembers: r10m required, others NotRequired (removes validator) - Add resolution_levels() method to S1RtcOrbitGroup - Apply same tuple types to S1RtcConditionsAttrs
1 parent 87498d4 commit f466006

2 files changed

Lines changed: 32 additions & 51 deletions

File tree

src/eopf_geozarr/data_api/s1_rtc.py

Lines changed: 31 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
from __future__ import annotations
3333

34-
from typing import Any, Literal, Self
34+
from typing import Any, Literal, NotRequired, Self
3535

3636
from pydantic import BaseModel, Field, model_validator
3737
from typing_extensions import TypedDict
@@ -71,8 +71,8 @@ class S1RtcOrbitGroupAttrs(BaseModel):
7171
zarr_conventions: list[dict[str, Any]]
7272
multiscales: Multiscales
7373
proj_code: str = Field(alias="proj:code")
74-
spatial_dimensions: list[str] = Field(alias="spatial:dimensions")
75-
spatial_bbox: list[float] = Field(alias="spatial:bbox")
74+
spatial_dimensions: tuple[Literal["y"], Literal["x"]] = Field(alias="spatial:dimensions")
75+
spatial_bbox: tuple[float, float, float, float] = Field(alias="spatial:bbox")
7676

7777
model_config = {"extra": "allow", "populate_by_name": True, "serialize_by_alias": True}
7878

@@ -85,50 +85,26 @@ def validate_zarr_conventions(self) -> Self:
8585
raise ValueError(f"Missing required zarr_conventions UUIDs: {missing}")
8686
return self
8787

88-
@model_validator(mode="after")
89-
def validate_spatial_dimensions(self) -> Self:
90-
if self.spatial_dimensions != ["y", "x"]:
91-
raise ValueError(
92-
f"spatial:dimensions must be ['y', 'x'], got {self.spatial_dimensions}"
93-
)
94-
return self
95-
96-
@model_validator(mode="after")
97-
def validate_spatial_bbox(self) -> Self:
98-
if len(self.spatial_bbox) != 4:
99-
raise ValueError(f"spatial:bbox must have 4 elements, got {len(self.spatial_bbox)}")
100-
return self
101-
10288

10389
class S1RtcResolutionAttrs(BaseModel):
10490
"""Attributes for a resolution-level group (r10m, r20m, ...)."""
10591

106-
spatial_shape: list[int] = Field(alias="spatial:shape")
107-
spatial_transform: list[float] = Field(alias="spatial:transform")
92+
spatial_shape: tuple[int, int] = Field(alias="spatial:shape")
93+
spatial_transform: tuple[float, float, float, float, float, float] = Field(
94+
alias="spatial:transform"
95+
)
10896

10997
model_config = {"extra": "allow", "populate_by_name": True, "serialize_by_alias": True}
11098

111-
@model_validator(mode="after")
112-
def validate_shape(self) -> Self:
113-
if len(self.spatial_shape) != 2:
114-
raise ValueError(f"spatial:shape must have 2 elements, got {len(self.spatial_shape)}")
115-
return self
116-
117-
@model_validator(mode="after")
118-
def validate_transform(self) -> Self:
119-
if len(self.spatial_transform) != 6:
120-
raise ValueError(
121-
f"spatial:transform must have 6 elements, got {len(self.spatial_transform)}"
122-
)
123-
return self
124-
12599

126100
class S1RtcConditionsAttrs(BaseModel):
127101
"""Attributes for the conditions group."""
128102

129103
proj_code: str = Field(alias="proj:code")
130-
spatial_dimensions: list[str] = Field(alias="spatial:dimensions")
131-
spatial_transform: list[float] = Field(alias="spatial:transform")
104+
spatial_dimensions: tuple[Literal["y"], Literal["x"]] = Field(alias="spatial:dimensions")
105+
spatial_transform: tuple[float, float, float, float, float, float] = Field(
106+
alias="spatial:transform"
107+
)
132108

133109
model_config = {"extra": "allow", "populate_by_name": True, "serialize_by_alias": True}
134110

@@ -213,33 +189,26 @@ def validate_has_gamma_area(self) -> Self:
213189
return self
214190

215191

216-
class S1RtcOrbitGroupMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg]
192+
class S1RtcOrbitGroupMembers(TypedDict, closed=True): # type: ignore[call-arg]
217193
"""Members for an orbit-direction group.
218194
219-
Contains resolution-level datasets and conditions.
220-
All optional to support incremental store construction.
195+
r10m is always required; overview levels and conditions are optional.
221196
"""
222197

223198
r10m: S1RtcNativeResolutionDataset
224-
r20m: S1RtcOverviewResolutionDataset
225-
r60m: S1RtcOverviewResolutionDataset
226-
r120m: S1RtcOverviewResolutionDataset
227-
r360m: S1RtcOverviewResolutionDataset
228-
r720m: S1RtcOverviewResolutionDataset
229-
conditions: S1RtcConditionsGroup
199+
r20m: NotRequired[S1RtcOverviewResolutionDataset]
200+
r60m: NotRequired[S1RtcOverviewResolutionDataset]
201+
r120m: NotRequired[S1RtcOverviewResolutionDataset]
202+
r360m: NotRequired[S1RtcOverviewResolutionDataset]
203+
r720m: NotRequired[S1RtcOverviewResolutionDataset]
204+
conditions: NotRequired[S1RtcConditionsGroup]
230205

231206

232207
class S1RtcOrbitGroup(
233208
GroupSpec[S1RtcOrbitGroupAttrs, S1RtcOrbitGroupMembers] # type: ignore[type-var]
234209
):
235210
"""One orbit direction (ascending or descending) with multiscale layout."""
236211

237-
@model_validator(mode="after")
238-
def validate_r10m_present(self) -> Self:
239-
if "r10m" not in self.members:
240-
raise ValueError("Orbit group must contain 'r10m' native resolution dataset")
241-
return self
242-
243212
@property
244213
def r10m(self) -> S1RtcNativeResolutionDataset:
245214
return self.members["r10m"]
@@ -252,6 +221,18 @@ def get_resolution(self, level: ResolutionLevel) -> GroupSpec[Any, Any] | None:
252221
"""Retrieve a resolution dataset by level name."""
253222
return self.members.get(level)
254223

224+
def resolution_levels(self) -> list[ResolutionLevel]:
225+
"""List available resolution levels in this orbit group."""
226+
all_levels: tuple[ResolutionLevel, ...] = (
227+
"r10m",
228+
"r20m",
229+
"r60m",
230+
"r120m",
231+
"r360m",
232+
"r720m",
233+
)
234+
return [lvl for lvl in all_levels if lvl in self.members]
235+
255236

256237
# ============================================================================
257238
# Root model (same pattern as S2 Sentinel2Root)

tests/test_data_api/test_s1_rtc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_s1_rtc_orbit_attrs(s1_rtc_json_example: dict[str, object]) -> None:
7474
attrs = model.descending.attributes
7575
assert len(attrs.zarr_conventions) == 3
7676
assert attrs.proj_code.startswith("EPSG:")
77-
assert attrs.spatial_dimensions == ["y", "x"]
77+
assert attrs.spatial_dimensions == ("y", "x")
7878
assert len(attrs.spatial_bbox) == 4
7979
assert len(attrs.multiscales.layout) == 6
8080
assert attrs.multiscales.layout[0].asset == "r10m"

0 commit comments

Comments
 (0)