3131
3232from __future__ import annotations
3333
34- from typing import Any , Literal , Self
34+ from typing import Any , Literal , NotRequired , Self
3535
3636from pydantic import BaseModel , Field , model_validator
3737from 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
10389class 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
126100class 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
232207class 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)
0 commit comments