[Git][debian-gis-team/eodag][master] 5 commits: Test rebuild.

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Thu Oct 8 17:36:10 BST 2026



Bas Couwenberg pushed to branch master at Debian GIS Project / eodag


Commits:
fef7f5d8 by Bas Couwenberg at 2026-10-08T18:23:36+02:00
Test rebuild.

- - - - -
146ee8d0 by Bas Couwenberg at 2026-10-08T18:27:13+02:00
New upstream version 4.9.2+ds
- - - - -
0d542588 by Bas Couwenberg at 2026-10-08T18:27:20+02:00
Update upstream source from tag 'upstream/4.9.2+ds'

Update to upstream version '4.9.2+ds'
with Debian dir c0905524cf66d7e83c1364e4e5c36744f441dc89
- - - - -
72cf8e58 by Bas Couwenberg at 2026-10-08T18:29:10+02:00
New upstream release.

- - - - -
cf65e084 by Bas Couwenberg at 2026-10-08T18:31:57+02:00
Refresh patches.

- - - - -


16 changed files:

- CHANGES.rst
- debian/changelog
- debian/patches/0001-System-importlib.metadata.patch
- debian/patches/0002-no-mypy-boto3-s3.patch
- docs/providers.rst
- eodag/api/product/_product.py
- eodag/api/product/metadata_mapping.py
- eodag/plugins/authentication/sas_auth.py
- eodag/plugins/manager.py
- eodag/plugins/search/qssearch.py
- eodag/resources/ext_collections.json
- eodag/resources/providers/planetary_computer.yml
- eodag/resources/providers/wekeo_main.yml
- pyproject.toml
- tests/units/test_auth_plugins.py
- tests/units/test_crunch.py


Changes:

=====================================
CHANGES.rst
=====================================
@@ -3,6 +3,46 @@ Release history
 ===============
 
 
+v4.9.2 (2026-10-07)
+===================
+
+Bug Fixes
+---------
+
+* **plugins**: SASAuth per collection cached token (`#2374`_, `8530060`_)
+
+Build System
+------------
+
+* Jsonpath-ng update (`#2380`_, `f3d4ae9`_)
+
+Documentation
+-------------
+
+* Update registration info for dedl provider (`#2379`_, `66d2d05`_)
+
+Refactoring
+-----------
+
+* **providers**: Update wekeo_main metadata mapping (`#2361`_, `f89e751`_)
+
+Testing
+-------
+
+* Adapt to shapely 2.2.0 (`#2378`_, `d0e0c5b`_)
+
+.. _#2361: https://github.com/CS-SI/eodag/pull/2361
+.. _#2374: https://github.com/CS-SI/eodag/pull/2374
+.. _#2378: https://github.com/CS-SI/eodag/pull/2378
+.. _#2379: https://github.com/CS-SI/eodag/pull/2379
+.. _#2380: https://github.com/CS-SI/eodag/pull/2380
+.. _66d2d05: https://github.com/CS-SI/eodag/commit/66d2d05b9a92bdfcc11645d7bf10bcf55aba1a0d
+.. _8530060: https://github.com/CS-SI/eodag/commit/853006005031d4de3e22f83e87da4fb1177a87b5
+.. _d0e0c5b: https://github.com/CS-SI/eodag/commit/d0e0c5b8f5dfdc1ecb65d49c3d8a39c89209a1fd
+.. _f3d4ae9: https://github.com/CS-SI/eodag/commit/f3d4ae9ef654454b3f7084b2091deeb26e1d0793
+.. _f89e751: https://github.com/CS-SI/eodag/commit/f89e7510ecb16a855a0beaa6136bbe2a6995d766
+
+
 v4.9.1 (2026-09-23)
 ===================
 


=====================================
debian/changelog
=====================================
@@ -1,3 +1,11 @@
+eodag (4.9.2+ds-1) UNRELEASED; urgency=medium
+
+  * Team upload.
+  * New upstream release.
+  * Refresh patches.
+
+ -- Bas Couwenberg <sebastic at debian.org>  Thu, 08 Oct 2026 18:28:44 +0200
+
 eodag (4.9.1+ds-1) unstable; urgency=medium
 
   * New upstream release.


=====================================
debian/patches/0001-System-importlib.metadata.patch
=====================================
@@ -11,8 +11,6 @@ Forwarded: https://github.com/CS-SI/eodag/pull/1814
  tests/units/test_core.py   | 2 +-
  5 files changed, 8 insertions(+), 9 deletions(-)
 
-diff --git a/eodag/plugins/base.py b/eodag/plugins/base.py
-index ed80918..de12cca 100644
 --- a/eodag/plugins/base.py
 +++ b/eodag/plugins/base.py
 @@ -19,7 +19,7 @@ from __future__ import annotations
@@ -24,7 +22,7 @@ index ed80918..de12cca 100644
  
  from eodag.utils.exceptions import PluginNotFoundError
  
-@@ -76,7 +76,7 @@ class PluginTopic(metaclass=EODAGPluginMount):
+@@ -76,7 +76,7 @@ class PluginTopic(metaclass=EODAGPluginM
  
          topic_group = cls.entrypoint_group or cls.__name__.lower()
  
@@ -33,8 +31,6 @@ index ed80918..de12cca 100644
              group=f"eodag.plugins.{topic_group}"
          ):
              try:
-diff --git a/eodag/plugins/manager.py b/eodag/plugins/manager.py
-index c2f6f64..975954e 100644
 --- a/eodag/plugins/manager.py
 +++ b/eodag/plugins/manager.py
 @@ -24,7 +24,7 @@ from collections.abc import Iterator
@@ -64,8 +60,6 @@ index c2f6f64..975954e 100644
      ) -> list[pathlib.Path]:
          """Return provider config paths exposed by an external plugin distribution."""
          config_paths: list[pathlib.Path] = []
-diff --git a/pyproject.toml b/pyproject.toml
-index 872fa49..155a74b 100644
 --- a/pyproject.toml
 +++ b/pyproject.toml
 @@ -39,7 +39,6 @@ dependencies = [
@@ -73,11 +67,9 @@ index 872fa49..155a74b 100644
      "click",
      "geojson",
 -    "importlib_metadata >= 5.0",
-     # temporarily pinned, see https://github.com/h2non/jsonpath-ng/issues/216
-     "jsonpath-ng < 1.8.0",
+     "jsonpath-ng != 1.8.0",
      "lxml",
-diff --git a/tests/test_requirements.py b/tests/test_requirements.py
-index 20afa62..1848ac9 100644
+     "orjson",
 --- a/tests/test_requirements.py
 +++ b/tests/test_requirements.py
 @@ -23,7 +23,7 @@ import sys
@@ -89,7 +81,7 @@ index 20afa62..1848ac9 100644
  from packaging.requirements import Requirement
  from packaging.utils import canonicalize_name
  from stdlib_list import stdlib_list
-@@ -138,7 +138,7 @@ class TestRequirements(unittest.TestCase):
+@@ -138,7 +138,7 @@ class TestRequirements(unittest.TestCase
          project_imports = get_project_imports(project_path)
          setup_requires = get_setup_requires(pyproject_path)
          setup_requires.update(get_optional_dependencies(pyproject_path, "all"))
@@ -98,8 +90,6 @@ index 20afa62..1848ac9 100644
          try:
              default_libs = stdlib_list()
          except FileNotFoundError:
-diff --git a/tests/units/test_core.py b/tests/units/test_core.py
-index a2149d4..9ac72eb 100644
 --- a/tests/units/test_core.py
 +++ b/tests/units/test_core.py
 @@ -1690,7 +1690,7 @@ class TestCore(TestCoreBase):


=====================================
debian/patches/0002-no-mypy-boto3-s3.patch
=====================================
@@ -7,14 +7,12 @@ Forwarded: not-needed
  tests/units/test_auth_plugins.py | 8 +++++++-
  1 file changed, 7 insertions(+), 1 deletion(-)
 
-diff --git a/tests/units/test_auth_plugins.py b/tests/units/test_auth_plugins.py
-index 5b56aef..b3c2b1b 100644
 --- a/tests/units/test_auth_plugins.py
 +++ b/tests/units/test_auth_plugins.py
-@@ -26,7 +26,12 @@ from unittest import mock
- import boto3
+@@ -28,7 +28,12 @@ import boto3
  import requests
  import responses
+ from concurrent.futures import ThreadPoolExecutor
 -from mypy_boto3_s3.service_resource import BucketObjectsCollection
 +try:
 +    from mypy_boto3_s3.service_resource import BucketObjectsCollection
@@ -25,7 +23,7 @@ index 5b56aef..b3c2b1b 100644
  from pystac.utils import now_in_utc
  from requests import Request, Response, Timeout
  from requests.auth import AuthBase
-@@ -814,6 +819,7 @@ class TestAuthPluginAwsAuth(BaseAuthPluginTest):
+@@ -817,6 +822,7 @@ class TestAuthPluginAwsAuth(BaseAuthPlug
          keys_dict = {"aws_profile": self.profile_name}
          self.assertDictEqual(keys_dict, plugin_auth_profile.config.credentials)
  


=====================================
docs/providers.rst
=====================================
@@ -890,7 +890,7 @@ No credentials are needed
       :columns: 2
       :class: sd-text-right
 
-      .. button-link:: https://hda.data.destination-earth.eu/ui
+      .. button-link:: https://platform.destine.eu
         :color: primary
         :outline:
         :tooltip: DEDL website
@@ -905,8 +905,8 @@ No credentials are needed
   You need a `DESP OpenID` account in order to authenticate.
 
   To create one go `here
-  <https://hda.data.destination-earth.eu/ui>`__, then click on `Sign In`, select the identity provider `DESP OpenID` and
-  then click `Authenticate`. Finally click on `Register` to create a new account.
+  <https://platform.destine.eu>`__, then click on `Sign In`, and then `Register` to create a new account.
+  Then use your ``username``, ``password`` in eodag credentials.
 
 ----
 


=====================================
eodag/api/product/_product.py
=====================================
@@ -65,7 +65,6 @@ from eodag.api.product.metadata_mapping import (
     NOT_AVAILABLE,
     NOT_MAPPED,
     OFFLINE_STATUS,
-    ONLINE_STATUS,
     normalize_bands,
 )
 from eodag.utils import (
@@ -484,25 +483,7 @@ class EOProduct:
                                 the download and authentication plugins.
         """
         download_plugin = plugins_manager.get_download_plugin(self)
-        if len(self.assets) > 0:
-            matching_url = next(iter(self.assets.values()))["href"]
-        elif self.properties.get("order:status") != ONLINE_STATUS:
-            matching_url = self.properties.get(
-                "eodag:order_link"
-            ) or self.properties.get("eodag:download_link")
-        else:
-            matching_url = self.properties.get("eodag:download_link")
-
-        try:
-            auth_plugin = next(
-                plugins_manager.get_auth_plugins(
-                    self.provider,
-                    matching_url=matching_url,
-                    matching_conf=download_plugin.config,
-                )
-            )
-        except StopIteration:
-            auth_plugin = None
+        auth_plugin = plugins_manager.get_auth_plugin(download_plugin, self)
         self.register_downloader(download_plugin, auth_plugin)
 
     def register_downloader(


=====================================
eodag/api/product/metadata_mapping.py
=====================================
@@ -754,7 +754,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
         def convert_dict_filter(
             input_dict: dict[Any, Any], jsonpath_filter_str: str
         ) -> dict[Any, Any]:
-            """Fitlers dict items using jsonpath"""
+            """Filters dict items using jsonpath"""
 
             jsonpath_filter = string_to_jsonpath(jsonpath_filter_str, force=True)
             if isinstance(jsonpath_filter, str) or not isinstance(input_dict, dict):
@@ -765,9 +765,17 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
             result = {}
             for match in matches:
                 # extract key index from matched jsonpath
-                matched_jsonpath_str = str(match.full_path)
-                matched_index = int(matched_jsonpath_str.split(".")[-1][1:-1])
-                key = keys_list[matched_index]
+                if not hasattr(match.path, "index") and not hasattr(
+                    match.path, "fields"
+                ):
+                    continue
+                key = (
+                    # jsonpath-ng <= 1.7.0
+                    keys_list[match.path.index]
+                    if hasattr(match.path, "index")
+                    # jsonpath-ng >= 1.9.0
+                    else match.path.fields[0]
+                )
                 result[key] = match.value
             return result
 
@@ -775,7 +783,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
         def convert_dict_filter_and_sub(
             input_dict: dict[Any, Any], args: str
         ) -> Union[dict[Any, Any], list[Any]]:
-            """Fitlers dict items using jsonpath and then apply recursive_sub_str"""
+            """Filters dict items using jsonpath and then apply recursive_sub_str"""
             jsonpath_filter_str, old, new = ast.literal_eval(args)
             filtered = MetadataFormatter.convert_dict_filter(
                 input_dict, jsonpath_filter_str


=====================================
eodag/plugins/authentication/sas_auth.py
=====================================
@@ -19,15 +19,29 @@ from __future__ import annotations
 
 import logging
 import re
+import threading
+from copy import copy
+from datetime import datetime, timedelta, timezone
 from json import JSONDecodeError
 from typing import TYPE_CHECKING, Optional
+from urllib.parse import urlsplit, urlunsplit
 
 import requests
+from requests.adapters import HTTPAdapter
 from requests.auth import AuthBase
+from urllib3 import Retry
 
 from eodag.api.product._assets import Asset
 from eodag.plugins.authentication.base import Authentication
-from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, deepcopy, format_dict_items
+from eodag.utils import (
+    HTTP_REQ_TIMEOUT,
+    REQ_RETRY_BACKOFF_FACTOR,
+    REQ_RETRY_STATUS_FORCELIST,
+    REQ_RETRY_TOTAL,
+    USER_AGENT,
+    deepcopy,
+    format_dict_items,
+)
 from eodag.utils.exceptions import AuthenticationError, TimeOutError
 
 if TYPE_CHECKING:
@@ -38,6 +52,32 @@ if TYPE_CHECKING:
 
 logger = logging.getLogger("eodag.auth.sas_auth")
 
+# Tokens are shared by all RequestsSASAuth instances to avoid one token request per asset/product.
+_SAS_TOKENS: dict[str, tuple[str, Optional[datetime]]] = {}
+_SAS_TOKENS_LOCK = threading.Lock()
+_SAS_FETCH_LOCKS: dict[str, threading.Lock] = {}
+_SAS_EXPIRY_MARGIN = timedelta(seconds=60)
+
+
+def _parse_expiry(value: object) -> Optional[datetime]:
+    """Parse a token expiry date, return ``None`` if missing or invalid"""
+    try:
+        expiry = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
+    except ValueError:
+        return None
+    return expiry if expiry.tzinfo else expiry.replace(tzinfo=timezone.utc)
+
+
+def _get_cached_token(keys: list[str]) -> Optional[str]:
+    """Return the first non-expired cached token found for the given keys"""
+    now = datetime.now(timezone.utc)
+    with _SAS_TOKENS_LOCK:
+        for key in keys:
+            entry = _SAS_TOKENS.get(key)
+            if entry and (entry[1] is None or entry[1] - _SAS_EXPIRY_MARGIN > now):
+                return entry[0]
+    return None
+
 
 class RequestsSASAuth(AuthBase):
     """A custom authentication class to be used with requests module"""
@@ -49,13 +89,66 @@ class RequestsSASAuth(AuthBase):
         headers: Optional[dict[str, str]] = None,
         ssl_verify: bool = True,
         matching_url: Optional[Pattern[str]] = None,
+        provider_collection: Optional[str] = None,
+        retry_total: int = REQ_RETRY_TOTAL,
+        retry_backoff_factor: int = REQ_RETRY_BACKOFF_FACTOR,
+        retry_status_forcelist: Optional[list[int]] = None,
     ) -> None:
         self.auth_uri = auth_uri
         self.signed_url_key = signed_url_key
         self.headers = headers
-        self.signed_urls: dict[str, str] = {}
         self.ssl_verify = ssl_verify
         self.matching_url = matching_url
+        self.provider_collection = provider_collection
+        self.retry_total = retry_total
+        self.retry_backoff_factor = retry_backoff_factor
+        self.retry_status_forcelist = (
+            REQ_RETRY_STATUS_FORCELIST
+            if retry_status_forcelist is None
+            else retry_status_forcelist
+        )
+
+    @staticmethod
+    def _get_container_url(url: str) -> Optional[str]:
+        """Return the Azure blob container URL, or None for other URLs."""
+        parsed_url = urlsplit(url)
+        path_parts = parsed_url.path.strip("/").split("/", 1)
+        if (
+            parsed_url.hostname
+            and parsed_url.hostname.endswith(".blob.core.windows.net")
+            and path_parts[0]
+        ):
+            return f"{parsed_url.scheme}://{parsed_url.netloc}/{path_parts[0]}"
+        return None
+
+    @staticmethod
+    def _apply_sas_token(url: str, signed_url: str) -> Optional[str]:
+        """Apply a cached SAS token to another blob URL in the same container."""
+        parsed_url = urlsplit(url)
+        parsed_signed_url = urlsplit(signed_url)
+        container_key = RequestsSASAuth._get_container_url(url)
+        if not container_key:
+            return None
+
+        if parsed_signed_url.scheme and parsed_signed_url.netloc:
+            if RequestsSASAuth._get_container_url(signed_url) != container_key:
+                return None
+            token = parsed_signed_url.query
+        else:
+            token = signed_url.lstrip("?")
+
+        if not token:
+            return None
+
+        return urlunsplit(
+            (
+                parsed_url.scheme,
+                parsed_url.netloc,
+                parsed_url.path,
+                token,
+                parsed_url.fragment,
+            )
+        )
 
     def __call__(self, request: PreparedRequest) -> PreparedRequest:
         """Perform the actual authentication"""
@@ -72,29 +165,75 @@ class RequestsSASAuth(AuthBase):
             for k, v in self.headers.items():
                 request.headers[k] = v
 
-        # request the signed_url
-        req_signed_url = self.auth_uri.format(url=request.url)
-        if req_signed_url not in self.signed_urls.keys():
-            logger.debug(f"Signed URL request: {req_signed_url}")
-            try:
-                response = requests.get(
-                    req_signed_url,
-                    headers=self.headers,
-                    timeout=HTTP_REQ_TIMEOUT,
-                    verify=self.ssl_verify,
+        request.url = self.sign_url(request.url or "")
+
+        return request
+
+    def sign_url(self, url: str) -> str:
+        """Return ``url`` signed with a (cached or freshly requested) SAS token"""
+        # Azure SAS tokens are scoped to a container, not an individual blob.
+        container_key = self._get_container_url(url)
+        if "{_collection}" in self.auth_uri and not self.provider_collection:
+            raise AuthenticationError(
+                "A provider collection is required to request a SAS token"
+            )
+        req_signed_url = self.auth_uri.format(
+            url=url, _collection=self.provider_collection or ""
+        )
+
+        cache_keys = [k for k in (container_key, req_signed_url) if k]
+        signed_url = _get_cached_token(cache_keys)
+        if signed_url is None:
+            # one fetch at a time per container, other threads then reuse its result
+            with _SAS_TOKENS_LOCK:
+                fetch_lock = _SAS_FETCH_LOCKS.setdefault(
+                    cache_keys[0], threading.Lock()
                 )
-                response.raise_for_status()
-                signed_url = response.json().get(self.signed_url_key)
-            except requests.exceptions.Timeout as exc:
-                raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
-            except (requests.RequestException, JSONDecodeError, KeyError) as e:
-                raise AuthenticationError("Could no get signed url", str(e)) from e
-            else:
-                self.signed_urls[req_signed_url] = signed_url
+            with fetch_lock:
+                signed_url = _get_cached_token(cache_keys)
+                if signed_url is None:
+                    signed_url = self._fetch_signed_url(
+                        url, req_signed_url, container_key
+                    )
 
-        request.url = self.signed_urls[req_signed_url]
+        return self._apply_sas_token(url, signed_url) or signed_url
 
-        return request
+    def _fetch_signed_url(
+        self, url: str, req_signed_url: str, container_key: Optional[str]
+    ) -> str:
+        """Request a signed url / token and store it in the shared cache"""
+        logger.debug(f"Signed URL request: {req_signed_url}")
+        try:
+            session = requests.Session()
+            retries = Retry(
+                total=self.retry_total,
+                backoff_factor=self.retry_backoff_factor,
+                status_forcelist=self.retry_status_forcelist,
+            )
+            session.mount(req_signed_url, HTTPAdapter(max_retries=retries))
+            response = session.get(
+                req_signed_url,
+                headers=self.headers,
+                timeout=HTTP_REQ_TIMEOUT,
+                verify=self.ssl_verify,
+            )
+            response.raise_for_status()
+            body = response.json()
+            signed_url = body[self.signed_url_key]
+        except requests.exceptions.Timeout as exc:
+            raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
+        except (requests.RequestException, JSONDecodeError, KeyError) as e:
+            raise AuthenticationError("Could no get signed url", str(e)) from e
+
+        cache_key = (
+            container_key
+            if container_key and self._apply_sas_token(url, signed_url)
+            else req_signed_url
+        )
+        expiry = _parse_expiry(body.get("msft:expiry"))
+        with _SAS_TOKENS_LOCK:
+            _SAS_TOKENS[cache_key] = (signed_url, expiry)
+        return signed_url
 
 
 class SASAuth(Authentication):
@@ -114,15 +253,27 @@ class SASAuth(Authentication):
           apiKey is used**): headers to be added to the requests
         * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should be
           verified in the requests; default: ``True``
+        * :attr:`~eodag.config.PluginConfig.retry_total` (``int``): total number of retries; default: ``3``
+        * :attr:`~eodag.config.PluginConfig.retry_backoff_factor` (``int``): retry backoff factor; default: ``2``
+        * :attr:`~eodag.config.PluginConfig.retry_status_forcelist` (``list[int]``): HTTP status codes to retry;
+          default: ``[401, 429, 500, 502, 503, 504]``
 
     """
 
+    provider_collection: Optional[str] = None
+
     def validate_config_credentials(self) -> None:
         """Validate configured credentials"""
         # credentials are optionnal
         pass
 
-    def authenticate(self) -> AuthBase:
+    def bind_collection(self, provider_collection: str) -> SASAuth:
+        """Return a product-specific auth plugin using its provider collection ID."""
+        auth_plugin = copy(self)
+        auth_plugin.provider_collection = provider_collection
+        return auth_plugin
+
+    def authenticate(self) -> RequestsSASAuth:
         """Authenticate"""
         self.validate_config_credentials()
 
@@ -140,12 +291,23 @@ class SASAuth(Authentication):
             )
             headers.update(headers_update)
 
+        provider_collection = getattr(self, "provider_collection", None)
         return RequestsSASAuth(
             auth_uri=self.config.auth_uri,
             signed_url_key=self.config.signed_url_key,
             headers=headers,
             ssl_verify=ssl_verify,
             matching_url=matching_url,
+            provider_collection=provider_collection,
+            retry_total=getattr(self.config, "retry_total", REQ_RETRY_TOTAL),
+            retry_backoff_factor=getattr(
+                self.config, "retry_backoff_factor", REQ_RETRY_BACKOFF_FACTOR
+            ),
+            retry_status_forcelist=getattr(
+                self.config,
+                "retry_status_forcelist",
+                REQ_RETRY_STATUS_FORCELIST,
+            ),
         )
 
     def presign_url(
@@ -156,8 +318,7 @@ class SASAuth(Authentication):
         """This method is used to presign a url to download an asset.
 
         :param asset: asset for which the url shall be presigned
-        :param expires_in: expiration time of the presigned url in seconds
+        :param expires_in: ignored, the token lifetime is set by the provider
         :returns: presigned url
         """
-        url = asset["href"]
-        return self.config.auth_uri.format(url=url)
+        return self.authenticate().sign_url(asset["href"])


=====================================
eodag/plugins/manager.py
=====================================
@@ -332,6 +332,27 @@ class PluginManager:
             )
         except StopIteration:
             auth_plugin = None
+        if auth_plugin and product is not None and product.collection:
+            bind_collection = getattr(auth_plugin, "bind_collection", None)
+            if bind_collection:
+                provider_config = self.providers.get_config(product.provider)
+                collection_def_config = (
+                    provider_config.products.get(product.collection, {})
+                    if provider_config
+                    else {}
+                )
+                if not collection_def_config and provider_config:
+                    collection_def_config = provider_config.products.get(
+                        GENERIC_COLLECTION, {}
+                    )
+                provider_collection = collection_def_config.get(
+                    "_collection", product.collection
+                )
+                if isinstance(provider_collection, str):
+                    provider_collection = provider_collection.format(
+                        collection=product.collection
+                    )
+                auth_plugin = bind_collection(provider_collection)
         return auth_plugin
 
     def get_auth_plugins(


=====================================
eodag/plugins/search/qssearch.py
=====================================
@@ -1885,21 +1885,6 @@ class PostJsonSearch(QueryStringSearch):
                     )
                     product.properties["_dc_qs"] = quote_plus(_dc_qs)
 
-            # workaround to add collection to wekeo cmems order links
-            if (
-                "eodag:order_link" in product.properties
-                and "collection" in product.properties["eodag:order_link"]
-                and "order" not in product.properties["eodag:order_link"]
-            ):
-                if product.collection is None:
-                    msg = (
-                        f"Cannot build order link for "
-                        f"{product}: collection is undefined"
-                    )
-                    raise MisconfiguredError(msg)
-                product.properties["eodag:order_link"] = product.properties[
-                    "eodag:order_link"
-                ].replace("collection", product.collection)
         return normalized
 
     def collect_search_urls(


=====================================
eodag/resources/ext_collections.json
=====================================
The diff for this file was not included because it is too large.

=====================================
eodag/resources/providers/planetary_computer.yml
=====================================
@@ -49,8 +49,8 @@ planetary_computer:
   auth:
     type: SASAuth
     matching_url: https://[-\w\.]+.blob.core.windows.net
-    auth_uri: 'https://planetarycomputer.microsoft.com/api/sas/v1/sign?href={url}'
-    signed_url_key: href
+    auth_uri: 'https://planetarycomputer.microsoft.com/api/sas/v1/token/{_collection}'
+    signed_url_key: token
     ssl_verify: true
     headers:
       Ocp-Apim-Subscription-Key: "{apikey}"


=====================================
eodag/resources/providers/wekeo_main.yml
=====================================
@@ -8,23 +8,22 @@ wekeo_main:
   # anchors to avoid duplications
   anchor_s1_sar: &s1_sar_params
     processing:level:
-      - '{{"processingLevel": "{processing:level}"}}'
-      - '$.id.`sub(/^[^_]([^_]+)_([^_]+)_([^_]+)_([0-4]+).*/, LEVEL\\4)`'
+      - '{{"processingLevel": "{processing:level#replace_str("L", "LEVEL")}"}}'
+      - '$.id.`sub(/^[^_]([^_]+)_([^_]+)_([^_]+)_([0-4]+).*/, L\\4)`'
     platform:
-      - '{{"platformSerialIdentifier": "{platform}"}}'
-      - '$.null'
-    instruments:
-      - '{{"instrumentShortName": "{instruments#csv_list}"}}'
-      - '$.null'
+      - '{{"platformSerialIdentifier": "{platform#replace_str_tuple(("sentinel-1a", "A"), ("sentinel-1b", "B"), ("sentinel-1c", "C"))}"}}'
+      - '{$.properties.metadata.platformSerialIdentifier#replace_str_tuple(("A", "sentinel-1a"), ("B", "sentinel-1b"), ("C", "sentinel-1c"))}'
+    instruments: '{$.properties.metadata.instrumentShortName#split(,)}'
     order:status:
       - '{{"online": "{order:status#get_group_name((?P<true>succeeded)|(?P<false>orderable))}"}}'
-      - '$.null'
+      - 'orderable'
     sar:polarizations:
       - '{{"polarisationChannels": "{sar:polarizations#csv_list(%26)}"}}'
-      - '$.null'
+      - '{$.properties.metadata.polarisationChannels#split(&)}'
     sat:relative_orbit:
       - '{{"relativeOrbitNumber": "{sat:relative_orbit}"}}'
-      - '$.null'
+      - '$.properties.metadata.relativeOrbitNumber'
+    constellation: '$.properties.metadata.platformShortName'
   anchor_orbit_cycle: &orbit_cycle
     sat:relative_orbit:
       - '{{"relativeOrbitNumber": "{sat:relative_orbit}"}}'
@@ -35,14 +34,6 @@ wekeo_main:
     sat:orbit_cycle:
       - '{{"cycle": "{sat:orbit_cycle}"}}'
       - '$.null'
-  anchor_id_from_date: &id_from_date
-    id:
-      - |
-        {{
-          "startdate": "{id#replace_str(r'^.*_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T\4%3A\5%3A00Z')}",
-          "enddate": "{id#replace_str(r'^.*_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T\4%3A\5%3A00Z')}"
-        }}
-      - '{$.id#remove_extension}'
   search:
     type: WekeoSearch
     api_endpoint: https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess/search
@@ -96,16 +87,16 @@ wekeo_main:
       order:status: 'orderable'
       processing:level:
         - '{{"processingLevel": "{processing:level}"}}'
-        - '$.null'
+        - '$.properties.metadata.processingLevel'
       product:type:
         - '{{"productType": "{product:type}"}}'
-        - '$.null'
+        - '$.properties.metadata.productType'
       product:timeliness:
         - '{{"timeliness": "{product:timeliness}"}}'
-        - '$.null'
+        - '$.properties.metadata.timeliness'
       sat:orbit_state:
-        - '{{"orbitDirection": "{sat:orbit_state}"}}'
-        - '$.null'
+        - '{{"orbitDirection": "{sat:orbit_state#to_upper}"}}'
+        - '{$.properties.metadata.orbitDirection#to_lower}'
       variable:
         - '{{"variable": "{variable}"}}'
         - '$.null'
@@ -170,81 +161,90 @@ wekeo_main:
     S2_MSI_L1C:
       _collection: EO:ESA:DAT:SENTINEL-2
       product:type: S2MSI1C
-      processing:level: S2MSI1C
+      processing:level: L1
       metadata_mapping:
+        id:
+          - |
+            {{
+              "startdate": "{id#replace_str(r'^.*_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T00%3A00%3A00Z')}",
+              "enddate": "{id#replace_str(r'^.*_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T23%3A59%3A59Z')}"
+            }}
+          - '{$.id#remove_extension}'
         processing:level:
-          - '{{"processingLevel": "{processing:level}"}}'
-          - '$.id.`sub(/^[^_]([^_]+)_([^_]+)_.*/, S2\\2)`'
+          - '{{"processingLevel": "{processing:level#replace_str_tuple(("L1", "S2MSI1C"), ("L2", "S2MSI2A"))}"}}'
+          - '{$.properties.metadata.processingLevel#replace_str_tuple(("S2MSI1C", "L1"), ("S2MSI2A", "L2"))}'
         platform:
-          - '{{"platformSerialIdentifier": "{platform}"}}'
-          - '$.null'
+          - '{{"platformSerialIdentifier": "{platform#replace_str_tuple(("sentinel-2a", "A"), ("sentinel-2b", "B"), ("sentinel-2c", "C"))}"}}'
+          - '{$.properties.metadata.platformSerialIdentifier#replace_str_tuple(("A", "sentinel-2a"), ("B", "sentinel-2b"), ("C", "sentinel-2c"))}'
         instruments:
           - '{{"instrumentShortName": "{instruments#csv_list}"}}'
-          - '$.null'
+          - '{$.properties.metadata.instrumentShortName#split(,)}'
         sat:relative_orbit:
           - '{{"relativeOrbitNumber": "{sat:relative_orbit}"}}'
-          - '$.null'
+          - '$.properties.metadata.relativeOrbitNumber'
+        constellation: '$.properties.metadata.platformShortName'
         eo:cloud_cover:
           - '{{"cloudCover": "{eo:cloud_cover}"}}'
-          - '$.null'
+          - '$.properties.metadata.cloudCover'
         eodag:order_link: 'https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess/download?{{"location": "{eodag:download_link}","product_id":"{id}.SAFE", "dataset_id": "EO:ESA:DAT:SENTINEL-2"}}'
     S2_MSI_L2A:
       _collection: EO:ESA:DAT:SENTINEL-2
       product:type: S2MSI2A
-      processing:level: S2MSI2A
+      processing:level: L2
       metadata_mapping_from_product: S2_MSI_L1C
     S3_LAN_HY:
       _collection: EO:ESA:DAT:SENTINEL-3
       product:type: SR_2_LAN_HY
-      processing:level: 2
+      processing:level: L2
       metadata_mapping:
         id:
             - |
               {{
-                "startdate": "{id#replace_str(r'^.*[A-Z]_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T\4%3A00%3A00Z')}",
-                "enddate": "{id#replace_str(r'^.*[A-Z]_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T\4%3A59%3A59Z')}"
+                "startdate": "{id#replace_str(r'^.*[A-Z_]_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T\4%3A00%3A00Z')}",
+                "enddate": "{id#replace_str(r'^.*[A-Z_]_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T\4%3A59%3A59Z')}"
               }}
             - '{$.id#remove_extension}'
         processing:level:
-          - '{{"processingLevel": "{processing:level}"}}'
-          - '2'
+          - '{{"processingLevel": "{processing:level#replace_str("L", "")}"}}'
+          - 'L2'
         platform:
-          - '{{"platformSerialIdentifier": "{platform}"}}'
-          - '$.null'
+          - '{{"platformSerialIdentifier": "{platform#replace_str_tuple(("sentinel-3a", "A"), ("sentinel-3b", "B"), ("sentinel-3c", "C"))}"}}'
+          - '{$.properties.metadata.platformSerialIdentifier#replace_str_tuple(("A", "sentinel-3a"), ("B", "sentinel-3b"), ("C", "sentinel-3c"))}'
         instruments:
           - '{{"instrumentShortName": "{instruments#csv_list}"}}'
-          - '$.null'
+          - '{$.properties.metadata.instrumentShortName#split(,)}'
         sat:relative_orbit:
           - '{{"relativeOrbitNumber": "{sat:relative_orbit}"}}'
-          - '$.null'
+          - '$.properties.metadata.relativeOrbitNumber'
+        constellation: '$.properties.metadata.platformShortName'
         eo:cloud_cover:
           - '{{"cloudCover": "{eo:cloud_cover}"}}'
-          - '$.null'
+          - '$.properties.metadata.cloudCover'
         eodag:order_link: 'https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess/download?{{"location": "{eodag:download_link}","product_id":"{id}.SEN3", "dataset_id": "EO:ESA:DAT:SENTINEL-3"}}'
     S3_LAN_SI:
       _collection: EO:ESA:DAT:SENTINEL-3
       product:type: SR_2_LAN_SI
-      processing:level: 2
+      processing:level: L2
       metadata_mapping_from_product: S3_LAN_HY
     S3_LAN_LI:
       _collection: EO:ESA:DAT:SENTINEL-3
       product:type: SR_2_LAN_LI
-      processing:level: 2
+      processing:level: L2
       metadata_mapping_from_product: S3_LAN_HY
     S3_OLCI_L2LFR:
       _collection: EO:ESA:DAT:SENTINEL-3
       product:type: OL_2_LFR___
-      processing:level: 2
+      processing:level: L2
       metadata_mapping_from_product: S3_LAN_HY
     S3_OLCI_L2LRR:
       _collection: EO:ESA:DAT:SENTINEL-3
       product:type: OL_2_LRR___
-      processing:level: 2
+      processing:level: L2
       metadata_mapping_from_product: S3_LAN_HY
     S3_SLSTR_L2:
       _collection: EO:ESA:DAT:SENTINEL-3
       product:type: SL_2_LST___
-      processing:level: 2
+      processing:level: L2
       metadata_mapping_from_product: S3_LAN_HY
     S3_EFR:
       _collection: EO:EUM:DAT:SENTINEL-3:OL_1_EFR___
@@ -310,20 +310,27 @@ wekeo_main:
         eodag:order_link: 'https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess/download?{{"location": "{eodag:download_link}","product_id":"{id}.SEN3", "dataset_id": "EO:EUM:DAT:SENTINEL-3:SR_2_WAT___"}}'
     S5P_L1B_IR_ALL:
       _collection: EO:ESA:DAT:SENTINEL-5P
-      processing:level: L1b
+      processing:level: L1
       metadata_mapping:
+        id:
+            - |
+              {{
+                "startdate": "{id#replace_str(r'^.*[A-Z]_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T\4%3A00%3A00Z')}",
+                "enddate": "{id#replace_str(r'^.*[A-Z]_([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])_.*$',r'\1-\2-\3T\4%3A59%3A59Z')}"
+              }}
+            - '{$.id#remove_extension}'
         processing:level:
-          - '{{"processingLevel": "{processing:level}"}}'
+          - '{{"processingLevel": "{processing:level#replace_str("L1", "L1b")}"}}'
           - '$.id.`sub(/^[^_]([^_]+)_([^_]+)_([^_]+)_.*/, \\3)`'
         instruments:
           - '{{"instrumentShortName": "{instruments#csv_list}"}}'
-          - '$.null'
+          - '{$.properties.metadata.instrumentShortName#split(,)}'
         processing_mode:
           - '{{"processingMode": "{processing_mode}"}}'
-          - '$.null'
+          - '$.properties.metadata.processingMode'
         sat:absolute_orbit:
           - '{{"orbitNumber": "{sat:absolute_orbit}"}}'
-          - '$.null'
+          - '$.properties.metadata.orbitNumber'
         eodag:order_link: 'https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess/download?{{"location": "{eodag:download_link}","product_id":"{id}.nc", "dataset_id": "EO:ESA:DAT:SENTINEL-5P"}}'
     S5P_L2_IR_ALL:
       _collection: EO:ESA:DAT:SENTINEL-5P
@@ -381,10 +388,10 @@ wekeo_main:
           - '$.null'
         instruments:
           - '{{"instrumentShortName": "{instruments#csv_list}"}}'
-          - '$.null'
+          - '{$.properties.metadata.instrumentShortName#split(,)}'
         format:
           - '{{"fileFormat": "{format}"}}'
-          - '$.null'
+          - '$.properties.metadata.fileFormat'
         eodag:order_link: 'https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess/download?{{"location": "{eodag:download_link}","product_id":"{id}", "dataset_id": "EO:JRC:DAT:CLMS"}}'
     CLMS_GLO_NDVI_1KM_LTS:
       _collection: EO:JRC:DAT:CLMS
@@ -415,7 +422,7 @@ wekeo_main:
     CLMS_GLO_DMP_333M:
       _collection: EO:JRC:DAT:CLMS
       product:type: dry-gross_dry_matter_productivity
-      _dataset_identifier: dry-gross_dry_matter_productivity/dmp_global_300m_10daily_v1
+      _dataset_identifier: dmp_global_300m_10daily_v1
       metadata_mapping_from_product: CLMS_GLO_NDVI_333M
     CLMS_GLO_GDMP_333M:
       _collection: EO:JRC:DAT:CLMS


=====================================
pyproject.toml
=====================================
@@ -40,8 +40,7 @@ dependencies = [
     "click",
     "geojson",
     "importlib_metadata >= 5.0",
-    # temporarily pinned, see https://github.com/h2non/jsonpath-ng/issues/216
-    "jsonpath-ng < 1.8.0",
+    "jsonpath-ng != 1.8.0",
     "lxml",
     "orjson",
     "pydantic >= 2.13.0",
@@ -212,7 +211,7 @@ include = ["eodag*"]
 "*" = ["LICENSE", "NOTICE", "py.typed"]
 
 [tool.setuptools_scm]
-fallback_version = "4.9.2.dev0"
+fallback_version = "4.9.3.dev0"
 
 [tool.isort]
 multi_line_output = 3


=====================================
tests/units/test_auth_plugins.py
=====================================
@@ -18,6 +18,7 @@
 
 import datetime as dt
 import pickle
+import time
 import unittest
 from datetime import datetime, timedelta, timezone
 from types import SimpleNamespace
@@ -26,6 +27,7 @@ from unittest import mock
 import boto3
 import requests
 import responses
+from concurrent.futures import ThreadPoolExecutor
 from mypy_boto3_s3.service_resource import BucketObjectsCollection
 from pystac.utils import now_in_utc
 from requests import Request, Response, Timeout
@@ -34,6 +36,7 @@ from requests.exceptions import RequestException
 
 from eodag.api.product._product import EOProduct
 from eodag.api.provider import ProvidersDict
+from eodag.plugins.authentication import sas_auth
 from eodag.plugins.authentication.eoiam import _EOIAMSessionAuth
 from eodag.utils import MockResponse
 from tests.context import (
@@ -1720,7 +1723,10 @@ class TestAuthPluginSASAuth(BaseAuthPluginTest):
         providers = ProvidersDict.from_configs(
             {
                 "foo_provider": {
-                    "products": {"foo_product": {}},
+                    "products": {
+                        "foo_product": {"_collection": "landsat-c2-l2"},
+                    },
+                    "download": {"type": "HTTPDownload"},
                     "auth": {
                         "type": "SASAuth",
                         "auth_uri": "http://foo.bar?href={url}",
@@ -1734,6 +1740,11 @@ class TestAuthPluginSASAuth(BaseAuthPluginTest):
         )
         cls.plugins_manager = PluginManager(providers)
 
+    def setUp(self):
+        super().setUp()
+        sas_auth._SAS_TOKENS.clear()
+        self.addCleanup(sas_auth._SAS_TOKENS.clear)
+
     def test_plugins_auth_sasauth_validate_credentials_ok(self):
         """SASAuth.validate_credentials must be ok on empty or non-empty credentials"""
         auth_plugin = self.get_auth_plugin("foo_provider")
@@ -1743,7 +1754,32 @@ class TestAuthPluginSASAuth(BaseAuthPluginTest):
         auth_plugin.config.credentials = {"apikey": "foo"}
         auth_plugin.validate_config_credentials()
 
-    @mock.patch("eodag.plugins.authentication.sas_auth.requests.get", autospec=True)
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
+    def test_plugins_auth_sasauth_uses_configured_retries(self, mock_requests_get):
+        auth_plugin = self.get_auth_plugin("foo_provider")
+        auth_plugin.config.retry_total = 5
+        auth_plugin.config.retry_backoff_factor = 3
+        auth_plugin.config.retry_status_forcelist = [429, 503]
+        mock_requests_get.return_value = mock.Mock()
+        mock_requests_get.return_value.json.return_value = {
+            "href": "https://example.com/blob?sig=token"
+        }
+
+        with mock.patch(
+            "eodag.plugins.authentication.sas_auth.HTTPAdapter"
+        ) as mock_http_adapter:
+            auth_plugin.authenticate()(mock.Mock(headers={}, url="url"))
+
+        retries = mock_http_adapter.call_args.kwargs["max_retries"]
+        self.assertEqual(retries.total, 5)
+        self.assertEqual(retries.backoff_factor, 3)
+        self.assertEqual(retries.status_forcelist, [429, 503])
+
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
     def test_plugins_auth_sasauth_text_token_authenticate_with_credentials(
         self, mock_requests_get
     ):
@@ -1774,11 +1810,13 @@ class TestAuthPluginSASAuth(BaseAuthPluginTest):
 
         # check SAS get request call arguments
         args, kwargs = mock_requests_get.call_args
-        self.assertEqual(args[0], auth_plugin.config.auth_uri.format(url=url))
+        self.assertEqual(args[1], auth_plugin.config.auth_uri.format(url=url))
         auth_plugin_headers = {"Ocp-Apim-Subscription-Key": "foo"}
         self.assertDictEqual(kwargs["headers"], dict(auth_plugin_headers, **USER_AGENT))
 
-    @mock.patch("eodag.plugins.authentication.sas_auth.requests.get", autospec=True)
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
     def test_plugins_auth_sasauth_text_token_authenticate_without_credentials(
         self, mock_requests_get
     ):
@@ -1808,11 +1846,181 @@ class TestAuthPluginSASAuth(BaseAuthPluginTest):
 
         # check SAS get request call arguments
         args, kwargs = mock_requests_get.call_args
-        self.assertEqual(args[0], auth_plugin.config.auth_uri.format(url=url))
+        self.assertEqual(args[1], auth_plugin.config.auth_uri.format(url=url))
         # check if headers only has the user agent as a request call argument
         self.assertEqual(kwargs["headers"], USER_AGENT)
 
-    @mock.patch("eodag.plugins.authentication.sas_auth.requests.get", autospec=True)
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
+    def test_plugins_auth_sasauth_reuses_token_for_blobs_in_container(
+        self, mock_requests_get
+    ):
+        auth_plugin = self.get_auth_plugin("foo_provider")
+        mock_requests_get.return_value = mock.Mock()
+        mock_requests_get.return_value.json.return_value = {
+            "href": "https://account.blob.core.windows.net/container/blob1?sig=token"
+        }
+
+        auth = auth_plugin.authenticate()
+        first_request = mock.Mock(
+            headers={},
+            url="https://account.blob.core.windows.net/container/blob1",
+        )
+        second_request = mock.Mock(
+            headers={},
+            url="https://account.blob.core.windows.net/container/blob2",
+        )
+
+        auth(first_request)
+        auth(second_request)
+
+        self.assertEqual(
+            first_request.url,
+            "https://account.blob.core.windows.net/container/blob1?sig=token",
+        )
+        self.assertEqual(
+            second_request.url,
+            "https://account.blob.core.windows.net/container/blob2?sig=token",
+        )
+        mock_requests_get.assert_called_once()
+
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
+    def test_plugins_auth_sasauth_shares_token_between_auth_instances(
+        self, mock_requests_get
+    ):
+        auth_plugin = self.get_auth_plugin("foo_provider")
+        mock_requests_get.return_value = mock.Mock()
+        mock_requests_get.return_value.json.return_value = {
+            "href": "https://account.blob.core.windows.net/container/blob1?sig=token"
+        }
+        url = "https://account.blob.core.windows.net/container/blob{}"
+
+        for i in range(3):
+            req = mock.Mock(headers={}, url=url.format(i))
+            auth_plugin.authenticate()(req)
+            self.assertEqual(req.url, url.format(i) + "?sig=token")
+
+        mock_requests_get.assert_called_once()
+
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
+    def test_plugins_auth_sasauth_concurrent_requests_fetch_token_once(
+        self, mock_requests_get
+    ):
+        auth_plugin = self.get_auth_plugin("foo_provider")
+        mock_requests_get.side_effect = lambda *a, **k: (
+            time.sleep(0.05)
+            or mock.Mock(
+                json=lambda: {
+                    "href": "https://account.blob.core.windows.net/container/b?sig=t"
+                }
+            )
+        )
+        url = "https://account.blob.core.windows.net/container/blob{}"
+
+        def call_auth(i):
+            req = mock.Mock(headers={}, url=url.format(i))
+            auth_plugin.authenticate()(req)
+            return req.url
+
+        with ThreadPoolExecutor(max_workers=8) as executor:
+            urls = list(executor.map(call_auth, range(8)))
+
+        self.assertEqual(urls, [url.format(i) + "?sig=t" for i in range(8)])
+        mock_requests_get.assert_called_once()
+
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
+    def test_plugins_auth_sasauth_refreshes_expired_token(self, mock_requests_get):
+        auth_plugin = self.get_auth_plugin("foo_provider")
+        expired = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
+        mock_requests_get.return_value = mock.Mock()
+        mock_requests_get.return_value.json.return_value = {
+            "href": "https://account.blob.core.windows.net/container/b?sig=t",
+            "msft:expiry": expired,
+        }
+        url = "https://account.blob.core.windows.net/container/blob"
+
+        auth_plugin.authenticate()(mock.Mock(headers={}, url=url))
+        auth_plugin.authenticate()(mock.Mock(headers={}, url=url))
+
+        self.assertEqual(mock_requests_get.call_count, 2)
+
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
+    def test_plugins_auth_sasauth_requests_collection_token(self, mock_requests_get):
+        mock_requests_get.return_value = mock.Mock()
+        mock_requests_get.return_value.json.return_value = {"token": "sig=token"}
+        product = EOProduct(
+            "foo_provider",
+            {"id": "test-item"},
+            collection="foo_product",
+        )
+        product.assets["data"] = {
+            "href": "https://account.blob.core.windows.net/landsat-c2/asset.tif"
+        }
+        download_plugin = self.plugins_manager.get_download_plugin(product)
+        auth_plugin = self.plugins_manager.get_auth_plugin(download_plugin, product)
+        self.assertIsNotNone(auth_plugin)
+        req = mock.Mock(
+            headers={},
+            url="https://account.blob.core.windows.net/landsat-c2/asset.tif",
+        )
+
+        with (
+            mock.patch.object(
+                auth_plugin.config,
+                "auth_uri",
+                "https://auth.example/token/{_collection}",
+            ),
+            mock.patch.object(auth_plugin.config, "signed_url_key", "token"),
+        ):
+            auth_plugin.authenticate()(req)
+
+        self.assertEqual(
+            mock_requests_get.call_args.args[1],
+            "https://auth.example/token/landsat-c2-l2",
+        )
+        self.assertEqual(
+            req.url,
+            "https://account.blob.core.windows.net/landsat-c2/asset.tif?sig=token",
+        )
+
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
+    def test_plugins_auth_sasauth_skips_unmatched_url(self, mock_requests_get):
+        auth_plugin = self.get_auth_plugin("foo_provider")
+        with mock.patch.object(
+            auth_plugin.config,
+            "matching_url",
+            r"https://[-\w\.]+.blob.core.windows.net",
+            create=True,
+        ):
+            auth = auth_plugin.authenticate()
+
+        req = mock.Mock(
+            headers={},
+            url="https://planetarycomputer.microsoft.com/api/data/v1/item/preview.png",
+        )
+        auth(req)
+
+        self.assertEqual(
+            req.url,
+            "https://planetarycomputer.microsoft.com/api/data/v1/item/preview.png",
+        )
+        self.assertEqual(req.headers, {})
+        mock_requests_get.assert_not_called()
+
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
     def test_plugins_auth_sasauth_request_error(self, mock_requests_get):
         """SASAuth.authenticate must raise an AuthenticationError if an error occurs"""
         auth_plugin = self.get_auth_plugin("foo_provider")
@@ -1830,8 +2038,15 @@ class TestAuthPluginSASAuth(BaseAuthPluginTest):
         with self.assertRaises(AuthenticationError):
             auth(req)
 
-    def test_plugins_download_http_presign_url(self):
+    @mock.patch(
+        "eodag.plugins.authentication.sas_auth.requests.Session.get", autospec=True
+    )
+    def test_plugins_download_http_presign_url(self, mock_requests_get):
         """should create a presigned url to download via HTTP"""
+        mock_requests_get.return_value = mock.Mock()
+        mock_requests_get.return_value.json.return_value = {
+            "href": "http://foo.bar.com/b1/a1/a1.json?sig=t"
+        }
         provider = "foo_provider"
         collection = "LANDSAT_C2_L1"
         product = EOProduct(
@@ -1848,10 +2063,11 @@ class TestAuthPluginSASAuth(BaseAuthPluginTest):
 
         auth_plugin = self.get_auth_plugin("foo_provider")
         url = auth_plugin.presign_url(product.assets["a1"])
-        expected_url = "http://foo.bar?href={url}".format(
-            url="http://foo.bar.com/b1/a1/a1.json"
+        self.assertEqual("http://foo.bar.com/b1/a1/a1.json?sig=t", url)
+        self.assertEqual(
+            mock_requests_get.call_args.args[1],
+            "http://foo.bar?href=http://foo.bar.com/b1/a1/a1.json",
         )
-        self.assertEqual(expected_url, url)
 
 
 class TestAuthPluginKeycloakOIDCPasswordAuth(BaseAuthPluginTest):


=====================================
tests/units/test_crunch.py
=====================================
@@ -20,9 +20,9 @@ from collections import UserList
 from typing import Any
 from unittest import mock
 
-import shapely
 from shapely import Polygon, geometry
 from shapely.errors import ShapelyError
+from shapely.geometry.base import BaseGeometry
 
 from eodag.api.product import EOProduct
 from eodag.api.search_result import SearchResult
@@ -575,16 +575,15 @@ class TestPluginCrunch(unittest.TestCase):
         self.assertEqual(len(filtered_result), 1)
 
         # Force fail search_geom intersection
-        def shapely_intersect_forced_exception(a, b, grid_size=None, **kwargs):
-            print("Mocked: no shapely.intersection allowed")
-            raise ShapelyError("No intersection allowed")
-
         with mock.patch.object(
-            shapely, "intersection", new=shapely_intersect_forced_exception
-        ):
+            BaseGeometry,
+            "intersection",
+            side_effect=ShapelyError("No intersection allowed"),
+        ) as mocked_intersection:
             filtered_result = search_results.crunch(
                 filter, geometry=geometry.box(10, 30, 20, 40)
             )
+            mocked_intersection.assert_called_once()
             self.assertEqual(len(filtered_result), 0)
 
     def test_crunch_property(self):



View it on GitLab: https://salsa.debian.org/debian-gis-team/eodag/-/compare/54d8796b001e1e0defee73bd91330263a075ef0e...cf65e0843c6bdf2f14de46de6e4cfdc57989ae3e

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/eodag/-/compare/54d8796b001e1e0defee73bd91330263a075ef0e...cf65e0843c6bdf2f14de46de6e4cfdc57989ae3e
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20261008/e98102ea/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list