eodag 3.0.1__py3-none-any.whl → 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. eodag/api/core.py +174 -138
  2. eodag/api/product/_assets.py +44 -15
  3. eodag/api/product/_product.py +58 -47
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +117 -90
  10. eodag/api/search_result.py +13 -23
  11. eodag/cli.py +26 -5
  12. eodag/config.py +86 -92
  13. eodag/plugins/apis/base.py +1 -1
  14. eodag/plugins/apis/ecmwf.py +42 -22
  15. eodag/plugins/apis/usgs.py +17 -16
  16. eodag/plugins/authentication/aws_auth.py +16 -13
  17. eodag/plugins/authentication/base.py +5 -3
  18. eodag/plugins/authentication/header.py +3 -3
  19. eodag/plugins/authentication/keycloak.py +4 -4
  20. eodag/plugins/authentication/oauth.py +7 -3
  21. eodag/plugins/authentication/openid_connect.py +22 -16
  22. eodag/plugins/authentication/sas_auth.py +4 -4
  23. eodag/plugins/authentication/token.py +41 -10
  24. eodag/plugins/authentication/token_exchange.py +1 -1
  25. eodag/plugins/base.py +4 -4
  26. eodag/plugins/crunch/base.py +4 -4
  27. eodag/plugins/crunch/filter_date.py +4 -4
  28. eodag/plugins/crunch/filter_latest_intersect.py +6 -6
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
  30. eodag/plugins/crunch/filter_overlap.py +4 -4
  31. eodag/plugins/crunch/filter_property.py +6 -7
  32. eodag/plugins/download/aws.py +146 -87
  33. eodag/plugins/download/base.py +38 -56
  34. eodag/plugins/download/creodias_s3.py +29 -0
  35. eodag/plugins/download/http.py +173 -183
  36. eodag/plugins/download/s3rest.py +10 -11
  37. eodag/plugins/manager.py +10 -20
  38. eodag/plugins/search/__init__.py +6 -5
  39. eodag/plugins/search/base.py +90 -46
  40. eodag/plugins/search/build_search_result.py +1048 -361
  41. eodag/plugins/search/cop_marine.py +22 -12
  42. eodag/plugins/search/creodias_s3.py +9 -73
  43. eodag/plugins/search/csw.py +11 -11
  44. eodag/plugins/search/data_request_search.py +19 -18
  45. eodag/plugins/search/qssearch.py +99 -258
  46. eodag/plugins/search/stac_list_assets.py +85 -0
  47. eodag/plugins/search/static_stac_search.py +4 -4
  48. eodag/resources/ext_product_types.json +1 -1
  49. eodag/resources/product_types.yml +1134 -325
  50. eodag/resources/providers.yml +906 -2006
  51. eodag/resources/stac_api.yml +2 -2
  52. eodag/resources/user_conf_template.yml +10 -9
  53. eodag/rest/cache.py +2 -2
  54. eodag/rest/config.py +3 -3
  55. eodag/rest/core.py +112 -82
  56. eodag/rest/errors.py +5 -5
  57. eodag/rest/server.py +33 -14
  58. eodag/rest/stac.py +41 -38
  59. eodag/rest/types/collections_search.py +3 -3
  60. eodag/rest/types/eodag_search.py +29 -23
  61. eodag/rest/types/queryables.py +42 -31
  62. eodag/rest/types/stac_search.py +15 -25
  63. eodag/rest/utils/__init__.py +14 -21
  64. eodag/rest/utils/cql_evaluate.py +6 -6
  65. eodag/rest/utils/rfc3339.py +2 -2
  66. eodag/types/__init__.py +141 -32
  67. eodag/types/bbox.py +2 -2
  68. eodag/types/download_args.py +3 -3
  69. eodag/types/queryables.py +183 -72
  70. eodag/types/search_args.py +4 -4
  71. eodag/types/whoosh.py +127 -3
  72. eodag/utils/__init__.py +153 -51
  73. eodag/utils/exceptions.py +28 -21
  74. eodag/utils/import_system.py +2 -2
  75. eodag/utils/repr.py +65 -6
  76. eodag/utils/requests.py +13 -13
  77. eodag/utils/rest.py +2 -2
  78. eodag/utils/s3.py +231 -0
  79. eodag/utils/stac_reader.py +10 -10
  80. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/METADATA +77 -76
  81. eodag-3.1.0.dist-info/RECORD +113 -0
  82. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  83. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +4 -2
  84. eodag/utils/constraints.py +0 -244
  85. eodag-3.0.1.dist-info/RECORD +0 -109
  86. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  87. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
@@ -20,16 +20,17 @@ from __future__ import annotations
20
20
  import logging
21
21
  import os
22
22
  from datetime import datetime, timezone
23
- from typing import TYPE_CHECKING
23
+ from typing import TYPE_CHECKING, Annotated
24
24
 
25
25
  import geojson
26
26
  from ecmwfapi import ECMWFDataServer, ECMWFService
27
27
  from ecmwfapi.api import APIException, Connection, get_apikey_values
28
+ from pydantic.fields import FieldInfo
28
29
 
29
30
  from eodag.plugins.apis.base import Api
30
31
  from eodag.plugins.search import PreparedSearch
31
32
  from eodag.plugins.search.base import Search
32
- from eodag.plugins.search.build_search_result import BuildPostSearchResult
33
+ from eodag.plugins.search.build_search_result import ECMWFSearch, ecmwf_mtd
33
34
  from eodag.utils import (
34
35
  DEFAULT_DOWNLOAD_TIMEOUT,
35
36
  DEFAULT_DOWNLOAD_WAIT,
@@ -43,13 +44,14 @@ from eodag.utils.exceptions import AuthenticationError, DownloadError
43
44
  from eodag.utils.logging import get_logging_verbose
44
45
 
45
46
  if TYPE_CHECKING:
46
- from typing import Any, Dict, List, Optional, Tuple, Union
47
+ from typing import Any, Optional, Union
47
48
 
48
49
  from requests.auth import AuthBase
49
50
 
50
51
  from eodag.api.product import EOProduct
51
52
  from eodag.api.search_result import SearchResult
52
53
  from eodag.config import PluginConfig
54
+ from eodag.types import S3SessionKwargs
53
55
  from eodag.types.download_args import DownloadConf
54
56
  from eodag.utils import DownloadedCallback, ProgressCallback, Unpack
55
57
 
@@ -58,7 +60,7 @@ logger = logging.getLogger("eodag.apis.ecmwf")
58
60
  ECMWF_MARS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
59
61
 
60
62
 
61
- class EcmwfApi(Api, BuildPostSearchResult):
63
+ class EcmwfApi(Api, ECMWFSearch):
62
64
  """A plugin that enables to build download-request and download data on ECMWF MARS.
63
65
 
64
66
  Builds a single ready-to-download :class:`~eodag.api.product._product.EOProduct`
@@ -69,15 +71,16 @@ class EcmwfApi(Api, BuildPostSearchResult):
69
71
  query).
70
72
 
71
73
  This class inherits from :class:`~eodag.plugins.apis.base.Api` for compatibility and
72
- :class:`~eodag.plugins.search.build_search_result.BuildPostSearchResult` for the creation
74
+ :class:`~eodag.plugins.search.build_search_result.ECMWFSearch` for the creation
73
75
  of the search result.
74
76
 
75
77
  :param provider: provider name
76
78
  :param config: Api plugin configuration:
77
79
 
78
80
  * :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): EcmwfApi
79
- * :attr:`~eodag.config.PluginConfig.api_endpoint` (``str``) (**mandatory**): url of the ecmwf api
80
- * :attr:`~eodag.config.PluginConfig.metadata_mapping` (``Dict[str, Union[str, list]]``): how
81
+ * :attr:`~eodag.config.PluginConfig.auth_endpoint` (``str``) (**mandatory**): url of
82
+ the authentication endpoint of the ecmwf api
83
+ * :attr:`~eodag.config.PluginConfig.metadata_mapping` (``dict[str, Union[str, list]]``): how
81
84
  parameters should be mapped between the provider and eodag; If a string is given, this is
82
85
  the mapping parameter returned by provider -> eodag parameter. If a list with 2 elements
83
86
  is given, the first one is the mapping eodag parameter -> provider query parameters
@@ -86,14 +89,19 @@ class EcmwfApi(Api, BuildPostSearchResult):
86
89
 
87
90
  def __init__(self, provider: str, config: PluginConfig) -> None:
88
91
  # init self.config.metadata_mapping using Search Base plugin
92
+ config.metadata_mapping = {
93
+ **ecmwf_mtd(),
94
+ **config.metadata_mapping,
95
+ }
89
96
  Search.__init__(self, provider, config)
90
97
 
91
98
  # needed by QueryStringSearch.build_query_string / format_free_text_search
92
99
  self.config.__dict__.setdefault("free_text_search_operations", {})
93
100
  # needed for compatibility
94
101
  self.config.__dict__.setdefault("pagination", {"next_page_query_obj": "{{}}"})
102
+ self.config.__dict__.setdefault("api_endpoint", "")
95
103
 
96
- def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
104
+ def do_search(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
97
105
  """Should perform the actual search request."""
98
106
  return [{}]
99
107
 
@@ -101,16 +109,16 @@ class EcmwfApi(Api, BuildPostSearchResult):
101
109
  self,
102
110
  prep: PreparedSearch = PreparedSearch(),
103
111
  **kwargs: Any,
104
- ) -> Tuple[List[EOProduct], Optional[int]]:
112
+ ) -> tuple[list[EOProduct], Optional[int]]:
105
113
  """Build ready-to-download SearchResult"""
106
114
 
107
115
  # check productType, dates, geometry, use defaults if not specified
108
116
  # productType
109
117
  if not kwargs.get("productType"):
110
118
  kwargs["productType"] = "%s_%s_%s" % (
111
- kwargs.get("dataset", "mars"),
112
- kwargs.get("type", ""),
113
- kwargs.get("levtype", ""),
119
+ kwargs.get("ecmwf:dataset", "mars"),
120
+ kwargs.get("ecmwf:type", ""),
121
+ kwargs.get("ecmwf:levtype", ""),
114
122
  )
115
123
  # start date
116
124
  if "startTimeFromAscendingNode" not in kwargs:
@@ -132,9 +140,9 @@ class EcmwfApi(Api, BuildPostSearchResult):
132
140
  if "geometry" in kwargs:
133
141
  kwargs["geometry"] = get_geometry_from_various(geometry=kwargs["geometry"])
134
142
 
135
- return BuildPostSearchResult.query(self, prep, **kwargs)
143
+ return ECMWFSearch.query(self, prep, **kwargs)
136
144
 
137
- def authenticate(self) -> Dict[str, Optional[str]]:
145
+ def authenticate(self) -> dict[str, Optional[str]]:
138
146
  """Check credentials and returns information needed for auth
139
147
 
140
148
  :returns: {key, url, email} dictionary
@@ -143,7 +151,7 @@ class EcmwfApi(Api, BuildPostSearchResult):
143
151
  # Get credentials from eodag or using ecmwf conf
144
152
  email = getattr(self.config, "credentials", {}).get("username", None)
145
153
  key = getattr(self.config, "credentials", {}).get("password", None)
146
- url = getattr(self.config, "api_endpoint", None)
154
+ url = getattr(self.config, "auth_endpoint", None)
147
155
  if not all([email, key, url]):
148
156
  key, url, email = get_apikey_values()
149
157
 
@@ -165,10 +173,10 @@ class EcmwfApi(Api, BuildPostSearchResult):
165
173
  def download(
166
174
  self,
167
175
  product: EOProduct,
168
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
176
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
169
177
  progress_callback: Optional[ProgressCallback] = None,
170
- wait: int = DEFAULT_DOWNLOAD_WAIT,
171
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
178
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
179
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
172
180
  **kwargs: Unpack[DownloadConf],
173
181
  ) -> Optional[str]:
174
182
  """Download data from ECMWF MARS"""
@@ -254,13 +262,13 @@ class EcmwfApi(Api, BuildPostSearchResult):
254
262
  def download_all(
255
263
  self,
256
264
  products: SearchResult,
257
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
265
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
258
266
  downloaded_callback: Optional[DownloadedCallback] = None,
259
267
  progress_callback: Optional[ProgressCallback] = None,
260
- wait: int = DEFAULT_DOWNLOAD_WAIT,
261
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
268
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
269
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
262
270
  **kwargs: Unpack[DownloadConf],
263
- ) -> List[str]:
271
+ ) -> list[str]:
264
272
  """
265
273
  Download all using parent (base plugin) method
266
274
  """
@@ -277,3 +285,15 @@ class EcmwfApi(Api, BuildPostSearchResult):
277
285
  def clear(self) -> None:
278
286
  """Clear search context"""
279
287
  pass
288
+
289
+ def discover_queryables(
290
+ self, **kwargs: Any
291
+ ) -> Optional[dict[str, Annotated[Any, FieldInfo]]]:
292
+ """Fetch queryables list from provider using metadata mapping
293
+
294
+ :param kwargs: additional filters for queryables (`productType` and other search
295
+ arguments)
296
+ :returns: fetched queryable parameters dict
297
+ """
298
+ product_type = kwargs.get("productType", None)
299
+ return self.queryables_from_metadata_mapping(product_type)
@@ -22,7 +22,7 @@ import os
22
22
  import shutil
23
23
  import tarfile
24
24
  import zipfile
25
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
25
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
26
26
 
27
27
  import requests
28
28
  from jsonpath_ng.ext import parse
@@ -61,6 +61,7 @@ if TYPE_CHECKING:
61
61
 
62
62
  from eodag.api.search_result import SearchResult
63
63
  from eodag.config import PluginConfig
64
+ from eodag.types import S3SessionKwargs
64
65
  from eodag.types.download_args import DownloadConf
65
66
  from eodag.utils import DownloadedCallback, Unpack
66
67
 
@@ -86,7 +87,7 @@ class UsgsApi(Api):
86
87
  file should be extracted; default: ``True``
87
88
  * :attr:`~eodag.config.PluginConfig.order_enabled` (``bool``): if the product has to
88
89
  be ordered to download it; default: ``False``
89
- * :attr:`~eodag.config.PluginConfig.metadata_mapping` (``Dict[str, Union[str, list]]``): how
90
+ * :attr:`~eodag.config.PluginConfig.metadata_mapping` (``dict[str, Union[str, list]]``): how
90
91
  parameters should be mapped between the provider and eodag; If a string is given, this is
91
92
  the mapping parameter returned by provider -> eodag parameter. If a list with 2 elements
92
93
  is given, the first one is the mapping eodag parameter -> provider query parameters
@@ -99,7 +100,7 @@ class UsgsApi(Api):
99
100
  # Same method as in base.py, Search.__init__()
100
101
  # Prepare the metadata mapping
101
102
  # Do a shallow copy, the structure is flat enough for this to be sufficient
102
- metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
103
+ metas: dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
103
104
  # Update the defaults with the mapping value. This will add any new key
104
105
  # added by the provider mapping that is not in the default metadata.
105
106
  metas.update(self.config.metadata_mapping)
@@ -127,7 +128,7 @@ class UsgsApi(Api):
127
128
  api.logout()
128
129
  continue
129
130
  except USGSError as e:
130
- if i == 0:
131
+ if i == 0 and os.path.isfile(api.TMPFILE):
131
132
  # `.usgs` API file key might be obsolete
132
133
  # Remove it and try again
133
134
  os.remove(api.TMPFILE)
@@ -138,7 +139,7 @@ class UsgsApi(Api):
138
139
  self,
139
140
  prep: PreparedSearch = PreparedSearch(),
140
141
  **kwargs: Any,
141
- ) -> Tuple[List[EOProduct], Optional[int]]:
142
+ ) -> tuple[list[EOProduct], Optional[int]]:
142
143
  """Search for data on USGS catalogues"""
143
144
  page = prep.page if prep.page is not None else DEFAULT_PAGE
144
145
  items_per_page = (
@@ -164,7 +165,7 @@ class UsgsApi(Api):
164
165
  start_date = kwargs.pop("startTimeFromAscendingNode", None)
165
166
  end_date = kwargs.pop("completionTimeFromAscendingNode", None)
166
167
  geom = kwargs.pop("geometry", None)
167
- footprint: Dict[str, str] = {}
168
+ footprint: dict[str, str] = {}
168
169
  if hasattr(geom, "bounds"):
169
170
  (
170
171
  footprint["lonmin"],
@@ -175,7 +176,7 @@ class UsgsApi(Api):
175
176
  else:
176
177
  footprint = geom
177
178
 
178
- final: List[EOProduct] = []
179
+ final: list[EOProduct] = []
179
180
  if footprint and len(footprint.keys()) == 4: # a rectangle (or bbox)
180
181
  lower_left = {
181
182
  "longitude": footprint["lonmin"],
@@ -295,10 +296,10 @@ class UsgsApi(Api):
295
296
  def download(
296
297
  self,
297
298
  product: EOProduct,
298
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
299
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
299
300
  progress_callback: Optional[ProgressCallback] = None,
300
- wait: int = DEFAULT_DOWNLOAD_WAIT,
301
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
301
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
302
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
302
303
  **kwargs: Unpack[DownloadConf],
303
304
  ) -> Optional[str]:
304
305
  """Download data from USGS catalogues"""
@@ -340,7 +341,7 @@ class UsgsApi(Api):
340
341
  product.properties["productId"],
341
342
  )
342
343
 
343
- req_urls: List[str] = []
344
+ req_urls: list[str] = []
344
345
  try:
345
346
  if len(download_request_results["data"]["preparingDownloads"]) > 0:
346
347
  req_urls.extend(
@@ -375,7 +376,7 @@ class UsgsApi(Api):
375
376
  logger.debug(f"Downloading {req_url}")
376
377
  ssl_verify = getattr(self.config, "ssl_verify", True)
377
378
 
378
- @self._download_retry(product, wait, timeout)
379
+ @self._order_download_retry(product, wait, timeout)
379
380
  def download_request(
380
381
  product: EOProduct,
381
382
  fs_path: str,
@@ -464,13 +465,13 @@ class UsgsApi(Api):
464
465
  def download_all(
465
466
  self,
466
467
  products: SearchResult,
467
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
468
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
468
469
  downloaded_callback: Optional[DownloadedCallback] = None,
469
470
  progress_callback: Optional[ProgressCallback] = None,
470
- wait: int = DEFAULT_DOWNLOAD_WAIT,
471
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
471
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
472
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
472
473
  **kwargs: Unpack[DownloadConf],
473
- ) -> List[str]:
474
+ ) -> list[str]:
474
475
  """
475
476
  Download all using parent (base plugin) method
476
477
  """
@@ -17,9 +17,10 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING, Dict
20
+ from typing import TYPE_CHECKING, Optional, cast
21
21
 
22
22
  from eodag.plugins.authentication.base import Authentication
23
+ from eodag.types import S3SessionKwargs
23
24
 
24
25
  if TYPE_CHECKING:
25
26
  from mypy_boto3_s3.client import S3Client
@@ -53,12 +54,12 @@ class AwsAuth(Authentication):
53
54
 
54
55
  def __init__(self, provider: str, config: PluginConfig) -> None:
55
56
  super(AwsAuth, self).__init__(provider, config)
56
- self.aws_access_key_id = None
57
- self.aws_secret_access_key = None
58
- self.aws_session_token = None
59
- self.profile_name = None
57
+ self.aws_access_key_id: Optional[str] = None
58
+ self.aws_secret_access_key: Optional[str] = None
59
+ self.aws_session_token: Optional[str] = None
60
+ self.profile_name: Optional[str] = None
60
61
 
61
- def authenticate(self) -> Dict[str, str]:
62
+ def authenticate(self) -> S3SessionKwargs:
62
63
  """Authenticate
63
64
 
64
65
  :returns: dict containing AWS/boto3 non-empty credentials
@@ -75,10 +76,12 @@ class AwsAuth(Authentication):
75
76
  )
76
77
  self.profile_name = credentials.get("aws_profile", self.profile_name)
77
78
 
78
- auth_keys = [
79
- "aws_access_key_id",
80
- "aws_secret_access_key",
81
- "aws_session_token",
82
- "profile_name",
83
- ]
84
- return {k: getattr(self, k) for k in auth_keys if getattr(self, k)}
79
+ auth_dict = cast(
80
+ S3SessionKwargs,
81
+ {
82
+ k: getattr(self, k)
83
+ for k in S3SessionKwargs.__annotations__
84
+ if getattr(self, k, None)
85
+ },
86
+ )
87
+ return auth_dict
@@ -17,7 +17,7 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING, Dict, Union
20
+ from typing import TYPE_CHECKING, Union
21
21
 
22
22
  from eodag.plugins.base import PluginTopic
23
23
  from eodag.utils.exceptions import MisconfiguredError
@@ -25,6 +25,8 @@ from eodag.utils.exceptions import MisconfiguredError
25
25
  if TYPE_CHECKING:
26
26
  from requests.auth import AuthBase
27
27
 
28
+ from eodag.types import S3SessionKwargs
29
+
28
30
 
29
31
  class Authentication(PluginTopic):
30
32
  """Plugins authentication Base plugin
@@ -34,11 +36,11 @@ class Authentication(PluginTopic):
34
36
 
35
37
  * :attr:`~eodag.config.PluginConfig.matching_url` (``str``): URL pattern to match with search plugin endpoint or
36
38
  download link
37
- * :attr:`~eodag.config.PluginConfig.matching_conf` (``Dict[str, Any]``): Part of the search or download plugin
39
+ * :attr:`~eodag.config.PluginConfig.matching_conf` (``dict[str, Any]``): Part of the search or download plugin
38
40
  configuration that needs authentication and helps identifying it
39
41
  """
40
42
 
41
- def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
43
+ def authenticate(self) -> Union[AuthBase, S3SessionKwargs]:
42
44
  """Authenticate"""
43
45
  raise NotImplementedError
44
46
 
@@ -17,7 +17,7 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING, Dict
20
+ from typing import TYPE_CHECKING
21
21
 
22
22
  from requests.auth import AuthBase
23
23
 
@@ -38,7 +38,7 @@ class HTTPHeaderAuth(Authentication):
38
38
  :param config: Authentication plugin configuration:
39
39
 
40
40
  * :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): HTTPHeaderAuth
41
- * :attr:`~eodag.config.PluginConfig.headers` (``Dict[str, str]``): dictionary containing
41
+ * :attr:`~eodag.config.PluginConfig.headers` (``dict[str, str]``): dictionary containing
42
42
  all keys/value pairs that should be added to the headers
43
43
 
44
44
  Below an example for the configuration in the providers config file is shown::
@@ -106,7 +106,7 @@ class HTTPHeaderAuth(Authentication):
106
106
  class HeaderAuth(AuthBase):
107
107
  """HeaderAuth custom authentication class to be used with requests module"""
108
108
 
109
- def __init__(self, authentication_headers: Dict[str, str]) -> None:
109
+ def __init__(self, authentication_headers: dict[str, str]) -> None:
110
110
  self.auth_headers = authentication_headers
111
111
 
112
112
  def __call__(self, request: PreparedRequest) -> PreparedRequest:
@@ -18,7 +18,7 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import logging
21
- from typing import TYPE_CHECKING, Any, Dict
21
+ from typing import TYPE_CHECKING, Any
22
22
 
23
23
  import requests
24
24
 
@@ -56,7 +56,7 @@ class KeycloakOIDCPasswordAuth(OIDCRefreshTokenBase):
56
56
  token should be added to the query string (``qs``) or to the header (``header``)
57
57
  * :attr:`~eodag.config.PluginConfig.token_qs_key` (``str``): (**mandatory if token_provision=qs**)
58
58
  key of the param added to the query string
59
- * :attr:`~eodag.config.PluginConfig.allowed_audiences` (``List[str]``) (**mandatory**):
59
+ * :attr:`~eodag.config.PluginConfig.allowed_audiences` (``list[str]``) (**mandatory**):
60
60
  The allowed audiences that have to be present in the user token.
61
61
  * :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): which error code is
62
62
  returned in case of an authentication error
@@ -130,7 +130,7 @@ class KeycloakOIDCPasswordAuth(OIDCRefreshTokenBase):
130
130
  key=getattr(self.config, "token_qs_key", None),
131
131
  )
132
132
 
133
- def _request_new_token(self) -> Dict[str, Any]:
133
+ def _request_new_token(self) -> dict[str, Any]:
134
134
  logger.debug("fetching new access token")
135
135
  req_data = {
136
136
  "client_id": self.config.client_id,
@@ -154,7 +154,7 @@ class KeycloakOIDCPasswordAuth(OIDCRefreshTokenBase):
154
154
  return self._request_new_token_error(e)
155
155
  return response.json()
156
156
 
157
- def _get_token_with_refresh_token(self) -> Dict[str, str]:
157
+ def _get_token_with_refresh_token(self) -> dict[str, str]:
158
158
  logger.debug("fetching access token with refresh token")
159
159
  req_data = {
160
160
  "client_id": self.config.client_id,
@@ -17,12 +17,13 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING, Dict, Optional
20
+ from typing import TYPE_CHECKING, Optional
21
21
 
22
22
  from eodag.plugins.authentication.base import Authentication
23
23
 
24
24
  if TYPE_CHECKING:
25
25
  from eodag.config import PluginConfig
26
+ from eodag.types import S3SessionKwargs
26
27
 
27
28
 
28
29
  class OAuth(Authentication):
@@ -43,9 +44,12 @@ class OAuth(Authentication):
43
44
  self.access_key: Optional[str] = None
44
45
  self.secret_key: Optional[str] = None
45
46
 
46
- def authenticate(self) -> Dict[str, str]:
47
+ def authenticate(self) -> S3SessionKwargs:
47
48
  """Authenticate"""
48
49
  self.validate_config_credentials()
49
50
  self.access_key = self.config.credentials["aws_access_key_id"]
50
51
  self.secret_key = self.config.credentials["aws_secret_access_key"]
51
- return {"access_key": self.access_key, "secret_key": self.secret_key}
52
+ return {
53
+ "aws_access_key_id": self.access_key,
54
+ "aws_secret_access_key": self.secret_key,
55
+ }
@@ -22,7 +22,7 @@ import re
22
22
  import string
23
23
  from datetime import datetime, timedelta, timezone
24
24
  from random import SystemRandom
25
- from typing import TYPE_CHECKING, Any, Dict, Optional
25
+ from typing import TYPE_CHECKING, Any, Optional
26
26
 
27
27
  import jwt
28
28
  import requests
@@ -68,10 +68,10 @@ class OIDCRefreshTokenBase(Authentication):
68
68
  self.session = requests.Session()
69
69
 
70
70
  self.access_token = ""
71
- self.access_token_expiration = datetime.min
71
+ self.access_token_expiration = datetime.min.replace(tzinfo=timezone.utc)
72
72
 
73
73
  self.refresh_token = ""
74
- self.refresh_token_expiration = datetime.min
74
+ self.refresh_token_expiration = datetime.min.replace(tzinfo=timezone.utc)
75
75
 
76
76
  try:
77
77
  response = requests.get(self.config.oidc_config_url)
@@ -88,7 +88,7 @@ class OIDCRefreshTokenBase(Authentication):
88
88
  self.authorization_endpoint = auth_config["authorization_endpoint"]
89
89
  self.algorithms = auth_config["id_token_signing_alg_values_supported"]
90
90
 
91
- def decode_jwt_token(self, token: str) -> Dict[str, Any]:
91
+ def decode_jwt_token(self, token: str) -> dict[str, Any]:
92
92
  """Decode JWT token."""
93
93
  try:
94
94
  key = self.jwks_client.get_signing_key_from_jwt(token).key
@@ -144,13 +144,13 @@ class OIDCRefreshTokenBase(Authentication):
144
144
 
145
145
  return self.access_token
146
146
 
147
- def _request_new_token(self) -> Dict[str, str]:
147
+ def _request_new_token(self) -> dict[str, str]:
148
148
  """Fetch the access token with a new authentication"""
149
149
  raise NotImplementedError(
150
150
  "Incomplete OIDC refresh token retrieval mechanism implementation"
151
151
  )
152
152
 
153
- def _request_new_token_error(self, e: requests.RequestException) -> Dict[str, str]:
153
+ def _request_new_token_error(self, e: requests.RequestException) -> dict[str, str]:
154
154
  """Handle RequestException raised by `self._request_new_token()`"""
155
155
  if self.access_token:
156
156
  # try using already retrieved token if authenticate() fails (OTP use-case)
@@ -186,7 +186,7 @@ class OIDCRefreshTokenBase(Authentication):
186
186
  )
187
187
  )
188
188
 
189
- def _get_token_with_refresh_token(self) -> Dict[str, str]:
189
+ def _get_token_with_refresh_token(self) -> dict[str, str]:
190
190
  """Fetch the access token with the refresh token"""
191
191
  raise NotImplementedError(
192
192
  "Incomplete OIDC refresh token retrieval mechanism implementation"
@@ -241,21 +241,21 @@ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
241
241
  authentication_uri_source=config**) The URL of the authentication backend of the OIDC provider
242
242
  * :attr:`~eodag.config.PluginConfig.user_consent_form_xpath` (``str``): The xpath to
243
243
  the user consent form. The form is searched in the content of the response to the authorization request
244
- * :attr:`~eodag.config.PluginConfig.user_consent_form_data` (``Dict[str, str]``): The data that
244
+ * :attr:`~eodag.config.PluginConfig.user_consent_form_data` (``dict[str, str]``): The data that
245
245
  will be passed with the POST request on the form 'action' URL. The data are given as
246
246
  key value pairs, the keys representing the data key and the value being either a 'constant'
247
247
  string value, or a string of the form 'xpath(<path-to-a-value-to-be-retrieved>)' and representing a
248
248
  value to be retrieved in the user consent form. The xpath must resolve directly to a
249
249
  string value, not to an HTML element. Example: ``xpath(//input[@name="sessionDataKeyConsent"]/@value)``
250
- * :attr:`~eodag.config.PluginConfig.additional_login_form_data` (``Dict[str, str]``): A mapping
250
+ * :attr:`~eodag.config.PluginConfig.additional_login_form_data` (``dict[str, str]``): A mapping
251
251
  giving additional data to be passed to the login POST request. The value follows
252
252
  the same rules as with user_consent_form_data
253
- * :attr:`~eodag.config.PluginConfig.exchange_url_error_pattern` (``Dict[str, str]``): Key/value
253
+ * :attr:`~eodag.config.PluginConfig.exchange_url_error_pattern` (``dict[str, str]``): Key/value
254
254
  pairs of patterns/messages. If exchange_url contains the given pattern, the associated
255
255
  message will be sent in an AuthenticationError
256
256
  * :attr:`~eodag.config.PluginConfig.client_secret` (``str``): The OIDC provider's client
257
257
  secret of the eodag provider
258
- * :attr:`~eodag.config.PluginConfig.token_exchange_params` (``Dict[str, str]``): mandatory
258
+ * :attr:`~eodag.config.PluginConfig.token_exchange_params` (``dict[str, str]``): mandatory
259
259
  keys for the dict: redirect_uri, client_id; A mapping between OIDC url query string
260
260
  and token handler query string params (only necessary if they are not the same as for OIDC).
261
261
  This is eodag provider dependant
@@ -298,7 +298,7 @@ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
298
298
  key=getattr(self.config, "token_qs_key", None),
299
299
  )
300
300
 
301
- def _request_new_token(self) -> Dict[str, str]:
301
+ def _request_new_token(self) -> dict[str, str]:
302
302
  """Fetch the access token with a new authentication"""
303
303
  logger.debug("Fetching access token from %s", self.token_endpoint)
304
304
  state = self.compute_state()
@@ -326,12 +326,12 @@ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
326
326
  return self._request_new_token_error(e)
327
327
  return token_response.json()
328
328
 
329
- def _get_token_with_refresh_token(self) -> Dict[str, str]:
329
+ def _get_token_with_refresh_token(self) -> dict[str, str]:
330
330
  """Fetch the access token with the refresh token"""
331
331
  logger.debug(
332
332
  "Fetching access token with refresh token from %s.", self.token_endpoint
333
333
  )
334
- token_data: Dict[str, Any] = {
334
+ token_data: dict[str, Any] = {
335
335
  "refresh_token": self.refresh_token,
336
336
  "grant_type": "refresh_token",
337
337
  }
@@ -379,6 +379,12 @@ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
379
379
 
380
380
  login_document = etree.HTML(authorization_response.text)
381
381
  login_forms = login_document.xpath(self.config.login_form_xpath)
382
+
383
+ if not login_forms:
384
+ # we assume user is already logged in
385
+ # no form found because we got redirected to the redirect_uri
386
+ return authorization_response
387
+
382
388
  login_form = login_forms[0]
383
389
 
384
390
  # Get the form data to pass to the login form from config or from the login form
@@ -435,7 +441,7 @@ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
435
441
  verify=ssl_verify,
436
442
  )
437
443
 
438
- def _prepare_token_post_data(self, token_data: Dict[str, Any]) -> Dict[str, Any]:
444
+ def _prepare_token_post_data(self, token_data: dict[str, Any]) -> dict[str, Any]:
439
445
  """Prepare the common data to post to the token URI"""
440
446
  token_data.update(
441
447
  {
@@ -471,7 +477,7 @@ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
471
477
  "The state received in the authorized url does not match initially computed state"
472
478
  )
473
479
  code = qs["code"][0]
474
- token_exchange_data: Dict[str, Any] = {
480
+ token_exchange_data: dict[str, Any] = {
475
481
  "code": code,
476
482
  "state": state,
477
483
  "grant_type": "authorization_code",
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import logging
21
21
  from json import JSONDecodeError
22
- from typing import TYPE_CHECKING, Dict, Optional
22
+ from typing import TYPE_CHECKING, Optional
23
23
 
24
24
  import requests
25
25
  from requests.auth import AuthBase
@@ -42,13 +42,13 @@ class RequestsSASAuth(AuthBase):
42
42
  self,
43
43
  auth_uri: str,
44
44
  signed_url_key: str,
45
- headers: Optional[Dict[str, str]] = None,
45
+ headers: Optional[dict[str, str]] = None,
46
46
  ssl_verify: bool = True,
47
47
  ) -> None:
48
48
  self.auth_uri = auth_uri
49
49
  self.signed_url_key = signed_url_key
50
50
  self.headers = headers
51
- self.signed_urls: Dict[str, str] = {}
51
+ self.signed_urls: dict[str, str] = {}
52
52
  self.ssl_verify = ssl_verify
53
53
 
54
54
  def __call__(self, request: PreparedRequest) -> PreparedRequest:
@@ -97,7 +97,7 @@ class SASAuth(Authentication):
97
97
  get the signed url
98
98
  * :attr:`~eodag.config.PluginConfig.signed_url_key` (``str``) (**mandatory**): key to
99
99
  get the signed url
100
- * :attr:`~eodag.config.PluginConfig.headers` (``Dict[str, str]``) (**mandatory if
100
+ * :attr:`~eodag.config.PluginConfig.headers` (``dict[str, str]``) (**mandatory if
101
101
  apiKey is used**): headers to be added to the requests
102
102
  * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should be
103
103
  verified in the requests; default: ``True``