eodag 2.12.1__py3-none-any.whl → 3.0.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 (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -168
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.1.dist-info/RECORD +0 -94
  92. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
@@ -17,196 +17,39 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- import logging
21
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
20
+ from eodag.plugins.download.base import Download
21
+ from eodag.plugins.search.base import Search
22
22
 
23
- from pydantic.fields import Field, FieldInfo
24
23
 
25
- if TYPE_CHECKING:
26
- from eodag.api.product import EOProduct
27
- from eodag.api.search_result import SearchResult
28
- from eodag.config import PluginConfig
29
- from eodag.utils import DownloadedCallback, ProgressCallback
30
-
31
- from eodag.plugins.base import PluginTopic
32
- from eodag.utils import (
33
- DEFAULT_DOWNLOAD_TIMEOUT,
34
- DEFAULT_DOWNLOAD_WAIT,
35
- DEFAULT_ITEMS_PER_PAGE,
36
- DEFAULT_PAGE,
37
- Annotated,
38
- deepcopy,
39
- )
40
-
41
- logger = logging.getLogger("eodag.apis.base")
42
-
43
-
44
- class Api(PluginTopic):
24
+ class Api(Search, Download):
45
25
  """Plugins API Base plugin
46
26
 
47
- An Api plugin has three download methods that it must implement:
27
+ An Api plugin inherit the methods from Search and Download plugins.
48
28
 
29
+ There are three methods that it must implement:
49
30
  - ``query``: search for products
50
31
  - ``download``: download a single :class:`~eodag.api.product._product.EOProduct`
51
32
  - ``download_all``: download multiple products from a :class:`~eodag.api.search_result.SearchResult`
52
33
 
53
34
  The download methods must:
54
35
 
55
- - download data in the ``outputs_prefix`` folder defined in the plugin's
36
+ - download data in the ``output_dir`` folder defined in the plugin's
56
37
  configuration or passed through kwargs
57
38
  - extract products from their archive (if relevant) if ``extract`` is set to True
58
39
  (True by default)
59
- - save a product in an archive/directory (in ``outputs_prefix``) whose name must be
40
+ - save a product in an archive/directory (in ``output_dir``) whose name must be
60
41
  the product's ``title`` property
61
42
  - update the product's ``location`` attribute once its data is downloaded (and
62
43
  eventually after it's extracted) to the product's location given as a file URI
63
44
  (e.g. 'file:///tmp/product_folder' on Linux or
64
45
  'file:///C:/Users/username/AppData/LOcal/Temp' on Windows)
65
- - save a *record* file in the directory ``outputs_prefix/.downloaded`` whose name
66
- is built on the MD5 hash of the product's ``remote_location`` attribute
67
- (``hashlib.md5(remote_location.encode("utf-8")).hexdigest()``) and whose content is
68
- the product's ``remote_location`` attribute itself.
46
+ - save a *record* file in the directory ``output_dir/.downloaded`` whose name
47
+ is built on the MD5 hash of the product's ``product_type`` and ``properties['id']``
48
+ attributes (``hashlib.md5((product.product_type+"-"+product.properties['id']).encode("utf-8")).hexdigest()``)
49
+ and whose content is the product's ``remote_location`` attribute itself.
69
50
  - not try to download a product whose ``location`` attribute already points to an
70
51
  existing file/directory
71
52
  - not try to download a product if its *record* file exists as long as the expected
72
53
  product's file/directory. If the *record* file only is found, it must be deleted
73
54
  (it certainly indicates that the download didn't complete)
74
55
  """
75
-
76
- def clear(self) -> None:
77
- """Method used to clear a search context between two searches."""
78
- pass
79
-
80
- def query(
81
- self,
82
- product_type: Optional[str] = None,
83
- items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
84
- page: int = DEFAULT_PAGE,
85
- count: bool = True,
86
- **kwargs: Any,
87
- ) -> Tuple[List[EOProduct], Optional[int]]:
88
- """Implementation of how the products must be searched goes here.
89
-
90
- This method must return a tuple with (1) a list of EOProduct instances (see eodag.api.product module)
91
- which will be processed by a Download plugin (2) and the total number of products matching
92
- the search criteria. If ``count`` is False, the second element returned must be ``None``.
93
- """
94
- raise NotImplementedError("A Api plugin must implement a method named query")
95
-
96
- def discover_product_types(self) -> Optional[Dict[str, Any]]:
97
- """Fetch product types list from provider using `discover_product_types` conf"""
98
- return None
99
-
100
- def discover_queryables(
101
- self, **kwargs: Any
102
- ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
103
- """Fetch queryables list from provider using `discover_queryables` conf
104
-
105
- :param kwargs: additional filters for queryables (`productType` and other search
106
- arguments)
107
- :type kwargs: Any
108
- :returns: fetched queryable parameters dict
109
- :rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
110
- """
111
- return None
112
-
113
- def get_defaults_as_queryables(
114
- self, product_type: str
115
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
116
- """
117
- Return given product type defaut settings as queryables
118
-
119
- :param product_type: given product type
120
- :type product_type: str
121
- :returns: queryable parameters dict
122
- :rtype: Dict[str, Annotated[Any, FieldInfo]]
123
- """
124
- defaults = deepcopy(self.config.products.get(product_type, {}))
125
- defaults.pop("metadata_mapping", None)
126
-
127
- queryables = {}
128
- for parameter, value in defaults.items():
129
- queryables[parameter] = Annotated[type(value), Field(default=value)]
130
- return queryables
131
-
132
- def download(
133
- self,
134
- product: EOProduct,
135
- auth: Optional[PluginConfig] = None,
136
- progress_callback: Optional[ProgressCallback] = None,
137
- wait: int = DEFAULT_DOWNLOAD_WAIT,
138
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
139
- **kwargs: Any,
140
- ) -> Optional[str]:
141
- """
142
- Base download method. Not available, it must be defined for each plugin.
143
-
144
- :param product: The EO product to download
145
- :type product: :class:`~eodag.api.product._product.EOProduct`
146
- :param auth: (optional) The configuration of a plugin of type Authentication
147
- :type auth: :class:`~eodag.config.PluginConfig`
148
- :param progress_callback: (optional) A progress callback
149
- :type progress_callback: :class:`~eodag.utils.ProgressCallback`
150
- :param wait: (optional) If download fails, wait time in minutes between two download tries
151
- :type wait: int
152
- :param timeout: (optional) If download fails, maximum time in minutes before stop retrying
153
- to download
154
- :type timeout: int
155
- :param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool)
156
- and `dl_url_params` (dict) can be provided as additional kwargs
157
- and will override any other values defined in a configuration
158
- file or with environment variables.
159
- :type kwargs: Union[str, bool, dict]
160
- :returns: The absolute path to the downloaded product in the local filesystem
161
- (e.g. '/tmp/product.zip' on Linux or
162
- 'C:\\Users\\username\\AppData\\Local\\Temp\\product.zip' on Windows)
163
- :rtype: str
164
- """
165
- raise NotImplementedError(
166
- "An Api plugin must implement a method named download"
167
- )
168
-
169
- def download_all(
170
- self,
171
- products: SearchResult,
172
- auth: Optional[PluginConfig] = None,
173
- downloaded_callback: Optional[DownloadedCallback] = None,
174
- progress_callback: Optional[ProgressCallback] = None,
175
- wait: int = DEFAULT_DOWNLOAD_WAIT,
176
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
177
- **kwargs: Any,
178
- ) -> List[str]:
179
- """
180
- Base download_all method.
181
-
182
- :param products: Products to download
183
- :type products: :class:`~eodag.api.search_result.SearchResult`
184
- :param auth: (optional) The configuration of a plugin of type Authentication
185
- :type auth: :class:`~eodag.config.PluginConfig`
186
- :param downloaded_callback: (optional) A method or a callable object which takes
187
- as parameter the ``product``. You can use the base class
188
- :class:`~eodag.api.product.DownloadedCallback` and override
189
- its ``__call__`` method. Will be called each time a product
190
- finishes downloading
191
- :type downloaded_callback: Callable[[:class:`~eodag.api.product._product.EOProduct`], None]
192
- or None
193
- :param progress_callback: (optional) A progress callback
194
- :type progress_callback: :class:`~eodag.utils.ProgressCallback`
195
- :param wait: (optional) If download fails, wait time in minutes between two download tries
196
- :type wait: int
197
- :param timeout: (optional) If download fails, maximum time in minutes before stop retrying
198
- to download
199
- :type timeout: int
200
- :param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool)
201
- and `dl_url_params` (dict) can be provided as additional kwargs
202
- and will override any other values defined in a configuration
203
- file or with environment variables.
204
- :type kwargs: Union[str, bool, dict]
205
- :returns: List of absolute paths to the downloaded products in the local
206
- filesystem (e.g. ``['/tmp/product.zip']`` on Linux or
207
- ``['C:\\Users\\username\\AppData\\Local\\Temp\\product.zip']`` on Windows)
208
- :rtype: list
209
- """
210
- raise NotImplementedError(
211
- "A Api plugin must implement a method named download_all"
212
- )
@@ -18,42 +18,47 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import logging
21
- from datetime import datetime
22
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
21
+ import os
22
+ from datetime import datetime, timezone
23
+ from typing import TYPE_CHECKING
23
24
 
24
25
  import geojson
25
26
  from ecmwfapi import ECMWFDataServer, ECMWFService
26
27
  from ecmwfapi.api import APIException, Connection, get_apikey_values
27
28
 
28
29
  from eodag.plugins.apis.base import Api
29
- from eodag.plugins.download.base import Download
30
+ from eodag.plugins.search import PreparedSearch
30
31
  from eodag.plugins.search.base import Search
31
32
  from eodag.plugins.search.build_search_result import BuildPostSearchResult
32
- from eodag.rest.stac import DEFAULT_MISSION_START_DATE
33
33
  from eodag.utils import (
34
34
  DEFAULT_DOWNLOAD_TIMEOUT,
35
35
  DEFAULT_DOWNLOAD_WAIT,
36
- DEFAULT_ITEMS_PER_PAGE,
37
- DEFAULT_PAGE,
36
+ DEFAULT_MISSION_START_DATE,
38
37
  get_geometry_from_various,
39
38
  path_to_uri,
39
+ sanitize,
40
40
  urlsplit,
41
41
  )
42
42
  from eodag.utils.exceptions import AuthenticationError, DownloadError
43
43
  from eodag.utils.logging import get_logging_verbose
44
44
 
45
45
  if TYPE_CHECKING:
46
+ from typing import Any, Dict, List, Optional, Tuple, Union
47
+
48
+ from requests.auth import AuthBase
49
+
46
50
  from eodag.api.product import EOProduct
47
51
  from eodag.api.search_result import SearchResult
48
52
  from eodag.config import PluginConfig
49
- from eodag.utils import DownloadedCallback, ProgressCallback
53
+ from eodag.types.download_args import DownloadConf
54
+ from eodag.utils import DownloadedCallback, ProgressCallback, Unpack
50
55
 
51
56
  logger = logging.getLogger("eodag.apis.ecmwf")
52
57
 
53
58
  ECMWF_MARS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
54
59
 
55
60
 
56
- class EcmwfApi(Download, Api, BuildPostSearchResult):
61
+ class EcmwfApi(Api, BuildPostSearchResult):
57
62
  """A plugin that enables to build download-request and download data on ECMWF MARS.
58
63
 
59
64
  Builds a single ready-to-download :class:`~eodag.api.product._product.EOProduct`
@@ -84,10 +89,7 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
84
89
 
85
90
  def query(
86
91
  self,
87
- product_type: Optional[str] = None,
88
- items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
89
- page: int = DEFAULT_PAGE,
90
- count: bool = True,
92
+ prep: PreparedSearch = PreparedSearch(),
91
93
  **kwargs: Any,
92
94
  ) -> Tuple[List[EOProduct], Optional[int]]:
93
95
  """Build ready-to-download SearchResult"""
@@ -112,7 +114,7 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
112
114
  if "completionTimeFromAscendingNode" not in kwargs:
113
115
  kwargs["completionTimeFromAscendingNode"] = getattr(
114
116
  self.config, "product_type_config", {}
115
- ).get("missionEndDate", None) or datetime.utcnow().isoformat(
117
+ ).get("missionEndDate", None) or datetime.now(timezone.utc).isoformat(
116
118
  timespec="seconds"
117
119
  )
118
120
 
@@ -120,15 +122,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
120
122
  if "geometry" in kwargs:
121
123
  kwargs["geometry"] = get_geometry_from_various(geometry=kwargs["geometry"])
122
124
 
123
- return BuildPostSearchResult.query(
124
- self, items_per_page=items_per_page, page=page, count=count, **kwargs
125
- )
125
+ return BuildPostSearchResult.query(self, prep, **kwargs)
126
126
 
127
127
  def authenticate(self) -> Dict[str, Optional[str]]:
128
128
  """Check credentials and returns information needed for auth
129
129
 
130
130
  :returns: {key, url, email} dictionary
131
- :rtype: dict
132
131
  :raises: :class:`~eodag.utils.exceptions.AuthenticationError`
133
132
  """
134
133
  # Get credentials from eodag or using ecmwf conf
@@ -156,21 +155,23 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
156
155
  def download(
157
156
  self,
158
157
  product: EOProduct,
159
- auth: Optional[PluginConfig] = None,
158
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
160
159
  progress_callback: Optional[ProgressCallback] = None,
161
160
  wait: int = DEFAULT_DOWNLOAD_WAIT,
162
161
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
163
- **kwargs: Any,
162
+ **kwargs: Unpack[DownloadConf],
164
163
  ) -> Optional[str]:
165
164
  """Download data from ECMWF MARS"""
166
165
  product_format = product.properties.get("format", "grib")
167
166
  product_extension = ECMWF_MARS_KNOWN_FORMATS.get(product_format, product_format)
167
+ kwargs["output_extension"] = kwargs.get(
168
+ "output_extension", f".{product_extension}"
169
+ )
168
170
 
169
171
  # Prepare download
170
172
  fs_path, record_filename = self._prepare_download(
171
173
  product,
172
174
  progress_callback=progress_callback,
173
- outputs_extension=f".{product_extension}",
174
175
  **kwargs,
175
176
  )
176
177
 
@@ -179,6 +180,13 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
179
180
  product.location = path_to_uri(fs_path)
180
181
  return fs_path
181
182
 
183
+ new_fs_path = os.path.join(
184
+ os.path.dirname(fs_path), sanitize(product.properties["title"])
185
+ )
186
+ if not os.path.isdir(new_fs_path):
187
+ os.makedirs(new_fs_path)
188
+ fs_path = os.path.join(new_fs_path, os.path.basename(fs_path))
189
+
182
190
  # get download request dict from product.location/downloadLink url query string
183
191
  # separate url & parameters
184
192
  download_request = geojson.loads(urlsplit(product.location).query)
@@ -222,13 +230,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
222
230
  fh.write(product.properties["downloadLink"])
223
231
  logger.debug("Download recorded in %s", record_filename)
224
232
 
225
- # do not try to extract or delete grib/netcdf
233
+ # do not try to extract a directory
226
234
  kwargs["extract"] = False
227
235
 
228
236
  product_path = self._finalize(
229
- fs_path,
237
+ new_fs_path,
230
238
  progress_callback=progress_callback,
231
- outputs_extension=f".{product_extension}",
232
239
  **kwargs,
233
240
  )
234
241
  product.location = path_to_uri(product_path)
@@ -237,12 +244,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
237
244
  def download_all(
238
245
  self,
239
246
  products: SearchResult,
240
- auth: Optional[PluginConfig] = None,
247
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
241
248
  downloaded_callback: Optional[DownloadedCallback] = None,
242
249
  progress_callback: Optional[ProgressCallback] = None,
243
250
  wait: int = DEFAULT_DOWNLOAD_WAIT,
244
251
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
245
- **kwargs: Any,
252
+ **kwargs: Unpack[DownloadConf],
246
253
  ) -> List[str]:
247
254
  """
248
255
  Download all using parent (base plugin) method
@@ -256,3 +263,7 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
256
263
  timeout=timeout,
257
264
  **kwargs,
258
265
  )
266
+
267
+ def clear(self) -> None:
268
+ """Clear search context"""
269
+ pass
@@ -18,10 +18,11 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import logging
21
+ import os
21
22
  import shutil
22
23
  import tarfile
23
24
  import zipfile
24
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
25
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
25
26
 
26
27
  import requests
27
28
  from jsonpath_ng.ext import parse
@@ -35,7 +36,7 @@ from eodag.api.product.metadata_mapping import (
35
36
  properties_from_json,
36
37
  )
37
38
  from eodag.plugins.apis.base import Api
38
- from eodag.plugins.download.base import Download
39
+ from eodag.plugins.search import PreparedSearch
39
40
  from eodag.utils import (
40
41
  DEFAULT_DOWNLOAD_TIMEOUT,
41
42
  DEFAULT_DOWNLOAD_WAIT,
@@ -52,17 +53,21 @@ from eodag.utils.exceptions import (
52
53
  NoMatchingProductType,
53
54
  NotAvailableError,
54
55
  RequestError,
56
+ ValidationError,
55
57
  )
56
58
 
57
59
  if TYPE_CHECKING:
60
+ from requests.auth import AuthBase
61
+
58
62
  from eodag.api.search_result import SearchResult
59
63
  from eodag.config import PluginConfig
60
- from eodag.utils import DownloadedCallback
64
+ from eodag.types.download_args import DownloadConf
65
+ from eodag.utils import DownloadedCallback, Unpack
61
66
 
62
67
  logger = logging.getLogger("eodag.apis.usgs")
63
68
 
64
69
 
65
- class UsgsApi(Download, Api):
70
+ class UsgsApi(Api):
66
71
  """A plugin that enables to query and download data on the USGS catalogues"""
67
72
 
68
73
  def __init__(self, provider: str, config: PluginConfig) -> None:
@@ -71,7 +76,7 @@ class UsgsApi(Download, Api):
71
76
  # Same method as in base.py, Search.__init__()
72
77
  # Prepare the metadata mapping
73
78
  # Do a shallow copy, the structure is flat enough for this to be sufficient
74
- metas = DEFAULT_METADATA_MAPPING.copy()
79
+ metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
75
80
  # Update the defaults with the mapping value. This will add any new key
76
81
  # added by the provider mapping that is not in the default metadata.
77
82
  metas.update(self.config.metadata_mapping)
@@ -98,30 +103,39 @@ class UsgsApi(Download, Api):
98
103
  except USGSAuthExpiredError:
99
104
  api.logout()
100
105
  continue
101
- except USGSError:
102
- raise AuthenticationError(
103
- "Please check your USGS credentials."
104
- ) from None
106
+ except USGSError as e:
107
+ if i == 0:
108
+ # `.usgs` API file key might be obsolete
109
+ # Remove it and try again
110
+ os.remove(api.TMPFILE)
111
+ continue
112
+ raise AuthenticationError("Please check your USGS credentials.") from e
105
113
 
106
114
  def query(
107
115
  self,
108
- product_type: Optional[str] = None,
109
- items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
110
- page: int = DEFAULT_PAGE,
111
- count: bool = True,
116
+ prep: PreparedSearch = PreparedSearch(),
112
117
  **kwargs: Any,
113
118
  ) -> Tuple[List[EOProduct], Optional[int]]:
114
119
  """Search for data on USGS catalogues"""
120
+ page = prep.page if prep.page is not None else DEFAULT_PAGE
121
+ items_per_page = (
122
+ prep.items_per_page
123
+ if prep.items_per_page is not None
124
+ else DEFAULT_ITEMS_PER_PAGE
125
+ )
115
126
  product_type = kwargs.get("productType")
116
127
  if product_type is None:
117
128
  raise NoMatchingProductType(
118
129
  "Cannot search on USGS without productType specified"
119
130
  )
131
+ if kwargs.get("sort_by"):
132
+ raise ValidationError("USGS does not support sorting feature")
120
133
 
121
134
  self.authenticate()
122
135
 
123
136
  product_type_def_params = self.config.products.get( # type: ignore
124
- product_type, self.config.products[GENERIC_PRODUCT_TYPE] # type: ignore
137
+ product_type,
138
+ self.config.products[GENERIC_PRODUCT_TYPE], # type: ignore
125
139
  )
126
140
  usgs_dataset = format_dict_items(product_type_def_params, **kwargs)["dataset"]
127
141
  start_date = kwargs.pop("startTimeFromAscendingNode", None)
@@ -159,11 +173,39 @@ class UsgsApi(Download, Api):
159
173
  max_results=items_per_page,
160
174
  starting_number=(1 + (page - 1) * items_per_page),
161
175
  )
162
- logger.info(
163
- f"Sending search request for {usgs_dataset} with {api_search_kwargs}"
164
- )
165
176
 
166
- results = api.scene_search(usgs_dataset, **api_search_kwargs)
177
+ # search by id
178
+ if searched_id := kwargs.get("id"):
179
+ dataset_filters = api.dataset_filters(usgs_dataset)
180
+ # ip pattern set as parameter queryable (first element of param conf list)
181
+ id_pattern = self.config.metadata_mapping["id"][0]
182
+ # loop on matching dataset_filters until one returns expected results
183
+ for dataset_filter in dataset_filters["data"]:
184
+ if id_pattern in dataset_filter["searchSql"]:
185
+ logger.debug(
186
+ f"Try using {dataset_filter['searchSql']} dataset filter to search by id on {usgs_dataset}"
187
+ )
188
+ full_api_search_kwargs = {
189
+ "where": {
190
+ "filter_id": dataset_filter["id"],
191
+ "value": searched_id,
192
+ },
193
+ **api_search_kwargs,
194
+ }
195
+ logger.info(
196
+ f"Sending search request for {usgs_dataset} with {full_api_search_kwargs}"
197
+ )
198
+ results = api.scene_search(
199
+ usgs_dataset, **full_api_search_kwargs
200
+ )
201
+ if len(results["data"]["results"]) == 1:
202
+ # search by id using this dataset_filter succeeded
203
+ break
204
+ else:
205
+ logger.info(
206
+ f"Sending search request for {usgs_dataset} with {api_search_kwargs}"
207
+ )
208
+ results = api.scene_search(usgs_dataset, **api_search_kwargs)
167
209
 
168
210
  # update results with storage info from download_options()
169
211
  results_by_entity_id = {
@@ -214,7 +256,7 @@ class UsgsApi(Download, Api):
214
256
  f"Product type {usgs_dataset} may not exist on USGS EE catalog"
215
257
  )
216
258
  api.logout()
217
- raise RequestError(e)
259
+ raise RequestError.from_error(e) from e
218
260
 
219
261
  api.logout()
220
262
 
@@ -230,11 +272,11 @@ class UsgsApi(Download, Api):
230
272
  def download(
231
273
  self,
232
274
  product: EOProduct,
233
- auth: Optional[PluginConfig] = None,
275
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
234
276
  progress_callback: Optional[ProgressCallback] = None,
235
277
  wait: int = DEFAULT_DOWNLOAD_WAIT,
236
278
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
237
- **kwargs: Any,
279
+ **kwargs: Unpack[DownloadConf],
238
280
  ) -> Optional[str]:
239
281
  """Download data from USGS catalogues"""
240
282
 
@@ -244,17 +286,17 @@ class UsgsApi(Download, Api):
244
286
  )
245
287
  progress_callback = ProgressCallback(disable=True)
246
288
 
247
- outputs_extension = cast(
289
+ output_extension = cast(
248
290
  str,
249
291
  self.config.products.get( # type: ignore
250
292
  product.product_type, self.config.products[GENERIC_PRODUCT_TYPE] # type: ignore
251
- ).get("outputs_extension", ".tar.gz"),
293
+ ).get("output_extension", ".tar.gz"),
252
294
  )
295
+ kwargs["output_extension"] = kwargs.get("output_extension", output_extension)
253
296
 
254
297
  fs_path, record_filename = self._prepare_download(
255
298
  product,
256
299
  progress_callback=progress_callback,
257
- outputs_extension=outputs_extension,
258
300
  **kwargs,
259
301
  )
260
302
  if not fs_path or not record_filename:
@@ -308,13 +350,14 @@ class UsgsApi(Download, Api):
308
350
  req_url = req_urls[0]
309
351
  progress_callback.reset()
310
352
  logger.debug(f"Downloading {req_url}")
353
+ ssl_verify = getattr(self.config, "ssl_verify", True)
311
354
 
312
355
  @self._download_retry(product, wait, timeout)
313
356
  def download_request(
314
357
  product: EOProduct,
315
358
  fs_path: str,
316
359
  progress_callback: ProgressCallback,
317
- **kwargs: Any,
360
+ **kwargs: Unpack[DownloadConf],
318
361
  ) -> None:
319
362
  try:
320
363
  with requests.get(
@@ -322,6 +365,7 @@ class UsgsApi(Download, Api):
322
365
  stream=True,
323
366
  headers=USER_AGENT,
324
367
  timeout=wait * 60,
368
+ verify=ssl_verify,
325
369
  ) as stream:
326
370
  try:
327
371
  stream.raise_for_status()
@@ -334,7 +378,9 @@ class UsgsApi(Download, Api):
334
378
  error_message = str(e)
335
379
  raise NotAvailableError(error_message)
336
380
  else:
337
- stream_size = int(stream.headers.get("content-length", 0))
381
+ stream_size = (
382
+ int(stream.headers.get("content-length", 0)) or None
383
+ )
338
384
  progress_callback.reset(total=stream_size)
339
385
  with open(fs_path, "wb") as fhandle:
340
386
  for chunk in stream.iter_content(chunk_size=64 * 1024):
@@ -357,13 +403,12 @@ class UsgsApi(Download, Api):
357
403
  api.logout()
358
404
 
359
405
  # Check downloaded file format
360
- if (outputs_extension == ".tar.gz" and tarfile.is_tarfile(fs_path)) or (
361
- outputs_extension == ".zip" and zipfile.is_zipfile(fs_path)
362
- ):
406
+ if (
407
+ kwargs["output_extension"] == ".tar.gz" and tarfile.is_tarfile(fs_path)
408
+ ) or (kwargs["output_extension"] == ".zip" and zipfile.is_zipfile(fs_path)):
363
409
  product_path = self._finalize(
364
410
  fs_path,
365
411
  progress_callback=progress_callback,
366
- outputs_extension=outputs_extension,
367
412
  **kwargs,
368
413
  )
369
414
  product.location = path_to_uri(product_path)
@@ -372,7 +417,7 @@ class UsgsApi(Download, Api):
372
417
  logger.info(
373
418
  "Downloaded product detected as a tar File, but was was expected to be a zip file"
374
419
  )
375
- new_fs_path = fs_path[: fs_path.index(outputs_extension)] + ".tar.gz"
420
+ new_fs_path = fs_path[: fs_path.index(output_extension)] + ".tar.gz"
376
421
  shutil.move(fs_path, new_fs_path)
377
422
  product.location = path_to_uri(new_fs_path)
378
423
  return new_fs_path
@@ -380,7 +425,7 @@ class UsgsApi(Download, Api):
380
425
  logger.info(
381
426
  "Downloaded product detected as a zip File, but was was expected to be a tar file"
382
427
  )
383
- new_fs_path = fs_path[: fs_path.index(outputs_extension)] + ".zip"
428
+ new_fs_path = fs_path[: fs_path.index(output_extension)] + ".zip"
384
429
  shutil.move(fs_path, new_fs_path)
385
430
  product.location = path_to_uri(new_fs_path)
386
431
  return new_fs_path
@@ -388,7 +433,7 @@ class UsgsApi(Download, Api):
388
433
  logger.warning(
389
434
  "Downloaded product is not a tar or a zip File. Please check its file type before using it"
390
435
  )
391
- new_fs_path = fs_path[: fs_path.index(outputs_extension)]
436
+ new_fs_path = fs_path[: fs_path.index(output_extension)]
392
437
  shutil.move(fs_path, new_fs_path)
393
438
  product.location = path_to_uri(new_fs_path)
394
439
  return new_fs_path
@@ -396,12 +441,12 @@ class UsgsApi(Download, Api):
396
441
  def download_all(
397
442
  self,
398
443
  products: SearchResult,
399
- auth: Optional[PluginConfig] = None,
444
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
400
445
  downloaded_callback: Optional[DownloadedCallback] = None,
401
446
  progress_callback: Optional[ProgressCallback] = None,
402
447
  wait: int = DEFAULT_DOWNLOAD_WAIT,
403
448
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
404
- **kwargs: Any,
449
+ **kwargs: Unpack[DownloadConf],
405
450
  ) -> List[str]:
406
451
  """
407
452
  Download all using parent (base plugin) method