Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

return 400 for datetime errors #670

Merged
merged 6 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
94 changes: 65 additions & 29 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,88 @@ 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.

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 ''.
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.")
vincentsarago marked this conversation as resolved.
Show resolved Hide resolved
return rfc3339_str_to_datetime(date_str)


def validate_interval_format(values: list) -> None:
"""
Validate the format of the interval string to ensure it contains at most
one forward slash.

Args:
interval (str or None): The interval string to convert to a tuple of
datetime.datetime objects, or None if no datetime is specified.
values (list): A list of strings split by '/' from the interval string.

Raises:
HTTPException: If the interval string contains more than one forward slash.
"""
if len(values) > 2:
raise HTTPException(
status_code=400,
detail="Interval string contains more than one forward slash.",
)


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 (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."
validate_interval_format(values)
jonhealy1 marked this conversation as resolved.
Show resolved Hide resolved

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
)

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])
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
vincentsarago marked this conversation as resolved.
Show resolved Hide resolved

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