Repository: CS-SI/eodag
Pull request #1001 (2024-01-19 19:03:41)

fix(server): raise error if search fallback fails
- raises an error in server mode if search fallback fails
- `raise_errors` enabled when search by id

github-actions[bot]: ## Test Results
    4 files  ±0      4 suites  ±0   4m 17s :stopwatch: +3s
  453 tests +1    450 :white_check_mark: +1   3 :zzz: ±0  0 :x: ±0 
1 812 runs  +4  1 734 :white_check_mark: +4  78 :zzz: ±0  0 :x: ±0 

Results for commit a0f62580. ± Comparison against base commit dda681ae.

[test-results]:data:application/gzip;base64,H4sIAFzIqmUC/02MTQ6DIBgFr2JYdwEfv/YyDUVISFUahJXp3UupiMuZ9zI7cn62G7oP7DagLft0wpSjTj6sBYHLIsqU6shpo8eWjakKd/Xy76L6x2k/F9EfNsYQDxPz+msSReCg1iSSslP9m1I1cWlWviZNWBafCiCNnQCuMAXLAXM5joo8DTMWYwYSjFBCApko+nwBnn/WsAgBAAA=

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

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

_Minimum allowed coverage is `70%`_

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

Patch:
diff --git a/eodag/api/core.py b/eodag/api/core.py
index a7a08e78..c7bb0c92 100644
--- a/eodag/api/core.py
+++ b/eodag/api/core.py
@@ -22,7 +22,7 @@ import os
 import re
 import shutil
 from operator import itemgetter
-from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Tuple, Union
 
 import geojson
 import pkg_resources
@@ -193,6 +193,7 @@ class EODataAccessGateway:
                         os.path.join(self.conf_dir, "shp"),
                     )
         self.set_locations_conf(locations_conf_path)
+        self.search_errors: Set = set()
 
     def get_version(self) -> str:
         """Get eodag package version"""
@@ -1004,6 +1005,7 @@ class EODataAccessGateway:
             search_kwargs.update(
                 page=1,
                 items_per_page=2,
+                raise_errors=raise_errors,
             )
             return self._search_by_id(
                 search_kwargs.pop("id"), provider=provider, **search_kwargs
@@ -1015,6 +1017,8 @@ class EODataAccessGateway:
             page=page,
             items_per_page=items_per_page,
         )
+
+        self.search_errors = set()
         # Loop over available providers and return the first non-empty results
         for i, search_plugin in enumerate(search_plugins):
             search_plugin.clear()
@@ -1765,7 +1769,7 @@ class EODataAccessGateway:
                         "available in the searched collection (e.g. SENTINEL2) instead of "
                         "the total number of products matching the search criteria"
                     )
-        except Exception:
+        except Exception as e:
             log_msg = f"No result from provider '{search_plugin.provider}' due to an error during search."
             if not raise_errors:
                 log_msg += " Raise verbosity of log messages for details"
@@ -1779,6 +1783,7 @@ class EODataAccessGateway:
                     "Error while searching on provider %s (ignored):",
                     search_plugin.provider,
                 )
+                self.search_errors.add((search_plugin.provider, e))
         return SearchResult(results), total_results
 
     def crunch(self, results: SearchResult, **kwargs: Any) -> SearchResult:
diff --git a/eodag/rest/utils.py b/eodag/rest/utils.py
index 1bf50dc8..e4a7051c 100644
--- a/eodag/rest/utils.py
+++ b/eodag/rest/utils.py
@@ -67,6 +67,7 @@ from eodag.utils.exceptions import (
     MisconfiguredError,
     NoMatchingProductType,
     NotAvailableError,
+    RequestError,
     UnsupportedProductType,
     ValidationError,
 )
@@ -503,6 +504,14 @@ def search_products(
 
         products, total = eodag_api.search(**criterias)
 
+        if not products and eodag_api.search_errors:
+            search_error = RequestError(
+                "No result could be obtained from any available provider and following "
+                "error(s) appeared while searching:"
+            )
+            search_error.history = eodag_api.search_errors
+            raise search_error
+
         products = filter_products(products, arguments, **criterias)
 
         response: Union[Dict[str, Any], SearchResult]
diff --git a/eodag/utils/exceptions.py b/eodag/utils/exceptions.py
index 48d31ace..dcfd788f 100644
--- a/eodag/utils/exceptions.py
+++ b/eodag/utils/exceptions.py
@@ -15,6 +15,12 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Set, Tuple
 
 
 class ValidationError(Exception):
@@ -70,9 +76,17 @@ class NotAvailableError(Exception):
 
 
 class RequestError(Exception):
-    """An error indicating that a HTTP request has failed. Usually eodag functions
+    """An error indicating that a request has failed. Usually eodag functions
     and methods should catch and skip this"""
 
+    history: Set[Tuple[Exception, str]] = set()
+
+    def __str__(self):
+        repr = super().__str__()
+        for err_tuple in self.history:
+            repr += f"\n- {str(err_tuple)}"
+        return repr
+
 
 class NoMatchingProductType(Exception):
     """An error indicating that eodag was unable to derive a product type from a set


Test patch:
diff --git a/tests/units/test_stac_utils.py b/tests/units/test_stac_utils.py
index 7e03f4ab..b211716d 100644
--- a/tests/units/test_stac_utils.py
+++ b/tests/units/test_stac_utils.py
@@ -26,7 +26,7 @@ import pytest
 
 from eodag.utils.exceptions import ValidationError
 from tests import TEST_RESOURCES_PATH, mock
-from tests.context import SearchResult
+from tests.context import RequestError, SearchResult
 
 
 class TestStacUtils(unittest.TestCase):
@@ -418,6 +418,24 @@ class TestStacUtils(unittest.TestCase):
         )
         self.assertEqual(call_kwargs["geometry"].bounds, (0.25, 43.2, 2.8, 43.9))
 
+    @mock.patch(
+        "eodag.plugins.search.qssearch.QueryStringSearch.do_search",
+        autospec=True,
+        side_effect=RequestError,
+    )
+    @mock.patch(
+        "eodag.plugins.search.qssearch.QueryStringSearch.count_hits",
+        autospec=True,
+        side_effect=RequestError,
+    )
+    def test_search_products_fail(self, mock_count_hits, mock_do_search):
+        """search_products fail must return an error"""
+        with self.assertRaisesRegex(
+            RequestError,
+            r"No result could be obtained from any available provider",
+        ):
+            self.rest_utils.search_products("S2_MSI_L1C", {})
+
     @mock.patch(
         "eodag.plugins.search.qssearch.PostJsonSearch._request",
         autospec=True,