55import logging
66from enum import Enum
77from re import match
8- from typing import Any , Optional , Union
8+ from typing import Annotated , Any , List , Optional , Union
99from uuid import UUID , uuid4
1010
1111import numpy as np
2222 BaseModel ,
2323 Extra ,
2424 Field ,
25+ RootModel ,
26+ StringConstraints ,
2527 ValidationError ,
2628 conlist ,
2729 constr ,
30+ field_validator ,
31+ model_validator ,
2832 validator ,
2933)
3034from shapely .geometry import Polygon
@@ -65,13 +69,14 @@ class ParameterReference(BaseModel, extra=Extra.forbid):
6569
6670
6771class ProcessNode (BaseModel , arbitrary_types_allowed = True ):
68- process_id : constr (regex = r'^\w+$' )
72+ process_id : Annotated [str , StringConstraints (pattern = r'^\w+$' )]
73+
6974 namespace : Optional [Optional [str ]] = None
7075 result : Optional [bool ] = False
7176 description : Optional [Optional [str ]] = None
7277 arguments : dict [
7378 str ,
74- Optional [
79+ Annotated [
7580 Union [
7681 ResultReference ,
7782 ParameterReference ,
@@ -87,11 +92,12 @@ class ProcessNode(BaseModel, arbitrary_types_allowed=True):
8792 # GeoJson, disable while https://github.com/developmentseed/geojson-pydantic/issues/92 is open
8893 Time ,
8994 float ,
90- str ,
9195 bool ,
9296 list ,
9397 dict ,
94- ]
98+ str ,
99+ ],
100+ Field (union_mode = 'left_to_right' ),
95101 ],
96102 ]
97103
@@ -133,9 +139,9 @@ class BoundingBox(BaseModel, arbitrary_types_allowed=True):
133139 east : float
134140 north : float
135141 south : float
136- base : Optional [float ]
137- height : Optional [float ]
138- crs : Optional [Union [str , int ]]
142+ base : Optional [float ] = None
143+ height : Optional [float ] = None
144+ crs : Optional [Union [str , int ]] = None
139145
140146 # validators
141147 _parse_crs : classmethod = crs_validator ('crs' )
@@ -153,48 +159,54 @@ def polygon(self) -> Polygon:
153159 )
154160
155161
156- class Date (BaseModel ):
157- __root__ : datetime .datetime
162+ class Date (RootModel ):
163+ root : datetime .datetime
158164
159- @validator ( "__root__ " , pre = True )
165+ @field_validator ( "root " , mode = "before" )
160166 def validate_time (cls , value : Any ) -> Any :
161167 if (
162168 isinstance (value , str )
163169 and len (value ) <= 11
164170 and match (r"[0-9]{4}[-/][0-9]{2}[-/][0-9]{2}T?" , value )
165171 ):
166172 return pendulum .parse (value )
167- raise ValidationError ("Could not parse `Date` from input." )
173+ raise ValueError ("Could not parse `Date` from input." )
168174
169175 def to_numpy (self ):
170- return np .datetime64 (self .__root__ )
176+ return np .datetime64 (self .root )
171177
172178 def __repr__ (self ):
173- return self .__root__ .__repr__ ()
179+ return self .root .__repr__ ()
180+
181+ def __gt__ (self , date1 ):
182+ return self .root > date1 .root
174183
175184
176- class DateTime (BaseModel ):
177- __root__ : datetime .datetime
185+ class DateTime (RootModel ):
186+ root : datetime .datetime
178187
179- @validator ( "__root__ " , pre = True )
188+ @field_validator ( "root " , mode = "before" )
180189 def validate_time (cls , value : Any ) -> Any :
181190 if isinstance (value , str ) and match (
182191 r"[0-9]{4}-[0-9]{2}-[0-9]{2}T?[0-9]{2}:[0-9]{2}:?([0-9]{2})?Z?" , value
183192 ):
184193 return pendulum .parse (value )
185- raise ValidationError ("Could not parse `DateTime` from input." )
194+ raise ValueError ("Could not parse `DateTime` from input." )
186195
187196 def to_numpy (self ):
188- return np .datetime64 (self .__root__ )
197+ return np .datetime64 (self .root )
189198
190199 def __repr__ (self ):
191- return self .__root__ .__repr__ ()
200+ return self .root .__repr__ ()
201+
202+ def __gt__ (self , date1 ):
203+ return self .root > date1 .root
192204
193205
194- class Time (BaseModel ):
195- __root__ : pendulum . Time
206+ class Time (RootModel ):
207+ root : datetime . time
196208
197- @validator ( "__root__ " , pre = True )
209+ @field_validator ( "root " , mode = "before" )
198210 def validate_time (cls , value : Any ) -> Any :
199211 if (
200212 isinstance (value , str )
@@ -203,145 +215,145 @@ def validate_time(cls, value: Any) -> Any:
203215 and match (r"[0-9]{2}:[0-9]{2}:?([0-9]{2})?Z?" , value )
204216 ):
205217 return pendulum .parse (value ).time ()
206- raise ValidationError ("Could not parse `Time` from input." )
218+ raise ValueError ("Could not parse `Time` from input." )
207219
208220 def to_numpy (self ):
209221 raise NotImplementedError
210222
211223 def __repr__ (self ):
212- return self .__root__ .__repr__ ()
224+ return self .time .__repr__ ()
213225
214226
215- class Year (BaseModel ):
216- __root__ : datetime .datetime
227+ class Year (RootModel ):
228+ root : datetime .datetime
217229
218- @validator ( "__root__ " , pre = True )
230+ @field_validator ( "root " , mode = "before" )
219231 def validate_time (cls , value : Any ) -> Any :
220232 if isinstance (value , str ) and len (value ) <= 4 and match (r"^\d{4}$" , value ):
221233 return pendulum .parse (value )
222- raise ValidationError ("Could not parse `Year` from input." )
234+ raise ValueError ("Could not parse `Year` from input." )
223235
224236 def to_numpy (self ):
225- return np .datetime64 (self .__root__ )
237+ return np .datetime64 (self .root )
226238
227239 def __repr__ (self ):
228- return self .__root__ .__repr__ ()
240+ return self .root .__repr__ ()
229241
230242
231- class Duration (BaseModel ):
232- __root__ : datetime .timedelta
243+ class Duration (RootModel ):
244+ root : datetime .timedelta
233245
234- @validator ( "__root__ " , pre = True )
246+ @field_validator ( "root " , mode = "before" )
235247 def validate_time (cls , value : Any ) -> Any :
236248 if isinstance (value , str ) and match (
237249 r"P[0-9]*Y?[0-9]*M?[0-9]*D?T?[0-9]*H?[0-9]*M?[0-9]*S?" , value
238250 ):
239251 return pendulum .parse (value ).as_timedelta ()
240- raise ValidationError ("Could not parse `Duration` from input." )
252+ raise ValueError ("Could not parse `Duration` from input." )
241253
242254 def to_numpy (self ):
243- return np .timedelta64 (self .__root__ )
255+ return np .timedelta64 (self .root )
244256
245257 def __repr__ (self ):
246- return self .__root__ .__repr__ ()
258+ return self .root .__repr__ ()
247259
248260
249- class TemporalInterval (BaseModel ):
250- __root__ : conlist (Union [Year , Date , DateTime , Time , None ], min_items = 2 , max_items = 2 )
261+ class TemporalInterval (RootModel ):
262+ root : conlist (Union [Year , Date , DateTime , Time , None ], min_length = 2 , max_length = 2 )
251263
252- @validator ( "__root__ " )
264+ @field_validator ( "root " )
253265 def validate_temporal_interval (cls , value : Any ) -> Any :
254266 start = value [0 ]
255267 end = value [1 ]
256268
257269 if start is None and end is None :
258- raise ValidationError ("Could not parse `TemporalInterval` from input." )
270+ raise ValueError ("Could not parse `TemporalInterval` from input." )
259271
260272 # Disambiguate the Time subtype
261273 if isinstance (start , Time ) or isinstance (end , Time ):
262274 if isinstance (start , Time ) and isinstance (end , Time ):
263- raise ValidationError (
275+ raise ValueError (
264276 "Ambiguous TemporalInterval, both start and end are of type `Time`"
265277 )
266278 if isinstance (start , Time ):
267279 if end is None :
268- raise ValidationError (
280+ raise ValueError (
269281 "Cannot disambiguate TemporalInterval, start is `Time` and end is `None`"
270282 )
271283 logger .warning (
272284 "Start time of temporal interval is of type `time`. Assuming same date as the end time."
273285 )
274286 start = DateTime (
275- __root__ = pendulum .datetime (
276- end .__root__ .year ,
277- end .__root__ .month ,
278- end .__root__ .day ,
279- start .__root__ .hour ,
280- start .__root__ .minute ,
281- start .__root__ .second ,
282- start .__root__ .microsecond ,
287+ root = pendulum .datetime (
288+ end .root .year ,
289+ end .root .month ,
290+ end .root .day ,
291+ start .root .hour ,
292+ start .root .minute ,
293+ start .root .second ,
294+ start .root .microsecond ,
283295 ).to_rfc3339_string ()
284296 )
285297 elif isinstance (end , Time ):
286298 if start is None :
287- raise ValidationError (
299+ raise ValueError (
288300 "Cannot disambiguate TemporalInterval, start is `None` and end is `Time`"
289301 )
290302 logger .warning (
291303 "End time of temporal interval is of type `time`. Assuming same date as the start time."
292304 )
293305 end = DateTime (
294- __root__ = pendulum .datetime (
295- start .__root__ .year ,
296- start .__root__ .month ,
297- start .__root__ .day ,
298- end .__root__ .hour ,
299- end .__root__ .minute ,
300- end .__root__ .second ,
301- end .__root__ .microsecond ,
306+ root = pendulum .datetime (
307+ start .root .year ,
308+ start .root .month ,
309+ start .root .day ,
310+ end .root .hour ,
311+ end .root .minute ,
312+ end .root .second ,
313+ end .root .microsecond ,
302314 ).to_rfc3339_string ()
303315 )
304316
305- if not (start is None or end is None ) and start . __root__ > end . __root__ :
306- raise ValidationError ("Start time > end time" )
317+ if not (start is None or end is None ) and start > end :
318+ raise ValueError ("Start time > end time" )
307319
308320 return [start , end ]
309321
310322 @property
311323 def start (self ):
312- return self .__root__ [0 ]
324+ return self .root [0 ]
313325
314326 @property
315327 def end (self ):
316- return self .__root__ [1 ]
328+ return self .root [1 ]
317329
318330 def __iter__ (self ):
319- return iter (self .__root__ )
331+ return iter (self .root )
320332
321333 def __getitem__ (self , item ):
322- return self .__root__ [item ]
334+ return self .root [item ]
323335
324336
325- class TemporalIntervals (BaseModel ):
326- __root__ : list [TemporalInterval ]
337+ class TemporalIntervals (RootModel ):
338+ root : list [TemporalInterval ]
327339
328340 def __iter__ (self ):
329- return iter (self .__root__ )
341+ return iter (self .root )
330342
331343 def __getitem__ (self , item ) -> TemporalInterval :
332- return self .__root__ [item ]
344+ return self .root [item ]
333345
334346
335347GeoJson = Union [FeatureCollection , Feature , GeometryCollection , MultiPolygon , Polygon ]
336348# The GeoJson spec (https://www.rfc-editor.org/rfc/rfc7946.html#ref-GJ2008) doesn't
337349# have a crs field anymore and recommends assuming it to be EPSG:4326, so we do the same.
338350
339351
340- class JobId (BaseModel ):
341- __root__ : str = Field (
342- regex = r"(eodc-jb-|jb-)[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"
352+ class JobId (RootModel ):
353+ root : str = Field (
354+ pattern = r"(eodc-jb-|jb-)[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"
343355 )
344356
345357
346- ResultReference .update_forward_refs ()
347- ProcessNode .update_forward_refs ()
358+ ResultReference .model_rebuild ()
359+ ProcessNode .model_rebuild ()
0 commit comments