10
10
Point ,
11
11
Polygon ,
12
12
)
13
- from pydantic import (
14
- BaseModel ,
15
- Field ,
16
- PrivateAttr ,
17
- TypeAdapter ,
18
- ValidationInfo ,
19
- field_validator ,
20
- model_validator ,
21
- )
13
+ from pydantic import AfterValidator , BaseModel , Field , TypeAdapter , model_validator
14
+ from typing_extensions import Annotated
22
15
23
16
from stac_pydantic .api .extensions .fields import FieldsExtension
24
17
from stac_pydantic .api .extensions .query import Operator
38
31
SearchDatetime = TypeAdapter (Optional [UtcDatetime ])
39
32
40
33
41
- class Search (BaseModel ):
42
- """
43
- The base class for STAC API searches.
34
+ def validate_bbox (v : Optional [BBox ]) -> Optional [BBox ]:
35
+ """Validate BBOX value."""
36
+ if v :
37
+ # Validate order
38
+ if len (v ) == 4 :
39
+ xmin , ymin , xmax , ymax = cast (Tuple [int , int , int , int ], v )
44
40
45
- https://github.com/radiantearth/stac-api-spec/blob/v1.0.0/item-search/README.md#query-parameter-table
46
- """
41
+ elif len (v ) == 6 :
42
+ xmin , ymin , min_elev , xmax , ymax , max_elev = cast (
43
+ Tuple [int , int , int , int , int , int ], v
44
+ )
45
+ if max_elev < min_elev :
46
+ raise ValueError (
47
+ "Maximum elevation must greater than minimum elevation"
48
+ )
49
+ else :
50
+ raise ValueError ("Bounding box must have 4 or 6 coordinates" )
47
51
48
- collections : Optional [List [str ]] = None
49
- ids : Optional [List [str ]] = None
50
- bbox : Optional [BBox ] = None
51
- intersects : Optional [Intersection ] = None
52
- datetime : Optional [str ] = None
53
- limit : Optional [int ] = 10
52
+ # Validate against WGS84
53
+ if xmin < - 180 or ymin < - 90 or xmax > 180 or ymax > 90 :
54
+ raise ValueError ("Bounding box must be within (-180, -90, 180, 90)" )
54
55
55
- # Private properties to store the parsed datetime values. Not part of the model schema.
56
- _start_date : Optional [dt ] = PrivateAttr (default = None )
57
- _end_date : Optional [dt ] = PrivateAttr (default = None )
56
+ if ymax < ymin :
57
+ raise ValueError ("Maximum latitude must be greater than minimum latitude" )
58
58
59
- # Properties to return the private values
60
- @property
61
- def start_date (self ) -> Optional [dt ]:
62
- return self ._start_date
59
+ return v
63
60
64
- @property
65
- def end_date (self ) -> Optional [dt ]:
66
- return self ._end_date
67
61
68
- # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
69
- @model_validator (mode = "before" )
70
- def validate_spatial (cls , values : Dict [str , Any ]) -> Dict [str , Any ]:
71
- if values .get ("intersects" ) and values .get ("bbox" ) is not None :
72
- raise ValueError ("intersects and bbox parameters are mutually exclusive" )
73
- return values
62
+ def str_to_datetimes (value : str ) -> List [Optional [dt ]]:
63
+ # Split on "/" and replace no value or ".." with None
64
+ values = [v if v and v != ".." else None for v in value .split ("/" )]
74
65
75
- @field_validator ("bbox" )
76
- @classmethod
77
- def validate_bbox (cls , v : BBox ) -> BBox :
78
- if v :
79
- # Validate order
80
- if len (v ) == 4 :
81
- xmin , ymin , xmax , ymax = cast (Tuple [int , int , int , int ], v )
82
- else :
83
- xmin , ymin , min_elev , xmax , ymax , max_elev = cast (
84
- Tuple [int , int , int , int , int , int ], v
85
- )
86
- if max_elev < min_elev :
87
- raise ValueError (
88
- "Maximum elevation must greater than minimum elevation"
89
- )
90
- # Validate against WGS84
91
- if xmin < - 180 or ymin < - 90 or xmax > 180 or ymax > 90 :
92
- raise ValueError ("Bounding box must be within (-180, -90, 180, 90)" )
93
-
94
- if ymax < ymin :
95
- raise ValueError (
96
- "Maximum longitude must be greater than minimum longitude"
97
- )
66
+ # Cast because pylance gets confused by the type adapter and annotated type
67
+ dates = cast (
68
+ List [Optional [dt ]],
69
+ [
70
+ # Use the type adapter to validate the datetime strings, strict is necessary
71
+ # due to pydantic issues #8736 and #8762
72
+ SearchDatetime .validate_strings (v , strict = True ) if v else None
73
+ for v in values
74
+ ],
75
+ )
76
+ return dates
98
77
99
- return v
100
78
101
- @field_validator ("datetime" , mode = "after" )
102
- @classmethod
103
- def validate_datetime (
104
- cls , value : Optional [str ], info : ValidationInfo
105
- ) -> Optional [str ]:
106
- # Split on "/" and replace no value or ".." with None
107
- if value is None :
108
- return value
109
- values = [v if v and v != ".." else None for v in value .split ("/" )]
79
+ def validate_datetime (v : Optional [str ]) -> Optional [str ]:
80
+ """Validate Datetime value."""
81
+ if v is not None :
82
+ dates = str_to_datetimes (v )
110
83
111
84
# If there are more than 2 dates, it's invalid
112
- if len (values ) > 2 :
85
+ if len (dates ) > 2 :
113
86
raise ValueError (
114
87
"Invalid datetime range. Too many values. Must match format: {begin_date}/{end_date}"
115
88
)
116
89
117
90
# If there is only one date, duplicate to use for both start and end dates
118
- if len (values ) == 1 :
119
- values = [values [0 ], values [0 ]]
120
-
121
- # Cast because pylance gets confused by the type adapter and annotated type
122
- dates = cast (
123
- List [Optional [dt ]],
124
- [
125
- # Use the type adapter to validate the datetime strings, strict is necessary
126
- # due to pydantic issues #8736 and #8762
127
- SearchDatetime .validate_strings (v , strict = True ) if v else None
128
- for v in values
129
- ],
130
- )
91
+ if len (dates ) == 1 :
92
+ dates = [dates [0 ], dates [0 ]]
131
93
132
94
# If there is a start and end date, check that the start date is before the end date
133
95
if dates [0 ] and dates [1 ] and dates [0 ] > dates [1 ]:
@@ -136,12 +98,44 @@ def validate_datetime(
136
98
"Must match format: {begin_date}/{end_date}"
137
99
)
138
100
139
- # Store the parsed dates
140
- info .data ["_start_date" ] = dates [0 ]
141
- info .data ["_end_date" ] = dates [1 ]
101
+ return v
102
+
103
+
104
+ class Search (BaseModel ):
105
+ """
106
+ The base class for STAC API searches.
107
+
108
+ https://github.com/radiantearth/stac-api-spec/blob/v1.0.0/item-search/README.md#query-parameter-table
109
+ """
110
+
111
+ collections : Optional [List [str ]] = None
112
+ ids : Optional [List [str ]] = None
113
+ bbox : Annotated [Optional [BBox ], AfterValidator (validate_bbox )] = None
114
+ intersects : Optional [Intersection ] = None
115
+ datetime : Annotated [Optional [str ], AfterValidator (validate_datetime )] = None
116
+ limit : Optional [int ] = 10
117
+
118
+ @property
119
+ def start_date (self ) -> Optional [dt ]:
120
+ start_date : Optional [dt ] = None
121
+ if self .datetime :
122
+ start_date = str_to_datetimes (self .datetime )[0 ]
123
+ return start_date
124
+
125
+ @property
126
+ def end_date (self ) -> Optional [dt ]:
127
+ end_date : Optional [dt ] = None
128
+ if self .datetime :
129
+ dates = str_to_datetimes (self .datetime )
130
+ end_date = dates [0 ] if len (dates ) == 1 else dates [1 ]
131
+ return end_date
142
132
143
- # Return the original string value
144
- return value
133
+ # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
134
+ @model_validator (mode = "before" )
135
+ def validate_spatial (cls , values : Dict [str , Any ]) -> Dict [str , Any ]:
136
+ if values .get ("intersects" ) and values .get ("bbox" ) is not None :
137
+ raise ValueError ("intersects and bbox parameters are mutually exclusive" )
138
+ return values
145
139
146
140
@property
147
141
def spatial_filter (self ) -> Optional [Intersection ]:
0 commit comments