Skip to content

Commit 23bbd6b

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

File tree

2 files changed

+155
-95
lines changed

2 files changed

+155
-95
lines changed

stac_fastapi/core/stac_fastapi/core/datetime_utils.py

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from stac_fastapi.types.rfc3339 import rfc3339_str_to_datetime
66

7+
from stac_fastapi.core.utilities import get_bool_env
78

89
def format_datetime_range(date_str: str) -> str:
910
"""
@@ -15,46 +16,72 @@ 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}"
57-
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+
def normalize(dt):
66+
"""Normalize datetime string and preserve millisecond precision."""
67+
dt = dt.strip()
68+
if not dt or dt == "..":
69+
return ".."
70+
dt_obj = rfc3339_str_to_datetime(dt)
71+
dt_utc = dt_obj.astimezone(timezone.utc)
72+
return dt_utc.isoformat(timespec="milliseconds").replace("+00:00", "Z")
73+
74+
if not isinstance(date_str, str):
75+
return "../.."
76+
77+
if "/" not in date_str:
78+
return f"{normalize(date_str)}/{normalize(date_str)}"
79+
80+
try:
81+
start, end = date_str.split("/", 1)
82+
except Exception:
83+
return "../.."
84+
return f"{normalize(start)}/{normalize(end)}"
5885

5986
# Borrowed from pystac - https://github.com/stac-utils/pystac/blob/f5e4cf4a29b62e9ef675d4a4dac7977b09f53c8f/pystac/utils.py#L370-L394
6087
def datetime_to_str(dt: datetime, timespec: str = "auto") -> str:

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

Lines changed: 88 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Dict, Optional, Union
1313

1414
from stac_fastapi.types.rfc3339 import DateTimeType
15+
from stac_fastapi.core.utilities import get_bool_env
1516

1617
logger = logging.getLogger(__name__)
1718

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

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"
104+
return result
105+
106+
else:
107+
if isinstance(interval, str):
108+
if "/" in interval:
109+
parts = interval.split("/")
110+
result["gte"] = (
111+
parts[0] if parts[0] != ".." else datetime_type.min.isoformat() + "Z"
112+
)
113+
result["lte"] = (
114+
parts[1]
115+
if len(parts) > 1 and parts[1] != ".."
116+
else datetime_type.max.isoformat() + "Z"
117+
)
118+
else:
119+
converted_time = interval if interval != ".." else None
120+
result["gte"] = result["lte"] = converted_time
121+
return result
122+
123+
if isinstance(interval, datetime_type):
124+
datetime_iso = interval.isoformat()
125+
result["gte"] = result["lte"] = datetime_iso
126+
elif isinstance(interval, tuple):
127+
start, end = interval
128+
# Ensure datetimes are converted to UTC and formatted with 'Z'
129+
if start:
130+
result["gte"] = start.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
131+
if end:
132+
result["lte"] = end.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
100133

101-
return result
134+
return result
102135

103136

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

0 commit comments

Comments
 (0)