Repository: CS-SI/eodag
Pull request #960 (2023-12-12 07:46:51)

Update dedl with develop last commits
Update dedl with develop last commits


Patch:
diff --git a/.gitignore b/.gitignore
index 0e6aad37..0fcb9b90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,7 +82,7 @@ celerybeat-schedule
 .env
 
 # virtualenv
-.venv/
+.venv*/
 venv/
 ENV/
 
diff --git a/README.rst b/README.rst
index 21f24e2e..d499623f 100644
--- a/README.rst
+++ b/README.rst
@@ -152,12 +152,12 @@ An eodag instance can be exposed through a STAC compliant REST api from the comm
 
     # search for items
     $ curl "http://127.0.0.1:5000/search?collections=S2_MSI_L1C&bbox=0,43,1,44&datetime=2018-01-20/2018-01-25" \
-    | jq ".context.matched"
+    | jq ".numberMatched"
     6
 
     # browse for items
     $ curl "http://127.0.0.1:5000/catalogs/S2_MSI_L1C/country/FRA/year/2021/month/01/day/25/cloud_cover/10/items" \
-    | jq ".context.matched"
+    | jq ".numberMatched"
     9
 
     # get download link
diff --git a/docs/api_reference/core.rst b/docs/api_reference/core.rst
index 6a5f8551..3aed3132 100644
--- a/docs/api_reference/core.rst
+++ b/docs/api_reference/core.rst
@@ -80,6 +80,7 @@ Misc
 
    EODataAccessGateway.group_by_extent
    EODataAccessGateway.guess_product_type
+   EODataAccessGateway.get_queryables
 
 .. autoclass:: eodag.api.core.EODataAccessGateway
    :members: set_preferred_provider, get_preferred_provider, update_providers_config, list_product_types,
diff --git a/docs/stac_rest.rst b/docs/stac_rest.rst
index f7cd8c9b..24509e9c 100644
--- a/docs/stac_rest.rst
+++ b/docs/stac_rest.rst
@@ -144,12 +144,12 @@ Example
 
     # search for items
     $ curl "http://127.0.0.1:5000/search?collections=S2_MSI_L1C&bbox=0,43,1,44&datetime=2018-01-20/2018-01-25" \
-    | jq ".context.matched"
+    | jq ".numberMatched"
     6
 
     # browse for items
     $ curl "http://127.0.0.1:5000/catalogs/S2_MSI_L1C/country/FRA/year/2021/month/01/day/25/cloud_cover/10/items" \
-    | jq ".context.matched"
+    | jq ".numberMatched"
     9
 
     # get download link
diff --git a/eodag/api/core.py b/eodag/api/core.py
index 4a9f5583..31130763 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
@@ -71,6 +71,7 @@ from eodag.utils.exceptions import (
     NoMatchingProductType,
     PluginImplementationError,
     RequestError,
+    UnsupportedProductType,
     UnsupportedProvider,
 )
 from eodag.utils.stac_reader import fetch_stac_items
@@ -244,6 +245,7 @@ class EODataAccessGateway:
 
             product_types_schema = Schema(
                 ID=fields.STORED,
+                alias=fields.ID,
                 abstract=fields.STORED,
                 instrument=fields.IDLIST,
                 platform=fields.ID,
@@ -496,9 +498,11 @@ class EODataAccessGateway:
                 for product_type_id in provider_supported_products:
                     if product_type_id == GENERIC_PRODUCT_TYPE:
                         continue
-                    product_type = dict(
-                        ID=product_type_id, **self.product_types_config[product_type_id]
-                    )
+                    config = self.product_types_config[product_type_id]
+                    if "alias" in config:
+                        config["_id"] = product_type_id
+                        product_type_id = config["alias"]
+                    product_type = dict(ID=product_type_id, **config)
                     if product_type_id not in product_types:
                         product_types.append(product_type)
                 return sorted(product_types, key=itemgetter("ID"))
@@ -820,8 +824,52 @@ class EODataAccessGateway:
         else:
             return sorted(tuple(self.providers_config.keys()))
 
+    def get_product_type_from_alias(self, alias_or_id: str) -> str:
+        """Return the ID of a product type by either its ID or alias
+
+        :param alias_or_id: Alias of the product type. If an existing ID is given, this
+                            method will directly return the given value.
+        :type alias_or_id: str
+        :returns: Internal name of the product type.
+        :rtype: str
+        """
+        product_types = [
+            k
+            for k, v in self.product_types_config.items()
+            if v.get("alias", None) == alias_or_id
+        ]
+
+        if len(product_types) > 1:
+            raise NoMatchingProductType(
+                f"Too many matching product types for alias {alias_or_id}: {product_types}"
+            )
+
+        if len(product_types) == 0:
+            if alias_or_id in self.product_types_config:
+                return alias_or_id
+            else:
+                raise NoMatchingProductType(
+                    f"Could not find product type from alias or ID {alias_or_id}"
+                )
+
+        return product_types[0]
+
+    def get_alias_from_product_type(self, product_type: str) -> str:
+        """Return the alias of a product type by its ID. If no alias was defined for the
+        given product type, its ID is returned instead.
+
+        :param product_type: product type ID
+        :type product_type: str
+        :returns: Alias of the product type or its ID if no alias has been defined for it.
+        :rtype: str
+        """
+        if product_type not in self.product_types_config:
+            raise NoMatchingProductType(product_type)
+
+        return self.product_types_config[product_type].get("alias", product_type)
+
     def guess_product_type(self, **kwargs: Any) -> List[str]:
-        """Find the eodag product type code that best matches a set of search params
+        """Find eodag product types codes that best match a set of search params
 
         :param kwargs: A set of search parameters as keywords arguments
         :returns: The best match for the given parameters
@@ -950,6 +998,9 @@ class EODataAccessGateway:
             return self._search_by_id(
                 search_kwargs.pop("id"), provider=provider, **search_kwargs
             )
+        # remove datacube query string from kwargs which was only needed for search-by-id
+        search_kwargs.pop("_dc_qs", None)
+
         search_kwargs.update(
             page=page,
             items_per_page=items_per_page,
@@ -1222,7 +1273,7 @@ class EODataAccessGateway:
         # Get the search plugin and the maximized value
         # of items_per_page if defined for the provider used.
         try:
-            product_type = (
+            product_type = self.get_product_type_from_alias(
                 kwargs.get("productType", None) or self.guess_product_type(**kwargs)[0]
             )
         except NoMatchingProductType:
@@ -1313,12 +1364,18 @@ class EODataAccessGateway:
                   of EO products retrieved (0 or 1)
         :rtype: tuple(:class:`~eodag.api.search_result.SearchResult`, int)
         """
-        get_search_plugins_kwargs = dict(
-            provider=provider, product_type=kwargs.get("productType", None)
-        )
+        product_type = kwargs.get("productType", None)
+        if product_type is not None:
+            try:
+                product_type = self.get_product_type_from_alias(product_type)
+            except NoMatchingProductType:
+                logger.warning("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
         )
+        # datacube query string
+        _dc_qs = kwargs.pop("_dc_qs", None)
 
         for plugin in search_plugins:
             logger.info(
@@ -1326,7 +1383,10 @@ class EODataAccessGateway:
             )
             logger.debug("Using plugin class for search: %s", plugin.__class__.__name__)
             plugin.clear()
-            results, _ = self._do_search(plugin, id=uid, **kwargs)
+            if isinstance(plugin, BuildPostSearchResult):
+                results, _ = self._do_search(plugin, id=uid, _dc_qs=_dc_qs, **kwargs)
+            else:
+                results, _ = self._do_search(plugin, id=uid, **kwargs)
             if len(results) == 1:
                 if not results[0].product_type:
                     # guess product type from properties
@@ -1423,7 +1483,13 @@ class EODataAccessGateway:
                 else:
                     return [], kwargs
 
+        if product_type is not None:
+            try:
+                product_type = self.get_product_type_from_alias(product_type)
+            except NoMatchingProductType:
+                logger.warning("unknown product type " + product_type)
         kwargs["productType"] = product_type
+
         if start is not None:
             kwargs["startTimeFromAscendingNode"] = start
         if end is not None:
@@ -1507,6 +1573,7 @@ class EODataAccessGateway:
                             search_plugin.provider, fetch_providers=False
                         )
                         if p["ID"] == product_type
+                        or ("_id" in p and p["_id"] == product_type)
                     ][0],
                     **{"productType": product_type},
                 )
@@ -1652,6 +1719,14 @@ class EODataAccessGateway:
                     else:
                         eo_product.product_type = guesses[0]
 
+                try:
+                    if eo_product.product_type is not None:
+                        eo_product.product_type = self.get_product_type_from_alias(
+                            eo_product.product_type
+                        )
+                except NoMatchingProductType:
+                    logger.warning("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(
                         eo_product
@@ -1891,13 +1966,7 @@ class EODataAccessGateway:
             max_connections=max_connections,
             timeout=timeout,
         )
-        nb_features = len(features)
         feature_collection = geojson.FeatureCollection(features)
-        feature_collection["context"] = {
-            "limit": nb_features,
-            "matched": nb_features,
-            "returned": nb_features,
-        }
 
         plugin = next(
             self._plugins_manager.get_search_plugins(
@@ -2003,3 +2072,58 @@ class EODataAccessGateway:
         plugin_conf = {"name": name}
         plugin_conf.update({key.replace("-", "_"): val for key, val in options.items()})
         return self._plugins_manager.get_crunch_plugin(name, **plugin_conf)
+
+    def get_queryables(
+        self, provider: Optional[str] = None, product_type: Optional[str] = None
+    ) -> Set[str]:
+        """Fetch the queryable properties for a given product type and/or provider.
+
+        :param product_type: (optional) The EODAG product type.
+        :type product_type: str
+        :param provider: (optional) The provider.
+        :type provider: str
+        :returns: A set containing the EODAG queryable properties.
+        :rtype: set
+        """
+        default_queryables = {"productType", "start", "end", "geom", "locations", "id"}
+        if provider is None and product_type is None:
+            return default_queryables
+
+        plugins = self._plugins_manager.get_search_plugins(product_type, provider)
+
+        # dictionary of the queryable properties of the providers supporting the given product type
+        all_queryable_properties = dict()
+        for plugin in plugins:
+            if (
+                product_type
+                and product_type not in plugin.config.products.keys()
+                and provider is None
+            ):
+                raise UnsupportedProductType(product_type)
+            elif product_type and product_type not in plugin.config.products.keys():
+                raise UnsupportedProductType(
+                    f"{product_type} is not available for provider {provider}"
+                )
+
+            provider_queryables = set(default_queryables)
+
+            metadata_mapping = deepcopy(getattr(plugin.config, "metadata_mapping", {}))
+
+            # product_type-specific metadata-mapping
+            metadata_mapping.update(
+                getattr(plugin.config, "products", {})
+                .get(product_type, {})
+                .get("metadata_mapping", {})
+            )
+
+            for key, value in metadata_mapping.items():
+                if isinstance(value, list) and "TimeFromAscendingNode" not in key:
+                    provider_queryables.add(key)
+
+            all_queryable_properties[plugin.provider] = provider_queryables
+
+        if provider is None:
+            # intersection of the queryables among the providers
+            return set.intersection(*all_queryable_properties.values())
+        else:
+            return all_queryable_properties[provider]
diff --git a/eodag/cli.py b/eodag/cli.py
index a47bfb32..d18fc9f9 100755
--- a/eodag/cli.py
+++ b/eodag/cli.py
@@ -453,6 +453,8 @@ def list_pt(ctx: Context, **kwargs: Any) -> None:
                     provider=provider, fetch_providers=fetch_providers
                 )
                 if pt["ID"] in guessed_product_types
+                or "alias" in pt
+                and pt["alias"] in guessed_product_types
             ]
         else:
             product_types = dag.list_product_types(
diff --git a/eodag/plugins/apis/cds.py b/eodag/plugins/apis/cds.py
index ae902a02..ba6579e9 100644
--- a/eodag/plugins/apis/cds.py
+++ b/eodag/plugins/apis/cds.py
@@ -19,14 +19,16 @@ from __future__ import annotations
 
 import logging
 from datetime import datetime
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
+from urllib.parse import unquote_plus
 
 import cdsapi
 import geojson
 import requests
 
+from eodag.api.product._assets import Asset
 from eodag.plugins.apis.base import Api
-from eodag.plugins.download.base import Download
+from eodag.plugins.download.http import HTTPDownload
 from eodag.plugins.search.base import Search
 from eodag.plugins.search.build_search_result import BuildPostSearchResult
 from eodag.rest.stac import DEFAULT_MISSION_START_DATE
@@ -54,7 +56,7 @@ logger = logging.getLogger("eodag.apis.cds")
 CDS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
 
 
-class CdsApi(Download, Api, BuildPostSearchResult):
+class CdsApi(HTTPDownload, Api, BuildPostSearchResult):
     """A plugin that enables to build download-request and download data on CDS API.
 
     Builds a single ready-to-download :class:`~eodag.api.product._product.EOProduct`
@@ -89,6 +91,22 @@ class CdsApi(Download, Api, BuildPostSearchResult):
     ) -> Tuple[List[EOProduct], Optional[int]]:
         """Build ready-to-download SearchResult"""
 
+        _dc_qs = kwargs.get("_dc_qs", None)
+        if _dc_qs is not None:
+            _dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
+            if "/" in _dc_qp.get("date", ""):
+                (
+                    kwargs["startTimeFromAscendingNode"],
+                    kwargs["completionTimeFromAscendingNode"],
+                ) = _dc_qp["date"].split("/")
+            else:
+                kwargs["startTimeFromAscendingNode"] = kwargs[
+                    "completionTimeFromAscendingNode"
+                ] = _dc_qp["date"]
+
+            if "/" in _dc_qp.get("area", ""):
+                kwargs["geometry"] = _dc_qp["area"].split("/")
+
         # check productType, dates, geometry, use defaults if not specified
         # productType
         if not kwargs.get("productType"):
@@ -180,32 +198,8 @@ class CdsApi(Download, Api, BuildPostSearchResult):
 
         return auth_dict
 
-    def download(
-        self,
-        product: EOProduct,
-        auth: Optional[PluginConfig] = None,
-        progress_callback: Optional[ProgressCallback] = None,
-        wait: int = DEFAULT_DOWNLOAD_WAIT,
-        timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
-    ) -> Optional[str]:
-        """Download data from providers using CDS API"""
-
-        product_extension = CDS_KNOWN_FORMATS[product.properties.get("format", "grib")]
-
-        # Prepare download
-        fs_path, record_filename = self._prepare_download(
-            product,
-            progress_callback=progress_callback,
-            outputs_extension=f".{product_extension}",
-            **kwargs,
-        )
-
-        if not fs_path or not record_filename:
-            if fs_path:
-                product.location = path_to_uri(fs_path)
-            return fs_path
-
+    def _prepare_download_link(self, product):
+        """Update product download link with http url obtained from cds api"""
         # get download request dict from product.location/downloadLink url query string
         # separate url & parameters
         query_str = "".join(urlsplit(product.location).fragment.split("?", 1)[1:])
@@ -233,26 +227,74 @@ class CdsApi(Download, Api, BuildPostSearchResult):
         )
         try:
             client = self._get_cds_client(**auth_dict)
-            client.retrieve(name=dataset_name, request=download_request, target=fs_path)
+            result = client._api(
+                "%s/resources/%s" % (client.url, dataset_name), download_request, "POST"
+            )
+            # update product download link through a new asset
+            product.assets["data"] = Asset(product, "data", {"href": result.location})
         except Exception as e:
             logger.error(e)
             raise DownloadError(e)
 
-        with open(record_filename, "w") as fh:
-            fh.write(product.properties["downloadLink"])
-        logger.debug("Download recorded in %s", record_filename)
+    def download(
+        self,
+        product: EOProduct,
+        auth: Optional[PluginConfig] = None,
+        progress_callback: Optional[ProgressCallback] = None,
+        wait: int = DEFAULT_DOWNLOAD_WAIT,
+        timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
+        **kwargs: Any,
+    ) -> Optional[str]:
+        """Download data from providers using CDS API"""
 
-        # do not try to extract or delete grib/netcdf
-        kwargs["extract"] = False
+        product_extension = CDS_KNOWN_FORMATS[product.properties.get("format", "grib")]
 
-        product_path = self._finalize(
-            fs_path,
+        # Prepare download
+        fs_path, record_filename = self._prepare_download(
+            product,
             progress_callback=progress_callback,
             outputs_extension=f".{product_extension}",
             **kwargs,
         )
-        product.location = path_to_uri(product_path)
-        return product_path
+
+        if not fs_path or not record_filename:
+            if fs_path:
+                product.location = path_to_uri(fs_path)
+            return fs_path
+
+        self._prepare_download_link(product)
+
+        try:
+            return super(CdsApi, self).download(
+                product,
+                progress_callback=progress_callback,
+                **kwargs,
+            )
+        except Exception as e:
+            logger.error(e)
+            raise DownloadError(e)
+
+    def _stream_download_dict(
+        self,
+        product: EOProduct,
+        auth: Optional[PluginConfig] = None,
+        progress_callback: Optional[ProgressCallback] = None,
+        wait: int = DEFAULT_DOWNLOAD_WAIT,
+        timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
+        **kwargs: Union[str, bool, Dict[str, Any]],
+    ) -> Dict[str, Any]:
+        """Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
+        It contains a generator to streamed download chunks and the response headers."""
+
+        self._prepare_download_link(product)
+        return super(CdsApi, self)._stream_download_dict(
+            product,
+            auth=auth,
+            progress_callback=progress_callback,
+            wait=wait,
+            timeout=timeout,
+            **kwargs,
+        )
 
     def download_all(
         self,
diff --git a/eodag/plugins/download/http.py b/eodag/plugins/download/http.py
index 0f668b15..2d419fcf 100644
--- a/eodag/plugins/download/http.py
+++ b/eodag/plugins/download/http.py
@@ -401,6 +401,8 @@ class HTTPDownload(Download):
                     progress_callback,
                     **kwargs,
                 )
+                if kwargs.get("asset", None) is None:
+                    product.location = path_to_uri(fs_path)
                 return fs_path
             except NotAvailableError as e:
                 if kwargs.get("asset", None) is not None:
diff --git a/eodag/plugins/search/build_search_result.py b/eodag/plugins/search/build_search_result.py
index 1ee48a87..a043805a 100644
--- a/eodag/plugins/search/build_search_result.py
+++ b/eodag/plugins/search/build_search_result.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 import hashlib
 import logging
 from typing import Any, Dict, List, Optional, Tuple
+from urllib.parse import quote_plus, unquote_plus
 
 import geojson
 import orjson
@@ -109,22 +110,39 @@ class BuildPostSearchResult(PostJsonSearch):
 
         result = results[0]
 
-        # update result with query parameters without pagination (or search-only params)
-        if isinstance(self.config.pagination["next_page_query_obj"], str) and hasattr(
-            self, "query_params_unpaginated"
-        ):
-            unpaginated_query_params = self.query_params_unpaginated
-        elif isinstance(self.config.pagination["next_page_query_obj"], str):
-            next_page_query_obj = orjson.loads(
-                self.config.pagination["next_page_query_obj"].format()
+        # datacube query string got from previous search
+        _dc_qs = kwargs.pop("_dc_qs", None)
+        if _dc_qs is not None:
+            qs = unquote_plus(unquote_plus(_dc_qs))
+            unpaginated_query_params = sorted_unpaginated_query_params = geojson.loads(
+                qs
             )
-            unpaginated_query_params = {
-                k: v
-                for k, v in self.query_params.items()
-                if (k, v) not in next_page_query_obj.items()
-            }
         else:
-            unpaginated_query_params = self.query_params
+            # update result with query parameters without pagination (or search-only params)
+            if isinstance(
+                self.config.pagination["next_page_query_obj"], str
+            ) and hasattr(self, "query_params_unpaginated"):
+                unpaginated_query_params = self.query_params_unpaginated
+            elif isinstance(self.config.pagination["next_page_query_obj"], str):
+                next_page_query_obj = orjson.loads(
+                    self.config.pagination["next_page_query_obj"].format()
+                )
+                unpaginated_query_params = {
+                    k: v[0] if (isinstance(v, list) and len(v) == 1) else v
+                    for k, v in self.query_params.items()
+                    if (k, v) not in next_page_query_obj.items()
+                }
+            else:
+                unpaginated_query_params = self.query_params
+
+            # query hash, will be used to build a product id
+            sorted_unpaginated_query_params = dict_items_recursive_sort(
+                unpaginated_query_params
+            )
+            qs = geojson.dumps(sorted_unpaginated_query_params)
+
+        query_hash = hashlib.sha1(str(qs).encode("UTF-8")).hexdigest()
+
         result = dict(result, **unpaginated_query_params)
 
         # update result with search args if not None (and not auth)
@@ -148,13 +166,6 @@ class BuildPostSearchResult(PostJsonSearch):
             if v not in (NOT_AVAILABLE, NOT_MAPPED)
         }
 
-        # query hash, will be used to build a product id
-        sorted_unpaginated_query_params = dict_items_recursive_sort(
-            unpaginated_query_params
-        )
-        qs = geojson.dumps(sorted_unpaginated_query_params)
-        query_hash = hashlib.sha1(str(qs).encode("UTF-8")).hexdigest()
-
         # build product id
         id_prefix = (product_type or self.provider).upper()
         product_id = "%s_%s_%s" % (
@@ -170,6 +181,7 @@ class BuildPostSearchResult(PostJsonSearch):
 
         # update downloadLink
         product_available_properties["downloadLink"] += f"?{qs}"
+        product_available_properties["_dc_qs"] = quote_plus(qs)
 
         # parse metadata needing downloadLink
         for param, mapping in self.config.metadata_mapping.items():
diff --git a/eodag/plugins/search/static_stac_search.py b/eodag/plugins/search/static_stac_search.py
index 8b64af76..502b5016 100644
--- a/eodag/plugins/search/static_stac_search.py
+++ b/eodag/plugins/search/static_stac_search.py
@@ -98,11 +98,6 @@ class StaticStacSearch(StacSearch):
         )
         nb_features = len(features)
         feature_collection = geojson.FeatureCollection(features)
-        feature_collection["context"] = {
-            "limit": nb_features,
-            "matched": nb_features,
-            "returned": nb_features,
-        }
 
         # save StaticStacSearch._request and mock it to make return loaded static results
         stacapi_request = self._request
diff --git a/eodag/resources/providers.yml b/eodag/resources/providers.yml
index dc4d34e8..642d1cc8 100644
--- a/eodag/resources/providers.yml
+++ b/eodag/resources/providers.yml
@@ -987,7 +987,7 @@
         # - '$.geometry'
         - '($.geometry.`str()`.`sub(/^\\[\\]$/, POLYGON((180 -90, 180 90, -180 90, -180 -90, 180 -90)))`)|($.geometry[*])'
       # The url of the quicklook
-      quicklook: '$.properties.quicklook'
+      quicklook: '$.properties.thumbnail'
       # The url to download the product "as is" (literal or as a template to be completed either after the search result
       # is obtained from the provider or during the eodag download phase)
       downloadLink: 'https://zipper.creodias.eu/download/{uid}'
@@ -2084,6 +2084,7 @@
     type: CdsApi
     api_endpoint: https://ads.atmosphere.copernicus.eu/api/v2
     extract: false
+    flatten_top_dirs: True
     metadata_mapping:
       productType: '$.productType'
       title: '$.id'
@@ -2400,6 +2401,7 @@
     type: CdsApi
     api_endpoint: https://cds.climate.copernicus.eu/api/v2
     extract: false
+    flatten_top_dirs: True
     metadata_mapping:
       productType: '$.productType'
       title: '$.id'
@@ -3064,11 +3066,15 @@
       geometry:
         - 'geometry={geometry#to_rounded_wkt}'
         - '($.geometry.`str()`.`sub(/^\\[\\]$/, POLYGON((180 -90, 180 90, -180 90, -180 -90, 180 -90)))`)|($.geometry[*])'
+      # The url of the quicklook
+      quicklook: '$.properties.thumbnail'
       # The url to download the product "as is" (literal or as a template to be completed either after the search result
       # is obtained from the provider or during the eodag download phase)
       downloadLink: 'https://catalogue.dataspace.copernicus.eu/odata/v1/Products({uid})/$value'
       # storageStatus: must be one of ONLINE, STAGING, OFFLINE
       storageStatus: '$.properties.status'
+      # Additional metadata provided by the providers but that don't appear in the reference spec
+      thumbnail: '$.properties.thumbnail'
   download: !plugin
     type: HTTPDownload
     base_uri: 'https://catalogue.dataspace.copernicus.eu/odata/v1/Products'
diff --git a/eodag/resources/stac.yml b/eodag/resources/stac.yml
index 67a6215c..2de3ea7f 100644
--- a/eodag/resources/stac.yml
+++ b/eodag/resources/stac.yml
@@ -153,19 +153,14 @@ items:
       href: '$.search_results.next'
       title: Next page
       type: application/geo+json
-      method: GET
+      method: '$.search_results.method'
+      body: '$.search_results.body'
   # time and date when the response was generated
   timeStamp: '$.search_results.timeStamp'
   # count request result
   numberMatched: '$.search_results.numberMatched'
   # len(features)
   numberReturned: '$.search_results.numberReturned'
-  # 0.7.0 stac-browser compatibility
-  context:
-    page: '$.search_results.properties.page'
-    limit: '$.search_results.properties.itemsPerPage'
-    matched: '$.search_results.properties.totalResults'
-    returned: '$.search_results.numberReturned'
 
 # https://stacspec.org/STAC-api.html#operation/getFeature
 item:
@@ -191,14 +186,16 @@ item:
     license: '{catalog[license]}'
     constellation: '$.product.properties.platform'
     platform: '$.product.properties.platformSerialIdentifier'
-    intruments:
+    instruments:
       - '$.product.properties.instrument'
     gsd: '$.product.properties.resolution'
     published: '$.product.properties.publicationDate'
     eo:cloud_cover: '$.product.properties.cloudCover'
+    eo:snow_cover: '$.product.properties.snowCover'
     processing:level: '$.product.properties.processingLevel'
     sat:orbit_state: '$.product.properties.orbitDirection'
     sat:relative_orbit: '$.product.properties.relativeOrbitNumber'
+    sat:absolute_orbit : '$.product.properties.orbitNumber'
     sar:product_type: '$.product.properties.productType'
     sar:instrument_mode: '$.product.properties.sensorMode'
     sar:polarizations: '$.product.properties.polarizationChannels'
@@ -225,6 +222,7 @@ item:
         href: "{catalog[url]}/items/{item[id]}/download"
         # https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#media-types
         type: 'application/zip'
+        _dc_qs: '$.product.properties._dc_qs'
     thumbnail:
         title: 'Thumbnail'
         href: '$.product.properties.quicklook'
diff --git a/eodag/resources/stac_provider.yml b/eodag/resources/stac_provider.yml
index 897e7e1a..680b1d73 100644
--- a/eodag/resources/stac_provider.yml
+++ b/eodag/resources/stac_provider.yml
@@ -20,7 +20,7 @@ search:
   results_entry: features
   pagination:
     next_page_query_obj: '{{"limit":{items_per_page},"page":{page}}}'
-    total_items_nb_key_path: '$.context.matched'
+    total_items_nb_key_path: '$.numberMatched'
     next_page_url_key_path: '$.links[?(@.rel="next")].href'
     next_page_query_obj_key_path: '$.links[?(@.rel="next")].body'
     next_page_merge_key_path: '$.links[?(@.rel="next")].merge'
diff --git a/eodag/rest/server.py b/eodag/rest/server.py
index a50b92bf..7e14d489 100755
--- a/eodag/rest/server.py
+++ b/eodag/rest/server.py
@@ -354,7 +354,7 @@ def stac_collections_item_download(
     provider = arguments.pop("provider", None)
 
     return download_stac_item_by_id_stream(
-        catalogs=[collection_id], item_id=item_id, provider=provider
+        catalogs=[collection_id], item_id=item_id, provider=provider, **arguments
     )
 
 
@@ -373,7 +373,11 @@ def stac_collections_item_download_asset(
     provider = arguments.pop("provider", None)
 
     return download_stac_item_by_id_stream(
-        catalogs=[collection_id], item_id=item_id, provider=provider, asset=asset_filter
+        catalogs=[collection_id],
+        item_id=item_id,
+        provider=provider,
+        asset=asset_filter,
+        **arguments,
     )
 
 
@@ -397,6 +401,7 @@ def stac_collections_item(collection_id: str, item_id: str, request: Request) ->
         root=url_root,
         catalogs=[collection_id],
         provider=provider,
+        **arguments,
     )
 
     if response:
@@ -467,8 +472,8 @@ def list_collection_queryables(
         fetch_collection_queryable_properties(*conf_args)
     )
 
-    for prop in provider_properties:
-        titled_name = re.sub(CAMEL_TO_SPACE_TITLED, " ", prop).title()
+    for prop in sorted(provider_properties):
+        titled_name = re.sub(CAMEL_TO_SPACE_TITLED, " ", prop.split(":")[-1]).title()
         queryables[prop] = QueryableProperty(description=titled_name)
 
     return queryables
@@ -542,7 +547,7 @@ def stac_catalogs_item_download(
     list_catalog = catalogs.strip("/").split("/")
 
     return download_stac_item_by_id_stream(
-        catalogs=list_catalog, item_id=item_id, provider=provider
+        catalogs=list_catalog, item_id=item_id, provider=provider, **arguments
     )
 
 
@@ -563,7 +568,11 @@ def stac_catalogs_item_download_asset(
     catalogs = catalogs.strip("/").split("/")
 
     return download_stac_item_by_id_stream(
-        catalogs=catalogs, item_id=item_id, provider=provider, asset=asset_filter
+        catalogs=catalogs,
+        item_id=item_id,
+        provider=provider,
+        asset=asset_filter,
+        **arguments,
     )
 
 
@@ -588,6 +597,7 @@ def stac_catalogs_item(catalogs: str, item_id: str, request: Request):
         root=url_root,
         catalogs=list_catalog,
         provider=provider,
+        **arguments,
     )
 
     if response:
@@ -704,7 +714,11 @@ def stac_search(
     provider = arguments.pop("provider", None)
 
     response = search_stac_items(
-        url=url, arguments=arguments, root=url_root, provider=provider
+        url=url,
+        arguments=arguments,
+        root=url_root,
+        provider=provider,
+        method=request.method,
     )
     resp = ORJSONResponse(
         content=response, status_code=200, media_type="application/json"
diff --git a/eodag/rest/stac.py b/eodag/rest/stac.py
index 395ae4c0..2665aaad 100644
--- a/eodag/rest/stac.py
+++ b/eodag/rest/stac.py
@@ -20,7 +20,6 @@ from __future__ import annotations
 import datetime
 import logging
 import os
-import re
 from collections import defaultdict
 from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
 from urllib.parse import parse_qs, urlencode, urlparse
@@ -253,16 +252,25 @@ class StacItem(StacCommon):
                 },
             )
 
-            # append provider query-arg to download link
+            # parse download link
+            url_parts = urlparse(str(product_item["assets"]["downloadLink"]["href"]))
+            query_dict = parse_qs(url_parts.query)
+            without_arg_url = (
+                f"{url_parts.scheme}://{url_parts.netloc}{url_parts.path}"
+                if url_parts.scheme
+                else f"{url_parts.netloc}{url_parts.path}"
+            )
+
+            # add provider to query-args
             if self.provider:
-                parts = urlparse(str(product_item["assets"]["downloadLink"]["href"]))
-                query_dict = parse_qs(parts.query)
                 query_dict.update(provider=[self.provider])
-                without_arg_url = (
-                    f"{parts.scheme}://{parts.netloc}{parts.path}"
-                    if parts.scheme
-                    else f"{parts.netloc}{parts.path}"
-                )
+            # add datacube query-string to query-args
+            _dc_qs = product_item["assets"]["downloadLink"].pop("_dc_qs", None)
+            if _dc_qs:
+                query_dict.update(_dc_qs=_dc_qs)
+
+            # update download link with up-to-date query-args
+            if query_dict:
                 product_item["assets"]["downloadLink"][
                     "href"
                 ] = f"{without_arg_url}?{urlencode(query_dict, doseq=True)}"
@@ -272,22 +280,13 @@ class StacItem(StacCommon):
             if getattr(product, "assets", False):
                 # replace origin asset urls with eodag-server ones
                 for asset_key, asset_value in origin_assets.items():
-
-                    parts = urlparse(product_item["assets"]["downloadLink"]["href"])
-                    if parts.query:
-                        query_dict = parse_qs(parts.query)
-                        without_arg_url = (
-                            f"{parts.scheme}://{parts.netloc}{parts.path}"
-                            if parts.scheme
-                            else f"{parts.netloc}{parts.path}"
-                        ) + f"/{asset_key}"
+                    origin_assets[asset_key]["href"] = without_arg_url
+                    if query_dict:
                         origin_assets[asset_key][
                             "href"
-                        ] = f"{without_arg_url}?{urlencode(query_dict, doseq=True)}"
+                        ] += f"/{asset_key}?{urlencode(query_dict, doseq=True)}"
                     else:
-                        origin_assets[asset_key][
-                            "href"
-                        ] = f"{product_item['assets']['downloadLink']['href']}/{asset_key}"
+                        origin_assets[asset_key]["href"] += f"/{asset_key}"
 
                 product_item["assets"] = dict(product_item["assets"], **origin_assets)
 
@@ -319,6 +318,18 @@ class StacItem(StacCommon):
             # remove empty properties
             product_item = self.__filter_item_properties_values(product_item)
 
+            # update item link with datacube query-string
+            if _dc_qs:
+                url_parts = urlparse(str(product_item["links"][0]["href"]))
+                without_arg_url = (
+                    f"{url_parts.scheme}://{url_parts.netloc}{url_parts.path}"
+                    if url_parts.scheme
+                    else f"{url_parts.netloc}{url_parts.path}"
+                )
+                product_item["links"][0][
+                    "href"
+                ] = f"{without_arg_url}?{urlencode(query_dict, doseq=True)}"
+
             item_list.append(product_item)
 
         return item_list
@@ -346,22 +357,6 @@ class StacItem(StacCommon):
             for i, _ in enumerate(items_model["links"]):
                 if items_model["links"][i]["rel"] == "self":
                     items_model["links"][i]["href"] = catalog["url"]
-            if "page=" not in self.url:
-                search_results.next = "%s&page=%s" % (
-                    self.url,
-                    search_results.properties["page"] + 1,
-                )
-            else:
-                search_results.next = re.sub(
-                    r"^(.*)(page=[0-9]+)(.*)$",
-                    r"\1page=%s\3" % (search_results.properties["page"] + 1),
-                    self.url,
-                )
-        else:
-            search_results.next = "%s?page=%s" % (
-                self.url,
-                search_results.properties["page"] + 1,
-            )
 
         search_results.timeStamp = (
             datetime.datetime.now(datetime.timezone.utc)
@@ -422,6 +417,7 @@ class StacItem(StacCommon):
                     provider=self.provider, fetch_providers=False
                 )
                 if pt["ID"] == product_type
+                or ("alias" in pt and pt["alias"] == product_type)
             ][0]
         except IndexError:
             raise NoMatchingProductType(
@@ -446,6 +442,9 @@ class StacItem(StacCommon):
             if (
                 v not in result_item_model["properties"].values()
                 and k not in self.stac_config["metadata_ignore"]
+                and not any(
+                    k in str(prop) for prop in result_item_model["properties"].values()
+                )
             ):
                 result_item_model["properties"]["oseo:" + k] = string_to_jsonpath(k, v)
 
@@ -638,7 +637,9 @@ class StacCollection(StacCommon):
                 providers = [
                     plugin.provider
                     for plugin in self.eodag_api._plugins_manager.get_search_plugins(
-                        product_type=product_type["ID"]
+                        product_type=(
+                            product_type.get("_id", None) or product_type["ID"]
+                        )
                     )
                 ]
             providers_models: List[Dict[str, Any]] = []
diff --git a/eodag/rest/utils.py b/eodag/rest/utils.py
index 4cee4d85..3b2c13a4 100644
--- a/eodag/rest/utils.py
+++ b/eodag/rest/utils.py
@@ -37,6 +37,7 @@ from typing import (
     Tuple,
     Union,
 )
+from urllib.parse import urlencode
 
 import dateutil.parser
 from dateutil import tz
@@ -534,7 +535,10 @@ def search_products(
 
 
 def search_product_by_id(
-    uid: str, product_type: Optional[str] = None, provider: Optional[str] = None
+    uid: str,
+    product_type: Optional[str] = None,
+    provider: Optional[str] = None,
+    **kwargs: Any,
 ) -> SearchResult:
     """Search a product by its id
 
@@ -544,6 +548,8 @@ def search_product_by_id(
     :type product_type: str
     :param provider: (optional) The provider to be used
     :type provider: str
+    :param kwargs: additional search parameters
+    :type kwargs: Any
     :returns: A search result
     :rtype: :class:`~eodag.api.search_result.SearchResult`
     :raises: :class:`~eodag.utils.exceptions.ValidationError`
@@ -551,7 +557,7 @@ def search_product_by_id(
     """
     try:
         products, _ = eodag_api.search(
-            id=uid, productType=product_type, provider=provider
+            id=uid, productType=product_type, provider=provider, **kwargs
         )
         return products
     except ValidationError:
@@ -637,6 +643,7 @@ def get_stac_item_by_id(
     catalogs: List[str],
     root: str = "/",
     provider: Optional[str] = None,
+    **kwargs: Any,
 ) -> Dict[str, Any]:
     """Build STAC item by id
 
@@ -650,12 +657,22 @@ def get_stac_item_by_id(
     :type root: str
     :param provider: (optional) Chosen provider
     :type provider: str
+    :param kwargs: additional search parameters
+    :type kwargs: Any
     :returns: Collection dictionary
     :rtype: dict
     """
     product_type = catalogs[0]
-    found_products = search_product_by_id(item_id, product_type=product_type)
+    _dc_qs = kwargs.get("_dc_qs", None)
+
+    found_products = search_product_by_id(
+        item_id, product_type=product_type, _dc_qs=_dc_qs
+    )
+
     if len(found_products) > 0:
+        found_products[0].product_type = eodag_api.get_alias_from_product_type(
+            found_products[0].product_type
+        )
         return StacItem(
             url=url,
             stac_config=stac_config,
@@ -672,6 +689,7 @@ def download_stac_item_by_id_stream(
     item_id: str,
     provider: Optional[str] = None,
     asset: Optional[str] = None,
+    **kwargs: Any,
 ) -> StreamingResponse:
     """Download item
 
@@ -681,10 +699,14 @@ def download_stac_item_by_id_stream(
     :type item_id: str
     :param provider: (optional) Chosen provider
     :type provider: str
+    :param kwargs: additional download parameters
+    :type kwargs: Any
     :returns: a stream of the downloaded data (zip file)
     :rtype: StreamingResponse
     """
     product_type = catalogs[0]
+    _dc_qs = kwargs.get("_dc_qs", None)
+
     search_plugin = next(
         eodag_api._plugins_manager.get_search_plugins(product_type, provider)
     )
@@ -707,8 +729,9 @@ def download_stac_item_by_id_stream(
         }
         product = EOProduct(provider or product_data["provider"], properties)
     else:
+
         search_results = search_product_by_id(
-            item_id, product_type=product_type, provider=provider
+            item_id, product_type=product_type, provider=provider, _dc_qs=_dc_qs
         )
         if len(search_results) > 0:
             product = search_results[0]
@@ -818,6 +841,7 @@ def search_stac_items(
     root: str = "/",
     catalogs: List[str] = [],
     provider: Optional[str] = None,
+    method: Optional[str] = "GET",
 ) -> Dict[str, Any]:
     """Get items collection dict for given catalogs list
 
@@ -831,6 +855,8 @@ def search_stac_items(
     :type catalogs: list
     :param provider: (optional) Chosen provider
     :type provider: str
+    :param method: (optional) search request HTTP method ('GET' or 'POST')
+    :type method: str
     :returns: Catalog dictionnary
     :rtype: dict
     """
@@ -838,6 +864,14 @@ def search_stac_items(
 
     catalog_url = url.replace("/items", "")
 
+    next_page_kwargs = {
+        key: value for key, value in arguments.copy().items() if value is not None
+    }
+    next_page_id = (
+        int(next_page_kwargs["page"]) + 1 if "page" in next_page_kwargs else 2
+    )
+    next_page_kwargs["page"] = next_page_id
+
     # use catalogs from path or if it is empty, collections from args
     if catalogs:
         result_catalog = StacCatalog(
@@ -860,7 +894,8 @@ def search_stac_items(
             root=root,
             provider=provider,
             eodag_api=eodag_api,
-            # handle only one collection per request (STAC allows multiple)
+            # handle only one collection
+            # per request (STAC allows multiple)
             catalogs=collections[0:1],
             url=catalog_url.replace("/search", f"/collections/{collections[0]}"),
         )
@@ -956,12 +991,25 @@ def search_stac_items(
                         **{"url": result_catalog.url, "root": result_catalog.root},
                     ),
                 )
+
         search_results = search_products(
             product_type=result_catalog.search_args["product_type"],
             arguments=search_products_arguments,
         )
 
-    return StacItem(
+    for record in search_results:
+        record.product_type = eodag_api.get_alias_from_product_type(record.product_type)
+
+    search_results.method = method
+    if method == "POST":
+        search_results.next = f"{url}"
+        search_results.body = next_page_kwargs
+
+    elif method == "GET":
+        next_query_string = urlencode(next_page_kwargs)
+        search_results.next = f"{url}?{next_query_string}"
+
+    items = StacItem(
         url=url,
         stac_config=stac_config,
         provider=provider,
@@ -975,6 +1023,8 @@ def search_stac_items(
         ),
     )
 
+    return items
+
 
 def get_stac_extension_oseo(url: str) -> Dict[str, str]:
     """Build STAC OGC / OpenSearch Extension for EO
@@ -1068,7 +1118,6 @@ class Queryables(BaseModel):
                 description="Datetime",
                 ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
             ),
-            "ids": QueryableProperty(description="IDs"),
         }
     )
     additional_properties: bool = Field(
@@ -1103,8 +1152,14 @@ def rename_to_stac_standard(key: str) -> str:
     stac_config_properties: Dict[str, Any] = stac_config["item"]["properties"]
 
     for stac_property, value in stac_config_properties.items():
+        if isinstance(value, list):
+            value = value[0]
         if str(value).endswith(key):
             return stac_property
+
+    if key in OSEO_METADATA_MAPPING:
+        return "oseo:" + key
+
     return key
 
 
@@ -1121,15 +1176,17 @@ def fetch_collection_queryable_properties(
     :rtype queryable_properties: set
     """
     # Fetch the metadata mapping for collection-specific queryables
-    args = [collection_id, provider] if provider else [collection_id]
-    search_plugin = next(eodag_api._plugins_manager.get_search_plugins(*args))
-    search_plugin.config
-    mapping: Dict[str, Any] = dict(search_plugin.config.metadata_mapping)
+    kwargs = {"product_type": collection_id}
+    if provider is not None:
+        kwargs["provider"] = provider
+    eodag_queryable_properties = eodag_api.get_queryables(**kwargs)
+
     # list of all the STAC standardized collection-specific queryables
     queryable_properties: Set[str] = set()
-    for key, value in mapping.items():
-        if isinstance(value, list) and "TimeFromAscendingNode" not in key:
-            queryable_properties.add(rename_to_stac_standard(key))
+    for prop in eodag_queryable_properties:
+        # remove pure eodag properties
+        if prop not in ["start", "end", "geom", "locations", "id"]:
+            queryable_properties.add(rename_to_stac_standard(prop))
     return queryable_properties
 
 


Test patch:
diff --git a/tests/context.py b/tests/context.py
index 86d21775..3b4c1d73 100644
--- a/tests/context.py
+++ b/tests/context.py
@@ -97,6 +97,7 @@ from eodag.utils.exceptions import (
     PluginImplementationError,
     RequestError,
     UnsupportedDatasetAddressScheme,
+    UnsupportedProductType,
     UnsupportedProvider,
     ValidationError,
     STACOpenerError,
diff --git a/tests/integration/test_core_config.py b/tests/integration/test_core_config.py
index 525a5391..ce09a795 100644
--- a/tests/integration/test_core_config.py
+++ b/tests/integration/test_core_config.py
@@ -57,9 +57,6 @@ class TestCoreProvidersConfig(TestCase):
         mock__request.return_value = mock.Mock()
         mock__request_side_effect = [
             {
-                "context": {
-                    "matched": 1,
-                },
                 "features": [
                     {
                         "id": "foo",
diff --git a/tests/resources/provider_responses/earth_search_search.json b/tests/resources/provider_responses/earth_search_search.json
index 9d8ee4a3..245287eb 100644
--- a/tests/resources/provider_responses/earth_search_search.json
+++ b/tests/resources/provider_responses/earth_search_search.json
@@ -2,12 +2,6 @@
     "type": "FeatureCollection",
     "stac_version": "1.0.0-beta.2",
     "stac_extensions": [],
-    "context": {
-        "page": 1,
-        "limit": 1,
-        "matched": 21830128,
-        "returned": 1
-    },
     "numberMatched": 21830128,
     "numberReturned": 1,
     "features": [
diff --git a/tests/units/test_apis_plugins.py b/tests/units/test_apis_plugins.py
index eb34273e..3336467e 100644
--- a/tests/units/test_apis_plugins.py
+++ b/tests/units/test_apis_plugins.py
@@ -16,6 +16,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import ast
+import io
 import logging
 import os
 import unittest
@@ -114,7 +115,7 @@ class TestApisPluginEcmwfApi(BaseApisPluginTest):
             "grid": "2/2",
             "param": "228164",  # total cloud cover parameter
             "time": "00",
-            "type": "pf",
+            "type": "cf",
             "class": "ti",
         }
 
@@ -825,19 +826,35 @@ class TestApisPluginCdsApi(BaseApisPluginTest):
         assert auth_dict["url"] == self.api_plugin.config.api_endpoint
         del self.api_plugin.config.credentials
 
+    @mock.patch("eodag.plugins.download.http.requests.head", autospec=True)
+    @mock.patch("eodag.plugins.download.http.requests.get", autospec=True)
     @mock.patch(
         "eodag.api.core.EODataAccessGateway.fetch_product_types_list", autospec=True
     )
     @mock.patch("eodag.plugins.apis.cds.CdsApi.authenticate", autospec=True)
-    @mock.patch("cdsapi.api.Client.retrieve", autospec=True)
+    @mock.patch("cdsapi.api.Client._api", autospec=True)
     def test_plugins_apis_cds_download(
-        self, mock_client_retrieve, mock_cds_authenticate, mock_fetch_product_types_list
+        self,
+        mock_client_api,
+        mock_cds_authenticate,
+        mock_fetch_product_types_list,
+        mock_get,
+        mock_head,
     ):
         """CdsApi.download must call the authenticate function and cdsapi Client retrieve"""
         mock_cds_authenticate.return_value = {
             "key": "foo:bar",
             "url": "http://foo.bar.baz",
         }
+        mock_client_api.return_value.location = "http://somewhere/something"
+
+        mock_get.return_value.__enter__.return_value.iter_content.return_value = (
+            io.BytesIO(b"some content")
+        )
+        mock_get.return_value.__enter__.return_value.headers = {
+            "content-disposition": ""
+        }
+        mock_head.return_value.headers = {"content-disposition": ""}
 
         dag = EODataAccessGateway()
         dag.set_preferred_provider("cop_ads")
@@ -853,16 +870,15 @@ class TestApisPluginCdsApi(BaseApisPluginTest):
         query_str = "".join(urlsplit(eoproduct.location).fragment.split("?", 1)[1:])
         expected_download_request = geojson.loads(query_str)
         expected_dataset_name = expected_download_request.pop("dataset")
-        expected_path = os.path.join(
-            output_data_path, "%s.grib" % eoproduct.properties["title"]
-        )
+        expected_url = f"{mock_cds_authenticate.return_value['url']}/resources/{expected_dataset_name}"
+        expected_path = os.path.join(output_data_path, eoproduct.properties["title"])
 
         path = eoproduct.download(outputs_prefix=output_data_path)
-        mock_client_retrieve.assert_called_once_with(
+        mock_client_api.assert_called_once_with(
             mock.ANY,  # instance
-            name=expected_dataset_name,
-            request=expected_download_request,
-            target=expected_path,
+            expected_url,
+            expected_download_request,
+            "POST",
         )
 
         assert path == expected_path
diff --git a/tests/units/test_core.py b/tests/units/test_core.py
index d127ac3c..9acf6e1e 100644
--- a/tests/units/test_core.py
+++ b/tests/units/test_core.py
@@ -43,6 +43,7 @@ from tests.context import (
     ProviderConfig,
     RequestError,
     SearchResult,
+    UnsupportedProductType,
     UnsupportedProvider,
     get_geometry_from_various,
     load_default_config,
@@ -383,7 +384,10 @@ class TestCore(TestCoreBase):
     def test_supported_product_types_in_unit_test(self):
         """Every product type must be referenced in the core unit test SUPPORTED_PRODUCT_TYPES class attribute"""
         for product_type in self.dag.list_product_types(fetch_providers=False):
-            self.assertIn(product_type["ID"], self.SUPPORTED_PRODUCT_TYPES.keys())
+            assert (
+                product_type["ID"] in self.SUPPORTED_PRODUCT_TYPES.keys()
+                or product_type["_id"] in self.SUPPORTED_PRODUCT_TYPES.keys()
+            )
 
     def test_list_product_types_ok(self):
         """Core api must correctly return the list of supported product types"""
@@ -393,6 +397,16 @@ class TestCore(TestCoreBase):
             self.assertListProductTypesRightStructure(product_type)
         # There should be no repeated product type in the output
         self.assertEqual(len(product_types), len(set(pt["ID"] for pt in product_types)))
+        # add alias for product type - should still work
+        products = self.dag.product_types_config
+        products["S2_MSI_L1C"]["alias"] = "S2_MSI_ALIAS"
+        product_types = self.dag.list_product_types(fetch_providers=False)
+        for product_type in product_types:
+            self.assertListProductTypesRightStructure(product_type)
+        # There should be no repeated product type in the output
+        self.assertEqual(len(product_types), len(set(pt["ID"] for pt in product_types)))
+        # use alias as id
+        self.assertIn("S2_MSI_ALIAS", [pt["ID"] for pt in product_types])
 
     def test_list_product_types_for_provider_ok(self):
         """Core api must correctly return the list of supported product types for a given provider"""
@@ -403,9 +417,14 @@ class TestCore(TestCoreBase):
             self.assertIsInstance(product_types, list)
             for product_type in product_types:
                 self.assertListProductTypesRightStructure(product_type)
-                self.assertIn(
-                    provider, self.SUPPORTED_PRODUCT_TYPES[product_type["ID"]]
-                )
+                if product_type["ID"] in self.SUPPORTED_PRODUCT_TYPES:
+                    self.assertIn(
+                        provider, self.SUPPORTED_PRODUCT_TYPES[product_type["ID"]]
+                    )
+                else:
+                    self.assertIn(
+                        provider, self.SUPPORTED_PRODUCT_TYPES[product_type["_id"]]
+                    )
 
     def test_list_product_types_for_unsupported_provider(self):
         """Core api must raise UnsupportedProvider error for list_product_types with unsupported provider"""
@@ -786,7 +805,10 @@ class TestCore(TestCoreBase):
         self.assertIn("platformSerialIdentifier", structure)
         self.assertIn("processingLevel", structure)
         self.assertIn("sensorType", structure)
-        self.assertIn(structure["ID"], self.SUPPORTED_PRODUCT_TYPES)
+        assert (
+            structure["ID"] in self.SUPPORTED_PRODUCT_TYPES
+            or structure["_id"] in self.SUPPORTED_PRODUCT_TYPES
+        )
 
     @mock.patch("eodag.api.core.open_dir", autospec=True)
     @mock.patch("eodag.api.core.exists_in", autospec=True, return_value=True)
@@ -980,6 +1002,94 @@ class TestCore(TestCoreBase):
         # run a 2nd time: check that it does not raise an error
         self.dag.update_providers_config(new_config)
 
+    def test_get_queryables(self):
+        """get_queryables must return queryables list adapted to provider and product-type"""
+        with self.assertRaises(UnsupportedProvider):
+            self.dag.get_queryables(provider="not_existing_provider")
+
+        with self.assertRaises(UnsupportedProductType):
+            self.dag.get_queryables(product_type="not_existing_product_type")
+
+        expected_result = {"productType", "start", "end", "geom", "locations", "id"}
+        queryables = self.dag.get_queryables()
+        self.assertSetEqual(queryables, expected_result)
+
+        expected_result = {
+            "start",
+            "end",
+            "geom",
+            "locations",
+            "productType",
+            "platformSerialIdentifier",
+            "instrument",
+            "processingLevel",
+            "resolution",
+            "organisationName",
+            "parentIdentifier",
+            "orbitNumber",
+            "orbitDirection",
+            "swathIdentifier",
+            "cloudCover",
+            "snowCover",
+            "sensorMode",
+            "polarizationMode",
+            "id",
+            "tileIdentifier",
+            "geometry",
+        }
+        queryables = self.dag.get_queryables(provider="peps")
+        self.assertSetEqual(queryables, expected_result)
+
+        expected_result = {
+            "start",
+            "end",
+            "geom",
+            "locations",
+            "productType",
+            "platformSerialIdentifier",
+            "instrument",
+            "processingLevel",
+            "resolution",
+            "organisationName",
+            "parentIdentifier",
+            "orbitNumber",
+            "orbitDirection",
+            "swathIdentifier",
+            "snowCover",
+            "sensorMode",
+            "polarizationMode",
+            "id",
+            "tileIdentifier",
+            "geometry",
+        }
+        queryables = self.dag.get_queryables(provider="peps", product_type="S1_SAR_GRD")
+        self.assertSetEqual(queryables, expected_result)
+
+        expected_result = {"productType", "start", "end", "geom", "locations", "id"}
+        queryables = self.dag.get_queryables(product_type="S2_MSI_L1C")
+        self.assertSetEqual(queryables, expected_result)
+
+        expected_result = {
+            "productType",
+            "start",
+            "end",
+            "geom",
+            "locations",
+            "geometry",
+            "platformSerialIdentifier",
+            "title",
+            "cloudCover",
+            "illuminationAzimuthAngle",
+            "illuminationZenithAngle",
+            "awsPath",
+            "productPath",
+            "id",
+        }
+        queryables = self.dag.get_queryables(
+            provider="aws_eos", product_type="S2_MSI_L1C"
+        )
+        self.assertSetEqual(queryables, expected_result)
+
 
 class TestCoreConfWithEnvVar(TestCoreBase):
     @classmethod
@@ -1468,6 +1578,19 @@ class TestCoreSearch(TestCoreBase):
         finally:
             self.dag.set_preferred_provider(prev_fav_provider)
 
+    def test__prepare_search_peps_plugins_product_available_with_alias(self):
+        """_prepare_search must return the search plugins when productType is defined and alias is used"""
+        products = self.dag.product_types_config
+        products["S2_MSI_L1C"]["alias"] = "S2_MSI_ALIAS"
+        prev_fav_provider = self.dag.get_preferred_provider()[0]
+        try:
+            self.dag.set_preferred_provider("peps")
+            base = {"productType": "S2_MSI_ALIAS"}
+            search_plugins, _ = self.dag._prepare_search(**base)
+            self.assertEqual(search_plugins[0].provider, "peps")
+        finally:
+            self.dag.set_preferred_provider(prev_fav_provider)
+
     def test__prepare_search_no_plugins_when_search_by_id(self):
         """_prepare_search must not return the search and auth plugins for a search by id"""
         base = {"id": "some_id", "provider": "some_provider"}
@@ -2062,3 +2185,38 @@ class TestCoreDownload(TestCoreBase):
         with self.assertLogs(level="INFO") as cm:
             self.dag.download(product)
             self.assertIn("Local product detected. Download skipped", str(cm.output))
+
+
+class TestCoreProductAlias(TestCoreBase):
+    @classmethod
+    def setUpClass(cls):
+        super(TestCoreProductAlias, cls).setUpClass()
+        cls.dag = EODataAccessGateway()
+        products = cls.dag.product_types_config
+        products["S2_MSI_L1C"]["alias"] = "S2_MSI_ALIAS"
+
+    def test_get_alias_from_product_type(self):
+        # return product alias
+        self.assertEqual(
+            "S2_MSI_ALIAS", self.dag.get_alias_from_product_type("S2_MSI_L1C")
+        )
+        # product type without alias
+        self.assertEqual(
+            "S1_SAR_GRD", self.dag.get_alias_from_product_type("S1_SAR_GRD")
+        )
+        # not existing product type
+        with self.assertRaises(NoMatchingProductType):
+            self.dag.get_alias_from_product_type("JUST_A_TYPE")
+
+    def test_get_product_type_from_alias(self):
+        # return product id
+        self.assertEqual(
+            "S2_MSI_L1C", self.dag.get_product_type_from_alias("S2_MSI_ALIAS")
+        )
+        # product type without alias
+        self.assertEqual(
+            "S1_SAR_GRD", self.dag.get_product_type_from_alias("S1_SAR_GRD")
+        )
+        # not existing product type
+        with self.assertRaises(NoMatchingProductType):
+            self.dag.get_product_type_from_alias("JUST_A_TYPE")
diff --git a/tests/units/test_http_server.py b/tests/units/test_http_server.py
index 538bdc6e..bba3adf8 100644
--- a/tests/units/test_http_server.py
+++ b/tests/units/test_http_server.py
@@ -31,6 +31,7 @@ from shapely.geometry import box
 from tests import mock
 from tests.context import (
     DEFAULT_ITEMS_PER_PAGE,
+    TEST_RESOURCES_PATH,
     AuthenticationError,
     SearchResult,
     parse_header,
@@ -48,6 +49,11 @@ class RequestTestCase(unittest.TestCase):
 
         cls.tested_product_type = "S2_MSI_L1C"
 
+        # load fake credentials to prevent providers needing auth for search to be pruned
+        os.environ["EODAG_CFG_FILE"] = os.path.join(
+            TEST_RESOURCES_PATH, "wrong_credentials_conf.yml"
+        )
+
         # Mock home and eodag conf directory to tmp dir
         cls.tmp_home_dir = TemporaryDirectory()
         cls.expanduser_mock = mock.patch(
@@ -679,6 +685,7 @@ class RequestTestCase(unittest.TestCase):
                 "id": "foo",
                 "provider": None,
                 "productType": self.tested_product_type,
+                "_dc_qs": None,
             },
         )
 
@@ -690,6 +697,7 @@ class RequestTestCase(unittest.TestCase):
                 "id": "foo",
                 "provider": None,
                 "productType": self.tested_product_type,
+                "_dc_qs": None,
             },
         )
 
@@ -829,11 +837,6 @@ class RequestTestCase(unittest.TestCase):
         response = self._request_valid(f"search?collections={self.tested_product_type}")
         self.assertIn("numberMatched", response)
         self.assertIn("numberReturned", response)
-        self.assertIn("context", response)
-        self.assertEqual(1, response["context"]["page"])
-        self.assertEqual(DEFAULT_ITEMS_PER_PAGE, response["context"]["limit"])
-        self.assertIn("matched", response["context"])
-        self.assertIn("returned", response["context"])
 
     def test_search_provider_in_downloadlink(self):
         """Search through eodag server and check that specified provider appears in downloadLink"""
@@ -972,7 +975,7 @@ class RequestTestCase(unittest.TestCase):
         self.assertEqual(response_filename, expected_file)
 
     @mock.patch(
-        "eodag.plugins.apis.cds.CdsApi.authenticate",
+        "eodag.plugins.apis.usgs.UsgsApi.authenticate",
         autospec=True,
     )
     @mock.patch(
@@ -990,13 +993,13 @@ class RequestTestCase(unittest.TestCase):
         # use an external python API provider for this test and reset downloader
         self._request_valid_raw.patchings[0].kwargs["return_value"][0][
             0
-        ].provider = "cop_cds"
+        ].provider = "usgs"
         self._request_valid_raw.patchings[0].kwargs["return_value"][0][
             0
         ].downloader = None
 
         self._request_valid_raw(
-            "collections/some-collection/items/foo/download?provider=cop_cds"
+            "collections/some-collection/items/foo/download?provider=usgs"
         )
         mock_download.assert_called_once()
         # downloaded file should have been immediatly deleted from the server
diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py
index 1a9db714..6e22aa91 100644
--- a/tests/units/test_search_plugins.py
+++ b/tests/units/test_search_plugins.py
@@ -1073,7 +1073,6 @@ class TestSearchPluginODataV4Search(BaseSearchPluginTest):
         geojson_geometry = self.search_criteria_s2_msi_l1c["geometry"].__geo_interface__
         mock__request.return_value = mock.Mock()
         result = {
-            "context": {"matched": 1},
             "features": [
                 {
                     "id": "foo",
@@ -1117,7 +1116,6 @@ class TestSearchPluginStacSearch(BaseSearchPluginTest):
         mock__request.return_value = mock.Mock()
         mock__request.return_value.json.side_effect = [
             {
-                "context": {"page": 1, "limit": 2, "matched": 1, "returned": 2},
                 "features": [
                     {
                         "id": "foo",
@@ -1171,7 +1169,6 @@ class TestSearchPluginStacSearch(BaseSearchPluginTest):
         mock__request.return_value = mock.Mock()
         mock__request.return_value.json.side_effect = [
             {
-                "context": {"matched": 3},
                 "features": [
                     {
                         "id": "foo",
@@ -1204,7 +1201,6 @@ class TestSearchPluginStacSearch(BaseSearchPluginTest):
         """The metadata mapping for a stac provider should not mix specific product-types metadata-mapping"""
         mock__request.return_value = mock.Mock()
         result = {
-            "context": {"matched": 1},
             "features": [
                 {
                     "id": "foo",
diff --git a/tests/units/test_stac_utils.py b/tests/units/test_stac_utils.py
index 265a8d7c..6b3fd0f1 100644
--- a/tests/units/test_stac_utils.py
+++ b/tests/units/test_stac_utils.py
@@ -486,3 +486,69 @@ class TestStacUtils(unittest.TestCase):
         )
         # check that no other asset have also been added to the response
         self.assertEqual(len(response["features"][0]["assets"]), 2)
+
+    @mock.patch(
+        "eodag.plugins.search.qssearch.QueryStringSearch._request",
+        autospec=True,
+    )
+    def test_search_stac_items_get(self, mock__request):
+        """search_stac_items runs with GET method"""
+        # mock the QueryStringSearch request with the S2_MSI_L1C peps response search dictionary
+        mock__request.return_value = mock.Mock()
+        mock__request.return_value.json.return_value = self.peps_resp_search_json
+
+        response = self.rest_utils.search_stac_items(
+            url="http://foo/search",
+            arguments={"collections": "S2_MSI_L1C"},
+            root="http://foo/",
+            method="GET",
+        )
+
+        mock__request.assert_called()
+
+        next_link = [link for link in response["links"] if link["rel"] == "next"][0]
+
+        self.assertEqual(
+            next_link,
+            {
+                "method": "GET",
+                "body": None,
+                "rel": "next",
+                "href": "http://foo/search?collections=S2_MSI_L1C&page=2",
+                "title": "Next page",
+                "type": "application/geo+json",
+            },
+        )
+
+    @mock.patch(
+        "eodag.plugins.search.qssearch.QueryStringSearch._request",
+        autospec=True,
+    )
+    def test_search_stac_items_post(self, mock__request):
+        """search_stac_items runs with GET method"""
+        # mock the QueryStringSearch request with the S2_MSI_L1C peps response search dictionary
+        mock__request.return_value = mock.Mock()
+        mock__request.return_value.json.return_value = self.peps_resp_search_json
+
+        response = self.rest_utils.search_stac_items(
+            url="http://foo/search",
+            arguments={"collections": ["S2_MSI_L1C"], "page": 2},
+            root="http://foo/",
+            method="POST",
+        )
+
+        mock__request.assert_called()
+
+        next_link = [link for link in response["links"] if link["rel"] == "next"][0]
+
+        self.assertEqual(
+            next_link,
+            {
+                "method": "POST",
+                "rel": "next",
+                "href": "http://foo/search",
+                "title": "Next page",
+                "type": "application/geo+json",
+                "body": {"collections": ["S2_MSI_L1C"], "page": 3},
+            },
+        )