eodag 2.12.1__py3-none-any.whl → 3.0.0b2__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 (78) hide show
  1. eodag/api/core.py +440 -321
  2. eodag/api/product/__init__.py +5 -1
  3. eodag/api/product/_assets.py +57 -2
  4. eodag/api/product/_product.py +89 -68
  5. eodag/api/product/metadata_mapping.py +181 -66
  6. eodag/api/search_result.py +48 -1
  7. eodag/cli.py +20 -6
  8. eodag/config.py +95 -6
  9. eodag/plugins/apis/base.py +8 -165
  10. eodag/plugins/apis/ecmwf.py +36 -24
  11. eodag/plugins/apis/usgs.py +40 -24
  12. eodag/plugins/authentication/aws_auth.py +2 -2
  13. eodag/plugins/authentication/header.py +31 -6
  14. eodag/plugins/authentication/keycloak.py +13 -84
  15. eodag/plugins/authentication/oauth.py +3 -3
  16. eodag/plugins/authentication/openid_connect.py +256 -46
  17. eodag/plugins/authentication/qsauth.py +3 -0
  18. eodag/plugins/authentication/sas_auth.py +8 -1
  19. eodag/plugins/authentication/token.py +92 -46
  20. eodag/plugins/authentication/token_exchange.py +120 -0
  21. eodag/plugins/download/aws.py +86 -91
  22. eodag/plugins/download/base.py +72 -40
  23. eodag/plugins/download/http.py +607 -264
  24. eodag/plugins/download/s3rest.py +28 -15
  25. eodag/plugins/manager.py +74 -57
  26. eodag/plugins/search/__init__.py +36 -0
  27. eodag/plugins/search/base.py +225 -18
  28. eodag/plugins/search/build_search_result.py +389 -32
  29. eodag/plugins/search/cop_marine.py +378 -0
  30. eodag/plugins/search/creodias_s3.py +15 -14
  31. eodag/plugins/search/csw.py +5 -7
  32. eodag/plugins/search/data_request_search.py +44 -20
  33. eodag/plugins/search/qssearch.py +508 -203
  34. eodag/plugins/search/static_stac_search.py +99 -36
  35. eodag/resources/constraints/climate-dt.json +13 -0
  36. eodag/resources/constraints/extremes-dt.json +8 -0
  37. eodag/resources/ext_product_types.json +1 -1
  38. eodag/resources/product_types.yml +1897 -34
  39. eodag/resources/providers.yml +3539 -3277
  40. eodag/resources/stac.yml +48 -54
  41. eodag/resources/stac_api.yml +71 -25
  42. eodag/resources/stac_provider.yml +5 -0
  43. eodag/resources/user_conf_template.yml +51 -3
  44. eodag/rest/__init__.py +6 -0
  45. eodag/rest/cache.py +70 -0
  46. eodag/rest/config.py +68 -0
  47. eodag/rest/constants.py +27 -0
  48. eodag/rest/core.py +757 -0
  49. eodag/rest/server.py +397 -258
  50. eodag/rest/stac.py +438 -307
  51. eodag/rest/types/collections_search.py +44 -0
  52. eodag/rest/types/eodag_search.py +232 -43
  53. eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
  54. eodag/rest/types/stac_search.py +277 -0
  55. eodag/rest/utils/__init__.py +216 -0
  56. eodag/rest/utils/cql_evaluate.py +119 -0
  57. eodag/rest/utils/rfc3339.py +65 -0
  58. eodag/types/__init__.py +99 -9
  59. eodag/types/bbox.py +15 -14
  60. eodag/types/download_args.py +31 -0
  61. eodag/types/search_args.py +58 -7
  62. eodag/types/whoosh.py +81 -0
  63. eodag/utils/__init__.py +72 -9
  64. eodag/utils/constraints.py +37 -37
  65. eodag/utils/exceptions.py +23 -17
  66. eodag/utils/repr.py +113 -0
  67. eodag/utils/requests.py +138 -0
  68. eodag/utils/rest.py +104 -0
  69. eodag/utils/stac_reader.py +100 -16
  70. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/METADATA +65 -44
  71. eodag-3.0.0b2.dist-info/RECORD +110 -0
  72. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/WHEEL +1 -1
  73. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/entry_points.txt +6 -5
  74. eodag/plugins/apis/cds.py +0 -540
  75. eodag/rest/utils.py +0 -1133
  76. eodag-2.12.1.dist-info/RECORD +0 -94
  77. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/LICENSE +0 -0
  78. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/top_level.txt +0 -0
eodag/config.py CHANGED
@@ -27,6 +27,7 @@ from typing import (
27
27
  ItemsView,
28
28
  Iterator,
29
29
  List,
30
+ Literal,
30
31
  Optional,
31
32
  Tuple,
32
33
  TypedDict,
@@ -40,13 +41,16 @@ import requests
40
41
  import yaml
41
42
  import yaml.constructor
42
43
  import yaml.parser
44
+ from annotated_types import Gt
43
45
  from jsonpath_ng import JSONPath
44
46
  from pkg_resources import resource_filename
45
47
  from requests.auth import AuthBase
48
+ from typing_extensions import Doc
46
49
 
47
50
  from eodag.utils import (
48
51
  HTTP_REQ_TIMEOUT,
49
52
  USER_AGENT,
53
+ Annotated,
50
54
  cached_yaml_load,
51
55
  cached_yaml_load_all,
52
56
  cast_scalar_value,
@@ -125,7 +129,11 @@ class ProviderConfig(yaml.YAMLObject):
125
129
  """
126
130
 
127
131
  name: str
132
+ group: str
128
133
  priority: int = 0 # Set default priority to 0
134
+ roles: List[str]
135
+ description: str
136
+ url: str
129
137
  api: PluginConfig
130
138
  search: PluginConfig
131
139
  products: Dict[str, Any]
@@ -138,7 +146,7 @@ class ProviderConfig(yaml.YAMLObject):
138
146
  yaml_tag = "!provider"
139
147
 
140
148
  @classmethod
141
- def from_yaml(cls, loader: yaml.Loader, node: Any) -> ProviderConfig:
149
+ def from_yaml(cls, loader: yaml.Loader, node: Any) -> Iterator[ProviderConfig]:
142
150
  """Build a :class:`~eodag.config.ProviderConfig` from Yaml"""
143
151
  cls.validate(tuple(node_key.value for node_key, _ in node.value))
144
152
  for node_key, node_value in node.value:
@@ -223,19 +231,77 @@ class PluginConfig(yaml.YAMLObject):
223
231
  next_page_url_key_path: Union[str, JSONPath]
224
232
  next_page_query_obj_key_path: Union[str, JSONPath]
225
233
  next_page_merge_key_path: Union[str, JSONPath]
234
+ count_tpl: str
226
235
  next_page_url_tpl: str
227
236
  next_page_query_obj: str
228
237
  count_endpoint: str
229
238
  start_page: int
230
239
 
240
+ class Sort(TypedDict):
241
+ """Configuration for sort during search"""
242
+
243
+ sort_by_default: List[Tuple[str, str]]
244
+ sort_by_tpl: str
245
+ sort_param_mapping: Dict[str, str]
246
+ sort_order_mapping: Dict[Literal["ascending", "descending"], str]
247
+ max_sort_params: Annotated[int, Gt(0)]
248
+
249
+ class OrderOnResponse(TypedDict):
250
+ """Configuration for order on-response during download"""
251
+
252
+ metadata_mapping: Dict[str, Union[str, List[str]]]
253
+
254
+ class OrderStatusSuccess(TypedDict):
255
+ """
256
+ Configuration to identify order status success during download
257
+
258
+ Order status response matching the following parameters are considered success
259
+ At least one is required
260
+ """
261
+
262
+ status: Annotated[str, Doc("Variable in the order status response json body")]
263
+ message: Annotated[str, Doc("Variable in the order status response json body")]
264
+ http_code: Annotated[int, Doc("HTTP code of the order status response")]
265
+
266
+ class OrderStatusOrdered(TypedDict):
267
+ """
268
+ Configuration to identify order status ordered during download
269
+ """
270
+
271
+ http_code: Annotated[int, Doc("HTTP code of the order status response")]
272
+
273
+ class OrderStatusRequest(TypedDict):
274
+ """
275
+ Order status request configuration
276
+ """
277
+
278
+ method: Annotated[str, Doc("Request HTTP method")]
279
+ headers: Annotated[Dict[str, Any], Doc("Request hearders")]
280
+
231
281
  class OrderStatusOnSuccess(TypedDict):
232
- """Configuration for order on-success during download"""
282
+ """Configuration for order status on-success during download"""
233
283
 
234
- need_search: bool
284
+ need_search: Annotated[bool, Doc("If a new search is needed on success")]
235
285
  result_type: str
236
286
  results_entry: str
237
287
  metadata_mapping: Dict[str, Union[str, List[str]]]
238
288
 
289
+ class OrderStatus(TypedDict):
290
+ """Configuration for order status during download"""
291
+
292
+ request: PluginConfig.OrderStatusRequest
293
+ metadata_mapping: Annotated[
294
+ Dict[str, Union[str, List[str]]],
295
+ Doc("Metadata-mapping used to parse order status response"),
296
+ ]
297
+ success: PluginConfig.OrderStatusSuccess
298
+ error: Annotated[
299
+ Dict[str, Any],
300
+ Doc("Part of the order status response that tells there is an error"),
301
+ ]
302
+ ordered: PluginConfig.OrderStatusOrdered
303
+ on_success: PluginConfig.OrderStatusOnSuccess
304
+
239
305
  name: str
240
306
  type: str
241
307
 
@@ -251,12 +317,15 @@ class PluginConfig(yaml.YAMLObject):
251
317
  result_type: str
252
318
  results_entry: str
253
319
  pagination: PluginConfig.Pagination
320
+ sort: PluginConfig.Sort
254
321
  query_params_key: str
255
- discover_metadata: Dict[str, str]
322
+ discover_metadata: Dict[str, Union[str, bool]]
256
323
  discover_product_types: Dict[str, Any]
257
324
  discover_queryables: Dict[str, Any]
258
325
  metadata_mapping: Dict[str, Union[str, List[str]]]
259
326
  free_params: Dict[Any, Any]
327
+ constraints_file_url: str
328
+ remove_from_queryables: List[str]
260
329
  free_text_search_operations: Dict[str, Any] # ODataV4Search
261
330
  metadata_pre_mapping: Dict[str, Any] # ODataV4Search
262
331
  data_request_url: str # DataRequestSearch
@@ -268,16 +337,30 @@ class PluginConfig(yaml.YAMLObject):
268
337
  max_connections: int # StaticStacSearch
269
338
  timeout: float # StaticStacSearch
270
339
  s3_bucket: str # CreodiasS3Search
340
+ end_date_excluded: bool # BuildSearchResult
341
+ remove_from_query: List[str] # BuildSearchResult
342
+ ssl_verify: bool
271
343
 
272
344
  # download -------------------------------------------------------------------------
273
345
  base_uri: str
274
346
  outputs_prefix: str
275
347
  extract: bool
348
+ outputs_extension: str
276
349
  order_enabled: bool # HTTPDownload
277
350
  order_method: str # HTTPDownload
278
351
  order_headers: Dict[str, str] # HTTPDownload
279
- order_status_on_success: PluginConfig.OrderStatusOnSuccess
352
+
353
+ order_on_response: PluginConfig.OrderOnResponse
354
+ order_status: PluginConfig.OrderStatus
355
+ no_auth_download: Annotated[
356
+ bool,
357
+ Doc(
358
+ "Do not authenticate the download request but only the order and order status ones."
359
+ ),
360
+ ]
280
361
  bucket_path_level: int # S3RestDownload
362
+ requester_pays: bool # AwsDownload
363
+ flatten_top_dirs: bool
281
364
 
282
365
  # auth -----------------------------------------------------------------------------
283
366
  credentials: Dict[str, str]
@@ -299,7 +382,13 @@ class PluginConfig(yaml.YAMLObject):
299
382
  token_exchange_post_data_method: str # OIDCAuthorizationCodeFlowAuth
300
383
  token_uri: str # OIDCAuthorizationCodeFlowAuth
301
384
  token_key: str # OIDCAuthorizationCodeFlowAuth
385
+ req_data: Dict[str, Any] # TokenAuth
302
386
  signed_url_key: str # SASAuth
387
+ refresh_uri: str # TokenAuth
388
+ refresh_token_key: str # TokenAuth
389
+ subject: Dict[str, Any] # TokenExchangeAuth
390
+ subject_issuer: str # TokenExchangeAuth
391
+ audience: str # TokenExchangeAuth
303
392
 
304
393
  yaml_loader = yaml.Loader
305
394
  yaml_dumper = yaml.SafeDumper
@@ -362,7 +451,7 @@ def load_config(config_path: str) -> Dict[str, ProviderConfig]:
362
451
  :returns: The default provider's configuration
363
452
  :rtype: dict
364
453
  """
365
- logger.debug(f"Loading configuration from {config_path}")
454
+ logger.debug("Loading configuration from %s", config_path)
366
455
  config: Dict[str, ProviderConfig] = {}
367
456
  try:
368
457
  # Providers configs are stored in this file as separated yaml documents
@@ -17,35 +17,16 @@
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`
@@ -63,150 +44,12 @@ class Api(PluginTopic):
63
44
  (e.g. 'file:///tmp/product_folder' on Linux or
64
45
  'file:///C:/Users/username/AppData/LOcal/Temp' on Windows)
65
46
  - 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.
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,9 +122,7 @@ 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
@@ -156,21 +156,23 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
156
156
  def download(
157
157
  self,
158
158
  product: EOProduct,
159
- auth: Optional[PluginConfig] = None,
159
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
160
160
  progress_callback: Optional[ProgressCallback] = None,
161
161
  wait: int = DEFAULT_DOWNLOAD_WAIT,
162
162
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
163
- **kwargs: Any,
163
+ **kwargs: Unpack[DownloadConf],
164
164
  ) -> Optional[str]:
165
165
  """Download data from ECMWF MARS"""
166
166
  product_format = product.properties.get("format", "grib")
167
167
  product_extension = ECMWF_MARS_KNOWN_FORMATS.get(product_format, product_format)
168
+ kwargs["outputs_extension"] = kwargs.get(
169
+ "outputs_extension", f".{product_extension}"
170
+ )
168
171
 
169
172
  # Prepare download
170
173
  fs_path, record_filename = self._prepare_download(
171
174
  product,
172
175
  progress_callback=progress_callback,
173
- outputs_extension=f".{product_extension}",
174
176
  **kwargs,
175
177
  )
176
178
 
@@ -179,6 +181,13 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
179
181
  product.location = path_to_uri(fs_path)
180
182
  return fs_path
181
183
 
184
+ new_fs_path = os.path.join(
185
+ os.path.dirname(fs_path), sanitize(product.properties["title"])
186
+ )
187
+ if not os.path.isdir(new_fs_path):
188
+ os.makedirs(new_fs_path)
189
+ fs_path = os.path.join(new_fs_path, os.path.basename(fs_path))
190
+
182
191
  # get download request dict from product.location/downloadLink url query string
183
192
  # separate url & parameters
184
193
  download_request = geojson.loads(urlsplit(product.location).query)
@@ -222,13 +231,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
222
231
  fh.write(product.properties["downloadLink"])
223
232
  logger.debug("Download recorded in %s", record_filename)
224
233
 
225
- # do not try to extract or delete grib/netcdf
234
+ # do not try to extract a directory
226
235
  kwargs["extract"] = False
227
236
 
228
237
  product_path = self._finalize(
229
- fs_path,
238
+ new_fs_path,
230
239
  progress_callback=progress_callback,
231
- outputs_extension=f".{product_extension}",
232
240
  **kwargs,
233
241
  )
234
242
  product.location = path_to_uri(product_path)
@@ -237,12 +245,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
237
245
  def download_all(
238
246
  self,
239
247
  products: SearchResult,
240
- auth: Optional[PluginConfig] = None,
248
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
241
249
  downloaded_callback: Optional[DownloadedCallback] = None,
242
250
  progress_callback: Optional[ProgressCallback] = None,
243
251
  wait: int = DEFAULT_DOWNLOAD_WAIT,
244
252
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
245
- **kwargs: Any,
253
+ **kwargs: Unpack[DownloadConf],
246
254
  ) -> List[str]:
247
255
  """
248
256
  Download all using parent (base plugin) method
@@ -256,3 +264,7 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
256
264
  timeout=timeout,
257
265
  **kwargs,
258
266
  )
267
+
268
+ def clear(self) -> None:
269
+ """Clear search context"""
270
+ pass