1
1
from datetime import datetime as dt
2
2
from typing import Any , Dict , List , Optional , Tuple , Union , cast
3
3
4
- from ciso8601 import parse_rfc3339
5
- from geojson_pydantic .geometries import ( # type: ignore
4
+ from geojson_pydantic .geometries import (
6
5
GeometryCollection ,
7
6
LineString ,
8
7
MultiLineString ,
11
10
Point ,
12
11
Polygon ,
13
12
)
14
- from pydantic import BaseModel , Field , field_validator , model_validator
13
+ from pydantic import BaseModel , Field , TypeAdapter , field_validator , model_validator
15
14
16
15
from stac_pydantic .api .extensions .fields import FieldsExtension
17
16
from stac_pydantic .api .extensions .query import Operator
18
17
from stac_pydantic .api .extensions .sort import SortExtension
19
- from stac_pydantic .shared import BBox
18
+ from stac_pydantic .shared import BBox , UtcDatetime
20
19
21
20
Intersection = Union [
22
21
Point ,
28
27
GeometryCollection ,
29
28
]
30
29
30
+ SearchDatetime = TypeAdapter (Optional [UtcDatetime ])
31
+
31
32
32
33
class Search (BaseModel ):
33
34
"""
@@ -43,23 +44,18 @@ class Search(BaseModel):
43
44
datetime : Optional [str ] = None
44
45
limit : int = 10
45
46
47
+ # Private properties to store the parsed datetime values. Not part of the model schema.
48
+ _start_date : Optional [dt ] = None
49
+ _end_date : Optional [dt ] = None
50
+
51
+ # Properties to return the private values
46
52
@property
47
53
def start_date (self ) -> Optional [dt ]:
48
- values = (self .datetime or "" ).split ("/" )
49
- if len (values ) == 1 :
50
- return None
51
- if values [0 ] == ".." or values [0 ] == "" :
52
- return None
53
- return parse_rfc3339 (values [0 ])
54
+ return self ._start_date
54
55
55
56
@property
56
57
def end_date (self ) -> Optional [dt ]:
57
- values = (self .datetime or "" ).split ("/" )
58
- if len (values ) == 1 :
59
- return parse_rfc3339 (values [0 ])
60
- if values [1 ] == ".." or values [1 ] == "" :
61
- return None
62
- return parse_rfc3339 (values [1 ])
58
+ return self ._end_date
63
59
64
60
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
65
61
@model_validator (mode = "before" )
@@ -102,30 +98,38 @@ def validate_bbox(cls, v: BBox) -> BBox:
102
98
103
99
@field_validator ("datetime" )
104
100
@classmethod
105
- def validate_datetime (cls , v : str ) -> str :
106
- if "/" in v :
107
- values = v .split ("/" )
108
- else :
109
- # Single date is interpreted as end date
110
- values = [".." , v ]
111
-
112
- dates : List [dt ] = []
113
- for value in values :
114
- if value == ".." or value == "" :
115
- continue
116
-
117
- dates .append (parse_rfc3339 (value ))
118
-
101
+ def validate_datetime (cls , value : str ) -> str :
102
+ # Split on "/" and replace no value or ".." with None
103
+ values = [v if v and v != ".." else None for v in value .split ("/" )]
104
+ # If there are more than 2 dates, it's invalid
119
105
if len (values ) > 2 :
120
- raise ValueError ("Invalid datetime range, must match format (begin_date, end_date)" )
121
-
122
- if not {".." , "" }.intersection (set (values )):
123
- if dates [0 ] > dates [1 ]:
124
- raise ValueError (
125
- "Invalid datetime range, must match format (begin_date, end_date)"
126
- )
127
-
128
- return v
106
+ raise ValueError (
107
+ "Invalid datetime range. Too many values. Must match format: {begin_date}/{end_date}"
108
+ )
109
+ # If there is only one date, insert a None for the start date
110
+ if len (values ) == 1 :
111
+ values .insert (0 , None )
112
+ # Cast because pylance gets confused by the type adapter and annotated type
113
+ dates = cast (
114
+ List [Optional [dt ]],
115
+ [
116
+ # Use the type adapter to validate the datetime strings, strict is necessary
117
+ # due to pydantic issues #8736 and #8762
118
+ SearchDatetime .validate_strings (v , strict = True ) if v else None
119
+ for v in values
120
+ ],
121
+ )
122
+ # If there is a start and end date, check that the start date is before the end date
123
+ if dates [0 ] and dates [1 ] and dates [0 ] > dates [1 ]:
124
+ raise ValueError (
125
+ "Invalid datetime range. Begin date after end date. "
126
+ "Must match format: {begin_date}/{end_date}"
127
+ )
128
+ # Store the parsed dates
129
+ cls ._start_date = dates [0 ]
130
+ cls ._end_date = dates [1 ]
131
+ # Return the original string value
132
+ return value
129
133
130
134
@property
131
135
def spatial_filter (self ) -> Optional [Intersection ]:
0 commit comments