diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index b1d40999..6c6ff116 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: