Repository: CS-SI/eodag
Pull request #837 (2023-09-20 13:33:23)

fix(server): opened time intervals
Fixes server-mode issues with opened time intervals filtering

github-actions[bot]: ## Test Results
       4 files  ±0         4 suites  ±0   7m 44s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "duration of all tests") +37s
   430 tests +2     427 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "passed tests") +2    3 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "skipped / disabled tests") ±0  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "failed tests") ±0 
1 720 runs  +8  1 644 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "passed tests") +4  76 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "skipped / disabled tests") +4  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "failed tests") ±0 

Results for commit abfe4ef0. ± Comparison against base commit ec4c868f.

[test-results]:data:application/gzip;base64,H4sIAOD1CmUC/03MSw7CIBSF4a00jB3w8gJuxiCPhNgWQ2Fk3LtQhXb4fzf3vJEPs9vQbeKXCW0l5BG2JJ1DXFtCg3rK+5HhXvetGNOIioOe4VWJDfA6zBWOJ5dSTH9JZW2bRNBefZMA54N+mwI6nDb3Pk+auCwh10D64R13HktsvNLKMGOpJhIIsRgTdpWguFbg0OcLiEj3swgBAAA=

github-actions[bot]: <strong>Code Coverage (Windows)</strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `76%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against abfe4ef080cf9a9c3cd2a18611d001358694a96e </p>

Patch:
diff --git a/eodag/rest/utils.py b/eodag/rest/utils.py
index 43afb22c..58bf5838 100644
--- a/eodag/rest/utils.py
+++ b/eodag/rest/utils.py
@@ -299,15 +299,25 @@ def get_datetime(arguments):
     :rtype: Tuple[Optional[str], Optional[str]]
     """
     datetime_str = arguments.pop("datetime", None)
+
     if datetime_str:
         datetime_split = datetime_str.split("/")
-        if len(datetime_split) == 1:
-            return get_date(datetime_split[0]), None
-        elif len(datetime_split) == 2:
-            return get_date(datetime_split[0]), get_date(datetime_split[1])
-    dtstart = get_date(arguments.pop("dtstart", None))
-    dtend = get_date(arguments.pop("dtend", None))
-    return dtstart, dtend
+        if len(datetime_split) > 1:
+            dtstart = datetime_split[0] if datetime_split[0] != ".." else None
+            dtend = datetime_split[1] if datetime_split[1] != ".." else None
+        elif len(datetime_split) == 1:
+            # same time for start & end if only one is given
+            dtstart, dtend = datetime_split[0:1] * 2
+        else:
+            return None, None
+
+        return get_date(dtstart), get_date(dtend)
+
+    else:
+        # return already set (dtstart, dtend) or None
+        dtstart = get_date(arguments.pop("dtstart", None))
+        dtend = get_date(arguments.pop("dtend", None))
+        return get_date(dtstart), get_date(dtend)
 
 
 def get_metadata_query_paths(metadata_mapping):
@@ -789,27 +799,8 @@ def search_stac_items(url, arguments, root="/", catalogs=[], provider=None):
             "totalResults": len(search_results),
         }
     else:
-        # get datetime
         if "datetime" in arguments.keys() and arguments["datetime"] is not None:
-            dtime_split = arguments.get("datetime", "").split("/")
-            if len(dtime_split) > 1:
-                arguments["dtstart"] = (
-                    dtime_split[0]
-                    if dtime_split[0] != ".."
-                    else datetime.datetime.min.isoformat() + "Z"
-                )
-                arguments["dtend"] = (
-                    dtime_split[1]
-                    if dtime_split[1] != ".."
-                    else datetime.datetime.now(datetime.timezone.utc)
-                    .isoformat()
-                    .replace("+00:00", "")
-                    + "Z"
-                )
-            elif len(dtime_split) == 1:
-                # same time for start & end if only one is given
-                arguments["dtstart"], arguments["dtend"] = dtime_split[0:1] * 2
-            arguments.pop("datetime")
+            arguments["dtstart"], arguments["dtend"] = get_datetime(arguments)
 
         search_products_arguments = dict(
             arguments,
@@ -817,12 +808,20 @@ def search_stac_items(url, arguments, root="/", catalogs=[], provider=None):
             **{"unserialized": "true", "provider": provider},
         )
 
-        # check if time filtering appears twice
+        # check if time filtering appears both in search arguments and catalog
         if set(["dtstart", "dtend"]) <= set(arguments.keys()) and set(
             ["dtstart", "dtend"]
         ) <= set(result_catalog.search_args.keys()):
-            search_date_min = dateutil.parser.parse(arguments["dtstart"])
-            search_date_max = dateutil.parser.parse(arguments["dtend"])
+            search_date_min = (
+                dateutil.parser.parse(arguments["dtstart"])
+                if arguments["dtstart"]
+                else datetime.datetime.min
+            )
+            search_date_max = (
+                dateutil.parser.parse(arguments["dtend"])
+                if arguments["dtend"]
+                else datetime.datetime.now()
+            )
             catalog_date_min = dateutil.parser.parse(
                 result_catalog.search_args["dtstart"]
             )


Test patch:
diff --git a/tests/units/test_http_server.py b/tests/units/test_http_server.py
index 6edda81d..538bdc6e 100644
--- a/tests/units/test_http_server.py
+++ b/tests/units/test_http_server.py
@@ -499,6 +499,37 @@ class RequestTestCase(unittest.TestCase):
                 geom=box(0, 43, 1, 44, ccw=False),
             ),
         )
+        self._request_valid(
+            f"search?collections={self.tested_product_type}&bbox=0,43,1,44&datetime=2018-01-20/..",
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-20T00:00:00",
+                geom=box(0, 43, 1, 44, ccw=False),
+            ),
+        )
+        self._request_valid(
+            f"search?collections={self.tested_product_type}&bbox=0,43,1,44&datetime=../2018-01-25",
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                end="2018-01-25T00:00:00",
+                geom=box(0, 43, 1, 44, ccw=False),
+            ),
+        )
+        self._request_valid(
+            f"search?collections={self.tested_product_type}&bbox=0,43,1,44&datetime=2018-01-20",
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-20T00:00:00",
+                end="2018-01-20T00:00:00",
+                geom=box(0, 43, 1, 44, ccw=False),
+            ),
+        )
 
     def test_date_search_from_items(self):
         """Search through eodag server collection/items endpoint using dates filering should return a valid response"""
@@ -582,6 +613,64 @@ class RequestTestCase(unittest.TestCase):
             [it["title"] for it in result.get("links", []) if it["rel"] == "child"],
         )
 
+    def test_catalog_browse_date_search(self):
+        """Browsing catalogs with date filtering through eodag server should return a valid response"""
+        self._request_valid(
+            f"catalogs/{self.tested_product_type}/year/2018/month/01/items",
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-01T00:00:00",
+                end="2018-02-01T00:00:00",
+            ),
+        )
+        # args & catalog intersection
+        self._request_valid(
+            f"catalogs/{self.tested_product_type}/year/2018/month/01/items?datetime=2018-01-20/2018-02-15",
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-20T00:00:00",
+                end="2018-02-01T00:00:00",
+            ),
+        )
+        self._request_valid(
+            f"catalogs/{self.tested_product_type}/year/2018/month/01/items?datetime=2018-01-20/..",
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-20T00:00:00",
+                end="2018-02-01T00:00:00",
+            ),
+        )
+        self._request_valid(
+            f"catalogs/{self.tested_product_type}/year/2018/month/01/items?datetime=../2018-01-05",
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-01T00:00:00",
+                end="2018-01-05T00:00:00",
+            ),
+        )
+        self._request_valid(
+            f"catalogs/{self.tested_product_type}/year/2018/month/01/items?datetime=2018-01-05",
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-05T00:00:00",
+                end="2018-01-05T00:00:00",
+            ),
+        )
+        result = self._request_valid(
+            f"catalogs/{self.tested_product_type}/year/2018/month/01/items?datetime=../2017-08-01",
+        )
+        self.assertEqual(len(result["features"]), 0)
+
     def test_search_item_id_from_catalog(self):
         """Search by id through eodag server /catalog endpoint should return a valid response"""
         self._request_valid(
@@ -650,6 +739,67 @@ class RequestTestCase(unittest.TestCase):
             ),
         )
 
+    def test_date_post_search(self):
+        """POST search with datetime filtering through eodag server should return a valid response"""
+        self._request_valid(
+            "search",
+            protocol="POST",
+            post_data={
+                "collections": [self.tested_product_type],
+                "datetime": "2018-01-20/2018-01-25",
+            },
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-20T00:00:00",
+                end="2018-01-25T00:00:00",
+            ),
+        )
+        self._request_valid(
+            "search",
+            protocol="POST",
+            post_data={
+                "collections": [self.tested_product_type],
+                "datetime": "2018-01-20/..",
+            },
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-20T00:00:00",
+            ),
+        )
+        self._request_valid(
+            "search",
+            protocol="POST",
+            post_data={
+                "collections": [self.tested_product_type],
+                "datetime": "../2018-01-25",
+            },
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                end="2018-01-25T00:00:00",
+            ),
+        )
+        self._request_valid(
+            "search",
+            protocol="POST",
+            post_data={
+                "collections": [self.tested_product_type],
+                "datetime": "2018-01-20",
+            },
+            expected_search_kwargs=dict(
+                productType=self.tested_product_type,
+                page=1,
+                items_per_page=DEFAULT_ITEMS_PER_PAGE,
+                start="2018-01-20T00:00:00",
+                end="2018-01-20T00:00:00",
+            ),
+        )
+
     def test_ids_post_search(self):
         """POST search with ids filtering through eodag server should return a valid response"""
         self._request_valid(
diff --git a/tests/units/test_stac_utils.py b/tests/units/test_stac_utils.py
index 997a45c0..2601ac32 100644
--- a/tests/units/test_stac_utils.py
+++ b/tests/units/test_stac_utils.py
@@ -219,9 +219,17 @@ class TestStacUtils(unittest.TestCase):
         self.assertEqual(dtstart, start)
         self.assertEqual(dtend, end)
 
+        dtstart, dtend = self.rest_utils.get_datetime({"datetime": f"../{end}"})
+        self.assertEqual(dtstart, None)
+        self.assertEqual(dtend, end)
+
+        dtstart, dtend = self.rest_utils.get_datetime({"datetime": f"{start}/.."})
+        self.assertEqual(dtstart, start)
+        self.assertEqual(dtend, None)
+
         dtstart, dtend = self.rest_utils.get_datetime({"datetime": start})
         self.assertEqual(dtstart, start)
-        self.assertIsNone(dtend)
+        self.assertEqual(dtstart, dtend)
 
         dtstart, dtend = self.rest_utils.get_datetime({"dtstart": start, "dtend": end})
         self.assertEqual(dtstart, start)