Repository: CS-SI/eodag
Pull request #1052 (2024-03-05 16:04:08)

fix: type hints related fixes and refactoring
Type hints fixes and code refactoring. Going from 264 mypy errors to 94


github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports/data/type-hints/./badge.svg)


## Code Coverage (Ubuntu)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     11       0  100.00%
cli.py                                         300      47  84.33%   61, 647-686, 788-839, 843
config.py                                      312      27  91.35%   80-82, 91, 99, 103-105, 178, 190, 385-387, 451-454, 501-502, 511-512, 591, 660-665, 667
crunch.py                                        6       6  0.00%    18-24
api/__init__.py                                  1       0  100.00%
api/core.py                                    742      79  89.35%   101-110, 383, 594, 638-641, 679, 784, 788-793, 819, 889, 941, 1042-1047, 1133-1145, 1185, 1187, 1191, 1212-1214, 1218-1229, 1242-1248, 1338-1341, 1370-1390, 1438, 1444-1447, 1456, 1807, 1840-1846, 2109, 2113-2116, 2130-2132, 2197, 2207, 2239, 2242
api/search_result.py                            44       6  86.36%   33-35, 70, 79, 86, 100
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          44       5  88.64%   27-29, 79, 129
api/product/_product.py                        208      32  84.62%   48-55, 59-61, 165-172, 256-257, 347, 383, 444, 458-461, 474, 498-501, 544-550
api/product/metadata_mapping.py                648      87  86.57%   66-68, 129-131, 232, 264-265, 317-329, 331, 342, 346-362, 401-404, 441, 462-465, 479, 487-488, 561-562, 586-587, 593-596, 611-612, 776, 822, 879-880, 923-928, 1059, 1073-1093, 1113, 1118, 1228, 1250, 1264, 1277-1296, 1335, 1387, 1425-1429, 1448
api/product/drivers/__init__.py                  6       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              1       0  100.00%
plugins/base.py                                 23       3  86.96%   25, 48, 55
plugins/manager.py                             130      11  91.54%   49-51, 93-98, 142, 271-272
plugins/apis/__init__.py                         1       0  100.00%
plugins/apis/base.py                            39      11  71.79%   36-43, 103, 107, 174, 219
plugins/apis/cds.py                            204      49  75.98%   72-80, 126-129, 176-188, 244, 309, 317-322, 335-342, 360-362, 383-385, 395-397, 411-412, 457, 460, 470, 482, 515, 517
plugins/apis/ecmwf.py                           98      15  84.69%   48-56, 157-159, 206-207, 233-235
plugins/apis/usgs.py                           168      36  78.57%   59-64, 125, 198, 232, 267-269, 274, 300-301, 306, 334-341, 350-355, 377-383, 385-391, 414
plugins/authentication/__init__.py               7       1  85.71%   31
plugins/authentication/aws_auth.py              20       2  90.00%   25-27
plugins/authentication/base.py                  19       2  89.47%   26, 47
plugins/authentication/generic.py               16       3  81.25%   28, 40, 50
plugins/authentication/header.py                17       1  94.12%   27
plugins/authentication/keycloak.py              88      17  80.68%   32-34, 159-160, 190-212, 238-243
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       103      64  37.86%   39-41, 138-150, 154-172, 180-222, 228-237, 246-286, 291-299, 304-305
plugins/authentication/qsauth.py                35       2  94.29%   32, 80
plugins/authentication/sas_auth.py              47       2  95.74%   32, 71
plugins/authentication/token.py                 75       8  89.33%   35-37, 102, 127, 154-157
plugins/crunch/__init__.py                       1       0  100.00%
plugins/crunch/base.py                          10       2  80.00%   25, 38
plugins/crunch/filter_date.py                   62      15  75.81%   30, 53-58, 72, 81, 90, 93, 105-107, 116-118, 125
plugins/crunch/filter_latest_intersect.py       50      10  80.00%   32-34, 51-52, 71, 80-83, 85, 92-95
plugins/crunch/filter_latest_tpl_name.py        33       2  93.94%   28, 86
plugins/crunch/filter_overlap.py                68      17  75.00%   28-30, 33, 82-85, 91, 99, 110-126
plugins/crunch/filter_property.py               33       8  75.76%   29, 60-65, 68-69, 85-89
plugins/download/__init__.py                     1       0  100.00%
plugins/download/aws.py                        490     165  66.33%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 453-454, 460-464, 497, 532, 537, 544, 574-582, 586, 624-632, 639-641, 682-756, 774-835, 846-851, 867-880, 909, 924-926, 929, 939-947, 955-968, 978-997, 1004-1016, 1057, 1083, 1128-1130, 1350
plugins/download/base.py                       261      57  78.16%   58-64, 145, 180, 319-320, 340-346, 377-381, 387-388, 432, 435-449, 461, 465, 538-542, 572-573, 581-598, 605-613, 615-619, 666, 689, 711, 719
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       418      97  76.79%   81-88, 122, 170, 191-198, 208-221, 258-263, 313, 345, 372-374, 384-392, 414, 453-457, 514, 587-643, 657, 681-683, 713-714, 722, 725, 741, 765, 788-789, 796, 855-861, 916-917, 923, 933, 999, 1017-1033
plugins/download/s3rest.py                     116      27  76.72%   55-58, 124, 165, 192, 222-229, 232-234, 238, 251-257, 265-266, 269-273, 296, 316-319
plugins/search/__init__.py                       1       0  100.00%
plugins/search/base.py                         101      13  87.13%   43-47, 101, 105, 131-135, 264, 284
plugins/search/build_search_result.py           56       6  89.29%   67, 112-113, 121, 132, 176
plugins/search/creodias_s3.py                   55       3  94.55%   56, 74, 108
plugins/search/csw.py                          107      83  22.43%   43-45, 57-58, 62-63, 74-122, 128-141, 149-181, 199-240
plugins/search/data_request_search.py          195      60  69.23%   52, 89-92, 108, 120, 139, 144, 149, 156, 169-172, 226-227, 231, 241-247, 252, 276, 286-297, 309, 311, 318-319, 321-322, 337-341, 374, 381, 392, 405, 411-423, 428
plugins/search/qssearch.py                     517      55  89.36%   86, 353-354, 448-454, 481-484, 557-558, 599, 617, 632, 685, 706, 709-710, 719, 730, 739, 750-751, 762, 822-827, 831-832, 860, 923, 942-959, 994, 1128-1129, 1150, 1192-1193, 1203-1209, 1249, 1262, 1282, 1370
plugins/search/static_stac_search.py            46       3  93.48%   39-40, 81
rest/__init__.py                                 1       0  100.00%
rest/core.py                                   168      13  92.26%   238, 506-507, 510, 512, 515, 518-520, 572, 579-583
rest/server.py                                 306      54  82.35%   81-82, 106, 129-130, 241-243, 259, 299-300, 312-328, 410-415, 445, 607-614, 645, 688-689, 782-784, 801-806, 836, 838, 842-843, 847-848
rest/stac.py                                   428      93  78.27%   59-61, 229-231, 273, 286-295, 314-320, 365, 402-404, 427, 462-463, 549-594, 639, 820, 885-887, 1106, 1116-1128, 1141-1163, 1177-1222, 1381-1382
rest/types/__init__.py                           1       0  100.00%
rest/types/eodag_search.py                     185       9  95.14%   52-55, 231-235, 288, 291, 359
rest/types/stac_queryables.py                   38       7  81.58%   28, 120-126, 129
rest/types/stac_search.py                      122      10  91.80%   48-51, 167, 182-184, 192, 196
rest/utils/__init__.py                         116      14  87.93%   53, 108-109, 128-130, 180, 190-204, 236
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           31       3  90.32%   78, 90, 92
types/__init__.py                               76       6  92.11%   53, 87, 152, 172, 177, 185
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                             81       0  100.00%
types/search_args.py                            70      18  74.29%   60-64, 71-88, 104
utils/__init__.py                              486      43  91.15%   82, 87, 108-110, 188-189, 198-225, 228, 242, 324-328, 404-408, 429-431, 513, 518, 528, 566-567, 963-966, 974-975, 1016-1017, 1097, 1181, 1199, 1372
utils/constraints.py                           123      42  65.85%   31, 84-93, 134, 139, 143, 154, 177-178, 189-197, 206, 220-236, 245-256
utils/exceptions.py                             37       2  94.59%   23, 93
utils/import_system.py                          30      20  33.33%   27, 67-81, 93-103
utils/logging.py                                29       1  96.55%   123
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/stac_reader.py                            89      27  69.66%   54-55, 62-83, 90-92, 96, 135, 149-152
TOTAL                                         8640    1567  81.86%
```

</details>


### Diff against develop

```
Filename                                    Stmts    Miss  Cover
----------------------------------------  -------  ------  --------
api/core.py                                    +2      +2  -0.24%
api/product/_assets.py                         +5      +2  -3.67%
api/product/_product.py                        +8      +2  -0.38%
api/product/metadata_mapping.py                +7      +3  -0.33%
plugins/apis/base.py                           +7      +3  -3.21%
plugins/apis/cds.py                             0      +3  -1.47%
plugins/apis/ecmwf.py                          +4      +3  -2.54%
plugins/apis/usgs.py                           +3      +2  -0.82%
plugins/authentication/keycloak.py             +6       0  +1.41%
plugins/authentication/openid_connect.py       +4      +4  -1.53%
plugins/download/aws.py                       +12      +5  -0.20%
plugins/download/base.py                       +2      +2  -0.60%
plugins/download/http.py                      +16      +5  -0.32%
plugins/download/s3rest.py                     +5      +3  -1.66%
plugins/search/base.py                        +10      +2  -0.78%
plugins/search/data_request_search.py          +5      +1  +0.28%
plugins/search/qssearch.py                     +3      +1  -0.13%
types/download_args.py                         +9       0  +100.00%
utils/__init__.py                              +3       0  +0.05%
utils/constraints.py                           +3      +2  -0.82%
utils/exceptions.py                            +1       0  +0.15%
TOTAL                                        +115     +45  -0.29%
```


Results for commit: 15c04b5f93b5ddf6f33dacdcff1c8f3304700a3e


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.12-ubuntu-latest/coverage.xml -->

Patch:
diff --git a/eodag/api/core.py b/eodag/api/core.py
index b3a2ea30..2407732d 100644
--- a/eodag/api/core.py
+++ b/eodag/api/core.py
@@ -87,6 +87,7 @@ from eodag.utils import (
 )
 from eodag.utils.exceptions import (
     AuthenticationError,
+    EodagError,
     MisconfiguredError,
     NoMatchingProductType,
     PluginImplementationError,
@@ -105,7 +106,8 @@ if TYPE_CHECKING:
     from eodag.plugins.crunch.base import Crunch
     from eodag.plugins.search.base import Search
     from eodag.types import ProviderSortables
-    from eodag.utils import Annotated, DownloadedCallback, ProgressCallback
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import Annotated, DownloadedCallback, ProgressCallback, Unpack
 
 logger = logging.getLogger("eodag.core")
 
@@ -502,10 +504,10 @@ class EODataAccessGateway:
             locations_config = load_yml_config(locations_conf_path)
 
             main_key = next(iter(locations_config))
-            locations_config = locations_config[main_key]
+            main_locations_config = locations_config[main_key]
 
             logger.info("Locations configuration loaded from %s" % locations_conf_path)
-            self.locations_config: List[Dict[str, Any]] = locations_config
+            self.locations_config: List[Dict[str, Any]] = main_locations_config
         else:
             logger.info(
                 "Could not load locations configuration from %s" % locations_conf_path
@@ -612,9 +614,8 @@ class EODataAccessGateway:
 
                 if not ext_product_types_conf:
                     # empty ext_product_types conf
-                    discover_kwargs = dict(provider=provider) if provider else {}
-                    ext_product_types_conf = self.discover_product_types(
-                        **discover_kwargs
+                    ext_product_types_conf = (
+                        self.discover_product_types(provider=provider) or {}
                     )
 
             # update eodag product types list with new conf
@@ -693,8 +694,8 @@ class EODataAccessGateway:
                 # or not in ext_product_types_conf (if eodag system conf != eodag conf used for ext_product_types_conf)
 
             # discover product types for user configured provider
-            provider_ext_product_types_conf = self.discover_product_types(
-                provider=provider
+            provider_ext_product_types_conf = (
+                self.discover_product_types(provider=provider) or {}
             )
 
             # update eodag product types list with new conf
@@ -738,7 +739,9 @@ class EODataAccessGateway:
                     auth_plugin = self._plugins_manager.get_auth_plugin(
                         search_plugin.provider
                     )
-                    if callable(getattr(auth_plugin, "authenticate", None)):
+                    if auth_plugin and callable(
+                        getattr(auth_plugin, "authenticate", None)
+                    ):
                         try:
                             search_plugin.auth = auth_plugin.authenticate()
                         except (AuthenticationError, MisconfiguredError) as e:
@@ -934,6 +937,8 @@ class EODataAccessGateway:
             )
             if kwargs.get(param, None) is not None
         }
+        if not self._product_types_index:
+            raise EodagError("Missing product types index")
         with self._product_types_index.searcher() as searcher:
             results = None
             # For each search key, do a guess and then upgrade the result (i.e. when
@@ -965,7 +970,7 @@ class EODataAccessGateway:
         locations: Optional[Dict[str, str]] = None,
         provider: Optional[str] = None,
         **kwargs: Any,
-    ) -> Tuple[SearchResult, int]:
+    ) -> Tuple[SearchResult, Optional[int]]:
         """Look for products matching criteria on known providers.
 
         The default behaviour is to look for products on the provider with the
@@ -1448,7 +1453,7 @@ class EODataAccessGateway:
                     # parameters which might not given an exact result are used
                     for result in results:
                         if result.properties["id"] == uid.split(".")[0]:
-                            return [results[0]], 1
+                            return SearchResult([results[0]]), 1
                 logger.info(
                     "Several products found for this id (%s). You may try searching using more selective criteria.",
                     results,
@@ -1676,7 +1681,7 @@ class EODataAccessGateway:
         can_authenticate = callable(getattr(auth_plugin, "authenticate", None))
 
         results: List[EOProduct] = []
-        total_results = 0
+        total_results: Optional[int] = 0
 
         try:
             if need_auth and auth_plugin and can_authenticate:
@@ -1779,7 +1784,11 @@ class EODataAccessGateway:
                     eo_product.register_downloader(download_plugin, auth_plugin)
 
             results.extend(res)
-            total_results = None if nb_res is None else total_results + nb_res
+            total_results = (
+                None
+                if (nb_res is None or total_results is None)
+                else total_results + nb_res
+            )
             if count:
                 logger.info(
                     "Found %s result(s) on provider '%s'",
@@ -1868,7 +1877,7 @@ class EODataAccessGateway:
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> List[str]:
         """Download all products resulting from a search.
 
@@ -2043,7 +2052,7 @@ class EODataAccessGateway:
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> str:
         """Download a single product.
 
diff --git a/eodag/api/product/_assets.py b/eodag/api/product/_assets.py
index d1897b5d..cf03bea7 100644
--- a/eodag/api/product/_assets.py
+++ b/eodag/api/product/_assets.py
@@ -19,12 +19,14 @@ from __future__ import annotations
 
 import re
 from collections import UserDict
-from typing import TYPE_CHECKING, Any, Dict, List
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
 
 from eodag.utils.exceptions import NotAvailableError
 
 if TYPE_CHECKING:
     from eodag.api.product import EOProduct
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import Unpack
 
 
 class AssetsDict(UserDict):
@@ -98,6 +100,9 @@ class Asset(UserDict):
     """
 
     product: EOProduct
+    size: int
+    filename: Optional[str]
+    rel_path: str
 
     def __init__(self, product: EOProduct, key: str, *args: Any, **kwargs: Any) -> None:
         self.product = product
@@ -113,7 +118,7 @@ class Asset(UserDict):
         """
         return self.data
 
-    def download(self, **kwargs: Any) -> str:
+    def download(self, **kwargs: Unpack[DownloadConf]) -> str:
         """Downloads a single asset
 
         :param kwargs: (optional) Additional named-arguments passed to `plugin.download()`
diff --git a/eodag/api/product/_product.py b/eodag/api/product/_product.py
index c48affb1..99ec8027 100644
--- a/eodag/api/product/_product.py
+++ b/eodag/api/product/_product.py
@@ -21,11 +21,13 @@ import base64
 import logging
 import os
 import re
+import tempfile
 import urllib.parse
 from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
 
 import requests
 from requests import RequestException
+from requests.auth import AuthBase
 from shapely import geometry, wkb, wkt
 from shapely.errors import ShapelyError
 
@@ -49,6 +51,8 @@ if TYPE_CHECKING:
     from eodag.plugins.apis.base import Api
     from eodag.plugins.authentication.base import Authentication
     from eodag.plugins.download.base import Download
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import Unpack
 
 try:
     from shapely.errors import GEOSException
@@ -308,7 +312,7 @@ class EOProduct:
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> str:
         """Download the EO product using the provided download plugin and the
         authenticator if necessary.
@@ -469,9 +473,13 @@ class EOProduct:
         if base_dir is not None:
             quicklooks_base_dir = os.path.abspath(os.path.realpath(base_dir))
         else:
-            quicklooks_base_dir = os.path.join(
-                self.downloader.config.outputs_prefix, "quicklooks"
+            tempdir = tempfile.gettempdir()
+            outputs_prefix = (
+                getattr(self.downloader.config, "outputs_prefix", tempdir)
+                if self.downloader
+                else tempdir
             )
+            quicklooks_base_dir = os.path.join(outputs_prefix, "quicklooks")
         if not os.path.isdir(quicklooks_base_dir):
             os.makedirs(quicklooks_base_dir)
         quicklook_file = os.path.join(
@@ -497,6 +505,8 @@ class EOProduct:
                 if self.downloader_auth is not None
                 else None
             )
+            if not isinstance(auth, AuthBase):
+                auth = None
             with requests.get(
                 self.properties["quicklook"],
                 stream=True,
diff --git a/eodag/api/product/metadata_mapping.py b/eodag/api/product/metadata_mapping.py
index 8a6fe9c4..de03bda5 100644
--- a/eodag/api/product/metadata_mapping.py
+++ b/eodag/api/product/metadata_mapping.py
@@ -26,6 +26,8 @@ from string import Formatter
 from typing import (
     TYPE_CHECKING,
     Any,
+    AnyStr,
+    Callable,
     Dict,
     Iterator,
     List,
@@ -40,7 +42,7 @@ import orjson
 import pyproj
 from dateutil.parser import isoparse
 from dateutil.tz import UTC, tzutc
-from jsonpath_ng.jsonpath import Child
+from jsonpath_ng.jsonpath import Child, JSONPath
 from lxml import etree
 from lxml.etree import XPathEvalError
 from shapely import wkt
@@ -151,7 +153,7 @@ def get_search_param(map_value: List[str]) -> str:
     return map_value[0]
 
 
-def format_metadata(search_param: str, *args: Tuple[Any], **kwargs: Any) -> str:
+def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
     """Format a string of form {<field_name>#<conversion_function>}
 
     The currently understood converters are:
@@ -203,8 +205,8 @@ def format_metadata(search_param: str, *args: Tuple[Any], **kwargs: Any) -> str:
         )
 
         def __init__(self) -> None:
-            self.custom_converter = None
-            self.custom_args = None
+            self.custom_converter: Optional[Callable] = None
+            self.custom_args: Optional[str] = None
 
         def get_field(self, field_name: str, args: Any, kwargs: Any) -> Any:
             conversion_func_spec = self.CONVERSION_REGEX.match(field_name)
@@ -479,12 +481,15 @@ def format_metadata(search_param: str, *args: Tuple[Any], **kwargs: Any) -> str:
         @staticmethod
         def convert_get_group_name(string: str, pattern: str) -> str:
             try:
-                return re.search(pattern, str(string)).lastgroup
+                match = re.search(pattern, str(string))
+                if match:
+                    return match.lastgroup or NOT_AVAILABLE
             except AttributeError:
-                logger.warning(
-                    "Could not extract property from %s using %s", string, pattern
-                )
-                return NOT_AVAILABLE
+                pass
+            logger.warning(
+                "Could not extract property from %s using %s", string, pattern
+            )
+            return NOT_AVAILABLE
 
         @staticmethod
         def convert_replace_str(string: str, args: str) -> str:
@@ -812,7 +817,10 @@ def format_metadata(search_param: str, *args: Tuple[Any], **kwargs: Any) -> str:
         @staticmethod
         def convert_get_dates_from_string(text: str, split_param="-"):
             reg = "[0-9]{8}" + split_param + "[0-9]{8}"
-            dates_str = re.search(reg, text).group()
+            match = re.search(reg, text)
+            if not match:
+                return NOT_AVAILABLE
+            dates_str = match.group()
             dates = dates_str.split(split_param)
             start_date = datetime.strptime(dates[0], "%Y%m%d")
             end_date = datetime.strptime(dates[1], "%Y%m%d")
@@ -926,13 +934,18 @@ def properties_from_json(
     discovery_pattern = discovery_config.get("metadata_pattern", None)
     discovery_path = discovery_config.get("metadata_path", None)
     if discovery_pattern and discovery_path:
-        discovered_properties = string_to_jsonpath(discovery_path).find(json)
+        discovery_jsonpath = string_to_jsonpath(discovery_path)
+        discovered_properties = (
+            discovery_jsonpath.find(json)
+            if isinstance(discovery_jsonpath, JSONPath)
+            else []
+        )
         for found_jsonpath in discovered_properties:
             if "metadata_path_id" in discovery_config.keys():
                 found_key_paths = string_to_jsonpath(
                     discovery_config["metadata_path_id"], force=True
                 ).find(found_jsonpath.value)
-                if not found_key_paths:
+                if not found_key_paths or isinstance(found_key_paths, int):
                     continue
                 found_key = found_key_paths[0].value
                 used_jsonpath = Child(
@@ -955,7 +968,9 @@ def properties_from_json(
                         discovery_config["metadata_path_value"], force=True
                     ).find(found_jsonpath.value)
                     properties[found_key] = (
-                        found_value_path[0].value if found_value_path else NOT_AVAILABLE
+                        found_value_path[0].value
+                        if found_value_path and not isinstance(found_value_path, int)
+                        else NOT_AVAILABLE
                     )
                 else:
                     # default value got from metadata_path
@@ -971,7 +986,7 @@ def properties_from_json(
 
 
 def properties_from_xml(
-    xml_as_text: str,
+    xml_as_text: AnyStr,
     mapping: Any,
     empty_ns_prefix: str = "ns",
     discovery_config: Optional[Dict[str, Any]] = None,
diff --git a/eodag/plugins/apis/base.py b/eodag/plugins/apis/base.py
index 38fb97e5..23703b9a 100644
--- a/eodag/plugins/apis/base.py
+++ b/eodag/plugins/apis/base.py
@@ -18,16 +18,10 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING
 
 from pydantic.fields import Field, FieldInfo
 
-if TYPE_CHECKING:
-    from eodag.api.product import EOProduct
-    from eodag.api.search_result import SearchResult
-    from eodag.config import PluginConfig
-    from eodag.utils import DownloadedCallback, ProgressCallback
-
 from eodag.plugins.base import PluginTopic
 from eodag.utils import (
     DEFAULT_DOWNLOAD_TIMEOUT,
@@ -38,6 +32,16 @@ from eodag.utils import (
     deepcopy,
 )
 
+if TYPE_CHECKING:
+    from typing import Any, Dict, List, Optional, Tuple, Union
+
+    from requests.auth import AuthBase
+
+    from eodag.api.product import EOProduct
+    from eodag.api.search_result import SearchResult
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import DownloadedCallback, ProgressCallback, Unpack
+
 logger = logging.getLogger("eodag.apis.base")
 
 
@@ -73,6 +77,11 @@ class Api(PluginTopic):
       (it certainly indicates that the download didn't complete)
     """
 
+    auth: Union[AuthBase, Dict[str, str]]
+    next_page_url: Optional[str]
+    next_page_query_obj: Optional[Dict[str, Any]]
+    _request: Any  # needed by deprecated load_stac_items
+
     def clear(self) -> None:
         """Method used to clear a search context between two searches."""
         pass
@@ -132,19 +141,19 @@ class Api(PluginTopic):
     def download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> Optional[str]:
         """
         Base download method. Not available, it must be defined for each plugin.
 
         :param product: The EO product to download
         :type product: :class:`~eodag.api.product._product.EOProduct`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Union[AuthBase, Dict[str, str]]
         :param progress_callback: (optional) A progress callback
         :type progress_callback: :class:`~eodag.utils.ProgressCallback`
         :param wait: (optional) If download fails, wait time in minutes between two download tries
@@ -169,20 +178,20 @@ class Api(PluginTopic):
     def download_all(
         self,
         products: SearchResult,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         downloaded_callback: Optional[DownloadedCallback] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> List[str]:
         """
         Base download_all method.
 
         :param products: Products to download
         :type products: :class:`~eodag.api.search_result.SearchResult`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Optional[Union[AuthBase, Dict[str, str]]]
         :param downloaded_callback: (optional) A method or a callable object which takes
                                     as parameter the ``product``. You can use the base class
                                     :class:`~eodag.api.product.DownloadedCallback` and override
diff --git a/eodag/plugins/apis/cds.py b/eodag/plugins/apis/cds.py
index c202fb8d..3a6ebcaf 100644
--- a/eodag/plugins/apis/cds.py
+++ b/eodag/plugins/apis/cds.py
@@ -19,7 +19,7 @@ from __future__ import annotations
 
 import logging
 from datetime import datetime, timedelta
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, cast
+from typing import TYPE_CHECKING, cast
 from urllib.parse import unquote_plus
 
 import cdsapi
@@ -69,15 +69,18 @@ from eodag.utils.exceptions import (
 from eodag.utils.logging import get_logging_verbose
 
 if TYPE_CHECKING:
+    from typing import Any, Dict, List, Optional, Set, Tuple, Union
+
+    from requests.auth import AuthBase
+
     from eodag.api.product import EOProduct
     from eodag.api.search_result import SearchResult
     from eodag.config import PluginConfig
-    from eodag.utils import DownloadedCallback, ProgressCallback
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import DownloadedCallback, ProgressCallback, Unpack
 
 logger = logging.getLogger("eodag.apis.cds")
 
-CDS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
-
 
 class CdsApi(HTTPDownload, Api, BuildPostSearchResult):
     """A plugin that enables to build download-request and download data on CDS API.
@@ -156,7 +159,7 @@ class CdsApi(HTTPDownload, Api, BuildPostSearchResult):
 
         return non_none_cfg.get(key, default)
 
-    def _preprocess_search_params(self, params: Dict[Any]) -> None:
+    def _preprocess_search_params(self, params: Dict[str, Any]) -> None:
         """Preprocess search parameters before making a request to the CDS API.
 
         This method is responsible for checking and updating the provided search parameters
@@ -305,7 +308,7 @@ class CdsApi(HTTPDownload, Api, BuildPostSearchResult):
         if not all([uid, api_key, url]):
             raise AuthenticationError("Missing authentication information")
 
-        auth_dict: Dict[str, str] = {"key": f"{uid}:{api_key}", "url": url}
+        auth_dict: Dict[str, str] = {"key": f"{uid}:{api_key}", "url": str(url)}
 
         client = self._get_cds_client(**auth_dict)
         try:
@@ -361,21 +364,18 @@ class CdsApi(HTTPDownload, Api, BuildPostSearchResult):
     def download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> Optional[str]:
         """Download data from providers using CDS API"""
-        product_format = product.properties.get("format", "grib")
-        product_extension = CDS_KNOWN_FORMATS.get(product_format, product_format)
 
         # Prepare download
         fs_path, record_filename = self._prepare_download(
             product,
             progress_callback=progress_callback,
-            outputs_extension=f".{product_extension}",
             **kwargs,
         )
 
@@ -399,11 +399,11 @@ class CdsApi(HTTPDownload, Api, BuildPostSearchResult):
     def _stream_download_dict(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> StreamResponse:
         """Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
         It contains a generator to streamed download chunks and the response headers."""
@@ -421,12 +421,12 @@ class CdsApi(HTTPDownload, Api, BuildPostSearchResult):
     def download_all(
         self,
         products: SearchResult,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         downloaded_callback: Optional[DownloadedCallback] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ):
         """
         Download all using parent (base plugin) method
diff --git a/eodag/plugins/apis/ecmwf.py b/eodag/plugins/apis/ecmwf.py
index bae35ac7..07155f95 100644
--- a/eodag/plugins/apis/ecmwf.py
+++ b/eodag/plugins/apis/ecmwf.py
@@ -20,7 +20,7 @@ from __future__ import annotations
 import logging
 import os
 from datetime import datetime
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING
 
 import geojson
 from ecmwfapi import ECMWFDataServer, ECMWFService
@@ -45,10 +45,15 @@ from eodag.utils.exceptions import AuthenticationError, DownloadError
 from eodag.utils.logging import get_logging_verbose
 
 if TYPE_CHECKING:
+    from typing import Any, Dict, List, Optional, Tuple, Union
+
+    from requests.auth import AuthBase
+
     from eodag.api.product import EOProduct
     from eodag.api.search_result import SearchResult
     from eodag.config import PluginConfig
-    from eodag.utils import DownloadedCallback, ProgressCallback
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import DownloadedCallback, ProgressCallback, Unpack
 
 logger = logging.getLogger("eodag.apis.ecmwf")
 
@@ -158,21 +163,23 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
     def download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> Optional[str]:
         """Download data from ECMWF MARS"""
         product_format = product.properties.get("format", "grib")
         product_extension = ECMWF_MARS_KNOWN_FORMATS.get(product_format, product_format)
+        kwargs["outputs_extension"] = kwargs.get(
+            "outputs_extension", f".{product_extension}"
+        )
 
         # Prepare download
         fs_path, record_filename = self._prepare_download(
             product,
             progress_callback=progress_callback,
-            outputs_extension=f".{product_extension}",
             **kwargs,
         )
 
@@ -237,7 +244,6 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
         product_path = self._finalize(
             new_fs_path,
             progress_callback=progress_callback,
-            outputs_extension=f".{product_extension}",
             **kwargs,
         )
         product.location = path_to_uri(product_path)
@@ -246,12 +252,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
     def download_all(
         self,
         products: SearchResult,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         downloaded_callback: Optional[DownloadedCallback] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> List[str]:
         """
         Download all using parent (base plugin) method
diff --git a/eodag/plugins/apis/usgs.py b/eodag/plugins/apis/usgs.py
index 46469ace..9aab1bc4 100644
--- a/eodag/plugins/apis/usgs.py
+++ b/eodag/plugins/apis/usgs.py
@@ -21,7 +21,7 @@ import logging
 import shutil
 import tarfile
 import zipfile
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
 
 import requests
 from jsonpath_ng.ext import parse
@@ -56,9 +56,12 @@ from eodag.utils.exceptions import (
 )
 
 if TYPE_CHECKING:
+    from requests.auth import AuthBase
+
     from eodag.api.search_result import SearchResult
     from eodag.config import PluginConfig
-    from eodag.utils import DownloadedCallback
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import DownloadedCallback, Unpack
 
 logger = logging.getLogger("eodag.apis.usgs")
 
@@ -72,7 +75,7 @@ class UsgsApi(Download, Api):
         # Same method as in base.py, Search.__init__()
         # Prepare the metadata mapping
         # Do a shallow copy, the structure is flat enough for this to be sufficient
-        metas = DEFAULT_METADATA_MAPPING.copy()
+        metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
         # Update the defaults with the mapping value. This will add any new key
         # added by the provider mapping that is not in the default metadata.
         metas.update(self.config.metadata_mapping)
@@ -233,11 +236,11 @@ class UsgsApi(Download, Api):
     def download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> Optional[str]:
         """Download data from USGS catalogues"""
 
@@ -253,11 +256,11 @@ class UsgsApi(Download, Api):
                 product.product_type, self.config.products[GENERIC_PRODUCT_TYPE]  # type: ignore
             ).get("outputs_extension", ".tar.gz"),
         )
+        kwargs["outputs_extension"] = kwargs.get("outputs_extension", outputs_extension)
 
         fs_path, record_filename = self._prepare_download(
             product,
             progress_callback=progress_callback,
-            outputs_extension=outputs_extension,
             **kwargs,
         )
         if not fs_path or not record_filename:
@@ -317,7 +320,7 @@ class UsgsApi(Download, Api):
             product: EOProduct,
             fs_path: str,
             progress_callback: ProgressCallback,
-            **kwargs: Any,
+            **kwargs: Unpack[DownloadConf],
         ) -> None:
             try:
                 with requests.get(
@@ -360,13 +363,12 @@ class UsgsApi(Download, Api):
         api.logout()
 
         # Check downloaded file format
-        if (outputs_extension == ".tar.gz" and tarfile.is_tarfile(fs_path)) or (
-            outputs_extension == ".zip" and zipfile.is_zipfile(fs_path)
-        ):
+        if (
+            kwargs["outputs_extension"] == ".tar.gz" and tarfile.is_tarfile(fs_path)
+        ) or (kwargs["outputs_extension"] == ".zip" and zipfile.is_zipfile(fs_path)):
             product_path = self._finalize(
                 fs_path,
                 progress_callback=progress_callback,
-                outputs_extension=outputs_extension,
                 **kwargs,
             )
             product.location = path_to_uri(product_path)
@@ -399,12 +401,12 @@ class UsgsApi(Download, Api):
     def download_all(
         self,
         products: SearchResult,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         downloaded_callback: Optional[DownloadedCallback] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> List[str]:
         """
         Download all using parent (base plugin) method
diff --git a/eodag/plugins/authentication/aws_auth.py b/eodag/plugins/authentication/aws_auth.py
index d1b9cf48..7b7c73ff 100644
--- a/eodag/plugins/authentication/aws_auth.py
+++ b/eodag/plugins/authentication/aws_auth.py
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Dict
 from eodag.plugins.authentication.base import Authentication
 
 if TYPE_CHECKING:
-    from botocore.client import S3
+    from mypy_boto3_s3.client import S3Client
 
     from eodag.config import PluginConfig
 
@@ -39,7 +39,7 @@ class AwsAuth(Authentication):
       will be skipped if AWS credentials are filled in eodag conf
     """
 
-    s3_client: S3
+    s3_client: S3Client
 
     def __init__(self, provider: str, config: PluginConfig) -> None:
         super(AwsAuth, self).__init__(provider, config)
diff --git a/eodag/plugins/authentication/header.py b/eodag/plugins/authentication/header.py
index fd263f67..69a79af8 100644
--- a/eodag/plugins/authentication/header.py
+++ b/eodag/plugins/authentication/header.py
@@ -60,7 +60,7 @@ class HTTPHeaderAuth(Authentication):
     Expect an undefined behaviour if you use empty braces in header value strings.
     """
 
-    def authenticate(self) -> AuthBase:
+    def authenticate(self) -> HeaderAuth:
         """Authenticate"""
         self.validate_config_credentials()
         headers = {
diff --git a/eodag/plugins/authentication/keycloak.py b/eodag/plugins/authentication/keycloak.py
index c0b77d45..8ed73f8b 100644
--- a/eodag/plugins/authentication/keycloak.py
+++ b/eodag/plugins/authentication/keycloak.py
@@ -19,7 +19,7 @@ from __future__ import annotations
 
 import logging
 from datetime import datetime
-from typing import TYPE_CHECKING, Dict, Union
+from typing import TYPE_CHECKING, Any, Dict, TypedDict
 
 import requests
 
@@ -82,7 +82,17 @@ class KeycloakOIDCPasswordAuth(Authentication):
     REQUIRED_PARAMS = ["auth_base_uri", "client_id", "client_secret", "token_provision"]
     # already retrieved token store, to be used if authenticate() fails (OTP use-case)
     retrieved_token: str = ""
-    token_info: Dict[str, Union[str, datetime]] = {}
+
+    class TokenInfo(TypedDict, total=False):
+        """Token infos"""
+
+        refresh_token: str
+        refresh_time: datetime
+        token_time: datetime
+        access_token_expiration: float
+        refresh_token_expiration: float
+
+    token_info: TokenInfo = {}
 
     def __init__(self, provider: str, config: PluginConfig) -> None:
         super(KeycloakOIDCPasswordAuth, self).__init__(provider, config)
@@ -149,7 +159,7 @@ class KeycloakOIDCPasswordAuth(Authentication):
         logger.debug("using already retrieved access token")
         return self.retrieved_token
 
-    def _request_new_token(self) -> Dict[str, str]:
+    def _request_new_token(self) -> Dict[str, Any]:
         logger.debug("fetching new access token")
         req_data = {
             "client_id": self.config.client_id,
@@ -184,7 +194,8 @@ class KeycloakOIDCPasswordAuth(Authentication):
             if not isinstance(auth_errors, list):
                 auth_errors = [auth_errors]
             if (
-                hasattr(e.response, "status_code")
+                e.response
+                and hasattr(e.response, "status_code")
                 and e.response.status_code in auth_errors
             ):
                 raise AuthenticationError(
@@ -196,7 +207,7 @@ class KeycloakOIDCPasswordAuth(Authentication):
                 import traceback as tb
 
                 logger.error(
-                    f"Provider {self.provider} returned {e.response.status_code}: {response_text}"
+                    f"Provider {self.provider} returned {getattr(e.response, 'status_code', '')}: {response_text}"
                 )
                 raise AuthenticationError(
                     "Something went wrong while trying to get access token:\n{}".format(
diff --git a/eodag/plugins/authentication/oauth.py b/eodag/plugins/authentication/oauth.py
index 2687796b..8f31a3e5 100644
--- a/eodag/plugins/authentication/oauth.py
+++ b/eodag/plugins/authentication/oauth.py
@@ -17,7 +17,7 @@
 # limitations under the License.
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Dict
+from typing import TYPE_CHECKING, Dict, Optional
 
 from eodag.plugins.authentication.base import Authentication
 
@@ -30,8 +30,8 @@ class OAuth(Authentication):
 
     def __init__(self, provider: str, config: PluginConfig) -> None:
         super(OAuth, self).__init__(provider, config)
-        self.access_key = None
-        self.secret_key = None
+        self.access_key: Optional[str] = None
+        self.secret_key: Optional[str] = None
 
     def authenticate(self) -> Dict[str, str]:
         """Authenticate"""
diff --git a/eodag/plugins/authentication/openid_connect.py b/eodag/plugins/authentication/openid_connect.py
index c6a7acf9..81a11ad4 100644
--- a/eodag/plugins/authentication/openid_connect.py
+++ b/eodag/plugins/authentication/openid_connect.py
@@ -28,7 +28,12 @@ from requests.auth import AuthBase
 
 from eodag.plugins.authentication import Authentication
 from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, parse_qs, repeatfunc, urlparse
-from eodag.utils.exceptions import AuthenticationError, MisconfiguredError, TimeOutError
+from eodag.utils.exceptions import (
+    AuthenticationError,
+    MisconfiguredError,
+    RequestError,
+    TimeOutError,
+)
 
 if TYPE_CHECKING:
     from requests import PreparedRequest, Response
@@ -198,7 +203,7 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
         }
         # Add the credentials
         login_data.update(self.config.credentials)
-        auth_uri = getattr(self.config, "authentication_uri", None)
+
         # Retrieve the authentication_uri from the login form if so configured
         if self.config.authentication_uri_source == "login-form":
             # Given that the login_form_xpath resolves to an HTML element, if suffices to add '/@action' to get
@@ -206,6 +211,14 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
             auth_uri = login_form.xpath(
                 self.config.login_form_xpath.rstrip("/") + "/@action"
             )[0]
+            if not auth_uri:
+                raise RequestError(
+                    f"Could not get auth_uri from {self.config.login_form_xpath}"
+                )
+        else:
+            auth_uri = getattr(self.config, "authentication_uri", None)
+            if not auth_uri:
+                raise MisconfiguredError("authentication_uri is missing")
         return self.session.post(
             auth_uri, data=login_data, headers=USER_AGENT, timeout=HTTP_REQ_TIMEOUT
         )
@@ -261,7 +274,7 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
                     "client_secret": self.config.client_secret,
                 }
             )
-        post_request_kwargs = {
+        post_request_kwargs: Any = {
             self.config.token_exchange_post_data_method: token_exchange_data
         }
         r = self.session.post(
diff --git a/eodag/plugins/download/aws.py b/eodag/plugins/download/aws.py
index 2bef3d1d..f6d78409 100644
--- a/eodag/plugins/download/aws.py
+++ b/eodag/plugins/download/aws.py
@@ -43,6 +43,7 @@ import requests
 from botocore.exceptions import ClientError, ProfileNotFound
 from botocore.handlers import disable_signing
 from lxml import etree
+from requests.auth import AuthBase
 from stream_zip import ZIP_AUTO, stream_zip
 
 from eodag.api.product.metadata_mapping import (
@@ -67,6 +68,7 @@ from eodag.utils import (
 from eodag.utils.exceptions import (
     AuthenticationError,
     DownloadError,
+    MisconfiguredError,
     NotAvailableError,
     TimeOutError,
 )
@@ -77,7 +79,8 @@ if TYPE_CHECKING:
     from eodag.api.product import EOProduct
     from eodag.api.search_result import SearchResult
     from eodag.config import PluginConfig
-    from eodag.utils import DownloadedCallback
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import DownloadedCallback, Unpack
 
 
 logger = logging.getLogger("eodag.download.aws")
@@ -231,12 +234,12 @@ class AwsDownload(Download):
     def download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
-    ) -> str:
+        **kwargs: Unpack[DownloadConf],
+    ) -> Optional[str]:
         """Download method for AWS S3 API.
 
         The product can be downloaded as it is, or as SAFE-formatted product.
@@ -247,7 +250,7 @@ class AwsDownload(Download):
 
         :param product: The EO product to download
         :type product: :class:`~eodag.api.product._product.EOProduct`
-        :param auth: (optional) The configuration of a plugin of type Authentication
+        :param auth: (optional) authenticated object
         :type auth: Union[AuthBase, Dict[str, str]]
         :param progress_callback: (optional) A method or a callable object
                                   which takes a current size and a maximum
@@ -263,6 +266,11 @@ class AwsDownload(Download):
         :returns: The absolute path to the downloaded product in the local filesystem
         :rtype: str
         """
+        if auth is None:
+            auth = {}
+        if isinstance(auth, AuthBase):
+            raise MisconfiguredError("Please use AwsAuth plugin with AwsDownload")
+
         if progress_callback is None:
             logger.info(
                 "Progress bar unavailable, please call product.download() instead of plugin.download()"
@@ -273,7 +281,7 @@ class AwsDownload(Download):
         product_local_path, record_filename = self._download_preparation(
             product, progress_callback=progress_callback, **kwargs
         )
-        if not record_filename:
+        if not record_filename or not product_local_path:
             return product_local_path
 
         product_conf = getattr(self.config, "products", {}).get(
@@ -385,8 +393,11 @@ class AwsDownload(Download):
         return product_local_path
 
     def _download_preparation(
-        self, product: EOProduct, progress_callback: ProgressCallback, **kwargs: Any
-    ) -> Tuple[str, Optional[str]]:
+        self,
+        product: EOProduct,
+        progress_callback: ProgressCallback,
+        **kwargs: Unpack[DownloadConf],
+    ) -> Tuple[Optional[str], Optional[str]]:
         """
         preparation for the download:
         - check if file was already downloaded
@@ -455,7 +466,10 @@ class AwsDownload(Download):
                 )
 
     def _get_bucket_names_and_prefixes(
-        self, product: EOProduct, asset_filter: str, ignore_assets: bool
+        self,
+        product: EOProduct,
+        asset_filter: Optional[str] = None,
+        ignore_assets: Optional[bool] = False,
     ) -> List[Tuple[str, Optional[str]]]:
         """
         retrieves the bucket names and path prefixes for the assets
@@ -484,7 +498,7 @@ class AwsDownload(Download):
                         rf"No asset key matching re.fullmatch(r'{asset_filter}') was found in {product}"
                     )
             else:
-                assets_values = getattr(product, "assets", {}).values()
+                assets_values = product.assets.values()
 
             bucket_names_and_prefixes = []
             for complementary_url in assets_values:
@@ -502,8 +516,8 @@ class AwsDownload(Download):
     def _do_authentication(
         self,
         bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
-        auth: Dict[str, str],
-    ) -> Tuple[Dict[str, Any], ResourceCollection[Any]]:
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
+    ) -> Tuple[Dict[str, Any], ResourceCollection]:
         """
         authenticates with s3 and retrieves the available objects
         raises an error when authentication is not possible
@@ -514,11 +528,19 @@ class AwsDownload(Download):
         :return: authenticated objects per bucket, list of available objects
         :rtype: Tuple[Dict[str, Any], ResourceCollection[Any]]
         """
+        if not isinstance(auth, (dict, type(None))):
+            raise AuthenticationError(
+                f"Incompatible authentication information, expected dict or None, got {type(auth)}"
+            )
+        if auth is None:
+            auth = {}
         authenticated_objects: Dict[str, Any] = {}
         auth_error_messages: Set[str] = set()
         for _, pack in enumerate(bucket_names_and_prefixes):
             try:
                 bucket_name, prefix = pack
+                if not prefix:
+                    continue
                 if bucket_name not in authenticated_objects:
                     # get Prefixes longest common base path
                     common_prefix = ""
@@ -533,7 +555,7 @@ class AwsDownload(Download):
                                 [
                                     p
                                     for b, p in bucket_names_and_prefixes
-                                    if b == bucket_name and common_prefix in p
+                                    if p and b == bucket_name and common_prefix in p
                                 ]
                             )
                             < prefixes_in_bucket
@@ -567,7 +589,7 @@ class AwsDownload(Download):
         self,
         bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
         authenticated_objects: Dict[str, Any],
-        asset_filter: str,
+        asset_filter: Optional[str],
         ignore_assets: bool,
         product: EOProduct,
     ) -> Set[Any]:
@@ -627,11 +649,11 @@ class AwsDownload(Download):
     def _stream_download_dict(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> StreamResponse:
         r"""
         Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
@@ -1314,12 +1336,12 @@ class AwsDownload(Download):
     def download_all(
         self,
         products: SearchResult,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         downloaded_callback: Optional[DownloadedCallback] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> List[str]:
         """
         download_all using parent (base plugin) method
diff --git a/eodag/plugins/download/base.py b/eodag/plugins/download/base.py
index 3d455dab..11550972 100644
--- a/eodag/plugins/download/base.py
+++ b/eodag/plugins/download/base.py
@@ -55,10 +55,13 @@ from eodag.utils.exceptions import (
 from eodag.utils.notebook import NotebookWidgets
 
 if TYPE_CHECKING:
+    from requests.auth import AuthBase
+
     from eodag.api.product import EOProduct
     from eodag.api.search_result import SearchResult
     from eodag.config import PluginConfig
-    from eodag.utils import DownloadedCallback
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import DownloadedCallback, Unpack
 
 
 logger = logging.getLogger("eodag.download.base")
@@ -109,19 +112,19 @@ class Download(PluginTopic):
     def download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> Optional[str]:
         r"""
         Base download method. Not available, it must be defined for each plugin.
 
         :param product: The EO product to download
         :type product: :class:`~eodag.api.product._product.EOProduct`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Optional[Union[AuthBase, Dict[str, str]]]
         :param progress_callback: (optional) A progress callback
         :type progress_callback: :class:`~eodag.utils.ProgressCallback`
         :param wait: (optional) If download fails, wait time in minutes between two download tries
@@ -146,19 +149,19 @@ class Download(PluginTopic):
     def _stream_download_dict(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> StreamResponse:
         r"""
         Base _stream_download_dict method. Not available, it must be defined for each plugin.
 
         :param product: The EO product to download
         :type product: :class:`~eodag.api.product._product.EOProduct`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Optional[Union[AuthBase, Dict[str, str]]]
         :param progress_callback: (optional) A progress callback
         :type progress_callback: :class:`~eodag.utils.ProgressCallback`
         :param wait: (optional) If download fails, wait time in minutes between two download tries
@@ -182,7 +185,7 @@ class Download(PluginTopic):
         self,
         product: EOProduct,
         progress_callback: Optional[ProgressCallback] = None,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> Tuple[Optional[str], Optional[str]]:
         """Check if file has already been downloaded, and prepare product download
 
@@ -234,7 +237,9 @@ class Download(PluginTopic):
             prefix,
             f"{sanitize(product.properties['title'])}{collision_avoidance_suffix}{outputs_extension}",
         )
-        fs_dir_path = fs_path.replace(outputs_extension, "")
+        fs_dir_path = (
+            fs_path.replace(outputs_extension, "") if outputs_extension else fs_path
+        )
         download_records_dir = os.path.join(prefix, ".downloaded")
         try:
             os.makedirs(download_records_dir)
@@ -319,7 +324,7 @@ class Download(PluginTopic):
         self,
         fs_path: str,
         progress_callback: Optional[ProgressCallback] = None,
-        **kwargs: Any,
+        **kwargs: Unpack[DownloadConf],
     ) -> str:
         """Finalize the download process.
 
@@ -464,12 +469,12 @@ class Download(PluginTopic):
     def download_all(
         self,
         products: SearchResult,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         downloaded_callback: Optional[DownloadedCallback] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> List[str]:
         """
         Base download_all method.
@@ -479,8 +484,8 @@ class Download(PluginTopic):
 
         :param products: Products to download
         :type products: :class:`~eodag.api.search_result.SearchResult`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Optional[Union[AuthBase, Dict[str, str]]]
         :param downloaded_callback: (optional) A method or a callable object which takes
                                     as parameter the ``product``. You can use the base class
                                     :class:`~eodag.api.product.DownloadedCallback` and override
@@ -638,7 +643,7 @@ class Download(PluginTopic):
         """
 
         def decorator(download: Callable[..., T]) -> Callable[..., T]:
-            def download_and_retry(*args: Any, **kwargs: Any) -> T:
+            def download_and_retry(*args: Any, **kwargs: Unpack[DownloadConf]) -> T:
                 # initiate retry loop
                 start_time = datetime.now()
                 stop_time = start_time + timedelta(minutes=timeout)
@@ -662,7 +667,7 @@ class Download(PluginTopic):
                                     f"Product is not available for download and order is not supported for"
                                     f" {self.provider}, {e}"
                                 )
-                            not_available_info = e
+                            not_available_info = str(e)
                             pass
 
                     if datetime_now >= product.next_try and datetime_now < stop_time:
diff --git a/eodag/plugins/download/http.py b/eodag/plugins/download/http.py
index 44563a2e..27480b54 100644
--- a/eodag/plugins/download/http.py
+++ b/eodag/plugins/download/http.py
@@ -25,7 +25,18 @@ import zipfile
 from datetime import datetime
 from email.message import Message
 from itertools import chain
-from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Tuple, Union
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Dict,
+    Iterator,
+    List,
+    Optional,
+    Tuple,
+    TypedDict,
+    Union,
+    cast,
+)
 from urllib.parse import parse_qs, urlparse
 
 import geojson
@@ -33,6 +44,7 @@ import requests
 import requests_ftp
 from lxml import etree
 from requests import RequestException
+from requests.auth import AuthBase
 from stream_zip import ZIP_AUTO, stream_zip
 
 from eodag.api.product.metadata_mapping import (
@@ -69,9 +81,11 @@ if TYPE_CHECKING:
     from requests import Response
 
     from eodag.api.product import EOProduct
+    from eodag.api.product._assets import Asset
     from eodag.api.search_result import SearchResult
     from eodag.config import PluginConfig
-    from eodag.utils import DownloadedCallback
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import DownloadedCallback, Unpack
 
 logger = logging.getLogger("eodag.download.http")
 
@@ -114,8 +128,8 @@ class HTTPDownload(Download):
     def orderDownload(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        auth: Optional[AuthBase] = None,
+        **kwargs: Unpack[DownloadConf],
     ) -> None:
         """Send product order request.
 
@@ -138,12 +152,16 @@ class HTTPDownload(Download):
 
         :param product: The EO product to order
         :type product: :class:`~eodag.api.product._product.EOProduct`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Optional[AuthBase]
         :param kwargs: download additional kwargs
         :type kwargs: Union[str, bool, dict]
         """
         order_method = getattr(self.config, "order_method", "GET").lower()
+        OrderKwargs = TypedDict(
+            "OrderKwargs", {"json": Dict[str, Union[Any, List[str]]]}, total=False
+        )
+        order_kwargs: OrderKwargs = {}
         if order_method == "post":
             # separate url & parameters
             parts = urlparse(str(product.properties["orderLink"]))
@@ -151,7 +169,8 @@ class HTTPDownload(Download):
             if not query_dict and parts.query:
                 query_dict = geojson.loads(parts.query)
             order_url = parts._replace(query=None).geturl()
-            order_kwargs = {"json": query_dict} if query_dict else {}
+            if query_dict:
+                order_kwargs["json"] = query_dict
         else:
             order_url = product.properties["orderLink"]
             order_kwargs = {}
@@ -204,8 +223,8 @@ class HTTPDownload(Download):
     def orderDownloadStatus(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        auth: Optional[AuthBase] = None,
+        **kwargs: Unpack[DownloadConf],
     ) -> None:
         """Send product order status request.
 
@@ -224,12 +243,16 @@ class HTTPDownload(Download):
 
         :param product: The ordered EO product
         :type product: :class:`~eodag.api.product._product.EOProduct`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Optional[AuthBase]
         :param kwargs: download additional kwargs
         :type kwargs: Union[str, bool, dict]
         """
         status_method = getattr(self.config, "order_status_method", "GET").lower()
+        StatusKwargs = TypedDict(
+            "StatusKwargs", {"json": Dict[str, Union[Any, List[str]]]}, total=False
+        )
+        status_kwargs: StatusKwargs = {}
         if status_method == "post":
             # separate url & parameters
             parts = urlparse(str(product.properties["orderStatusLink"]))
@@ -335,7 +358,7 @@ class HTTPDownload(Download):
                             new_search_metadata_mapping = (
                                 self.config.order_status_on_success["metadata_mapping"]
                             )
-                            order_metadata_mapping_jsonpath = {}
+                            order_metadata_mapping_jsonpath: Dict[str, Any] = {}
                             order_metadata_mapping_jsonpath = (
                                 mtd_cfg_as_conversion_and_querypath(
                                     new_search_metadata_mapping,
@@ -375,11 +398,11 @@ class HTTPDownload(Download):
     def download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> Optional[str]:
         """Download a product using HTTP protocol.
 
@@ -387,23 +410,25 @@ class HTTPDownload(Download):
         the user is warned, it is renamed to remove the zip extension and
         no further treatment is done (no extraction)
         """
+        if auth is not None and not isinstance(auth, AuthBase):
+            raise MisconfiguredError(f"Incompatible auth plugin: {type(auth)}")
+
         if progress_callback is None:
             logger.info(
                 "Progress bar unavailable, please call product.download() instead of plugin.download()"
             )
             progress_callback = ProgressCallback(disable=True)
 
-        kwargs.pop("outputs_extension", "parameter not available in kwargs")
         outputs_extension = getattr(self.config, "products", {}).get(
             product.product_type, {}
         ).get("outputs_extension", None) or getattr(
             self.config, "outputs_extension", ".zip"
         )
+        kwargs["outputs_extension"] = kwargs.get("outputs_extension", outputs_extension)
 
         fs_path, record_filename = self._prepare_download(
             product,
             progress_callback=progress_callback,
-            outputs_extension=outputs_extension,
             **kwargs,
         )
         if not fs_path or not record_filename:
@@ -436,11 +461,11 @@ class HTTPDownload(Download):
         @self._download_retry(product, wait, timeout)
         def download_request(
             product: EOProduct,
-            auth: PluginConfig,
+            auth: AuthBase,
             progress_callback: ProgressCallback,
             wait: int,
             timeout: int,
-            **kwargs: Dict[str, Any],
+            **kwargs: Unpack[DownloadConf],
         ) -> None:
             chunks = self._stream_download(product, auth, progress_callback, **kwargs)
 
@@ -476,10 +501,10 @@ class HTTPDownload(Download):
                 if not new_fs_path.endswith(".tar"):
                     new_fs_path += ".tar"
                 shutil.move(fs_path, new_fs_path)
+                kwargs["outputs_extension"] = ".tar"
                 product_path = self._finalize(
                     new_fs_path,
                     progress_callback=progress_callback,
-                    outputs_extension=".tar",
                     **kwargs,
                 )
                 product.location = path_to_uri(product_path)
@@ -505,7 +530,6 @@ class HTTPDownload(Download):
         product_path = self._finalize(
             fs_path,
             progress_callback=progress_callback,
-            outputs_extension=outputs_extension,
             **kwargs,
         )
         product.location = path_to_uri(product_path)
@@ -531,11 +555,11 @@ class HTTPDownload(Download):
     def _stream_download_dict(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> StreamResponse:
         r"""
         Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
@@ -543,8 +567,8 @@ class HTTPDownload(Download):
 
         :param product: The EO product to download
         :type product: :class:`~eodag.api.product._product.EOProduct`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Optional[Union[AuthBase, Dict[str, str]]]
         :param progress_callback: (optional) A progress callback
         :type progress_callback: :class:`~eodag.utils.ProgressCallback`
         :param wait: (optional) If download fails, wait time in minutes between two download tries
@@ -560,6 +584,9 @@ class HTTPDownload(Download):
         :returns: Dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
         :rtype: dict
         """
+        if auth is not None and not isinstance(auth, AuthBase):
+            raise MisconfiguredError(f"Incompatible auth plugin: {type(auth)}")
+
         # download assets if exist instead of remote_location
         if len(product.assets) > 0 and not getattr(self.config, "ignore_assets", False):
             try:
@@ -625,12 +652,13 @@ class HTTPDownload(Download):
         auth_errors = getattr(self.config, "auth_error_code", [None])
         if not isinstance(auth_errors, list):
             auth_errors = [auth_errors]
+        response_text = e.response.text.strip() if e.response else ""
         if e.response and e.response.status_code in auth_errors:
             raise AuthenticationError(
                 "HTTP Error %s returned, %s\nPlease check your credentials for %s"
                 % (
                     e.response.status_code,
-                    e.response.text.strip(),
+                    response_text,
                     self.provider,
                 )
             )
@@ -638,8 +666,8 @@ class HTTPDownload(Download):
         elif product.properties.get("storageStatus", ONLINE_STATUS) != ONLINE_STATUS:
             msg = (
                 ordered_message
-                if ordered_message and not e.response.text.strip()
-                else e.response.text.strip()
+                if ordered_message and not response_text
+                else response_text
             )
             raise NotAvailableError(
                 "%s(initially %s) requested, returned: %s"
@@ -655,15 +683,15 @@ class HTTPDownload(Download):
             logger.error(
                 "Error while getting resource :\n%s\n%s",
                 tb.format_exc(),
-                e.response.text.strip(),
+                response_text,
             )
 
     def _stream_download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[AuthBase] = None,
         progress_callback: Optional[ProgressCallback] = None,
-        **kwargs: Dict[str, Any],
+        **kwargs: Unpack[DownloadConf],
     ) -> Iterator[Any]:
         """
         fetches a zip file containing the assets of a given product as a stream
@@ -671,7 +699,7 @@ class HTTPDownload(Download):
         :param product: product for which the assets should be downloaded
         :type product: :class:`~eodag.api.product._product.EOProduct`
         :param auth: The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :type auth: Optional[Union[AuthBase, Dict[str, str]]]
         :param progress_callback: A method or a callable object
                                   which takes a current size and a maximum
                                   size as inputs and handle progress bar
@@ -751,9 +779,10 @@ class HTTPDownload(Download):
     def _stream_download_assets(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[AuthBase] = None,
         progress_callback: Optional[ProgressCallback] = None,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        assets_values: List[Asset] = [],
+        **kwargs: Unpack[DownloadConf],
     ) -> Iterator[Tuple[str, datetime, int, Any, Iterator[Any]]]:
         if progress_callback is None:
             logger.info("Progress bar unavailable, please call product.download()")
@@ -766,8 +795,6 @@ class HTTPDownload(Download):
         if not assets_urls:
             raise NotAvailableError("No assets available for %s" % product)
 
-        assets_values = kwargs.get("assets_values", [])
-
         # get extra parameters to pass to the query
         params = kwargs.pop("dl_url_params", None) or getattr(
             self.config, "dl_url_params", {}
@@ -846,15 +873,20 @@ class HTTPDownload(Download):
                             "content-disposition", None
                         )
                         if asset_content_disposition:
-                            asset.filename = parse_header(
-                                asset_content_disposition
-                            ).get_param("filename", None)
+                            asset.filename = cast(
+                                Optional[str],
+                                parse_header(asset_content_disposition).get_param(
+                                    "filename", None
+                                ),
+                            )
 
                     if not getattr(asset, "filename", None):
                         # default filename extracted from path
                         asset.filename = os.path.basename(asset.rel_path)
 
-                    asset.rel_path = os.path.join(asset_rel_dir, asset.filename)
+                    asset.rel_path = os.path.join(
+                        asset_rel_dir, cast(str, asset.filename)
+                    )
 
                     if len(assets_values) == 1:
                         # apply headers to asset
@@ -875,9 +907,9 @@ class HTTPDownload(Download):
         product: EOProduct,
         fs_dir_path: str,
         record_filename: str,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[AuthBase] = None,
         progress_callback: Optional[ProgressCallback] = None,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> str:
         """Download product assets if they exist"""
         if progress_callback is None:
@@ -979,7 +1011,7 @@ class HTTPDownload(Download):
         return fs_dir_path
 
     def _handle_asset_exception(
-        self, e: RequestException, asset: Dict[str, Any], raise_errors: bool = False
+        self, e: RequestException, asset: Asset, raise_errors: bool = False
     ) -> None:
         # check if error is identified as auth_error in provider conf
         auth_errors = getattr(self.config, "auth_error_code", [None])
@@ -1002,8 +1034,8 @@ class HTTPDownload(Download):
 
     def _get_asset_sizes(
         self,
-        assets_values: List[Dict[str, Any]],
-        auth: Optional[PluginConfig],
+        assets_values: List[Asset],
+        auth: Optional[AuthBase],
         params: Optional[Dict[str, str]],
         zipped: bool = False,
     ) -> int:
@@ -1032,12 +1064,14 @@ class HTTPDownload(Download):
                     )
                 if not getattr(asset, "size", 0):
                     # size from HEAD header / content-disposition / size
-                    asset.size = int(header_content_disposition.get_param("size", 0))
+                    size_str = str(header_content_disposition.get_param("size", 0))
+                    asset.size = int(size_str) if size_str.isdigit() else 0
                 if not getattr(asset, "filename", 0):
                     # filename from HEAD header / content-disposition / size
-                    asset.filename = header_content_disposition.get_param(
+                    asset_filename = header_content_disposition.get_param(
                         "filename", None
                     )
+                    asset.filename = str(asset_filename) if asset_filename else None
 
                 if not getattr(asset, "size", 0):
                     # GET request for size
@@ -1053,11 +1087,12 @@ class HTTPDownload(Download):
                         asset.size = int(stream.headers.get("Content-length", 0))
                         if not getattr(asset, "size", 0):
                             # size from GET header / content-disposition / size
-                            asset.size = int(
+                            size_str = str(
                                 parse_header(
                                     stream.headers.get("content-disposition", "")
                                 ).get_param("size", 0)
                             )
+                            asset.size = int(size_str) if size_str.isdigit() else 0
 
                 total_size += asset.size
         return total_size
@@ -1065,12 +1100,12 @@ class HTTPDownload(Download):
     def download_all(
         self,
         products: SearchResult,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         downloaded_callback: Optional[DownloadedCallback] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ):
         """
         Download all using parent (base plugin) method
diff --git a/eodag/plugins/download/s3rest.py b/eodag/plugins/download/s3rest.py
index c28c5217..dd9eced7 100644
--- a/eodag/plugins/download/s3rest.py
+++ b/eodag/plugins/download/s3rest.py
@@ -20,12 +20,13 @@ from __future__ import annotations
 import logging
 import os
 import os.path
-from typing import TYPE_CHECKING, Any, Dict, Optional, Union
+from typing import TYPE_CHECKING, Dict, Optional, Union
 from xml.dom import minidom
 from xml.parsers.expat import ExpatError
 
 import requests
 from requests import RequestException
+from requests.auth import AuthBase
 
 from eodag.api.product.metadata_mapping import OFFLINE_STATUS, ONLINE_STATUS
 from eodag.plugins.download.base import Download
@@ -45,6 +46,7 @@ from eodag.utils import (
 from eodag.utils.exceptions import (
     AuthenticationError,
     DownloadError,
+    MisconfiguredError,
     NotAvailableError,
     RequestError,
 )
@@ -52,6 +54,8 @@ from eodag.utils.exceptions import (
 if TYPE_CHECKING:
     from eodag.api.product import EOProduct
     from eodag.config import PluginConfig
+    from eodag.types.download_args import DownloadConf
+    from eodag.utils import Unpack
 
 logger = logging.getLogger("eodag.download.s3rest")
 
@@ -90,18 +94,18 @@ class S3RestDownload(Download):
     def download(
         self,
         product: EOProduct,
-        auth: Optional[PluginConfig] = None,
+        auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
         progress_callback: Optional[ProgressCallback] = None,
         wait: int = DEFAULT_DOWNLOAD_WAIT,
         timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
-        **kwargs: Union[str, bool, Dict[str, Any]],
+        **kwargs: Unpack[DownloadConf],
     ) -> Optional[str]:
         """Download method for S3 REST API.
 
         :param product: The EO product to download
         :type product: :class:`~eodag.api.product._product.EOProduct`
-        :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :param auth: (optional) authenticated object
+        :type auth: Optional[Union[AuthBase, Dict[str, str]]]
         :param progress_callback: (optional) A method or a callable object
                                   which takes a current size and a maximum
                                   size as inputs and handle progress bar
@@ -116,6 +120,9 @@ class S3RestDownload(Download):
         :returns: The absolute path to the downloaded product in the local filesystem
         :rtype: str
         """
+        if auth is not None and not isinstance(auth, AuthBase):
+            raise MisconfiguredError(f"Incompatible auth plugin: {type(auth)}")
+
         if progress_callback is None:
             logger.info(
                 "Progress bar unavailable, please call product.download() instead of plugin.download()"
@@ -134,10 +141,10 @@ class S3RestDownload(Download):
         @self._download_retry(product, wait, timeout)
         def download_request(
             product: EOProduct,
-            auth: PluginConfig,
+            auth: AuthBase,
             progress_callback: ProgressCallback,
             ordered_message: str,
-            **kwargs: Any,
+            **kwargs: Unpack[DownloadConf],
         ):
             # check order status
             if product.properties.get("orderStatusLink", None):
diff --git a/eodag/plugins/search/base.py b/eodag/plugins/search/base.py
index 1a2dfacb..aadc7744 100644
--- a/eodag/plugins/search/base.py
+++ b/eodag/plugins/search/base.py
@@ -18,10 +18,10 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
 
 import orjson
-from pydantic.fields import Field, FieldInfo
+from pydantic.fields import Field
 
 from eodag.api.product.metadata_mapping import (
     DEFAULT_METADATA_MAPPING,
@@ -40,6 +40,9 @@ from eodag.utils import (
 from eodag.utils.exceptions import ValidationError
 
 if TYPE_CHECKING:
+    from pydantic.fields import FieldInfo
+    from requests.auth import AuthBase
+
     from eodag.api.product import EOProduct
     from eodag.config import PluginConfig
 
@@ -55,11 +58,18 @@ class Search(PluginTopic):
     :type config: :class:`~eodag.config.PluginConfig`
     """
 
+    auth: Union[AuthBase, Dict[str, str]]
+    next_page_url: Optional[str]
+    next_page_query_obj: Optional[Dict[str, Any]]
+    total_items_nb: int
+    need_count: bool
+    _request: Any  # needed by deprecated load_stac_items
+
     def __init__(self, provider: str, config: PluginConfig) -> None:
         super(Search, self).__init__(provider, config)
         # Prepare the metadata mapping
         # Do a shallow copy, the structure is flat enough for this to be sufficient
-        metas = DEFAULT_METADATA_MAPPING.copy()
+        metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
         # Update the defaults with the mapping value. This will add any new key
         # added by the provider mapping that is not in the default metadata
         if self.config.metadata_mapping:
@@ -174,7 +184,7 @@ class Search(PluginTopic):
 
     def get_metadata_mapping(
         self, product_type: Optional[str] = None
-    ) -> Dict[str, str]:
+    ) -> Dict[str, Union[str, List[str]]]:
         """Get the plugin metadata mapping configuration (product type specific if exists)
 
         :param product_type: the desired product type
@@ -182,9 +192,11 @@ class Search(PluginTopic):
         :returns: The product type specific metadata-mapping
         :rtype: dict
         """
-        return self.config.products.get(product_type, {}).get(
-            "metadata_mapping", self.config.metadata_mapping
-        )
+        if product_type:
+            return self.config.products.get(product_type, {}).get(
+                "metadata_mapping", self.config.metadata_mapping
+            )
+        return self.config.metadata_mapping
 
     def get_sort_by_arg(self, kwargs: Dict[str, Any]) -> Optional[SortByList]:
         """Extract the "sortBy" argument from the kwargs or the provider default sort configuration
diff --git a/eodag/plugins/search/creodias_s3.py b/eodag/plugins/search/creodias_s3.py
index c016a050..97f0a9e6 100644
--- a/eodag/plugins/search/creodias_s3.py
+++ b/eodag/plugins/search/creodias_s3.py
@@ -65,16 +65,17 @@ def _update_assets(product: EOProduct, config: PluginConfig, auth: AwsAuth):
         try:
             auth_dict = auth.authenticate()
             required_creds = ["aws_access_key_id", "aws_secret_access_key"]
-            if not all(getattr(auth, x) for x in required_creds):
+            if not all(x in auth_dict for x in required_creds):
                 raise MisconfiguredError(
                     f"Incomplete credentials for {product.provider}, missing "
-                    f"{[x for x in required_creds if not getattr(auth, x)]}"
+                    f"{[x for x in required_creds if x not in auth_dict]}"
                 )
             if not getattr(auth, "s3_client", None):
                 auth.s3_client = boto3.client(
                     "s3",
                     endpoint_url=config.base_uri,
-                    **auth_dict,
+                    aws_access_key_id=auth_dict["aws_access_key_id"],
+                    aws_secret_access_key=auth_dict["aws_secret_access_key"],
                 )
             logger.debug("Listing assets in %s", prefix)
             product.assets = AssetsDict(product)
diff --git a/eodag/plugins/search/data_request_search.py b/eodag/plugins/search/data_request_search.py
index 4de7583b..9a864a59 100644
--- a/eodag/plugins/search/data_request_search.py
+++ b/eodag/plugins/search/data_request_search.py
@@ -20,7 +20,7 @@ from __future__ import annotations
 import logging
 import time
 from datetime import datetime, timedelta
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
 
 import requests
 
@@ -62,6 +62,8 @@ class DataRequestSearch(Search):
         - if finished - fetch the result of the job
     """
 
+    data_request_id: Optional[str]
+
     def __init__(self, provider: str, config: PluginConfig) -> None:
         super(DataRequestSearch, self).__init__(provider, config)
         self.config.__dict__.setdefault("result_type", "json")
@@ -106,7 +108,7 @@ class DataRequestSearch(Search):
             self.config.pagination["next_page_url_key_path"] = string_to_jsonpath(
                 self.config.pagination.get("next_page_url_key_path", None)
             )
-        self.download_info = {}
+        self.download_info: Dict[str, Any] = {}
         self.data_request_id = None
 
     def discover_product_types(self) -> Optional[Dict[str, Any]]:
@@ -137,16 +139,20 @@ class DataRequestSearch(Search):
             raise ValidationError(f"{self.provider} does not support sorting feature")
 
         product_type = kwargs.get("productType", None)
+
+        if product_type is None:
+            raise ValidationError("Required productType is missing")
+
         # replace "product_type" to "providerProductType" in search args if exists
         # for compatibility with DataRequestSearch method
         if kwargs.get("product_type"):
             kwargs["providerProductType"] = kwargs.pop("product_type", None)
-        provider_product_type = self._map_product_type(product_type or "")
+        provider_product_type = cast(str, self._map_product_type(product_type or ""))
         keywords = {k: v for k, v in kwargs.items() if k != "auth" and v is not None}
 
         if provider_product_type and provider_product_type != GENERIC_PRODUCT_TYPE:
             keywords["productType"] = provider_product_type
-        elif product_type:
+        else:
             keywords["productType"] = product_type
 
         # provider product type specific conf
@@ -279,9 +285,10 @@ class DataRequestSearch(Search):
     def _cancel_request(self, data_request_id: str) -> None:
         logger.info("deleting request job %s", data_request_id)
         delete_url = f"{self.config.data_request_url}/{data_request_id}"
+        headers = getattr(self.auth, "headers", USER_AGENT)
         try:
             delete_resp = requests.delete(
-                delete_url, headers=self.auth.headers, timeout=HTTP_REQ_TIMEOUT
+                delete_url, headers=headers, timeout=HTTP_REQ_TIMEOUT
             )
             delete_resp.raise_for_status()
         except requests.exceptions.Timeout as exc:
@@ -292,9 +299,10 @@ class DataRequestSearch(Search):
     def _check_request_status(self, data_request_id: str) -> bool:
         logger.debug("checking status of request job %s", data_request_id)
         status_url = self.config.status_url + data_request_id
+        headers = getattr(self.auth, "headers", USER_AGENT)
         try:
             status_resp = requests.get(
-                status_url, headers=self.auth.headers, timeout=HTTP_REQ_TIMEOUT
+                status_url, headers=headers, timeout=HTTP_REQ_TIMEOUT
             )
             status_resp.raise_for_status()
         except requests.exceptions.Timeout as exc:
@@ -323,10 +331,9 @@ class DataRequestSearch(Search):
         url = self.config.result_url.format(
             jobId=data_request_id, items_per_page=items_per_page, page=page
         )
+        headers = getattr(self.auth, "headers", USER_AGENT)
         try:
-            return requests.get(
-                url, headers=self.auth.headers, timeout=HTTP_REQ_TIMEOUT
-            ).json()
+            return requests.get(url, headers=headers, timeout=HTTP_REQ_TIMEOUT).json()
         except requests.exceptions.Timeout as exc:
             raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
         except requests.RequestException:
diff --git a/eodag/plugins/search/qssearch.py b/eodag/plugins/search/qssearch.py
index f6875ae4..ea153faf 100644
--- a/eodag/plugins/search/qssearch.py
+++ b/eodag/plugins/search/qssearch.py
@@ -20,7 +20,18 @@ from __future__ import annotations
 import logging
 import re
 from collections.abc import Iterable
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, cast
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Callable,
+    Dict,
+    List,
+    Optional,
+    Set,
+    Tuple,
+    TypedDict,
+    cast,
+)
 from urllib.error import URLError
 from urllib.request import Request, urlopen
 
@@ -32,6 +43,7 @@ from pydantic import create_model
 from pydantic.fields import FieldInfo
 from requests import Response
 from requests.adapters import HTTPAdapter
+from requests.auth import AuthBase
 
 from eodag.api.product import EOProduct
 from eodag.api.product.metadata_mapping import (
@@ -67,6 +79,7 @@ from eodag.utils.exceptions import (
     MisconfiguredError,
     RequestError,
     TimeOutError,
+    ValidationError,
 )
 
 if TYPE_CHECKING:
@@ -341,7 +354,7 @@ class QueryStringSearch(Search):
             return None
         else:
             try:
-                conf_update_dict = {
+                conf_update_dict: Dict[str, Any] = {
                     "providers_config": {},
                     "product_types_config": {},
                 }
@@ -463,14 +476,12 @@ class QueryStringSearch(Search):
         :param count: (optional) To trigger a count request
         :type count: bool
         """
-        product_type = kwargs.get("productType", None)
+        product_type = kwargs.get("productType", product_type)
         if product_type == GENERIC_PRODUCT_TYPE:
             logger.warning(
                 "GENERIC_PRODUCT_TYPE is not a real product_type and should only be used internally as a template"
             )
             return [], 0
-        # remove "product_type" from search args if exists for compatibility with QueryStringSearch methods
-        kwargs.pop("product_type", None)
 
         sort_by_arg: Optional[SortByList] = self.get_sort_by_arg(kwargs)
         sort_by_qs, _ = (
@@ -511,6 +522,9 @@ class QueryStringSearch(Search):
                 }
             )
 
+        if product_type is None:
+            raise ValidationError("Required productType is missing")
+
         qp, qs = self.build_query_string(product_type, **keywords)
 
         self.query_params = qp
@@ -552,7 +566,7 @@ class QueryStringSearch(Search):
 
         # Build the final query string, in one go without quoting it
         # (some providers do not operate well with urlencoded and quoted query strings)
-        quote_via: Callable[[Any], str] = lambda x, *_args, **_kwargs: x
+        quote_via: Callable[[Any, str, str, str], str] = lambda x, *_args, **_kwargs: x
         return (
             query_params,
             urlencode(query_params, doseq=True, quote_via=quote_via),
@@ -1219,7 +1233,10 @@ class PostJsonSearch(QueryStringSearch):
         timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
         try:
             # auth if needed
-            kwargs = {}
+            RequestsKwargs = TypedDict(
+                "RequestsKwargs", {"auth": AuthBase}, total=False
+            )
+            kwargs: RequestsKwargs = {}
             if (
                 getattr(self.config, "need_auth", False)
                 and hasattr(self, "auth")
diff --git a/eodag/types/download_args.py b/eodag/types/download_args.py
new file mode 100644
index 00000000..99e4a4ef
--- /dev/null
+++ b/eodag/types/download_args.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
+#
+# This file is part of EODAG project
+#     https://www.github.com/CS-SI/EODAG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import annotations
+
+from typing import Dict, TypedDict
+
+
+class DownloadConf(TypedDict, total=False):
+    """Download configuration"""
+
+    outputs_prefix: str
+    outputs_extension: str
+    extract: bool
+    dl_url_params: Dict[str, str]
+    delete_archive: bool
+    asset: str
diff --git a/eodag/utils/__init__.py b/eodag/utils/__init__.py
index 45c5b7b0..99bd705a 100644
--- a/eodag/utils/__init__.py
+++ b/eodag/utils/__init__.py
@@ -34,6 +34,7 @@ import os
 import re
 import shutil
 import string
+import sys
 import types
 import unicodedata
 import warnings
@@ -75,12 +76,16 @@ from urllib.parse import (  # noqa; noqa
 )
 from urllib.request import url2pathname
 
-try:
+if sys.version_info >= (3, 9):
     from typing import Annotated, get_args, get_origin  # noqa
-except ImportError:
-    # for python < 3.9
+else:
     from typing_extensions import Annotated, get_args, get_origin  # type: ignore # noqa
 
+if sys.version_info >= (3, 12):
+    from typing import Unpack  # type: ignore # noqa
+else:
+    from typing_extensions import Unpack  # noqa
+
 import click
 import orjson
 import shapefile
diff --git a/eodag/utils/constraints.py b/eodag/utils/constraints.py
index e59d4f7b..907c4b02 100644
--- a/eodag/utils/constraints.py
+++ b/eodag/utils/constraints.py
@@ -17,7 +17,7 @@
 # limitations under the License.
 import copy
 import logging
-from typing import Any, Dict, List, Set, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Set, Union
 
 import requests
 
@@ -27,6 +27,9 @@ from eodag.plugins.search.base import Search
 from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, deepcopy
 from eodag.utils.exceptions import TimeOutError, ValidationError
 
+if TYPE_CHECKING:
+    from requests.auth import AuthBase
+
 logger = logging.getLogger("eodag.constraints")
 
 
@@ -171,10 +174,11 @@ def fetch_constraints(
         headers = USER_AGENT
         logger.debug("fetching constraints from %s", constraints_url)
         if hasattr(plugin, "auth"):
+            auth = plugin.auth if isinstance(plugin.auth, AuthBase) else None
             res = requests.get(
                 constraints_url,
                 headers=headers,
-                auth=plugin.auth,
+                auth=auth,
                 timeout=HTTP_REQ_TIMEOUT,
             )
         else:
diff --git a/eodag/utils/exceptions.py b/eodag/utils/exceptions.py
index cd8d4af1..6dcc1236 100644
--- a/eodag/utils/exceptions.py
+++ b/eodag/utils/exceptions.py
@@ -23,7 +23,11 @@ if TYPE_CHECKING:
     from typing import Optional, Set, Tuple
 
 
-class ValidationError(Exception):
+class EodagError(Exception):
+    """General EODAG error"""
+
+
+class ValidationError(EodagError):
     """Error validating data"""
 
     def __init__(self, message: str, parameters: Set[str] = set()) -> None:
@@ -31,52 +35,52 @@ class ValidationError(Exception):
         self.parameters = parameters
 
 
-class PluginNotFoundError(Exception):
+class PluginNotFoundError(EodagError):
     """Error when looking for a plugin class that was not defined"""
 
 
-class PluginImplementationError(Exception):
+class PluginImplementationError(EodagError):
     """Error when a plugin does not behave as expected"""
 
 
-class MisconfiguredError(Exception):
+class MisconfiguredError(EodagError):
     """An error indicating a Search Plugin that is not well configured"""
 
 
-class AddressNotFound(Exception):
+class AddressNotFound(EodagError):
     """An error indicating the address of a subdataset was not found"""
 
 
-class UnsupportedProvider(Exception):
+class UnsupportedProvider(EodagError):
     """An error indicating that eodag does not support a provider"""
 
 
-class UnsupportedProductType(Exception):
+class UnsupportedProductType(EodagError):
     """An error indicating that eodag does not support a product type"""
 
     def __init__(self, product_type: str) -> None:
         self.product_type = product_type
 
 
-class UnsupportedDatasetAddressScheme(Exception):
+class UnsupportedDatasetAddressScheme(EodagError):
     """An error indicating that eodag does not yet support an address scheme for
     accessing raster subdatasets"""
 
 
-class AuthenticationError(Exception):
+class AuthenticationError(EodagError):
     """An error indicating that an authentication plugin did not succeeded
     authenticating a user"""
 
 
-class DownloadError(Exception):
+class DownloadError(EodagError):
     """An error indicating something wrong with the download process"""
 
 
-class NotAvailableError(Exception):
+class NotAvailableError(EodagError):
     """An error indicating that the product is not available for download"""
 
 
-class RequestError(Exception):
+class RequestError(EodagError):
     """An error indicating that a request has failed. Usually eodag functions
     and methods should catch and skip this"""
 
@@ -90,12 +94,12 @@ class RequestError(Exception):
         return repr
 
 
-class NoMatchingProductType(Exception):
+class NoMatchingProductType(EodagError):
     """An error indicating that eodag was unable to derive a product type from a set
     of search parameters"""
 
 
-class STACOpenerError(Exception):
+class STACOpenerError(EodagError):
     """An error indicating that a STAC file could not be opened"""
 
 
diff --git a/pyproject.toml b/pyproject.toml
index 781cdd35..a5d48920 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,8 +7,6 @@ fallback_version = "2.12.2.dev0"
 
 [[tool.mypy.overrides]]
 module = [
-    "boto3",
-    "botocore.*",
     "cdsapi",
     "click",
     "click.*",
diff --git a/setup.cfg b/setup.cfg
index 1d15f8af..b7fb993c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -91,6 +91,8 @@ dev =
     responses < 0.24.0
     fastapi[all]
     stdlib-list
+    boto3-stubs[essential]
+    types-lxml
 
 notebook = tqdm[notebook]
 tutorials =


Test patch:
diff --git a/tests/integration/test_core_search.py b/tests/integration/test_core_search.py
index 2dfa238e..573726bc 100644
--- a/tests/integration/test_core_search.py
+++ b/tests/integration/test_core_search.py
@@ -29,6 +29,7 @@ from tests.context import (
     NoMatchingProductType,
     RequestError,
     USGSError,
+    ValidationError,
 )
 from tests.utils import mock
 
@@ -77,9 +78,14 @@ class TestCoreSearch(unittest.TestCase):
     ):
         # QueryStringSearch / peps
         self.dag.set_preferred_provider("peps")
-        self.assertRaises(RequestError, self.dag.search, raise_errors=True)
+        self.assertRaises(ValidationError, self.dag.search, raise_errors=True)
+        self.assertRaises(
+            RequestError, self.dag.search, productType="foo", raise_errors=True
+        )
         # search iterator
-        self.assertRaises(RequestError, next, self.dag.search_iter_page())
+        self.assertRaises(
+            RequestError, next, self.dag.search_iter_page(productType="foo")
+        )
 
     @mock.patch(
         "eodag.plugins.search.qssearch.requests.post",
@@ -147,9 +153,14 @@ class TestCoreSearch(unittest.TestCase):
     ):
         # ODataV4Search / onda
         self.dag.set_preferred_provider("onda")
-        self.assertRaises(RequestError, self.dag.search, raise_errors=True)
+        self.assertRaises(ValidationError, self.dag.search, raise_errors=True)
+        self.assertRaises(
+            RequestError, self.dag.search, productType="foo", raise_errors=True
+        )
         # search iterator
-        self.assertRaises(RequestError, next, self.dag.search_iter_page())
+        self.assertRaises(
+            RequestError, next, self.dag.search_iter_page(productType="foo")
+        )
 
     @mock.patch(
         "eodag.plugins.apis.usgs.api.scene_search", autospec=True, side_effect=USGSError
diff --git a/tests/units/test_download_plugins.py b/tests/units/test_download_plugins.py
index 7bd8cc2a..dfd05175 100644
--- a/tests/units/test_download_plugins.py
+++ b/tests/units/test_download_plugins.py
@@ -1323,7 +1323,7 @@ class TestDownloadPluginAws(BaseDownloadPluginTest):
         plugin.download(self.product, outputs_prefix=self.output_dir)
 
         mock_get_authenticated_objects.assert_called_once_with(
-            plugin, "somebucket", "path/to/some", None
+            plugin, "somebucket", "path/to/some", {}
         )
         self.assertEqual(mock_get_chunk_dest_path.call_count, 0)
         self.assertEqual(mock_finalize_s2_safe_product.call_count, 0)
@@ -1366,7 +1366,7 @@ class TestDownloadPluginAws(BaseDownloadPluginTest):
         plugin.download(self.product, outputs_prefix=self.output_dir)
 
         mock_get_authenticated_objects.assert_called_once_with(
-            plugin, "somebucket", "path/to/some", None
+            plugin, "somebucket", "path/to/some", {}
         )
         self.assertEqual(mock_get_chunk_dest_path.call_count, 0)
         self.assertEqual(mock_finalize_s2_safe_product.call_count, 0)
@@ -1434,11 +1434,9 @@ class TestDownloadPluginAws(BaseDownloadPluginTest):
         )
         self.assertEqual(mock_get_authenticated_objects.call_count, 2)
         mock_get_authenticated_objects.assert_any_call(
-            plugin, "somebucket", "path/to/some", None
-        )
-        mock_get_authenticated_objects.assert_any_call(
-            plugin, "example", "here/is", None
+            plugin, "somebucket", "path/to/some", {}
         )
+        mock_get_authenticated_objects.assert_any_call(plugin, "example", "here/is", {})
         self.assertEqual(mock_get_chunk_dest_path.call_count, 2)
         mock_get_chunk_dest_path.assert_any_call(
             plugin, product=self.product, chunk=mock.ANY, build_safe=True
@@ -1515,7 +1513,7 @@ class TestDownloadPluginAws(BaseDownloadPluginTest):
             timeout=HTTP_REQ_TIMEOUT,
         )
         mock_get_authenticated_objects.assert_called_once_with(
-            plugin, "example", "", None
+            plugin, "example", "", {}
         )
         self.assertEqual(mock_get_chunk_dest_path.call_count, 4)
         mock_get_chunk_dest_path.assert_any_call(