Repository: CS-SI/eodag
Pull request #1269 (2024-07-23 09:08:15)

refactor: type hints fixes and mypy in tox
Fixes type hints and adds `mypy` check in `tox -e linters`

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


## Code Coverage (Ubuntu)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                      8       0  100.00%
cli.py                                         303      50  83.50%   654-700, 802-853, 857
config.py                                      356      27  92.42%   81-83, 92, 100, 104-106, 183, 195, 468-470, 534-537, 584-585, 594-595, 674, 743-748, 750
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    752      75  90.03%   373, 598, 642-645, 683, 720, 750, 798, 802-807, 833, 929, 1012, 1149, 1239-1251, 1291, 1293, 1321, 1325-1336, 1349-1355, 1445-1448, 1481-1501, 1557, 1574-1577, 1589-1592, 1614-1621, 1947, 1980-1986, 2253, 2257-2260, 2274-2276, 2311
api/search_result.py                            51       4  92.16%   75, 84, 91, 105
api/product/__init__.py                          6       0  100.00%
api/product/_assets.py                          48       5  89.58%   80, 157, 167, 170-174
api/product/_product.py                        187      20  89.30%   70-72, 242-243, 325, 354, 415, 429-432, 445, 469-472, 515-521
api/product/metadata_mapping.py                675      81  88.00%   131-133, 234, 266-267, 313-314, 324-336, 338, 349, 355-367, 412-413, 450, 471-474, 497, 505-506, 582-583, 607-608, 614-617, 632-633, 782, 828, 1002-1007, 1138, 1152-1172, 1192, 1197, 1307, 1329, 1343, 1356-1375, 1414, 1466, 1504-1508, 1527
api/product/drivers/__init__.py                  6       0  100.00%
api/product/drivers/base.py                      6       1  83.33%   41
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 21       2  90.48%   48, 55
plugins/manager.py                             130      12  90.77%   105-110, 160, 201, 223, 227, 253, 292-293
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           91       8  91.21%   150-152, 199-200, 226-228
plugins/apis/usgs.py                           180      31  82.78%   132, 233, 267, 302-304, 309, 335-336, 341, 371-378, 389-394, 416-422, 424-430, 453
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              19       0  100.00%
plugins/authentication/base.py                  17       2  88.24%   34, 47
plugins/authentication/generic.py               14       2  85.71%   40, 50
plugins/authentication/header.py                19       0  100.00%
plugins/authentication/keycloak.py              46       4  91.30%   132, 156-161
plugins/authentication/oauth.py                 13       7  46.15%   32-34, 38-41
plugins/authentication/openid_connect.py       183      17  90.71%   119, 133-158, 166, 320-323, 347
plugins/authentication/qsauth.py                34       1  97.06%   83
plugins/authentication/sas_auth.py              47       1  97.87%   76
plugins/authentication/token.py                 88      16  81.82%   79, 107, 109, 131-143, 198-202
plugins/authentication/token_exchange.py        35      19  45.71%   74-80, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       1  90.00%   40
plugins/crunch/filter_date.py                   59      14  76.27%   51-56, 70, 79, 88, 91, 103-105, 114-116, 123
plugins/crunch/filter_latest_intersect.py       47       8  82.98%   51-52, 71, 80-83, 85, 92-95
plugins/crunch/filter_latest_tpl_name.py        31       1  96.77%   87
plugins/crunch/filter_overlap.py                66      18  72.73%   28-30, 72-75, 82-85, 91, 99, 110-126
plugins/crunch/filter_property.py               30       7  76.67%   58-63, 66-67, 83-87
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        489     163  66.67%   273, 286, 353-356, 370-374, 420-422, 426, 460-461, 467-471, 504, 539, 543, 550, 580-588, 592, 630-638, 645-647, 688-762, 780-841, 852-857, 873-886, 915, 930-932, 935, 945-953, 961-974, 984-1015, 1022-1034, 1075, 1101, 1146-1148, 1368
plugins/download/base.py                       253      51  79.84%   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, 688, 710, 718
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       534     129  75.84%   203-215, 217-218, 253-256, 320-323, 325-326, 333-338, 356-371, 388-390, 402, 450, 457-463, 481, 495, 509, 517-519, 535-540, 551, 569, 611-615, 637, 677, 722, 736-742, 778-842, 860, 893-902, 928-929, 956-961, 967, 970, 986, 1003-1004, 1034-1035, 1042, 1104-1110, 1165-1166, 1172, 1182, 1218, 1254, 1272-1288, 1314-1316
plugins/download/s3rest.py                     116      24  79.31%   121, 157, 164, 199, 229-236, 239-241, 245, 258-264, 272-273, 276-280, 303, 324-327
plugins/search/__init__.py                      22       0  100.00%
plugins/search/base.py                         128       9  92.97%   104, 108, 121, 271, 291, 350-351, 371, 380
plugins/search/build_search_result.py          181      23  87.29%   97, 141-142, 148, 159, 295-298, 327, 384-401, 463, 466, 476, 493, 521, 523
plugins/search/cop_marine.py                   197      47  76.14%   55, 63-65, 71-72, 88, 90, 93, 128-130, 142-143, 183-192, 196, 199, 203, 221, 251, 255, 270, 274, 278, 282, 286-290, 296-299, 302-316, 333, 356, 359, 365
plugins/search/creodias_s3.py                   55       3  94.55%   56, 74, 108
plugins/search/csw.py                          105      81  22.86%   58-59, 63-64, 72-120, 126-139, 147-179, 197-238
plugins/search/data_request_search.py          200      67  66.50%   90-93, 109, 121, 125-126, 137, 142, 147, 154, 167-170, 224-225, 229, 239-245, 250, 276-279, 287-298, 315, 317, 324-325, 327-328, 346-350, 383, 393, 404, 417, 423-438, 443
plugins/search/qssearch.py                     664      80  87.95%   391, 395-401, 409-410, 516-522, 572, 588, 598, 625, 627, 670-673, 747-748, 796, 815, 830, 888, 909, 912-913, 922-923, 932-933, 942-943, 970, 1041-1046, 1050-1059, 1093, 1115, 1175, 1224, 1298-1302, 1362, 1365, 1371-1372, 1393, 1420-1432, 1439, 1471-1473, 1483-1489, 1519, 1542, 1557, 1579, 1688-1698
plugins/search/static_stac_search.py            72      10  86.11%   101-128, 144, 157
rest/__init__.py                                 4       2  50.00%   21-22
rest/cache.py                                   33       7  78.79%   35-37, 53-55, 59, 68
rest/config.py                                  26       0  100.00%
rest/constants.py                                7       0  100.00%
rest/core.py                                   234      26  88.89%   281, 365, 475, 702, 709-757
rest/server.py                                 283      53  81.27%   108, 131-133, 246-248, 304-305, 317-333, 425-430, 458, 626-633, 662, 706-707, 730, 803-805, 822-827, 856, 858, 862-863, 867-868
rest/stac.py                                   464     120  74.14%   322, 344, 392-395, 419-445, 450-456, 479-481, 504, 539-540, 568, 581, 626-666, 694-710, 789-802, 809, 865-866, 933, 1001-1003, 1222, 1232-1244, 1257-1279, 1293-1338, 1500-1501
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     179       5  97.21%   232-236, 289, 292, 360
rest/types/queryables.py                        56       1  98.21%   174
rest/types/stac_search.py                      126       7  94.44%   130, 176, 191-193, 201, 205
rest/utils/__init__.py                          93      12  87.10%   108-109, 128-130, 182, 192-206
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       3  86.96%   49, 61, 63
types/__init__.py                              114      14  87.72%   53, 70, 129-132, 199, 213-222, 232, 253, 266
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
types/whoosh.py                                 15       0  100.00%
utils/__init__.py                              500      37  92.60%   85, 90, 194-195, 204-231, 234, 248, 330-334, 410-414, 435-437, 519, 534, 572-573, 969-972, 980-981, 1022-1023, 1205
utils/constraints.py                           119      37  68.91%   94-103, 144, 149, 153, 164, 190-192, 202, 216-232, 241-252
utils/exceptions.py                             35       1  97.14%   93
utils/import_system.py                          28      19  32.14%   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/repr.py                                   30       8  73.33%   36, 38, 42, 76, 94-101
utils/requests.py                               55      11  80.00%   69, 96, 98, 100, 102, 104, 123, 131-133, 141
utils/rest.py                                   36       1  97.22%   57
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 143, 159-162, 215-224, 234-264
TOTAL                                         9534    1624  82.97%
```

</details>


### Diff against develop

```
Filename                                     Stmts    Miss  Cover
-----------------------------------------  -------  ------  -------
cli.py                                          -2      -1  +0.22%
config.py                                       +5       0  +0.11%
api/core.py                                    -10      -9  +1.05%
api/search_result.py                            -3      -2  +3.27%
api/product/_assets.py                          -4      -3  +4.96%
api/product/_product.py                         -8      -7  +3.15%
api/product/metadata_mapping.py                 -3      -2  +0.24%
api/product/drivers/base.py                     -2      -1  +8.33%
plugins/base.py                                 -2      -1  +3.52%
plugins/manager.py                              -4      -3  +1.96%
plugins/apis/ecmwf.py                           -8      -7  +6.36%
plugins/apis/usgs.py                            -6      -5  +2.13%
plugins/authentication/aws_auth.py              -3      -2  +9.09%
plugins/authentication/base.py                  -2      -1  +4.03%
plugins/authentication/generic.py               -2      -1  +4.46%
plugins/authentication/header.py                -2      -1  +4.76%
plugins/authentication/keycloak.py              -3      -2  +3.54%
plugins/authentication/oauth.py                 -2      -1  -0.52%
plugins/authentication/openid_connect.py        -2      -2  +0.98%
plugins/authentication/qsauth.py                -2      -1  +2.62%
plugins/authentication/sas_auth.py              -2      -1  +1.95%
plugins/authentication/token.py                 -2      -1  +0.71%
plugins/crunch/base.py                          -2      -1  +6.67%
plugins/crunch/filter_date.py                   -2      -1  +0.86%
plugins/crunch/filter_latest_intersect.py       -3      -2  +2.98%
plugins/crunch/filter_latest_tpl_name.py        -2      -1  +2.83%
plugins/crunch/filter_overlap.py                -2      -1  +0.67%
plugins/crunch/filter_property.py               -2      -1  +1.67%
plugins/download/aws.py                         -7      -6  +0.74%
plugins/download/base.py                        -7      -6  +1.76%
plugins/download/http.py                        -6      -6  +0.84%
plugins/download/s3rest.py                      -3      -3  +2.00%
plugins/search/__init__.py                      -4      -3  +11.54%
plugins/search/base.py                          -5      -4  +2.74%
plugins/search/build_search_result.py           -2      -1  +0.40%
plugins/search/cop_marine.py                    -3      -2  +0.64%
plugins/search/csw.py                           -3      -2  -0.29%
plugins/search/data_request_search.py           -2      -1  +0.16%
plugins/search/qssearch.py                      +9      +3  -0.29%
plugins/search/static_stac_search.py            -3      -2  +2.11%
rest/core.py                                    -4      -4  +1.50%
rest/server.py                                  -3      -2  +0.50%
rest/stac.py                                    -2      -3  +0.53%
rest/types/eodag_search.py                      -5      -4  +2.10%
rest/types/queryables.py                        -2      -1  +1.66%
rest/types/stac_search.py                       -5      -4  +2.84%
rest/utils/__init__.py                          -2      -1  +0.78%
types/__init__.py                               +4      +3  -2.28%
utils/__init__.py                               -3      -2  +0.35%
utils/exceptions.py                             -2      -1  +2.55%
utils/import_system.py                          -2      -1  -1.19%
TOTAL                                         -144    -114  +0.93%
```


Results for commit: c85ba13cdf588a5da3a6541dcd7daab9c13bf4da


_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 -->
github-actions[bot]: ## Test Results
    4 files  ±0      4 suites  ±0   6m 6s :stopwatch: -1s
  553 tests ±0    550 :white_check_mark: ±0    3 :zzz: ±0  0 :x: ±0 
2 212 runs  ±0  2 110 :white_check_mark: ±0  102 :zzz: ±0  0 :x: ±0 

Results for commit ec115f07. ± Comparison against base commit feef2bfd.

[test-results]:data:application/gzip;base64,H4sIAEJ0n2YC/02MSQ6DMAxFr4Ky7sI2ZGgvU6UZpKgMVUhWqHdvoBBYvufvtzAfejezR9PdGjbnkCrYHHUK01iwFaKIckrrkfP2oOecjdkUnOodPutPFV6Hvohz4WKc4m5iHtcmEdJOR5MQoap/E6GOLtGNr00zDUNIBZgziNyDVNxYIe/aKfUiEI5IKCkJ0OsOpLXs+wPilJW/CQEAAA==


Patch:
diff --git a/eodag/config.py b/eodag/config.py
index 27e9161f..87831224 100644
--- a/eodag/config.py
+++ b/eodag/config.py
@@ -246,6 +246,14 @@ class PluginConfig(yaml.YAMLObject):
         sort_order_mapping: Dict[Literal["ascending", "descending"], str]
         max_sort_params: Annotated[int, Gt(0)]
 
+    class DiscoverMetadata(TypedDict):
+        """Configuration for metadata discovery"""
+
+        auto_discovery: bool
+        metadata_pattern: str
+        search_param: str
+        metadata_path: str
+
     class OrderOnResponse(TypedDict):
         """Configuration for order on-response during download"""
 
@@ -319,7 +327,7 @@ class PluginConfig(yaml.YAMLObject):
     pagination: PluginConfig.Pagination
     sort: PluginConfig.Sort
     query_params_key: str
-    discover_metadata: Dict[str, Union[str, bool]]
+    discover_metadata: PluginConfig.DiscoverMetadata
     discover_product_types: Dict[str, Any]
     discover_queryables: Dict[str, Any]
     metadata_mapping: Dict[str, Union[str, List[str]]]
diff --git a/eodag/plugins/authentication/openid_connect.py b/eodag/plugins/authentication/openid_connect.py
index 842a8be6..3c816c9f 100644
--- a/eodag/plugins/authentication/openid_connect.py
+++ b/eodag/plugins/authentication/openid_connect.py
@@ -482,7 +482,7 @@ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
         if not match:
             return value
         value_from_xpath = form_element.xpath(
-            self.CONFIG_XPATH_REGEX.match(value).groupdict("xpath_value")["xpath_value"]
+            match.groupdict("xpath_value")["xpath_value"]
         )
         if len(value_from_xpath) == 1:
             return value_from_xpath[0]
@@ -512,9 +512,10 @@ class CodeAuthorizedAuth(AuthBase):
     def __call__(self, request: PreparedRequest) -> PreparedRequest:
         """Perform the actual authentication"""
         if self.where == "qs":
-            parts = urlparse(request.url)
+            parts = urlparse(str(request.url))
             query_dict = parse_qs(parts.query)
-            query_dict.update({self.key: self.token})
+            if self.key is not None:
+                query_dict.update({self.key: [self.token]})
             url_without_args = parts._replace(query="").geturl()
 
             request.prepare_url(url_without_args, query_dict)
diff --git a/eodag/plugins/authentication/token.py b/eodag/plugins/authentication/token.py
index 165751ed..d171eab1 100644
--- a/eodag/plugins/authentication/token.py
+++ b/eodag/plugins/authentication/token.py
@@ -197,7 +197,8 @@ class RequestsTokenAuth(AuthBase):
         if self.where == "qs":
             parts = urlparse(str(request.url))
             qs = parse_qs(parts.query)
-            qs[self.qs_key] = self.token  # type: ignore
+            if self.qs_key is not None:
+                qs[self.qs_key] = [self.token]
             request.url = urlunparse(
                 (
                     parts.scheme,
diff --git a/eodag/plugins/download/aws.py b/eodag/plugins/download/aws.py
index c77766f0..49bd4d5a 100644
--- a/eodag/plugins/download/aws.py
+++ b/eodag/plugins/download/aws.py
@@ -774,7 +774,7 @@ class AwsDownload(Download):
         build_safe: bool,
         progress_callback: ProgressCallback,
         assets_values: List[Dict[str, Any]],
-    ) -> Iterator[Tuple[str, datetime, int, Any, Iterator[Any]]]:
+    ) -> Iterator[Any]:
         """Yield product data chunks"""
 
         chunk_size = 4096 * 1024
diff --git a/eodag/plugins/download/base.py b/eodag/plugins/download/base.py
index 1ba2bcbc..2724ba6c 100644
--- a/eodag/plugins/download/base.py
+++ b/eodag/plugins/download/base.py
@@ -670,7 +670,7 @@ class Download(PluginTopic):
                             not_available_info = str(e)
 
                     if datetime_now >= product.next_try and datetime_now < stop_time:
-                        wait_seconds = (
+                        wait_seconds: Union[float, int] = (
                             datetime_now - product.next_try + timedelta(minutes=wait)
                         ).seconds
                         retry_count += 1
diff --git a/eodag/plugins/download/http.py b/eodag/plugins/download/http.py
index 9437c3d1..51bd67a3 100644
--- a/eodag/plugins/download/http.py
+++ b/eodag/plugins/download/http.py
@@ -33,7 +33,6 @@ from typing import (
     Iterator,
     List,
     Optional,
-    Tuple,
     TypedDict,
     Union,
     cast,
@@ -1011,8 +1010,11 @@ class HTTPDownload(Download):
                     "content-disposition"
                 ] = f"attachment; filename={filename}"
                 content_type = product.headers.get("Content-Type")
-                if filename and not content_type:
-                    product.headers["Content-Type"] = guess_file_type(filename)
+                guessed_content_type = (
+                    guess_file_type(filename) if filename and not content_type else None
+                )
+                if guessed_content_type is not None:
+                    product.headers["Content-Type"] = guessed_content_type
 
                 progress_callback.reset(total=stream_size)
                 for chunk in self.stream.iter_content(chunk_size=64 * 1024):
@@ -1027,7 +1029,7 @@ class HTTPDownload(Download):
         progress_callback: Optional[ProgressCallback] = None,
         assets_values: List[Asset] = [],
         **kwargs: Unpack[DownloadConf],
-    ) -> Iterator[Tuple[str, datetime, int, Any, Iterator[Any]]]:
+    ) -> Iterator[Any]:
         if progress_callback is None:
             logger.info("Progress bar unavailable, please call product.download()")
             progress_callback = ProgressCallback(disable=True)
@@ -1201,7 +1203,9 @@ class HTTPDownload(Download):
             # start reading chunks to set asset.rel_path
             first_chunks_tuple = next(chunks_tuples)
             chunks = chain(iter([first_chunks_tuple]), chunks_tuples)
-            chunks_tuples = [(assets_values[0].rel_path, None, None, None, chunks)]
+            chunks_tuples = iter(
+                [(assets_values[0].rel_path, None, None, None, chunks)]
+            )
 
         for chunk_tuple in chunks_tuples:
             asset_path = chunk_tuple[0]
diff --git a/eodag/plugins/download/s3rest.py b/eodag/plugins/download/s3rest.py
index 84601076..f3349661 100644
--- a/eodag/plugins/download/s3rest.py
+++ b/eodag/plugins/download/s3rest.py
@@ -153,6 +153,8 @@ class S3RestDownload(Download):
             bucket_name, prefix = get_bucket_name_and_prefix(
                 url=product.location, bucket_path_level=self.config.bucket_path_level
             )
+            if prefix is None:
+                raise DownloadError(f"Could not extract prefix from {product.location}")
 
             if (
                 bucket_name is None
diff --git a/eodag/plugins/manager.py b/eodag/plugins/manager.py
index 0aa7c9b8..c28645cf 100644
--- a/eodag/plugins/manager.py
+++ b/eodag/plugins/manager.py
@@ -111,7 +111,11 @@ class PluginManager:
                         "Check that the plugin module (%s) is importable",
                         entry_point.module_name,
                     )
-                if entry_point.dist and entry_point.dist.key != "eodag":
+                if (
+                    entry_point.dist
+                    and entry_point.dist.key != "eodag"
+                    and entry_point.dist.location is not None
+                ):
                     # use plugin providers if any
                     plugin_providers_config_path = [
                         str(x)
diff --git a/eodag/plugins/search/__init__.py b/eodag/plugins/search/__init__.py
index 3a4e88f7..56fcac80 100644
--- a/eodag/plugins/search/__init__.py
+++ b/eodag/plugins/search/__init__.py
@@ -36,8 +36,8 @@ class PreparedSearch:
     """An object collecting needed information for search."""
 
     product_type: Optional[str] = None
-    page: int = DEFAULT_PAGE
-    items_per_page: int = DEFAULT_ITEMS_PER_PAGE
+    page: Optional[int] = DEFAULT_PAGE
+    items_per_page: Optional[int] = DEFAULT_ITEMS_PER_PAGE
     auth: Optional[Union[AuthBase, Dict[str, str]]] = None
     auth_plugin: Optional[Authentication] = None
     count: bool = True
diff --git a/eodag/plugins/search/cop_marine.py b/eodag/plugins/search/cop_marine.py
index cc531055..6177472b 100644
--- a/eodag/plugins/search/cop_marine.py
+++ b/eodag/plugins/search/cop_marine.py
@@ -247,7 +247,7 @@ class CopMarineSearch(StaticStacSearch):
         items_per_page = prep.items_per_page
 
         # only return 1 page if pagination is disabled
-        if page > 1 and items_per_page <= 0:
+        if page is None or items_per_page is None or page > 1 and items_per_page <= 0:
             return ([], 0) if prep.count else ([], None)
 
         product_type = kwargs.get("productType", prep.product_type)
diff --git a/eodag/plugins/search/qssearch.py b/eodag/plugins/search/qssearch.py
index bd9728bb..878d062f 100644
--- a/eodag/plugins/search/qssearch.py
+++ b/eodag/plugins/search/qssearch.py
@@ -23,6 +23,7 @@ from copy import copy as copy_copy
 from typing import (
     TYPE_CHECKING,
     Any,
+    Callable,
     Dict,
     List,
     Optional,
@@ -203,7 +204,10 @@ class QueryStringSearch(Search):
     :type config: str
     """
 
-    extract_properties = {"xml": properties_from_xml, "json": properties_from_json}
+    extract_properties: Dict[str, Callable[..., Dict[str, Any]]] = {
+        "xml": properties_from_xml,
+        "json": properties_from_json,
+    }
 
     def __init__(self, provider: str, config: PluginConfig) -> None:
         super(QueryStringSearch, self).__init__(provider, config)
@@ -631,7 +635,7 @@ class QueryStringSearch(Search):
                         )
                     )
 
-        field_definitions = dict()
+        field_definitions: Dict[str, Any] = dict()
         for json_param, json_mtd in constraint_params.items():
             param = (
                 get_queryable_from_provider(
@@ -786,7 +790,7 @@ class QueryStringSearch(Search):
             prep.need_count = True
             prep.total_items_nb = None
 
-        for collection in self.get_collections(prep, **kwargs):
+        for collection in self.get_collections(prep, **kwargs) or (None,):
             # skip empty collection if one is required in api_endpoint
             if "{collection}" in self.config.api_endpoint and not collection:
                 continue
@@ -1059,20 +1063,19 @@ class QueryStringSearch(Search):
                 total_results = int(count_results)
         return total_results
 
-    def get_collections(
-        self, prep: PreparedSearch, **kwargs: Any
-    ) -> Tuple[Set[Dict[str, Any]], ...]:
+    def get_collections(self, prep: PreparedSearch, **kwargs: Any) -> Tuple[str, ...]:
         """Get the collection to which the product belongs"""
         # See https://earth.esa.int/web/sentinel/missions/sentinel-2/news/-
         # /asset_publisher/Ac0d/content/change-of
         # -format-for-new-sentinel-2-level-1c-products-starting-on-6-december
         product_type: Optional[str] = kwargs.get("productType")
+        collection: Optional[str] = None
         if product_type is None and (
             not hasattr(prep, "product_type_def_params")
             or not prep.product_type_def_params
         ):
-            collections: Set[Dict[str, Any]] = set()
-            collection: Optional[str] = getattr(self.config, "collection", None)
+            collections: Set[str] = set()
+            collection = getattr(self.config, "collection", None)
             if collection is None:
                 try:
                     for product_type, product_config in self.config.products.items():
@@ -1090,18 +1093,26 @@ class QueryStringSearch(Search):
                 collections.add(collection)
             return tuple(collections)
 
-        collection: Optional[str] = getattr(self.config, "collection", None)
+        collection = getattr(self.config, "collection", None)
         if collection is None:
             collection = (
                 prep.product_type_def_params.get("collection", None) or product_type
             )
-        return (collection,) if not isinstance(collection, list) else tuple(collection)
+
+        if collection is None:
+            return ()
+        elif not isinstance(collection, list):
+            return (collection,)
+        else:
+            return tuple(collection)
 
     def _request(
         self,
         prep: PreparedSearch,
     ) -> Response:
         url = prep.url
+        if url is None:
+            raise ValidationError("Cannot request empty URL")
         info_message = prep.info_message
         exception_message = prep.exception_message
         try:
@@ -1347,8 +1358,11 @@ class PostJsonSearch(QueryStringSearch):
                     "specific_qssearch"
                 ].get("merge_responses", None)
 
-                self.count_hits = lambda *x, **y: 1
-                self._request = super(PostJsonSearch, self)._request
+                def count_hits(self, *x, **y):
+                    return 1
+
+                def _request(self, *x, **y):
+                    return super(PostJsonSearch, self)._request(*x, **y)
 
                 try:
                     eo_products, total_items = super(PostJsonSearch, self).query(
@@ -1449,7 +1463,7 @@ class PostJsonSearch(QueryStringSearch):
             auth_conf_dict = getattr(prep.auth_plugin.config, "credentials", {})
         else:
             auth_conf_dict = {}
-        for collection in self.get_collections(prep, **kwargs):
+        for collection in self.get_collections(prep, **kwargs) or (None,):
             try:
                 search_endpoint: str = self.config.api_endpoint.rstrip("/").format(
                     **dict(collection=collection, **auth_conf_dict)
@@ -1472,7 +1486,11 @@ class PostJsonSearch(QueryStringSearch):
                         if getattr(self.config, "merge_responses", False):
                             total_results = _total_results or 0
                         else:
-                            total_results += _total_results or 0
+                            total_results = (
+                                (_total_results or 0)
+                                if total_results is None
+                                else total_results + (_total_results or 0)
+                            )
                 if "next_page_query_obj" in self.config.pagination and isinstance(
                     self.config.pagination["next_page_query_obj"], str
                 ):
@@ -1497,6 +1515,8 @@ class PostJsonSearch(QueryStringSearch):
         prep: PreparedSearch,
     ) -> Response:
         url = prep.url
+        if url is None:
+            raise ValidationError("Cannot request empty URL")
         info_message = prep.info_message
         exception_message = prep.exception_message
         timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
@@ -1515,7 +1535,10 @@ class PostJsonSearch(QueryStringSearch):
                 kwargs["auth"] = prep.auth
 
             # perform the request using the next page arguments if they are defined
-            if getattr(self, "next_page_query_obj", None):
+            if (
+                hasattr(self, "next_page_query_obj")
+                and self.next_page_query_obj is not None
+            ):
                 prep.query_params = self.next_page_query_obj
             if info_message:
                 logger.info(info_message)
diff --git a/eodag/rest/core.py b/eodag/rest/core.py
index 06c8d595..656fb593 100644
--- a/eodag/rest/core.py
+++ b/eodag/rest/core.py
@@ -80,7 +80,7 @@ from eodag.utils.exceptions import (
 )
 
 if TYPE_CHECKING:
-    from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+    from typing import Any, Dict, List, Optional, Tuple, Union
 
     from fastapi import Request
     from requests.auth import AuthBase
@@ -215,7 +215,7 @@ def search_stac_items(
         }
 
         search_results = eodag_api.search(count=True, **criteria)
-        total = search_results.number_matched
+        total = search_results.number_matched or 0
         if search_request.crunch:
             search_results = crunch_products(
                 search_results, search_request.crunch, **criteria
@@ -588,9 +588,9 @@ def get_stac_extension_oseo(url: str) -> Dict[str, str]:
     :rtype: dict
     """
 
-    apply_method: Callable[[str, str], str] = lambda _, x: str(x).replace(
-        "$.product.", "$."
-    )
+    def apply_method(_: str, x: str) -> str:
+        return str(x).replace("$.product.", "$.")
+
     item_mapping = dict_items_recursive_apply(stac_config["item"], apply_method)
 
     # all properties as string type by default
diff --git a/eodag/rest/stac.py b/eodag/rest/stac.py
index 5f042a72..de9c2e75 100644
--- a/eodag/rest/stac.py
+++ b/eodag/rest/stac.py
@@ -21,7 +21,7 @@ import logging
 import os
 from collections import defaultdict
 from datetime import datetime, timezone
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
 from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
 
 import dateutil.parser
@@ -149,16 +149,18 @@ class StacCommon:
         ):
             for i, bbox in enumerate(self.data["extent"]["spatial"]["bbox"]):
                 self.data["extent"]["spatial"]["bbox"][i] = [float(x) for x in bbox]
-        # "None" values to None
-        apply_method: Callable[[str, str], Optional[str]] = lambda _, v: (
-            None if v == "None" else v
-        )
-        self.data = dict_items_recursive_apply(self.data, apply_method)
-        # ids and titles as str
-        apply_method: Callable[[str, str], Optional[str]] = lambda k, v: (
-            str(v) if k in ["title", "id"] else v
-        )
-        self.data = dict_items_recursive_apply(self.data, apply_method)
+
+        def apply_method_none(_: str, v: str) -> Optional[str]:
+            """ "None" values to None"""
+            return None if v == "None" else v
+
+        self.data = dict_items_recursive_apply(self.data, apply_method_none)
+
+        def apply_method_ids(k, v):
+            """ids and titles as str"""
+            return str(v) if k in ["title", "id"] else v
+
+        self.data = dict_items_recursive_apply(self.data, apply_method_ids)
 
         # empty stac_extensions: "" to []
         if not self.data.get("stac_extensions", True):
diff --git a/eodag/rest/types/eodag_search.py b/eodag/rest/types/eodag_search.py
index d619bbf0..2557d83b 100644
--- a/eodag/rest/types/eodag_search.py
+++ b/eodag/rest/types/eodag_search.py
@@ -49,10 +49,7 @@ from eodag.rest.utils.cql_evaluate import EodagEvaluator
 from eodag.utils import DEFAULT_ITEMS_PER_PAGE
 
 if TYPE_CHECKING:
-    try:
-        from typing import Self
-    except ImportError:
-        from _typeshed import Self
+    from typing_extensions import Self
 
 Geometry = Union[
     Dict[str, Any],
diff --git a/eodag/rest/types/queryables.py b/eodag/rest/types/queryables.py
index 2b623747..89073807 100644
--- a/eodag/rest/types/queryables.py
+++ b/eodag/rest/types/queryables.py
@@ -51,14 +51,16 @@ class QueryablesGetParams(BaseModel):
         dumped: Dict[str, Any] = handler(self)
         return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
 
-    @computed_field
+    # use [prop-decorator] mypy error code when mypy==1.12 is released
+    @computed_field  # type: ignore[misc]
     @property
     def start_datetime(self) -> Optional[str]:
         """Extract start_datetime property from datetime"""
         start = str_to_interval(self.datetime)[0]
         return start.strftime("%Y-%m-%dT%H:%M:%SZ") if start else None
 
-    @computed_field
+    # use [prop-decorator] mypy error code when mypy==1.12 is released
+    @computed_field  # type: ignore[misc]
     @property
     def end_datetime(self) -> Optional[str]:
         """Extract end_datetime property from datetime"""
diff --git a/eodag/rest/types/stac_search.py b/eodag/rest/types/stac_search.py
index fe3c1382..6f04da23 100644
--- a/eodag/rest/types/stac_search.py
+++ b/eodag/rest/types/stac_search.py
@@ -48,10 +48,7 @@ from eodag.rest.utils.rfc3339 import rfc3339_str_to_datetime, str_to_interval
 from eodag.utils.exceptions import ValidationError
 
 if TYPE_CHECKING:
-    try:
-        from typing import Self
-    except ImportError:
-        from _typeshed import Self
+    from typing_extensions import Self
 
 NumType = Union[float, int]
 
diff --git a/eodag/types/__init__.py b/eodag/types/__init__.py
index a8049e70..8db0286e 100644
--- a/eodag/types/__init__.py
+++ b/eodag/types/__init__.py
@@ -200,7 +200,7 @@ def python_field_definition_to_json(
             "%s must be an instance of Annotated" % python_field_definition
         )
 
-    json_field_definition = dict()
+    json_field_definition: Dict[str, Any] = dict()
 
     python_field_args = get_args(python_field_definition)
 
@@ -210,6 +210,10 @@ def python_field_definition_to_json(
         type_data = python_type_to_json(type(enum_args[0]))
         if isinstance(type_data, str):
             json_field_definition["type"] = type_data
+        elif type_data is None:
+            json_field_definition["type"] = json_field_definition[
+                "min"
+            ] = json_field_definition["max"] = None
         else:
             json_field_definition["type"] = [row["type"] for row in type_data]
             json_field_definition["min"] = [
@@ -224,6 +228,10 @@ def python_field_definition_to_json(
         field_type = python_type_to_json(python_field_args[0])
         if isinstance(field_type, str):
             json_field_definition["type"] = field_type
+        elif field_type is None:
+            json_field_definition["type"] = json_field_definition[
+                "min"
+            ] = json_field_definition["max"] = None
         else:
             json_field_definition["type"] = [row["type"] for row in field_type]
             json_field_definition["min"] = [
diff --git a/eodag/types/download_args.py b/eodag/types/download_args.py
index 99e4a4ef..817a2343 100644
--- a/eodag/types/download_args.py
+++ b/eodag/types/download_args.py
@@ -17,7 +17,7 @@
 # limitations under the License.
 from __future__ import annotations
 
-from typing import Dict, TypedDict
+from typing import Dict, Optional, TypedDict
 
 
 class DownloadConf(TypedDict, total=False):
@@ -28,4 +28,4 @@ class DownloadConf(TypedDict, total=False):
     extract: bool
     dl_url_params: Dict[str, str]
     delete_archive: bool
-    asset: str
+    asset: Optional[str]
diff --git a/eodag/utils/__init__.py b/eodag/utils/__init__.py
index a536d489..44f13187 100644
--- a/eodag/utils/__init__.py
+++ b/eodag/utils/__init__.py
@@ -55,6 +55,7 @@ from typing import (
     Any,
     Callable,
     Dict,
+    Iterable,
     Iterator,
     List,
     Mapping,
@@ -1435,7 +1436,7 @@ def cast_scalar_value(value: Any, new_type: Any) -> Any:
 class StreamResponse:
     """Represents a streaming response"""
 
-    content: Iterator[bytes]
+    content: Iterable[bytes]
     headers: Optional[Mapping[str, str]] = None
     media_type: Optional[str] = None
     status_code: Optional[int] = None
diff --git a/eodag/utils/notebook.py b/eodag/utils/notebook.py
index 8adb6cdc..1bfe6141 100644
--- a/eodag/utils/notebook.py
+++ b/eodag/utils/notebook.py
@@ -23,7 +23,7 @@ from typing import Any, Optional
 def check_ipython() -> bool:
     """Check if called from ipython"""
     try:
-        __IPYTHON__
+        __IPYTHON__  # type: ignore[name-defined]
         return True
     except NameError:
         return False
@@ -32,7 +32,7 @@ def check_ipython() -> bool:
 def check_notebook() -> bool:
     """Check if called from a notebook"""
     try:
-        shell = get_ipython().__class__.__name__
+        shell = get_ipython().__class__.__name__  # type: ignore[name-defined]
         if shell == "ZMQInteractiveShell":
             return True  # Jupyter notebook or qtconsole
         elif shell == "TerminalInteractiveShell":
@@ -69,7 +69,7 @@ class NotebookWidgets:
         if not self.is_notebook:
             return None
 
-        self.html_box.data = html_value
+        setattr(self.html_box, "data", html_value)
 
         if not self.html_box_shown:
             self._html_handle = self.display(self.html_box, display_id=True)
@@ -83,5 +83,5 @@ class NotebookWidgets:
         if not self.is_notebook:
             return None
 
-        self.html_box.data = ""
+        setattr(self.html_box, "data", "")
         self._update_display(self.html_box, display_id=self._html_handle.display_id)
diff --git a/eodag/utils/requests.py b/eodag/utils/requests.py
index 9fda6e13..15a58432 100644
--- a/eodag/utils/requests.py
+++ b/eodag/utils/requests.py
@@ -32,7 +32,7 @@ logger = logging.getLogger("eodag.utils.requests")
 def fetch_json(
     file_url: str,
     req_session: Optional[requests.Session] = None,
-    auth: Optional[requests.AuthBase] = None,
+    auth: Optional[requests.auth.AuthBase] = None,
     timeout: float = HTTP_REQ_TIMEOUT,
 ) -> Any:
     """
@@ -105,7 +105,9 @@ class LocalFileAdapter(requests.adapters.BaseAdapter):
         else:
             return 200, "OK"
 
-    def send(self, req: requests.PreparedRequest, **kwargs: Any) -> requests.Response:
+    def send(
+        self, request: requests.PreparedRequest, *args: Any, **kwargs: Any
+    ) -> requests.Response:
         """Wraps a file, described in request, in a Response object.
 
         :param req: The PreparedRequest being "sent".
@@ -117,19 +119,20 @@ class LocalFileAdapter(requests.adapters.BaseAdapter):
         """
         response = requests.Response()
 
-        path_url = uri_to_path(req.url)
-
-        if req.method is None or req.url is None:
+        if request.method is None or request.url is None:
             raise RequestError("Method or url of the request is missing")
-        response.status_code, response.reason = self._chkpath(req.method, path_url)
-        if response.status_code == 200 and req.method.lower() != "head":
+
+        path_url = uri_to_path(request.url)
+
+        response.status_code, response.reason = self._chkpath(request.method, path_url)
+        if response.status_code == 200 and request.method.lower() != "head":
             try:
                 response.raw = open(path_url, "rb")
             except (OSError, IOError) as err:
                 response.status_code = 500
                 response.reason = str(err)
-        response.url = req.url
-        response.request = req
+        response.url = request.url
+        response.request = request
 
         return response
 
diff --git a/eodag/utils/stac_reader.py b/eodag/utils/stac_reader.py
index 54fdd718..a7b6e22a 100644
--- a/eodag/utils/stac_reader.py
+++ b/eodag/utils/stac_reader.py
@@ -128,7 +128,7 @@ def fetch_stac_items(
     # URI opener used by PySTAC internally, instantiated here
     # to retrieve the timeout.
     _text_opener = _TextOpener(timeout, ssl_verify)
-    pystac.StacIO.read_text = _text_opener
+    pystac.StacIO.read_text = _text_opener  # type: ignore[assignment]
 
     stac_obj = pystac.read_file(stac_path)
     # Single STAC item
@@ -213,7 +213,7 @@ def fetch_stac_collections(
 
     # URI opener used by PySTAC internally, instantiated here to retrieve the timeout.
     _text_opener = _TextOpener(timeout, ssl_verify)
-    pystac.StacIO.read_text = _text_opener
+    pystac.StacIO.read_text = _text_opener  # type: ignore[assignment]
 
     stac_obj = pystac.read_file(stac_path)
     if isinstance(stac_obj, pystac.Catalog):
diff --git a/pyproject.toml b/pyproject.toml
index 5ea9e71c..26141d4c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,6 +5,12 @@ build-backend = "setuptools.build_meta"
 [tool.setuptools_scm]
 fallback_version = "3.0.0b3.dev0"
 
+[tool.coverage.report]
+exclude_lines = [
+    "pragma: no cover",
+    "if TYPE_CHECKING:"
+]
+
 [[tool.mypy.overrides]]
 module = [
     "click",
diff --git a/setup.cfg b/setup.cfg
index c7084c2c..3cb5e503 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -31,6 +31,7 @@ classifiers =
     Topic :: Internet :: WWW/HTTP :: Indexing/Search
     Topic :: Scientific/Engineering :: GIS
     Topic :: Software Development :: Libraries :: Python Modules
+    Typing :: Typed
 
 [options]
 packages = find:
@@ -117,6 +118,12 @@ dev =
     stdlib-list
     boto3-stubs[essential]
     types-lxml
+    types-cachetools
+    types-requests
+    types-python-dateutil
+    types-setuptools
+    types-tqdm
+    mypy
 docs =
     eodag[all]
     sphinx
diff --git a/tox.ini b/tox.ini
index e5f2973e..af8ad6dc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -84,6 +84,6 @@ commands =
 
 [testenv:linters]
 basepython = python3
-skip_install = true
-deps = pre-commit
-commands = pre-commit run --all-files
+commands =
+    pre-commit run --all-files
+    python -m mypy -p eodag


Test patch:
diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py
index 0dd28fef..bad66140 100644
--- a/tests/units/test_search_plugins.py
+++ b/tests/units/test_search_plugins.py
@@ -764,9 +764,7 @@ class TestSearchPluginPostJsonSearch(BaseSearchPluginTest):
         self.assertEqual(len(products), number_of_products)
         self.assertIsInstance(products[0], EOProduct)
 
-    @mock.patch(
-        "eodag.plugins.search.qssearch.QueryStringSearch._request", autospec=True
-    )
+    @mock.patch("eodag.plugins.search.qssearch.PostJsonSearch._request", autospec=True)
     def test_plugins_search_postjsonsearch_count_and_search_awseos_s2l2a(
         self, mock__request
     ):