Skip to content

Commit

Permalink
return 400 for datetime errors (#670)
Browse files Browse the repository at this point in the history
* return HTTPException

* update test

* update validate interval format

* update changelog

* remove validate interval function

* catch iso8601.ParseError
  • Loading branch information
jonhealy1 authored Apr 24, 2024
1 parent cae2278 commit 0bd592e
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 29 deletions.
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

* Return 400 for datetime errors ([#670](https://github.com/stac-utils/stac-fastapi/pull/670))

## [2.5.3] - 2024-04-23

### Fixed
Expand Down
78 changes: 50 additions & 28 deletions stac_fastapi/types/stac_fastapi/types/rfc3339.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Optional, Tuple, Union

import iso8601
from fastapi import HTTPException
from pystac.utils import datetime_to_str

RFC33339_PATTERN = (
Expand Down Expand Up @@ -45,53 +46,74 @@ def rfc3339_str_to_datetime(s: str) -> datetime:
return iso8601.parse_date(s)


def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]:
"""Extract a tuple of datetimes from an interval string.
def parse_single_date(date_str: str) -> datetime:
"""
Parse a single RFC3339 date string into a datetime object.
Args:
date_str (str): A string representing the date in RFC3339 format.
Returns:
datetime: A datetime object parsed from the date_str.
Raises:
ValueError: If the date_str is empty or contains the placeholder '..'.
"""
if ".." in date_str or not date_str:
raise ValueError("Invalid date format.")
return rfc3339_str_to_datetime(date_str)


Interval strings are defined by
OGC API - Features Part 1 for the datetime query parameter value. These follow the
form '1985-04-12T23:20:50.52Z/1986-04-12T23:20:50.52Z', and allow either the start
or end (but not both) to be open-ended with '..' or ''.
def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]:
"""
Extract a tuple of datetime objects from an interval string defined by the OGC API.
The interval can either be a single datetime or a range with start and end datetime.
Args:
interval (str or None): The interval string to convert to a tuple of
datetime.datetime objects, or None if no datetime is specified.
interval (Optional[str]): The interval string to convert to datetime objects,
or None if no datetime is specified.
Returns:
Optional[DateTimeType]: A tuple of datetime.datetime objects or None if
input is None.
Optional[DateTimeType]: A tuple of datetime.datetime objects or
None if input is None.
Raises:
ValueError: If the string is not a valid interval string and not None.
HTTPException: If the string is not valid for various reasons such as being empty,
having more than one slash, or if date formats are invalid.
"""
if interval is None:
return None

if not interval:
raise ValueError("Empty interval string is invalid.")
raise HTTPException(status_code=400, detail="Empty interval string is invalid.")

values = interval.split("/")
if len(values) == 1:
# Single date for == date case
return rfc3339_str_to_datetime(values[0])
elif len(values) > 2:
raise ValueError(
f"Interval string '{interval}' contains more than one forward slash."
if len(values) > 2:
raise HTTPException(
status_code=400,
detail="Interval string contains more than one forward slash.",
)

start = None
end = None
if values[0] not in ["..", ""]:
start = rfc3339_str_to_datetime(values[0])
if values[1] not in ["..", ""]:
end = rfc3339_str_to_datetime(values[1])
try:
start = parse_single_date(values[0]) if values[0] not in ["..", ""] else None
end = (
parse_single_date(values[1])
if len(values) > 1 and values[1] not in ["..", ""]
else None
)
except (ValueError, iso8601.ParseError) as e:
raise HTTPException(status_code=400, detail=str(e))

if start is None and end is None:
raise ValueError("Double open-ended intervals are not allowed.")
raise HTTPException(
status_code=400, detail="Double open-ended intervals are not allowed."
)
if start is not None and end is not None and start > end:
raise ValueError("Start datetime cannot be before end datetime.")
else:
return start, end
raise HTTPException(
status_code=400, detail="Start datetime cannot be before end datetime."
)

return start, end


def now_in_utc() -> datetime:
Expand Down
6 changes: 5 additions & 1 deletion stac_fastapi/types/tests/test_rfc3339.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import timezone

import pytest
from fastapi import HTTPException

from stac_fastapi.types.rfc3339 import (
now_in_utc,
Expand Down Expand Up @@ -86,8 +87,11 @@ def test_parse_valid_str_to_datetime(test_input):

@pytest.mark.parametrize("test_input", invalid_intervals)
def test_parse_invalid_interval_to_datetime(test_input):
with pytest.raises(ValueError):
with pytest.raises(HTTPException) as exc_info:
str_to_interval(test_input)
assert (
exc_info.value.status_code == 400
), "Should return a 400 status code for invalid intervals"


@pytest.mark.parametrize("test_input", valid_intervals)
Expand Down

0 comments on commit 0bd592e

Please sign in to comment.