Repository: CS-SI/eodag

Issue:
Support geodes provider

Support geodes provider : https://geodes.cnes.fr/migration-de-peps-vers-geodes/ 


Deprecate peps provider
Add geodes provider

Discussion:
@sbrunato: Hello @savmickael , the support of this new provider is planned, we'll keep you informed when it is done.

Related to https://github.com/CS-SI/eodag/discussions/1184@aaiche-meoss: Hello @sbrunato, PEPS wouldn't be available from October. 
Unless I'm mistaken, I see that you haven't yet updated the eodag tool. Could we know when it woud be updated? Thank you. @sbrunato: Hello @aaiche-meoss , work is in progress and `geodes` provider should be available when `peps` service stops

Solution patch:
diff --git a/docs/getting_started_guide/providers.rst b/docs/getting_started_guide/providers.rst
index 55b28631a..d3bc844e7 100644
--- a/docs/getting_started_guide/providers.rst
+++ b/docs/getting_started_guide/providers.rst
@@ -23,6 +23,7 @@ Products from the following providers are made avaiable through ``eodag``:
   Storage download
 * `ecmwf <https://www.ecmwf.int/>`_: European Centre for Medium-Range Weather Forecasts
 * `eumetsat_ds <https://data.eumetsat.int>`_: EUMETSAT Data Store (European Organisation for the Exploitation of Meteorological Satellites)
+* `geodes <https://geodes.cnes.fr>`_: French National Space Agency (CNES) Earth Observation portal
 * `hydroweb_next <https://hydroweb.next.theia-land.fr>`_: hydroweb.next thematic hub for hydrology data access
 * `meteoblue <https://content.meteoblue.com/en/business-solutions/weather-apis/dataset-api>`_: Meteoblue forecast
 * `onda <https://www.onda-dias.eu/cms/>`_: Serco DIAS
diff --git a/docs/getting_started_guide/register.rst b/docs/getting_started_guide/register.rst
index cf7a38101..03317eac6 100644
--- a/docs/getting_started_guide/register.rst
+++ b/docs/getting_started_guide/register.rst
@@ -145,6 +145,12 @@ Create an account `here <https://eoportal.eumetsat.int/userMgmt/register.faces>`
 Then use the consumer key as `username` and the consumer secret as `password` from `here
 <https://api.eumetsat.int/api-key/>`__ in eodag credentials.
 
+``geodes``
+^^^^^^^^^^
+Go to `https://geodes-portal.cnes.fr <https://https://geodes-portal.cnes.fr>`_, then login or create an account by
+clicking on ``Log in`` in the top-right corner. Once logged-in, create an API key in the user settings page, and used it
+as *apikey* in EODAG provider auth credentials.
+
 ``hydroweb_next``
 ^^^^^^^^^^^^^^^^^
 Go to `https://hydroweb.next.theia-land.fr <https://hydroweb.next.theia-land.fr>`_, then login or create an account by
diff --git a/docs/index.rst b/docs/index.rst
index 64a6969ce..68e9ecc5b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -27,31 +27,32 @@ types (Sentinel 1, Sentinel 2, Sentinel 3, Landsat, etc.) that can be searched a
 
     [Growing list of] supported providers
       `astraea_eod <https://eod-catalog-svc-prod.astraea.earth/api.html>`_,
-      `aws_eos <https://eos.com/>`_,
+      `aws_eos <https://eos.com>`_,
       `cop_ads <https://ads.atmosphere.copernicus.eu>`_,
       `cop_cds <https://cds.climate.copernicus.eu>`_,
-      `cop_dataspace <https://dataspace.copernicus.eu/>`_,
+      `cop_dataspace <https://dataspace.copernicus.eu>`_,
       `cop_marine <https://marine.copernicus.eu>`_,
-      `creodias <https://creodias.eu/>`_,
-      `creodias_s3 <https://creodias.eu/>`_,
+      `creodias <https://creodias.eu>`_,
+      `creodias_s3 <https://creodias.eu>`_,
       `dedl <https://hda.data.destination-earth.eu/ui>`_,
       `dedt_lumi <https://polytope.lumi.apps.dte.destination-earth.eu/openapi>`_,
       `earth_search <https://www.element84.com/earth-search/>`_,
       `earth_search_gcs <https://cloud.google.com/storage/docs/public-datasets>`_,
-      `ecmwf <https://www.ecmwf.int/>`_,
+      `ecmwf <https://www.ecmwf.int>`_,
+      `geodes <https://data.eumetsat.int>`_,
       `eumetsat_ds <https://data.eumetsat.int>`_,
       `hydroweb_next <https://hydroweb.next.theia-land.fr>`_,
       `meteoblue <https://content.meteoblue.com/en/business-solutions/weather-apis/dataset-api>`_,
-      `onda <https://www.onda-dias.eu/cms/>`_,
+      `onda <https://www.onda-dias.eu/cms>`_,
       `peps <https://peps.cnes.fr/rocket/#/home>`_,
-      `planetary_computer <https://planetarycomputer.microsoft.com/>`_,
+      `planetary_computer <https://planetarycomputer.microsoft.com>`_,
       `sara <https://copernicus.nci.org.au>`_,
-      `theia <https://theia.cnes.fr/atdistrib/rocket/>`_,
-      `usgs <https://earthexplorer.usgs.gov/>`_,
-      `usgs_satapi_aws <https://landsatlook.usgs.gov/sat-api/>`_,
+      `theia <https://theia.cnes.fr/atdistrib/rocket>`_,
+      `usgs <https://earthexplorer.usgs.gov>`_,
+      `usgs_satapi_aws <https://geodes.cnes.fr>`_,
       `wekeo_cmems <https://www.wekeo.eu>`_,
-      `wekeo_ecmwf <https://www.wekeo.eu/>`_,
-      `wekeo_main <https://www.wekeo.eu/>`_
+      `wekeo_ecmwf <https://www.wekeo.eu>`_,
+      `wekeo_main <https://www.wekeo.eu>`_
 
 EODAG has the following primary features:
 
diff --git a/docs/notebooks/api_user_guide/1_overview.ipynb b/docs/notebooks/api_user_guide/1_overview.ipynb
index b1c4eca44..def0b7b9f 100644
--- a/docs/notebooks/api_user_guide/1_overview.ipynb
+++ b/docs/notebooks/api_user_guide/1_overview.ipynb
@@ -120,8 +120,8 @@
      "name": "stderr",
      "output_type": "stream",
      "text": [
-      "2023-10-17 16:51:54,405 eodag.config                     [INFO    ] Loading user configuration from: /home/anesson/.config/eodag/eodag.yml\n",
-      "2023-10-17 16:51:54,592 eodag.core                       [INFO    ] Locations configuration loaded from /home/anesson/.config/eodag/locations.yml\n"
+      "2024-10-22 14:50:41,809 eodag.config                     [INFO    ] Loading user configuration from: /home/sylvain/.config/eodag/eodag.yml\n",
+      "2024-10-22 14:50:41,859 eodag.core                       [INFO    ] Locations configuration loaded from /home/sylvain/.config/eodag/locations.yml\n"
      ]
     }
    ],
@@ -145,7 +145,7 @@
     {
      "data": {
       "text/plain": [
-       "['S1_SAR_GRD', 'S1_SAR_OCN', 'S1_SAR_SLC', 'S2_MSI_L1C', 'S2_MSI_L2A']"
+       "['S1_SAR_GRD', 'S1_SAR_OCN', 'S1_SAR_SLC', 'S2_MSI_L1C']"
       ]
      },
      "execution_count": 5,
@@ -172,17 +172,20 @@
     {
      "data": {
       "text/plain": [
-       "['astraea_eod',\n",
+       "['peps',\n",
+       " 'astraea_eod',\n",
        " 'aws_eos',\n",
        " 'cop_dataspace',\n",
        " 'creodias',\n",
+       " 'creodias_s3',\n",
+       " 'dedl',\n",
        " 'earth_search',\n",
        " 'earth_search_gcs',\n",
+       " 'geodes',\n",
        " 'onda',\n",
-       " 'peps',\n",
        " 'sara',\n",
        " 'usgs',\n",
-       " 'wekeo']"
+       " 'wekeo_main']"
       ]
      },
      "execution_count": 6,
diff --git a/docs/notebooks/api_user_guide/2_providers_products_available.ipynb b/docs/notebooks/api_user_guide/2_providers_products_available.ipynb
index 64896afc3..696ff46de 100644
--- a/docs/notebooks/api_user_guide/2_providers_products_available.ipynb
+++ b/docs/notebooks/api_user_guide/2_providers_products_available.ipynb
@@ -18,17 +18,8 @@
      "name": "stderr",
      "output_type": "stream",
      "text": [
-      "2024-08-09 09:14:18,559 eodag.config                     [INFO    ] Loading user configuration from: /home/sylvain/workspace/eodag/eodag/resources/user_conf_template.yml\n",
-      "2024-08-09 09:14:18,611 eodag.core                       [INFO    ] usgs: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,613 eodag.core                       [INFO    ] aws_eos: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,614 eodag.core                       [INFO    ] meteoblue: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,615 eodag.core                       [INFO    ] hydroweb_next: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,618 eodag.core                       [INFO    ] wekeo_main: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,619 eodag.core                       [INFO    ] wekeo_ecmwf: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,621 eodag.core                       [INFO    ] wekeo_cmems: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,622 eodag.core                       [INFO    ] creodias_s3: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,624 eodag.core                       [INFO    ] dedl: provider needing auth for search has been pruned because no credentials could be found\n",
-      "2024-08-09 09:14:18,658 eodag.core                       [INFO    ] Locations configuration loaded from /home/sylvain/.config/eodag/locations.yml\n"
+      "2024-10-22 14:59:44,334 eodag.config                     [INFO    ] Loading user configuration from: /home/sylvain/.config/eodag/eodag.yml\n",
+      "2024-10-22 14:59:44,377 eodag.core                       [INFO    ] Locations configuration loaded from /home/sylvain/.config/eodag/locations.yml\n"
      ]
     }
    ],
@@ -63,22 +54,32 @@
       "text/plain": [
        "['peps',\n",
        " 'astraea_eod',\n",
+       " 'aws_eos',\n",
        " 'cop_ads',\n",
        " 'cop_cds',\n",
        " 'cop_dataspace',\n",
        " 'cop_marine',\n",
        " 'creodias',\n",
+       " 'creodias_s3',\n",
+       " 'dedl',\n",
        " 'dedt_lumi',\n",
        " 'earth_search',\n",
        " 'earth_search_cog',\n",
        " 'earth_search_gcs',\n",
        " 'ecmwf',\n",
        " 'eumetsat_ds',\n",
+       " 'geodes',\n",
+       " 'hydroweb_next',\n",
+       " 'meteoblue',\n",
        " 'onda',\n",
        " 'planetary_computer',\n",
        " 'sara',\n",
        " 'theia',\n",
-       " 'usgs_satapi_aws']"
+       " 'usgs',\n",
+       " 'usgs_satapi_aws',\n",
+       " 'wekeo_cmems',\n",
+       " 'wekeo_ecmwf',\n",
+       " 'wekeo_main']"
       ]
      },
      "execution_count": 2,
@@ -100,7 +101,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "eodag has 18 providers already configured.\n"
+      "eodag has 28 providers already configured.\n"
      ]
     }
    ],
@@ -125,12 +126,18 @@
       "text/plain": [
        "['peps',\n",
        " 'astraea_eod',\n",
+       " 'aws_eos',\n",
        " 'cop_dataspace',\n",
        " 'creodias',\n",
+       " 'creodias_s3',\n",
+       " 'dedl',\n",
        " 'earth_search',\n",
        " 'earth_search_gcs',\n",
+       " 'geodes',\n",
        " 'onda',\n",
-       " 'sara']"
+       " 'sara',\n",
+       " 'usgs',\n",
+       " 'wekeo_main']"
       ]
      },
      "execution_count": 4,
@@ -178,7 +185,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "EODAG has 216 product types stored in its internal catalog.\n"
+      "EODAG has 248 product types stored in its internal catalog.\n"
      ]
     }
    ],
diff --git a/docs/notebooks/api_user_guide/3_configuration.ipynb b/docs/notebooks/api_user_guide/3_configuration.ipynb
index 9b64e66be..34b4a300b 100644
--- a/docs/notebooks/api_user_guide/3_configuration.ipynb
+++ b/docs/notebooks/api_user_guide/3_configuration.ipynb
@@ -46,25 +46,26 @@
        " 'cop_dataspace',\n",
        " 'cop_marine',\n",
        " 'creodias',\n",
+       " 'creodias_s3',\n",
+       " 'dedl',\n",
        " 'dedt_lumi',\n",
        " 'earth_search',\n",
        " 'earth_search_cog',\n",
        " 'earth_search_gcs',\n",
        " 'ecmwf',\n",
        " 'eumetsat_ds',\n",
+       " 'geodes',\n",
        " 'hydroweb_next',\n",
        " 'meteoblue',\n",
        " 'onda',\n",
        " 'planetary_computer',\n",
        " 'sara',\n",
        " 'theia',\n",
+       " 'usgs',\n",
        " 'usgs_satapi_aws',\n",
        " 'wekeo_cmems',\n",
        " 'wekeo_ecmwf',\n",
-       " 'wekeo_main',\n",
-       " 'creodias_s3',\n",
-       " 'dedl',\n",
-       " 'usgs']"
+       " 'wekeo_main']"
       ]
      },
      "execution_count": 2,
@@ -279,7 +280,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.10"
+   "version": "3.10.12"
   },
   "nbsphinx": {
    "execute": "always"
diff --git a/docs/plugins.rst b/docs/plugins.rst
index 52abea5e2..d7aceaf49 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -60,6 +60,8 @@ The providers are implemented with a triplet of *Search/Authentication/Download*
 +------------------------+------------------------------------+---------------------------------+------------------+
 | ``eumetsat_ds``        | |QueryStringSearch|                | |TokenAuth|                     | |HTTPDownload|   |
 +------------------------+------------------------------------+---------------------------------+------------------+
+| ``geodes``             | |StacSearch|                       | |HTTPHeaderAuth|                | |HTTPDownload|   |
++------------------------+------------------------------------+---------------------------------+------------------+
 | ``hydroweb_next``      | |StacSearch|                       | |HTTPHeaderAuth|                | |HTTPDownload|   |
 +------------------------+------------------------------------+---------------------------------+------------------+
 | ``meteoblue``          | |BuildPostSearchResult|            | |HttpQueryStringAuth|           | |HTTPDownload|   |
diff --git a/docs/stac.rst b/docs/stac.rst
index 35517f32e..f840b015d 100644
--- a/docs/stac.rst
+++ b/docs/stac.rst
@@ -11,9 +11,9 @@ STAC client
 -----------
 
 STAC API providers can be configured to be used for `search` and `download` using EODAG.
-Some providers (*astraea_eod*, *earth_search*, *usgs_satapi_aws*) are already implemented
-and new providers can be dynamically added by the user. Static catalogs can also be
-fetched by EODAG.
+Some providers (*astraea_eod*, *dedl*, *earth_search*, *geodes*, *hydroweb_next*, *planetary_computer*,
+*usgs_satapi_aws*) are already implemented and new providers can be dynamically added by the user.
+Static catalogs can also be fetched by EODAG.
 
 STAC server
 -----------
diff --git a/eodag/api/core.py b/eodag/api/core.py
index fa4b1505a..c9918fb35 100644
--- a/eodag/api/core.py
+++ b/eodag/api/core.py
@@ -1561,7 +1561,7 @@ def _search_by_id(
             try:
                 product_type = self.get_product_type_from_alias(product_type)
             except NoMatchingProductType:
-                logger.warning("product type %s not found", product_type)
+                logger.debug("product type %s not found", product_type)
         get_search_plugins_kwargs = dict(provider=provider, product_type=product_type)
         search_plugins = self._plugins_manager.get_search_plugins(
             **get_search_plugins_kwargs
@@ -1929,7 +1929,7 @@ def _do_search(
                             eo_product.product_type
                         )
                 except NoMatchingProductType:
-                    logger.warning("product type %s not found", eo_product.product_type)
+                    logger.debug("product type %s not found", eo_product.product_type)
 
                 if eo_product.search_intersection is not None:
                     download_plugin = self._plugins_manager.get_download_plugin(
diff --git a/eodag/config.py b/eodag/config.py
index f3fd1c820..b0a8b6627 100644
--- a/eodag/config.py
+++ b/eodag/config.py
@@ -268,6 +268,14 @@ class DiscoverProductTypes(TypedDict, total=False):
 
         #: URL from which the product types can be fetched
         fetch_url: Optional[str]
+        #: HTTP method used to fetch product types
+        fetch_method: str
+        #: Request body to fetch product types using POST method
+        fetch_body: Dict[str, Any]
+        #: The f-string template for pagination requests.
+        next_page_url_tpl: str
+        #: Index of the starting page for pagination requests.
+        start_page: int
         #: Type of the provider result
         result_type: str
         #: JsonPath to the list of product types
diff --git a/eodag/plugins/download/http.py b/eodag/plugins/download/http.py
index 3337db495..72b52cadd 100644
--- a/eodag/plugins/download/http.py
+++ b/eodag/plugins/download/http.py
@@ -924,6 +924,8 @@ def _stream_download(
             logger.info("Progress bar unavailable, please call product.download()")
             progress_callback = ProgressCallback(disable=True)
 
+        ssl_verify = getattr(self.config, "ssl_verify", True)
+
         ordered_message = ""
         if (
             "orderLink" in product.properties
@@ -974,6 +976,7 @@ def _stream_download(
             params=params,
             headers=USER_AGENT,
             timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
+            verify=ssl_verify,
             **req_kwargs,
         ) as self.stream:
             try:
diff --git a/eodag/plugins/search/qssearch.py b/eodag/plugins/search/qssearch.py
index a3b93f869..15f4b4042 100644
--- a/eodag/plugins/search/qssearch.py
+++ b/eodag/plugins/search/qssearch.py
@@ -437,15 +437,70 @@ def clear(self) -> None:
     def discover_product_types(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
         """Fetch product types list from provider using `discover_product_types` conf
 
+        :returns: configuration dict containing fetched product types information
+        """
+        unpaginated_fetch_url = self.config.discover_product_types.get("fetch_url")
+        if not unpaginated_fetch_url:
+            return None
+
+        # product types pagination
+        next_page_url_tpl = self.config.discover_product_types.get("next_page_url_tpl")
+        page = self.config.discover_product_types.get("start_page", 1)
+
+        if not next_page_url_tpl:
+            # no pagination
+            return self.discover_product_types_per_page(**kwargs)
+
+        conf_update_dict: Dict[str, Any] = {
+            "providers_config": {},
+            "product_types_config": {},
+        }
+
+        while True:
+            fetch_url = next_page_url_tpl.format(url=unpaginated_fetch_url, page=page)
+
+            conf_update_dict_per_page = self.discover_product_types_per_page(
+                fetch_url=fetch_url, **kwargs
+            )
+
+            if (
+                not conf_update_dict_per_page
+                or not conf_update_dict_per_page.get("providers_config")
+                or conf_update_dict_per_page.items() <= conf_update_dict.items()
+            ):
+                # conf_update_dict_per_page is empty or a subset on existing conf
+                break
+            else:
+                conf_update_dict["providers_config"].update(
+                    conf_update_dict_per_page["providers_config"]
+                )
+                conf_update_dict["product_types_config"].update(
+                    conf_update_dict_per_page["product_types_config"]
+                )
+
+            page += 1
+
+        return conf_update_dict
+
+    def discover_product_types_per_page(
+        self, **kwargs: Any
+    ) -> Optional[Dict[str, Any]]:
+        """Fetch product types list from provider using `discover_product_types` conf
+        using paginated ``kwargs["fetch_url"]``
+
         :returns: configuration dict containing fetched product types information
         """
         try:
             prep = PreparedSearch()
 
-            fetch_url = self.config.discover_product_types.get("fetch_url")
+            # url from discover_product_types() or conf
+            fetch_url: Optional[str] = kwargs.get("fetch_url")
             if fetch_url is None:
-                return None
-            prep.url = fetch_url.format(**self.config.__dict__)
+                if fetch_url := self.config.discover_product_types.get("fetch_url"):
+                    fetch_url = fetch_url.format(**self.config.__dict__)
+                else:
+                    return None
+            prep.url = fetch_url
 
             # get auth if available
             if "auth" in kwargs:
@@ -475,7 +530,14 @@ def discover_product_types(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
                 "Skipping error while fetching product types for " "{} {} instance:"
             ).format(self.provider, self.__class__.__name__)
 
-            response = QueryStringSearch._request(self, prep)
+            # Query using appropriate method
+            fetch_method = self.config.discover_product_types.get("fetch_method", "GET")
+            fetch_body = self.config.discover_product_types.get("fetch_body", {})
+            if fetch_method == "POST" and isinstance(self, PostJsonSearch):
+                prep.query_params = fetch_body
+                response = self._request(prep)
+            else:
+                response = QueryStringSearch._request(self, prep)
         except (RequestError, KeyError, AttributeError):
             return None
         else:
diff --git a/eodag/resources/providers.yml b/eodag/resources/providers.yml
index 19ca0c3e1..f939c8699 100644
--- a/eodag/resources/providers.yml
+++ b/eodag/resources/providers.yml
@@ -7425,3 +7425,130 @@
     matching_url: s3://
     matching_conf:
       s3_endpoint: https://s3.waw3-1.cloudferro.com
+
+---
+!provider # MARK: geodes
+  name: geodes
+  priority: 0
+  roles:
+    - host
+  description: French National Space Agency (CNES) Earth Observation portal
+  url: https://geodes.cnes.fr
+  search: !plugin
+    type: StacSearch
+    api_endpoint: https://geodes-portal.cnes.fr/api/stac/search
+    need_auth: false
+    ssl_verify: false
+    discover_queryables:
+      fetch_url: null
+      product_type_fetch_url: null
+    discover_product_types:
+      fetch_url: https://geodes-portal.cnes.fr/api/stac/collections
+      fetch_method: POST
+      fetch_body:
+        limit: 10000
+    pagination:
+      total_items_nb_key_path: '$.context.matched'
+      # 1000 is ok and 2000 fails
+      max_items_per_page: 1000
+    sort:
+      sort_by_tpl: '{{"sortBy": [ {{"field": "{sort_param}", "direction": "{sort_order}" }} ] }}'
+      sort_by_default:
+        - !!python/tuple [startTimeFromAscendingNode, ASC]
+      sort_param_mapping:
+        id: spaceborne:s2TakeId
+        startTimeFromAscendingNode: temporal:startDate
+        completionTimeFromAscendingNode: temporal:endDate
+        platformSerialIdentifier: spaceborne:satellitePlatform
+        cloudCover: spaceborne:cloudCover
+    metadata_mapping:
+      uid: '$.id'
+      # OpenSearch Parameters for Collection Search (Table 3)
+      productType:
+        - '{{"query":{{"dataType":{{"eq":"{productType}"}}}}}}'
+        - '$.properties."dataType"'
+      processingLevel:
+        - '{{"query":{{"spaceborne:productLevel":{{"eq":"{processingLevel}"}}}}}}'
+        - '$.properties."spaceborne:productLevel"'
+      platformSerialIdentifier:
+        - '{{"query":{{"spaceborne:satellitePlatform":{{"eq":"{platformSerialIdentifier}"}}}}}}'
+        - '$.properties."spaceborne:satellitePlatform"'
+      instrument:
+        - '{{"query":{{"spaceborne:satelliteSensor":{{"eq":"{instrument}"}}}}}}'
+        - '$.properties."spaceborne:satelliteSensor"'
+      # INSPIRE obligated OpenSearch Parameters for Collection Search (Table 4)
+      title: '{$.properties."accessService:endpointURL"#replace_str(r"^.*\/(\w+)\.zip$",r"\1")}'
+      keyword: '$.properties."spaceborne:keywords"'
+      # OpenSearch Parameters for Product Search (Table 5)
+      orbitNumber:
+        - '{{"query":{{"spaceborne:absoluteOrbitID":{{"eq":"{orbitNumber}"}}}}}}'
+        - '$.properties."spaceborne:absoluteOrbitID"'
+      orbitDirection:
+        - '{{"query":{{"spaceborne:orbitDirection":{{"eq":"{orbitDirection}"}}}}}}'
+        - '$.properties."spaceborne:orbitDirection"'
+      swathIdentifier:
+        - '{{"query":{{"spaceborne:swath":{{"eq":"{swathIdentifier}"}}}}}}'
+        - '$.properties."spaceborne:swath"'
+      cloudCover:
+        - '{{"query":{{"spaceborne:cloudCover":{{"lte":"{cloudCover}"}}}}}}'
+        - '$.properties."spaceborne:cloudCover"'
+      sensorMode:
+        - '{{"query":{{"spaceborne:sensorMode":{{"eq":"{sensorMode}"}}}}}}'
+        - '$.properties."spaceborne:sensorMode"'
+      # OpenSearch Parameters for Acquistion Parameters Search (Table 6)
+      startTimeFromAscendingNode:
+        - '{{"query":{{"temporal:endDate":{{"lte":"{startTimeFromAscendingNode#to_iso_utc_datetime}"}}}}}}'
+        - '$.properties."temporal:startDate"'
+      completionTimeFromAscendingNode:
+        - '{{"query":{{"temporal:startDate":{{"gte":"{completionTimeFromAscendingNode#to_iso_utc_datetime}"}}}}}}'
+        - '$.properties."temporal:endDate"'
+      polarizationChannels:
+        - '{{"query":{{"spaceborne:polarization":{{"eq":"{polarizationChannels}"}}}}}}'
+        - '$.properties."spaceborne:polarization"'
+      # Custom parameters (not defined in the base document referenced above)
+      id:
+        - '{{"query":{{"accessService:endpointURL":{{"contains":"{id}"}}}}}}'
+        - '{$.properties."accessService:endpointURL"#replace_str(r"^.*\/(\w+)\.zip$",r"\1")}'
+      geometry:
+        - '{{"intersects":{geometry#to_geojson}}}'
+        - '($.geometry.`str()`.`sub(/^None$/, POLYGON((180 -90, 180 90, -180 90, -180 -90, 180 -90)))`)|($.geometry[*])'
+      providerProductType:
+        - '{{"query":{{"spaceborne:productType":{{"eq":"{providerProductType}"}}}}}}'
+        - '$.null'
+      downloadLink: '$.assets[?(@.roles[0] == "data")].href'
+      quicklook: '$.assets[?(@.roles[0] == "overview")].href'
+      thumbnail: '$.assets[?(@.roles[0] == "overview")].href'
+      # storageStatus set to ONLINE for consistency between providers
+      storageStatus: '{$.null#replace_str("Not Available","ONLINE")}'
+  products:
+    S1_SAR_OCN:
+      providerProductType: OCN
+      productType: PEPS_S1_L2
+      metadata_mapping:
+        cloudCover: '$.null'
+    S1_SAR_GRD:
+      providerProductType: GRD
+      productType: PEPS_S1_L1
+      metadata_mapping:
+        cloudCover: '$.null'
+    S1_SAR_SLC:
+      providerProductType: SLC
+      productType: PEPS_S1_L1
+      metadata_mapping:
+        cloudCover: '$.null'
+    S2_MSI_L1C:
+      productType: PEPS_S2_L1C
+    S2_MSI_L2A_MAJA:
+      productType: MUSCATE_SENTINEL2_SENTINEL2_L2A
+    GENERIC_PRODUCT_TYPE:
+      productType: '{productType}'
+  download: !plugin
+    type: HTTPDownload
+    ssl_verify: false
+    ignore_assets: true
+    archive_depth: 2
+  auth: !plugin
+    type: HTTPHeaderAuth
+    matching_url: https://geodes-portal.cnes.fr
+    headers:
+      X-API-Key: "{apikey}"
diff --git a/eodag/resources/user_conf_template.yml b/eodag/resources/user_conf_template.yml
index 3ae98246c..e9729747c 100644
--- a/eodag/resources/user_conf_template.yml
+++ b/eodag/resources/user_conf_template.yml
@@ -15,42 +15,19 @@
 # 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.
-peps:
-    priority: # Lower value means lower priority (Default: 1)
-    search:  # Search parameters configuration
-    download:
-        extract:  # whether to extract the downloaded products, only applies to archived products (true or false, Default: true).
-        output_dir: # where to store downloaded products, as an absolute file path (Default: local temporary directory)
-        dl_url_params:  # additional parameters to pass over to the download url as an url parameter
-        delete_archive: # whether to delete the downloaded archives (true or false, Default: true).
-    auth:
-        credentials:
-            username:
-            password:
-theia:
+astraea_eod:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
-    download:
-        extract:
-        output_dir:
-        dl_url_params:
+    search: # Search parameters configuration
     auth:
         credentials:
-            ident:
-            pass:
-usgs:
-    priority: # Lower value means lower priority (Default: 0)
-    api:
-        extract:
+            aws_access_key_id:
+            aws_secret_access_key:
+            aws_profile:
+    download:
         output_dir:
-        dl_url_params:
-        product_location_scheme:
-        credentials:
-            username:
-            password:
 aws_eos:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     search_auth:
         credentials:
             api_key:
@@ -61,9 +38,27 @@ aws_eos:
             aws_profile:
     download:
         output_dir:
-creodias:
+cop_ads:
+    priority: # Lower value means lower priority (Default: 0)
+    search: # Search parameters configuration
+    download:
+        extract:
+        output_dir:
+    auth:
+        credentials:
+            apikey:
+cop_cds:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
+    download:
+        extract:
+        output_dir:
+    auth:
+        credentials:
+            apikey:
+cop_dataspace:
+    priority: # Lower value means lower priority (Default: 0)
+    search: # Search parameters configuration
     download:
         extract:
         output_dir:
@@ -71,10 +66,15 @@ creodias:
         credentials:
             username:
             password:
-            totp: # set totp dynamically, see https://eodag.readthedocs.io/en/latest/getting_started_guide/configure.html#authenticate-using-an-otp-one-time-password-two-factor-authentication
-onda:
+cop_marine:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
+    download:
+        extract:
+        output_dir:
+creodias:
+    priority: # Lower value means lower priority (Default: 0)
+    search: # Search parameters configuration
     download:
         extract:
         output_dir:
@@ -82,29 +82,37 @@ onda:
         credentials:
             username:
             password:
-astraea_eod:
+            totp: # set totp dynamically, see https://eodag.readthedocs.io/en/latest/getting_started_guide/configure.html#authenticate-using-an-otp-one-time-password-two-factor-authentication
+creodias_s3:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
+    download:
+        output_dir:
     auth:
         credentials:
             aws_access_key_id:
             aws_secret_access_key:
-            aws_profile:
+dedl:
+    priority: # Lower value means lower priority (Default: 0)
+    search: # Search parameters configuration
     download:
         output_dir:
-usgs_satapi_aws:
-    priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
     auth:
         credentials:
-            aws_access_key_id:
-            aws_secret_access_key:
-            aws_profile:
+            username:
+            password:
+dedt_lumi:
+    priority: # Lower value means lower priority (Default: 0)
+    search:
     download:
         output_dir:
+    auth:
+        credentials:
+            username:
+            password:
 earth_search:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     auth:
         credentials:
             aws_access_key_id:
@@ -114,12 +122,12 @@ earth_search:
         output_dir:
 earth_search_cog:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     download:
         output_dir:
 earth_search_gcs:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     auth:
         credentials:
             aws_access_key_id:
@@ -133,46 +141,44 @@ ecmwf:
         credentials:
             username:
             password:
-cop_ads:
+eumetsat_ds:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
-    download:
-        extract:
+    search: # Search parameters configuration
         output_dir:
     auth:
-      credentials:
-        apikey:
-cop_cds:
-    priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+        credentials:
+            username:
+            password:
     download:
-        extract:
         output_dir:
+geodes:
+    priority: # Lower value means lower priority (Default: 0)
+    search: # Search parameters configuration
     auth:
         credentials:
             apikey:
-sara:
-    priority: # Lower value means lower priority (Default: 0)
-    search:  # Search parameters configuration
     download:
         extract:
         output_dir:
-        delete_archive:
+hydroweb_next:
+    priority: # Lower value means lower priority (Default: 0)
+    search: # Search parameters configuration
     auth:
         credentials:
-            username:
-            password:
+            apikey:
+    download:
+        output_dir:
 meteoblue:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     auth:
         credentials:
             apikey:
     download:
         output_dir:
-cop_dataspace:
+onda:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     download:
         extract:
         output_dir:
@@ -180,59 +186,69 @@ cop_dataspace:
         credentials:
             username:
             password:
-planetary_computer:
-    priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+peps:
+    priority: # Lower value means lower priority (Default: 1)
+    search: # Search parameters configuration
+    download:
+        extract: # whether to extract the downloaded products, only applies to archived products (true or false, Default: true).
+        output_dir: # where to store downloaded products, as an absolute file path (Default: local temporary directory)
+        dl_url_params: # additional parameters to pass over to the download url as an url parameter
+        delete_archive: # whether to delete the downloaded archives (true or false, Default: true).
     auth:
         credentials:
-            apikey:
-    download:
-        output_dir:
-hydroweb_next:
+            username:
+            password:
+planetary_computer:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     auth:
         credentials:
             apikey:
     download:
         output_dir:
-wekeo_main:
+sara:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     download:
+        extract:
         output_dir:
+        delete_archive:
     auth:
         credentials:
             username:
             password:
-wekeo_ecmwf:
+theia:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     download:
+        extract:
         output_dir:
+        dl_url_params:
     auth:
         credentials:
-            username:
-            password:
-wekeo_cmems:
+            ident:
+            pass:
+usgs:
     priority: # Lower value means lower priority (Default: 0)
-    search:
-    download:
+    api:
+        extract:
         output_dir:
-    auth:
+        dl_url_params:
+        product_location_scheme:
         credentials:
             username:
             password:
-creodias_s3:
+usgs_satapi_aws:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
-    download:
-        output_dir:
+    search: # Search parameters configuration
     auth:
         credentials:
-          aws_access_key_id:
-          aws_secret_access_key:
-dedt_lumi:
+            aws_access_key_id:
+            aws_secret_access_key:
+            aws_profile:
+    download:
+        output_dir:
+wekeo_cmems:
     priority: # Lower value means lower priority (Default: 0)
     search:
     download:
@@ -241,28 +257,21 @@ dedt_lumi:
         credentials:
             username:
             password:
-dedl:
+wekeo_ecmwf:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     download:
         output_dir:
-    auth:
-      credentials:
-        username:
-        password:
-eumetsat_ds:
-    priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
-        output_dir:
     auth:
         credentials:
             username:
             password:
-    download:
-        output_dir:
-cop_marine:
+wekeo_main:
     priority: # Lower value means lower priority (Default: 0)
-    search:   # Search parameters configuration
+    search: # Search parameters configuration
     download:
-        extract:
         output_dir:
+    auth:
+        credentials:
+            username:
+            password:


Test patch:
diff --git a/tests/integration/test_core_search.py b/tests/integration/test_core_search.py
index 46b838448..69d20246c 100644
--- a/tests/integration/test_core_search.py
+++ b/tests/integration/test_core_search.py
@@ -237,6 +237,7 @@ def test_core_search_fallback_find_nothing(
                 "peps",
                 "cop_dataspace",
                 "creodias",
+                "geodes",
                 "onda",
                 "sara",
                 "wekeo_main",
@@ -272,6 +273,7 @@ def test_core_search_fallback_raise_errors(self, mock_get, mock_request):
                 "peps",
                 "cop_dataspace",
                 "creodias",
+                "geodes",
                 "onda",
                 "sara",
                 "wekeo_main",
@@ -306,6 +308,7 @@ def test_core_search_fallback_find_on_first(self, mock_get, mock_request):
                 "peps",
                 "cop_dataspace",
                 "creodias",
+                "geodes",
                 "onda",
                 "sara",
                 "wekeo_main",
@@ -362,6 +365,7 @@ def test_core_search_fallback_find_on_fourth(
                 "peps",
                 "cop_dataspace",
                 "creodias",
+                "geodes",
                 "onda",
                 "sara",
                 "wekeo_main",
@@ -404,6 +408,7 @@ def test_core_search_fallback_find_on_fourth_empty_results(self, mock_query):
                 "peps",
                 "cop_dataspace",
                 "creodias",
+                "geodes",
                 "onda",
                 "sara",
                 "wekeo_main",
@@ -438,6 +443,7 @@ def test_core_search_fallback_given_provider(self, mock_query):
                 "peps",
                 "cop_dataspace",
                 "creodias",
+                "geodes",
                 "onda",
                 "sara",
                 "wekeo_main",
diff --git a/tests/units/test_core.py b/tests/units/test_core.py
index 282ec5392..0670b1c3a 100644
--- a/tests/units/test_core.py
+++ b/tests/units/test_core.py
@@ -261,6 +261,7 @@ class TestCore(TestCoreBase):
             "creodias_s3",
             "dedl",
             "earth_search",
+            "geodes",
             "onda",
             "peps",
             "planetary_computer",
@@ -272,6 +273,7 @@ class TestCore(TestCoreBase):
             "cop_dataspace",
             "creodias",
             "creodias_s3",
+            "geodes",
             "onda",
             "peps",
             "sara",
@@ -289,6 +291,7 @@ class TestCore(TestCoreBase):
             "creodias",
             "creodias_s3",
             "dedl",
+            "geodes",
             "onda",
             "peps",
             "sara",
@@ -303,6 +306,7 @@ class TestCore(TestCoreBase):
             "dedl",
             "earth_search",
             "earth_search_gcs",
+            "geodes",
             "onda",
             "peps",
             "sara",
@@ -323,7 +327,7 @@ class TestCore(TestCoreBase):
         ],
         "S2_MSI_L2AP": ["wekeo_main"],
         "S2_MSI_L2A_COG": ["earth_search_cog"],
-        "S2_MSI_L2A_MAJA": ["theia"],
+        "S2_MSI_L2A_MAJA": ["geodes", "theia"],
         "S2_MSI_L2B_MAJA_SNOW": ["theia"],
         "S2_MSI_L2B_MAJA_WATER": ["theia"],
         "S2_MSI_L3A_WASP": ["theia"],
@@ -592,6 +596,7 @@ class TestCore(TestCoreBase):
         "earth_search_gcs",
         "ecmwf",
         "eumetsat_ds",
+        "geodes",
         "hydroweb_next",
         "meteoblue",
         "onda",
@@ -1596,6 +1601,16 @@ def test_available_sortables(self):
                 "max_sort_params": 1,
             },
             "cop_marine": None,
+            "geodes": {
+                "max_sort_params": None,
+                "sortables": [
+                    "id",
+                    "startTimeFromAscendingNode",
+                    "completionTimeFromAscendingNode",
+                    "platformSerialIdentifier",
+                    "cloudCover",
+                ],
+            },
             "hydroweb_next": {
                 "sortables": [
                     "id",
diff --git a/tests/units/test_download_plugins.py b/tests/units/test_download_plugins.py
index d51f3454d..1e0050584 100644
--- a/tests/units/test_download_plugins.py
+++ b/tests/units/test_download_plugins.py
@@ -316,6 +316,7 @@ def test_plugins_download_http_zip_file_ok(self, mock_requests_session):
             params={},
             headers=USER_AGENT,
             timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
+            verify=True,
         )
 
     @mock.patch("eodag.plugins.download.http.requests.Session.request", autospec=True)
@@ -401,6 +402,7 @@ def test_plugins_download_http_tar_file_with_zip_extension_ok(
             params={},
             headers=USER_AGENT,
             timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
+            verify=True,
         )
 
     @mock.patch(
diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py
index c40fb27b0..3d18d8b02 100644
--- a/tests/units/test_search_plugins.py
+++ b/tests/units/test_search_plugins.py
@@ -445,6 +445,109 @@ def test_plugins_search_querystringsearch_discover_product_types(
         # restore configuration
         search_plugin.config.discover_product_types["results_entry"] = results_entry
 
+    def test_plugins_search_querystringsearch_discover_product_types_paginated(self):
+        """QueryStringSearch.discover_product_types must handle pagination"""
+        # One of the providers that has a QueryStringSearch Search plugin and discover_product_types configured
+        provider = "astraea_eod"
+        search_plugin = self.get_search_plugin(self.product_type, provider)
+
+        # change onfiguration for this test to filter out some collections
+        discover_product_types_conf = search_plugin.config.discover_product_types
+        search_plugin.config.discover_product_types[
+            "fetch_url"
+        ] = "https://foo.bar/collections"
+        search_plugin.config.discover_product_types[
+            "next_page_url_tpl"
+        ] = "{url}?page={page}"
+        search_plugin.config.discover_product_types["start_page"] = 0
+
+        with responses.RequestsMock(
+            assert_all_requests_are_fired=True
+        ) as mock_requests_post:
+            mock_requests_post.add(
+                responses.GET,
+                "https://foo.bar/collections?page=0",
+                json={
+                    "collections": [
+                        {
+                            "id": "foo_collection",
+                            "title": "The FOO collection",
+                            "billing": "free",
+                        }
+                    ]
+                },
+            )
+            mock_requests_post.add(
+                responses.GET,
+                "https://foo.bar/collections?page=1",
+                json={
+                    "collections": [
+                        {
+                            "id": "bar_collection",
+                            "title": "The BAR non-free collection",
+                            "billing": "non-free",
+                        },
+                    ]
+                },
+            )
+            mock_requests_post.add(
+                responses.GET,
+                "https://foo.bar/collections?page=2",
+                json={"collections": []},
+            )
+            conf_update_dict = search_plugin.discover_product_types()
+            self.assertIn("foo_collection", conf_update_dict["providers_config"])
+            self.assertIn("foo_collection", conf_update_dict["product_types_config"])
+            self.assertIn("bar_collection", conf_update_dict["providers_config"])
+            self.assertIn("bar_collection", conf_update_dict["product_types_config"])
+            self.assertEqual(
+                conf_update_dict["providers_config"]["foo_collection"]["productType"],
+                "foo_collection",
+            )
+            self.assertEqual(
+                conf_update_dict["product_types_config"]["foo_collection"]["title"],
+                "The FOO collection",
+            )
+
+        # restore configuration
+        search_plugin.config.discover_product_types = discover_product_types_conf
+
+    @mock.patch("eodag.plugins.search.qssearch.PostJsonSearch._request", autospec=True)
+    def test_plugins_search_querystringsearch_discover_product_types_post(
+        self, mock__request
+    ):
+        """QueryStringSearch.discover_product_types must be able to query using POST requests"""
+        # One of the providers that has a QueryStringSearch.discover_product_types configured with POST requests
+        provider = "geodes"
+        search_plugin = self.get_search_plugin(self.product_type, provider)
+
+        mock__request.return_value = mock.Mock()
+        mock__request.return_value.json.return_value = {
+            "collections": [
+                {
+                    "id": "foo_collection",
+                    "title": "The FOO collection",
+                },
+                {
+                    "id": "bar_collection",
+                    "title": "The BAR collection",
+                },
+            ]
+        }
+        conf_update_dict = search_plugin.discover_product_types()
+        self.assertIn("foo_collection", conf_update_dict["providers_config"])
+        self.assertIn("foo_collection", conf_update_dict["product_types_config"])
+        self.assertIn("bar_collection", conf_update_dict["providers_config"])
+        self.assertIn("bar_collection", conf_update_dict["product_types_config"])
+        self.assertEqual(
+            conf_update_dict["providers_config"]["foo_collection"]["productType"],
+            "foo_collection",
+        )
+        self.assertEqual(
+            conf_update_dict["product_types_config"]["foo_collection"]["title"],
+            "The FOO collection",
+        )
+
     @mock.patch("eodag.plugins.search.qssearch.requests.get", autospec=True)
     def test_plugins_search_querystringsearch_discover_product_types_with_query_param(
         self, mock__request