Skip to content

Commit 0a028e0

Browse files
Yuri ZmytrakovYuri Zmytrakov
authored andcommitted
ensure backward datetime compatibility
1 parent 407bcf6 commit 0a028e0

File tree

2 files changed

+159
-93
lines changed

2 files changed

+159
-93
lines changed

stac_fastapi/core/stac_fastapi/core/datetime_utils.py

Lines changed: 68 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from datetime import datetime, timezone
44

5+
from stac_fastapi.core.utilities import get_bool_env
56
from stac_fastapi.types.rfc3339 import rfc3339_str_to_datetime
67

78

@@ -15,45 +16,73 @@ def format_datetime_range(date_str: str) -> str:
1516
Returns:
1617
str: A string formatted as 'YYYY-MM-DDTHH:MM:SSZ/YYYY-MM-DDTHH:MM:SSZ', with '..' used if any element is None.
1718
"""
18-
MIN_DATE_NANOS = datetime(1970, 1, 1, tzinfo=timezone.utc)
19-
MAX_DATE_NANOS = datetime(2262, 4, 11, 23, 47, 16, 854775, tzinfo=timezone.utc)
20-
21-
def normalize(dt):
22-
"""Normalize datetime string and preserve millisecond precision."""
23-
dt = dt.strip()
24-
if not dt or dt == "..":
25-
return ".."
26-
dt_utc = rfc3339_str_to_datetime(dt).astimezone(timezone.utc)
27-
if dt_utc < MIN_DATE_NANOS:
28-
dt_utc = MIN_DATE_NANOS
29-
if dt_utc > MAX_DATE_NANOS:
30-
dt_utc = MAX_DATE_NANOS
31-
return dt_utc.isoformat(timespec="auto").replace("+00:00", "Z")
32-
33-
if not isinstance(date_str, str):
34-
return f"{MIN_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}/{MAX_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}"
35-
36-
if "/" not in date_str:
37-
return f"{normalize(date_str)}/{normalize(date_str)}"
38-
39-
try:
40-
start, end = date_str.split("/", 1)
41-
except Exception:
42-
return f"{MIN_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}/{MAX_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}"
43-
44-
normalized_start = normalize(start)
45-
normalized_end = normalize(end)
46-
47-
if normalized_start == "..":
48-
normalized_start = MIN_DATE_NANOS.isoformat(timespec="auto").replace(
49-
"+00:00", "Z"
50-
)
51-
if normalized_end == "..":
52-
normalized_end = MAX_DATE_NANOS.isoformat(timespec="auto").replace(
53-
"+00:00", "Z"
54-
)
55-
56-
return f"{normalized_start}/{normalized_end}"
19+
use_datetime_nanos = get_bool_env("USE_DATETIME_NANOS", default=True)
20+
21+
if use_datetime_nanos:
22+
print(use_datetime_nanos)
23+
MIN_DATE_NANOS = datetime(1970, 1, 1, tzinfo=timezone.utc)
24+
MAX_DATE_NANOS = datetime(2262, 4, 11, 23, 47, 16, 854775, tzinfo=timezone.utc)
25+
26+
def normalize(dt):
27+
"""Normalize datetime string and preserve nano second precision."""
28+
dt = dt.strip()
29+
if not dt or dt == "..":
30+
return ".."
31+
dt_utc = rfc3339_str_to_datetime(dt).astimezone(timezone.utc)
32+
if dt_utc < MIN_DATE_NANOS:
33+
dt_utc = MIN_DATE_NANOS
34+
if dt_utc > MAX_DATE_NANOS:
35+
dt_utc = MAX_DATE_NANOS
36+
return dt_utc.isoformat(timespec="auto").replace("+00:00", "Z")
37+
38+
if not isinstance(date_str, str):
39+
return f"{MIN_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}/{MAX_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}"
40+
41+
if "/" not in date_str:
42+
return f"{normalize(date_str)}/{normalize(date_str)}"
43+
44+
try:
45+
start, end = date_str.split("/", 1)
46+
except Exception:
47+
return f"{MIN_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}/{MAX_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}"
48+
49+
normalized_start = normalize(start)
50+
normalized_end = normalize(end)
51+
52+
if normalized_start == "..":
53+
normalized_start = MIN_DATE_NANOS.isoformat(timespec="auto").replace(
54+
"+00:00", "Z"
55+
)
56+
if normalized_end == "..":
57+
normalized_end = MAX_DATE_NANOS.isoformat(timespec="auto").replace(
58+
"+00:00", "Z"
59+
)
60+
61+
return f"{normalized_start}/{normalized_end}"
62+
63+
else:
64+
print(use_datetime_nanos)
65+
66+
def normalize(dt):
67+
"""Normalize datetime string and preserve millisecond precision."""
68+
dt = dt.strip()
69+
if not dt or dt == "..":
70+
return ".."
71+
dt_obj = rfc3339_str_to_datetime(dt)
72+
dt_utc = dt_obj.astimezone(timezone.utc)
73+
return dt_utc.isoformat(timespec="milliseconds").replace("+00:00", "Z")
74+
75+
if not isinstance(date_str, str):
76+
return "../.."
77+
78+
if "/" not in date_str:
79+
return f"{normalize(date_str)}/{normalize(date_str)}"
80+
81+
try:
82+
start, end = date_str.split("/", 1)
83+
except Exception:
84+
return "../.."
85+
return f"{normalize(start)}/{normalize(end)}"
5786

5887

5988
# Borrowed from pystac - https://github.com/stac-utils/pystac/blob/f5e4cf4a29b62e9ef675d4a4dac7977b09f53c8f/pystac/utils.py#L370-L394

stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database/datetime.py

Lines changed: 91 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from datetime import timezone
1212
from typing import Dict, Optional, Union
1313

14+
from stac_fastapi.core.utilities import get_bool_env
1415
from stac_fastapi.types.rfc3339 import DateTimeType
1516

1617
logger = logging.getLogger(__name__)
@@ -38,67 +39,103 @@ def return_date(
3839
always containing 'gte' and 'lte' keys.
3940
"""
4041
result: Dict[str, Optional[str]] = {"gte": None, "lte": None}
41-
MIN_DATE_NANOS = datetime_type(1970, 1, 1, tzinfo=timezone.utc)
42-
MAX_DATE_NANOS = datetime_type(2262, 4, 11, 23, 47, 16, 854775, tzinfo=timezone.utc)
43-
42+
use_datetime_nanos = get_bool_env("USE_DATETIME_NANOS", default=True)
4443
if interval is None:
4544
return result
4645

47-
if isinstance(interval, str):
48-
if "/" in interval:
49-
parts = interval.split("/")
50-
result["gte"] = (
51-
parts[0] if parts[0] != ".." else MIN_DATE_NANOS.isoformat() + "Z"
52-
)
53-
result["lte"] = (
54-
parts[1]
55-
if len(parts) > 1 and parts[1] != ".."
56-
else MAX_DATE_NANOS.isoformat() + "Z"
46+
if use_datetime_nanos:
47+
MIN_DATE_NANOS = datetime_type(1970, 1, 1, tzinfo=timezone.utc)
48+
MAX_DATE_NANOS = datetime_type(
49+
2262, 4, 11, 23, 47, 16, 854775, tzinfo=timezone.utc
50+
)
51+
52+
if isinstance(interval, str):
53+
if "/" in interval:
54+
parts = interval.split("/")
55+
result["gte"] = (
56+
parts[0] if parts[0] != ".." else MIN_DATE_NANOS.isoformat() + "Z"
57+
)
58+
result["lte"] = (
59+
parts[1]
60+
if len(parts) > 1 and parts[1] != ".."
61+
else MAX_DATE_NANOS.isoformat() + "Z"
62+
)
63+
else:
64+
converted_time = interval if interval != ".." else None
65+
result["gte"] = result["lte"] = converted_time
66+
return result
67+
68+
if isinstance(interval, datetime_type):
69+
dt_utc = (
70+
interval.astimezone(timezone.utc)
71+
if interval.tzinfo
72+
else interval.replace(tzinfo=timezone.utc)
5773
)
58-
else:
59-
converted_time = interval if interval != ".." else None
60-
result["gte"] = result["lte"] = converted_time
74+
if dt_utc < MIN_DATE_NANOS:
75+
dt_utc = MIN_DATE_NANOS
76+
elif dt_utc > MAX_DATE_NANOS:
77+
dt_utc = MAX_DATE_NANOS
78+
datetime_iso = dt_utc.isoformat()
79+
result["gte"] = result["lte"] = datetime_iso
80+
elif isinstance(interval, tuple):
81+
start, end = interval
82+
# Ensure datetimes are converted to UTC and formatted with 'Z'
83+
if start:
84+
start_utc = (
85+
start.astimezone(timezone.utc)
86+
if start.tzinfo
87+
else start.replace(tzinfo=timezone.utc)
88+
)
89+
if start_utc < MIN_DATE_NANOS:
90+
start_utc = MIN_DATE_NANOS
91+
elif start_utc > MAX_DATE_NANOS:
92+
start_utc = MAX_DATE_NANOS
93+
result["gte"] = start_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
94+
if end:
95+
end_utc = (
96+
end.astimezone(timezone.utc)
97+
if end.tzinfo
98+
else end.replace(tzinfo=timezone.utc)
99+
)
100+
if end_utc < MIN_DATE_NANOS:
101+
end_utc = MIN_DATE_NANOS
102+
elif end_utc > MAX_DATE_NANOS:
103+
end_utc = MAX_DATE_NANOS
104+
result["lte"] = end_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
105+
61106
return result
62107

63-
if isinstance(interval, datetime_type):
64-
dt_utc = (
65-
interval.astimezone(timezone.utc)
66-
if interval.tzinfo
67-
else interval.replace(tzinfo=timezone.utc)
68-
)
69-
if dt_utc < MIN_DATE_NANOS:
70-
dt_utc = MIN_DATE_NANOS
71-
elif dt_utc > MAX_DATE_NANOS:
72-
dt_utc = MAX_DATE_NANOS
73-
datetime_iso = dt_utc.isoformat()
74-
result["gte"] = result["lte"] = datetime_iso
75-
elif isinstance(interval, tuple):
76-
start, end = interval
77-
# Ensure datetimes are converted to UTC and formatted with 'Z'
78-
if start:
79-
start_utc = (
80-
start.astimezone(timezone.utc)
81-
if start.tzinfo
82-
else start.replace(tzinfo=timezone.utc)
83-
)
84-
if start_utc < MIN_DATE_NANOS:
85-
start_utc = MIN_DATE_NANOS
86-
elif start_utc > MAX_DATE_NANOS:
87-
start_utc = MAX_DATE_NANOS
88-
result["gte"] = start_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
89-
if end:
90-
end_utc = (
91-
end.astimezone(timezone.utc)
92-
if end.tzinfo
93-
else end.replace(tzinfo=timezone.utc)
94-
)
95-
if end_utc < MIN_DATE_NANOS:
96-
end_utc = MIN_DATE_NANOS
97-
elif end_utc > MAX_DATE_NANOS:
98-
end_utc = MAX_DATE_NANOS
99-
result["lte"] = end_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
108+
else:
109+
if isinstance(interval, str):
110+
if "/" in interval:
111+
parts = interval.split("/")
112+
result["gte"] = (
113+
parts[0]
114+
if parts[0] != ".."
115+
else datetime_type.min.isoformat() + "Z"
116+
)
117+
result["lte"] = (
118+
parts[1]
119+
if len(parts) > 1 and parts[1] != ".."
120+
else datetime_type.max.isoformat() + "Z"
121+
)
122+
else:
123+
converted_time = interval if interval != ".." else None
124+
result["gte"] = result["lte"] = converted_time
125+
return result
126+
127+
if isinstance(interval, datetime_type):
128+
datetime_iso = interval.isoformat()
129+
result["gte"] = result["lte"] = datetime_iso
130+
elif isinstance(interval, tuple):
131+
start, end = interval
132+
# Ensure datetimes are converted to UTC and formatted with 'Z'
133+
if start:
134+
result["gte"] = start.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
135+
if end:
136+
result["lte"] = end.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
100137

101-
return result
138+
return result
102139

103140

104141
def extract_date(date_str: str) -> date:

0 commit comments

Comments
 (0)