Skip to content

Commit 0bd592e

Browse files
authored
return 400 for datetime errors (#670)
* return HTTPException * update test * update validate interval format * update changelog * remove validate interval function * catch iso8601.ParseError
1 parent cae2278 commit 0bd592e

File tree

3 files changed

+59
-29
lines changed

3 files changed

+59
-29
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Fixed
6+
7+
* Return 400 for datetime errors ([#670](https://github.com/stac-utils/stac-fastapi/pull/670))
8+
59
## [2.5.3] - 2024-04-23
610

711
### Fixed

stac_fastapi/types/stac_fastapi/types/rfc3339.py

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Optional, Tuple, Union
55

66
import iso8601
7+
from fastapi import HTTPException
78
from pystac.utils import datetime_to_str
89

910
RFC33339_PATTERN = (
@@ -45,53 +46,74 @@ def rfc3339_str_to_datetime(s: str) -> datetime:
4546
return iso8601.parse_date(s)
4647

4748

48-
def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]:
49-
"""Extract a tuple of datetimes from an interval string.
49+
def parse_single_date(date_str: str) -> datetime:
50+
"""
51+
Parse a single RFC3339 date string into a datetime object.
52+
53+
Args:
54+
date_str (str): A string representing the date in RFC3339 format.
55+
56+
Returns:
57+
datetime: A datetime object parsed from the date_str.
58+
59+
Raises:
60+
ValueError: If the date_str is empty or contains the placeholder '..'.
61+
"""
62+
if ".." in date_str or not date_str:
63+
raise ValueError("Invalid date format.")
64+
return rfc3339_str_to_datetime(date_str)
65+
5066

51-
Interval strings are defined by
52-
OGC API - Features Part 1 for the datetime query parameter value. These follow the
53-
form '1985-04-12T23:20:50.52Z/1986-04-12T23:20:50.52Z', and allow either the start
54-
or end (but not both) to be open-ended with '..' or ''.
67+
def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]:
68+
"""
69+
Extract a tuple of datetime objects from an interval string defined by the OGC API.
70+
The interval can either be a single datetime or a range with start and end datetime.
5571
5672
Args:
57-
interval (str or None): The interval string to convert to a tuple of
58-
datetime.datetime objects, or None if no datetime is specified.
73+
interval (Optional[str]): The interval string to convert to datetime objects,
74+
or None if no datetime is specified.
5975
6076
Returns:
61-
Optional[DateTimeType]: A tuple of datetime.datetime objects or None if
62-
input is None.
77+
Optional[DateTimeType]: A tuple of datetime.datetime objects or
78+
None if input is None.
6379
6480
Raises:
65-
ValueError: If the string is not a valid interval string and not None.
81+
HTTPException: If the string is not valid for various reasons such as being empty,
82+
having more than one slash, or if date formats are invalid.
6683
"""
6784
if interval is None:
6885
return None
6986

7087
if not interval:
71-
raise ValueError("Empty interval string is invalid.")
88+
raise HTTPException(status_code=400, detail="Empty interval string is invalid.")
7289

7390
values = interval.split("/")
74-
if len(values) == 1:
75-
# Single date for == date case
76-
return rfc3339_str_to_datetime(values[0])
77-
elif len(values) > 2:
78-
raise ValueError(
79-
f"Interval string '{interval}' contains more than one forward slash."
91+
if len(values) > 2:
92+
raise HTTPException(
93+
status_code=400,
94+
detail="Interval string contains more than one forward slash.",
8095
)
8196

82-
start = None
83-
end = None
84-
if values[0] not in ["..", ""]:
85-
start = rfc3339_str_to_datetime(values[0])
86-
if values[1] not in ["..", ""]:
87-
end = rfc3339_str_to_datetime(values[1])
97+
try:
98+
start = parse_single_date(values[0]) if values[0] not in ["..", ""] else None
99+
end = (
100+
parse_single_date(values[1])
101+
if len(values) > 1 and values[1] not in ["..", ""]
102+
else None
103+
)
104+
except (ValueError, iso8601.ParseError) as e:
105+
raise HTTPException(status_code=400, detail=str(e))
88106

89107
if start is None and end is None:
90-
raise ValueError("Double open-ended intervals are not allowed.")
108+
raise HTTPException(
109+
status_code=400, detail="Double open-ended intervals are not allowed."
110+
)
91111
if start is not None and end is not None and start > end:
92-
raise ValueError("Start datetime cannot be before end datetime.")
93-
else:
94-
return start, end
112+
raise HTTPException(
113+
status_code=400, detail="Start datetime cannot be before end datetime."
114+
)
115+
116+
return start, end
95117

96118

97119
def now_in_utc() -> datetime:

stac_fastapi/types/tests/test_rfc3339.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import timezone
22

33
import pytest
4+
from fastapi import HTTPException
45

56
from stac_fastapi.types.rfc3339 import (
67
now_in_utc,
@@ -86,8 +87,11 @@ def test_parse_valid_str_to_datetime(test_input):
8687

8788
@pytest.mark.parametrize("test_input", invalid_intervals)
8889
def test_parse_invalid_interval_to_datetime(test_input):
89-
with pytest.raises(ValueError):
90+
with pytest.raises(HTTPException) as exc_info:
9091
str_to_interval(test_input)
92+
assert (
93+
exc_info.value.status_code == 400
94+
), "Should return a 400 status code for invalid intervals"
9195

9296

9397
@pytest.mark.parametrize("test_input", valid_intervals)

0 commit comments

Comments
 (0)