From 0bd592e8f5c87a51011a03bbd6592d254f1a2a4f Mon Sep 17 00:00:00 2001 From: Jonathan Healy Date: Wed, 24 Apr 2024 15:01:11 +0800 Subject: [PATCH] return 400 for datetime errors (#670) * return HTTPException * update test * update validate interval format * update changelog * remove validate interval function * catch iso8601.ParseError --- CHANGES.md | 4 + .../types/stac_fastapi/types/rfc3339.py | 78 ++++++++++++------- stac_fastapi/types/tests/test_rfc3339.py | 6 +- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 57498b21..0f057de7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index b1d40999..1277c998 100644 --- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py +++ b/stac_fastapi/types/stac_fastapi/types/rfc3339.py @@ -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 = ( @@ -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: diff --git a/stac_fastapi/types/tests/test_rfc3339.py b/stac_fastapi/types/tests/test_rfc3339.py index 23f6242b..8d83dbb9 100644 --- a/stac_fastapi/types/tests/test_rfc3339.py +++ b/stac_fastapi/types/tests/test_rfc3339.py @@ -1,6 +1,7 @@ from datetime import timezone import pytest +from fastapi import HTTPException from stac_fastapi.types.rfc3339 import ( now_in_utc, @@ -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)