Repository: stac-utils/stac-fastapi
Pull request #803 (2025-02-28 18:07:29)

fix collection-search POST model
same as https://github.com/stac-utils/stac-pydantic/pull/172 and https://github.com/stac-utils/stac-pydantic/pull/167


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 8d7a3f2..1b72c3c 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 0e8eb88..7007207 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 @@ from typing import List, Optional, Tuple, cast
 
 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 @@ Either a date-time or an interval, open or closed. Date and time expressions adh
 
     # 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 @@ Either a date-time or an interval, open or closed. Date and time expressions adh
                     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 @@ Either a date-time or an interval, open or closed. Date and time expressions adh
             )
 
         # 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


Test patch:
diff --git a/stac_fastapi/extensions/tests/test_collection_search.py b/stac_fastapi/extensions/tests/test_collection_search.py
index 4c5f641..50fa79b 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 @@ class DummyPostClient(BaseCollectionSearchClient):
         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(