From 499aa5093af40d7d1fae201fc8d5ce0f12be69e2 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 24 Apr 2024 02:10:53 +0800 Subject: [PATCH 1/6] return HTTPException --- .../types/stac_fastapi/types/rfc3339.py | 91 +++++++++++++------ 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index b1d40999e..6c6ff116a 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,85 @@ 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) + + +def validate_interval_format(values: list) -> None: + """ + Validate the format of the interval string to ensure it contains at most + one forward slash. - 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: + values (list): A list of strings split by '/' from the interval string. + + Raises: + ValueError: If the interval string contains more than one forward slash. + """ + if len(values) > 2: + raise ValueError("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 (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." + validate_interval_format(values) + + 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)) 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: From 05edbab0c5a4705238e58bd560388ee0ede0b3a5 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 24 Apr 2024 11:41:18 +0800 Subject: [PATCH 2/6] update test --- stac_fastapi/types/tests/test_rfc3339.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/types/tests/test_rfc3339.py b/stac_fastapi/types/tests/test_rfc3339.py index 23f6242bc..7cadcfac1 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,14 @@ 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" + assert "Start datetime cannot be before end datetime." in str( + exc_info.value.detail + ), "Error message does not match expected output" @pytest.mark.parametrize("test_input", valid_intervals) From 369c83094a6e4800d68573426fa0b7d977494bae Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 24 Apr 2024 11:53:17 +0800 Subject: [PATCH 3/6] update validate interval format --- stac_fastapi/types/stac_fastapi/types/rfc3339.py | 7 +++++-- stac_fastapi/types/tests/test_rfc3339.py | 3 --- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index 6c6ff116a..6b1fc8fcc 100644 --- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py +++ b/stac_fastapi/types/stac_fastapi/types/rfc3339.py @@ -73,10 +73,13 @@ def validate_interval_format(values: list) -> None: values (list): A list of strings split by '/' from the interval string. Raises: - ValueError: If the interval string contains more than one forward slash. + HTTPException: If the interval string contains more than one forward slash. """ if len(values) > 2: - raise ValueError("Interval string contains more than one forward slash.") + raise HTTPException( + status_code=400, + detail="Interval string contains more than one forward slash.", + ) def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]: diff --git a/stac_fastapi/types/tests/test_rfc3339.py b/stac_fastapi/types/tests/test_rfc3339.py index 7cadcfac1..8d83dbb9d 100644 --- a/stac_fastapi/types/tests/test_rfc3339.py +++ b/stac_fastapi/types/tests/test_rfc3339.py @@ -92,9 +92,6 @@ def test_parse_invalid_interval_to_datetime(test_input): assert ( exc_info.value.status_code == 400 ), "Should return a 400 status code for invalid intervals" - assert "Start datetime cannot be before end datetime." in str( - exc_info.value.detail - ), "Error message does not match expected output" @pytest.mark.parametrize("test_input", valid_intervals) From 5cc45937832e09e9a6386eadf214191046363f36 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 24 Apr 2024 11:58:44 +0800 Subject: [PATCH 4/6] update changelog --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 57498b21f..0f057de71 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 From 73402d9b58c0291f18d3b128a1d417ccaf4bd1aa Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 24 Apr 2024 13:50:48 +0800 Subject: [PATCH 5/6] remove validate interval function --- .../types/stac_fastapi/types/rfc3339.py | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index 6b1fc8fcc..cd10621ae 100644 --- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py +++ b/stac_fastapi/types/stac_fastapi/types/rfc3339.py @@ -64,24 +64,6 @@ def parse_single_date(date_str: str) -> datetime: 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: - 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. @@ -106,7 +88,11 @@ def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]: raise HTTPException(status_code=400, detail="Empty interval string is invalid.") values = interval.split("/") - validate_interval_format(values) + if len(values) > 2: + raise HTTPException( + status_code=400, + detail="Interval string contains more than one forward slash.", + ) try: start = parse_single_date(values[0]) if values[0] not in ["..", ""] else None From 65ce2ca09544837f1ef9b33e5a63f0bd5dd04359 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 24 Apr 2024 14:35:28 +0800 Subject: [PATCH 6/6] catch iso8601.ParseError --- stac_fastapi/types/stac_fastapi/types/rfc3339.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index cd10621ae..1277c998a 100644 --- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py +++ b/stac_fastapi/types/stac_fastapi/types/rfc3339.py @@ -101,7 +101,7 @@ def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]: if len(values) > 1 and values[1] not in ["..", ""] else None ) - except ValueError as e: + except (ValueError, iso8601.ParseError) as e: raise HTTPException(status_code=400, detail=str(e)) if start is None and end is None: