diff --git a/CHANGES.md b/CHANGES.md index 8d7a3f2dc..1b72c3c45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Fixed + +- Fix collection-search POST request model: + - Fix pydantic model to make sure class variables `_start_date` and `_end_date` not edited (ported from stac-pydantic) + - Fix bbox validation to allow anti-meridian crossing (ported from stac-pydantic) + ## [5.0.2] - 2025-01-30 ### Fixed diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/request.py index 0e8eb8824..7007207a4 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/request.py @@ -5,7 +5,7 @@ import attr from fastapi import Query -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, PrivateAttr, ValidationInfo, field_validator from stac_pydantic.api.search import SearchDatetime from stac_pydantic.shared import BBox from typing_extensions import Annotated @@ -64,8 +64,8 @@ class BaseCollectionSearchPostRequest(BaseModel): # Private properties to store the parsed datetime values. # Not part of the model schema. - _start_date: Optional[dt] = None - _end_date: Optional[dt] = None + _start_date: Optional[dt] = PrivateAttr(default=None) + _end_date: Optional[dt] = PrivateAttr(default=None) # Properties to return the private values @property @@ -94,35 +94,33 @@ def validate_bbox(cls, v: BBox) -> BBox: raise ValueError( "Maximum elevation must greater than minimum elevation" ) - - if xmax < xmin: - raise ValueError( - "Maximum longitude must be greater than minimum longitude" - ) + # Validate against WGS84 + if xmin < -180 or ymin < -90 or xmax > 180 or ymax > 90: + raise ValueError("Bounding box must be within (-180, -90, 180, 90)") if ymax < ymin: raise ValueError( "Maximum longitude must be greater than minimum longitude" ) - # Validate against WGS84 - if xmin < -180 or ymin < -90 or xmax > 180 or ymax > 90: - raise ValueError("Bounding box must be within (-180, -90, 180, 90)") - return v - @field_validator("datetime") + @field_validator("datetime", mode="after") @classmethod - def validate_datetime(cls, value: str) -> str: + def validate_datetime( + cls, value: Optional[str], info: ValidationInfo + ) -> Optional[str]: """validate datetime.""" # Split on "/" and replace no value or ".." with None + if value is None: + return value values = [v if v and v != ".." else None for v in value.split("/")] # If there are more than 2 dates, it's invalid if len(values) > 2: raise ValueError( - """Invalid datetime range. Too many values. - Must match format: {begin_date}/{end_date}""" + """Invalid datetime range. Too many values. """ + """Must match format: {begin_date}/{end_date}""" ) # If there is only one date, duplicate to use for both start and end dates @@ -149,8 +147,8 @@ def validate_datetime(cls, value: str) -> str: ) # Store the parsed dates - cls._start_date = dates[0] - cls._end_date = dates[1] + info.data["_start_date"] = dates[0] + info.data["_end_date"] = dates[1] # Return the original string value return value diff --git a/stac_fastapi/extensions/tests/test_collection_search.py b/stac_fastapi/extensions/tests/test_collection_search.py index 4c5f641ad..50fa79be7 100644 --- a/stac_fastapi/extensions/tests/test_collection_search.py +++ b/stac_fastapi/extensions/tests/test_collection_search.py @@ -1,4 +1,5 @@ import json +from datetime import datetime, timezone from urllib.parse import quote_plus import attr @@ -88,6 +89,19 @@ def post_all_collections( return search_request.model_dump() +def test_datetime_clean(): + # ref: https://github.com/stac-utils/stac-pydantic/issues/170 + utcnow = datetime.now(timezone.utc) + utcnow_str = utcnow.isoformat() + search = BaseCollectionSearchPostRequest(datetime=utcnow_str) + assert search.start_date == utcnow + assert search.end_date == utcnow + + search = BaseCollectionSearchPostRequest() + assert not search.start_date + assert not search.end_date + + def test_collection_search_extension_default(): """Test GET - /collections endpoint with collection-search ext.""" api = StacApi(