Skip to content

Commit 7c5c693

Browse files
Yuri ZmytrakovYuri Zmytrakov
authored andcommitted
fix: ensure datetime uses nano seconds
1 parent c1a7bc1 commit 7c5c693

File tree

3 files changed

+60
-12
lines changed

3 files changed

+60
-12
lines changed

stac_fastapi/core/stac_fastapi/core/datetime_utils.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,45 @@ def format_datetime_range(date_str: str) -> str:
1515
Returns:
1616
str: A string formatted as 'YYYY-MM-DDTHH:MM:SSZ/YYYY-MM-DDTHH:MM:SSZ', with '..' used if any element is None.
1717
"""
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)
1820

1921
def normalize(dt):
2022
"""Normalize datetime string and preserve millisecond precision."""
2123
dt = dt.strip()
2224
if not dt or dt == "..":
2325
return ".."
24-
dt_obj = rfc3339_str_to_datetime(dt)
25-
dt_utc = dt_obj.astimezone(timezone.utc)
26-
return dt_utc.isoformat(timespec="milliseconds").replace("+00:00", "Z")
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")
2732

2833
if not isinstance(date_str, str):
29-
return "../.."
34+
return f"{MIN_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}/{MAX_DATE_NANOS.isoformat(timespec='auto').replace('+00:00','Z')}"
3035

3136
if "/" not in date_str:
3237
return f"{normalize(date_str)}/{normalize(date_str)}"
3338

3439
try:
3540
start, end = date_str.split("/", 1)
3641
except Exception:
37-
return "../.."
38-
return f"{normalize(start)}/{normalize(end)}"
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}"
3957

4058

4159
# 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: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import re
99
from datetime import date
1010
from datetime import datetime as datetime_type
11+
from datetime import timezone
1112
from typing import Dict, Optional, Union
1213

1314
from stac_fastapi.types.rfc3339 import DateTimeType
@@ -37,6 +38,8 @@ def return_date(
3738
always containing 'gte' and 'lte' keys.
3839
"""
3940
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)
4043

4144
if interval is None:
4245
return result
@@ -45,28 +48,55 @@ def return_date(
4548
if "/" in interval:
4649
parts = interval.split("/")
4750
result["gte"] = (
48-
parts[0] if parts[0] != ".." else datetime_type.min.isoformat() + "Z"
51+
parts[0] if parts[0] != ".." else MIN_DATE_NANOS.isoformat() + "Z"
4952
)
5053
result["lte"] = (
5154
parts[1]
5255
if len(parts) > 1 and parts[1] != ".."
53-
else datetime_type.max.isoformat() + "Z"
56+
else MAX_DATE_NANOS.isoformat() + "Z"
5457
)
5558
else:
5659
converted_time = interval if interval != ".." else None
5760
result["gte"] = result["lte"] = converted_time
5861
return result
5962

6063
if isinstance(interval, datetime_type):
61-
datetime_iso = interval.isoformat()
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()
6274
result["gte"] = result["lte"] = datetime_iso
6375
elif isinstance(interval, tuple):
6476
start, end = interval
6577
# Ensure datetimes are converted to UTC and formatted with 'Z'
6678
if start:
67-
result["gte"] = start.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
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"
6889
if end:
69-
result["lte"] = end.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
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"
70100

71101
return result
72102

stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/mappings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class Geometry(Protocol): # noqa
142142
"type": "object",
143143
"properties": {
144144
# Common https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md
145-
"datetime": {"type": "date"},
145+
"datetime": {"type": "date_nanos"},
146146
"start_datetime": {"type": "date"},
147147
"end_datetime": {"type": "date"},
148148
"created": {"type": "date"},

0 commit comments

Comments
 (0)