eodag 3.1.0b1__py3-none-any.whl → 3.2.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 (85) hide show
  1. eodag/api/core.py +69 -63
  2. eodag/api/product/_assets.py +49 -13
  3. eodag/api/product/_product.py +41 -30
  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 +85 -79
  10. eodag/api/search_result.py +13 -23
  11. eodag/cli.py +4 -4
  12. eodag/config.py +77 -80
  13. eodag/plugins/apis/base.py +1 -1
  14. eodag/plugins/apis/ecmwf.py +12 -15
  15. eodag/plugins/apis/usgs.py +12 -11
  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 +20 -14
  22. eodag/plugins/authentication/sas_auth.py +4 -4
  23. eodag/plugins/authentication/token.py +7 -7
  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 +4 -4
  32. eodag/plugins/download/aws.py +137 -77
  33. eodag/plugins/download/base.py +8 -17
  34. eodag/plugins/download/creodias_s3.py +2 -2
  35. eodag/plugins/download/http.py +30 -32
  36. eodag/plugins/download/s3rest.py +5 -4
  37. eodag/plugins/manager.py +10 -20
  38. eodag/plugins/search/__init__.py +6 -5
  39. eodag/plugins/search/base.py +38 -42
  40. eodag/plugins/search/build_search_result.py +286 -336
  41. eodag/plugins/search/cop_marine.py +22 -12
  42. eodag/plugins/search/creodias_s3.py +8 -78
  43. eodag/plugins/search/csw.py +11 -11
  44. eodag/plugins/search/data_request_search.py +19 -18
  45. eodag/plugins/search/qssearch.py +84 -151
  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 +848 -398
  50. eodag/resources/providers.yml +1038 -1115
  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 +24 -24
  56. eodag/rest/errors.py +5 -5
  57. eodag/rest/server.py +3 -11
  58. eodag/rest/stac.py +41 -38
  59. eodag/rest/types/collections_search.py +3 -3
  60. eodag/rest/types/eodag_search.py +23 -23
  61. eodag/rest/types/queryables.py +40 -28
  62. eodag/rest/types/stac_search.py +15 -25
  63. eodag/rest/utils/__init__.py +11 -21
  64. eodag/rest/utils/cql_evaluate.py +6 -6
  65. eodag/rest/utils/rfc3339.py +2 -2
  66. eodag/types/__init__.py +97 -29
  67. eodag/types/bbox.py +2 -2
  68. eodag/types/download_args.py +2 -2
  69. eodag/types/queryables.py +5 -2
  70. eodag/types/search_args.py +4 -4
  71. eodag/types/whoosh.py +1 -3
  72. eodag/utils/__init__.py +82 -41
  73. eodag/utils/exceptions.py +2 -2
  74. eodag/utils/import_system.py +2 -2
  75. eodag/utils/requests.py +2 -2
  76. eodag/utils/rest.py +2 -2
  77. eodag/utils/s3.py +231 -0
  78. eodag/utils/stac_reader.py +10 -10
  79. {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info}/METADATA +12 -10
  80. eodag-3.2.0.dist-info/RECORD +113 -0
  81. {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info}/WHEEL +1 -1
  82. {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info}/entry_points.txt +1 -0
  83. eodag-3.1.0b1.dist-info/RECORD +0 -108
  84. {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info/licenses}/LICENSE +0 -0
  85. {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info}/top_level.txt +0 -0
@@ -23,25 +23,14 @@ import logging
23
23
  import re
24
24
  from collections import OrderedDict
25
25
  from datetime import datetime, timedelta
26
- from typing import (
27
- TYPE_CHECKING,
28
- Annotated,
29
- Any,
30
- Dict,
31
- List,
32
- Optional,
33
- Set,
34
- Tuple,
35
- Union,
36
- cast,
37
- )
26
+ from typing import TYPE_CHECKING, Annotated, Any, Optional, Union
38
27
  from urllib.parse import quote_plus, unquote_plus
39
28
 
40
29
  import geojson
41
30
  import orjson
42
31
  from dateutil.parser import isoparse
43
32
  from dateutil.tz import tzutc
44
- from jsonpath_ng import Child, Fields, Root
33
+ from dateutil.utils import today
45
34
  from pydantic import Field
46
35
  from pydantic.fields import FieldInfo
47
36
  from requests.auth import AuthBase
@@ -51,19 +40,18 @@ from typing_extensions import get_args
51
40
  from eodag.api.product import EOProduct
52
41
  from eodag.api.product.metadata_mapping import (
53
42
  NOT_AVAILABLE,
54
- NOT_MAPPED,
43
+ OFFLINE_STATUS,
55
44
  format_metadata,
56
- format_query_params,
57
- mtd_cfg_as_conversion_and_querypath,
58
45
  properties_from_json,
59
46
  )
60
47
  from eodag.api.search_result import RawSearchResult
61
48
  from eodag.plugins.search import PreparedSearch
62
49
  from eodag.plugins.search.qssearch import PostJsonSearch, QueryStringSearch
63
50
  from eodag.types import json_field_definition_to_python
64
- from eodag.types.queryables import Queryables
51
+ from eodag.types.queryables import Queryables, QueryablesDict
65
52
  from eodag.utils import (
66
- HTTP_REQ_TIMEOUT,
53
+ DEFAULT_MISSION_START_DATE,
54
+ DEFAULT_SEARCH_TIMEOUT,
67
55
  deepcopy,
68
56
  dict_items_recursive_sort,
69
57
  get_geometry_from_various,
@@ -77,9 +65,11 @@ if TYPE_CHECKING:
77
65
 
78
66
  logger = logging.getLogger("eodag.search.build_search_result")
79
67
 
68
+ ECMWF_PREFIX = "ecmwf:"
69
+
80
70
  # keywords from ECMWF keyword database + "dataset" (not part of database but exists)
81
71
  # database: https://confluence.ecmwf.int/display/UDOC/Keywords+in+MARS+and+Dissemination+requests
82
- ECMWF_KEYWORDS = [
72
+ ECMWF_KEYWORDS = {
83
73
  "dataset",
84
74
  "accuracy",
85
75
  "activity",
@@ -143,10 +133,10 @@ ECMWF_KEYWORDS = [
143
133
  "truncation",
144
134
  "type",
145
135
  "use",
146
- ]
136
+ }
147
137
 
148
138
  # additional keywords from copernicus services
149
- COP_DS_KEYWORDS = [
139
+ COP_DS_KEYWORDS = {
150
140
  "aerosol_type",
151
141
  "altitude",
152
142
  "product_type",
@@ -201,57 +191,32 @@ COP_DS_KEYWORDS = [
201
191
  "variable_type",
202
192
  "version",
203
193
  "year",
204
- ]
194
+ }
205
195
 
196
+ ALLOWED_KEYWORDS = ECMWF_KEYWORDS | COP_DS_KEYWORDS
206
197
 
207
- def keywords_to_mdt(
208
- keywords: List[str], prefix: Optional[str] = None
209
- ) -> Dict[str, Any]:
210
- """
211
- Make metadata mapping dict from a list of keywords
198
+ END = "completionTimeFromAscendingNode"
212
199
 
213
- prefix:keyword:
214
- - keyword
215
- - $."prefix:keyword"
200
+ START = "startTimeFromAscendingNode"
216
201
 
217
- >>> keywords_to_mdt(["month", "year"])
218
- {'month': ['month', '$."month"'], 'year': ['year', '$."year"']}
219
- >>> keywords_to_mdt(["month", "year"], "ecmwf")
220
- {'ecmwf:month': ['month', '$."ecmwf:month"'], 'ecmwf:year': ['year', '$."ecmwf:year"']}
221
202
 
222
- :param keywords: List of keywords to be converted
223
- :param prefix: prefix to be added to the parameter in the mapping
224
- :return: metadata mapping dict
203
+ def ecmwf_mtd() -> dict[str, Any]:
225
204
  """
226
- mdt: Dict[str, Any] = {}
227
- for keyword in keywords:
228
- key = f"{prefix}:{keyword}" if prefix else keyword
229
- mdt[key] = [keyword, f'$."{key}"']
230
- return mdt
231
-
205
+ Make metadata mapping dict from a list of defined ECMWF Keywords
232
206
 
233
- def strip_quotes(value: Any) -> Any:
234
- """Strip superfluous quotes from elements (added by mapping converter to_geojson).
207
+ We automatically add the #to_geojson convert to prevent modification of entries by eval() in the metadata mapping.
235
208
 
236
- >>> strip_quotes("'abc'")
237
- 'abc'
238
- >>> strip_quotes(["'abc'", '"def'])
239
- ['abc', 'def']
209
+ keyword:
210
+ - keyword
211
+ - $."keyword"#to_geojson
240
212
 
241
- :param value: value from which quotes should be removed (should be either str or list)
242
- :return: value without quotes
243
- :raises: NotImplementedError
213
+ :return: metadata mapping dict
244
214
  """
245
- if isinstance(value, (list, tuple)):
246
- return [strip_quotes(v) for v in value]
247
- elif isinstance(value, dict):
248
- raise NotImplementedError("Dict value is not supported.")
249
- else:
250
- return str(value).strip("'\"")
215
+ return {k: [k, f'{{$."{k}"#to_geojson}}'] for k in ALLOWED_KEYWORDS}
251
216
 
252
217
 
253
218
  def _update_properties_from_element(
254
- prop: Dict[str, Any], element: Dict[str, Any], values: List[str]
219
+ prop: dict[str, Any], element: dict[str, Any], values: list[str]
255
220
  ) -> None:
256
221
  """updates a property dict with the given values based on the information from the element dict
257
222
  e.g. the type is set based on the type of the element
@@ -318,7 +283,7 @@ def _update_properties_from_element(
318
283
 
319
284
  def ecmwf_format(v: str) -> str:
320
285
  """Add ECMWF prefix to value v if v is a ECMWF keyword."""
321
- return "ecmwf:" + v if v in ECMWF_KEYWORDS + COP_DS_KEYWORDS else v
286
+ return ECMWF_PREFIX + v if v in ALLOWED_KEYWORDS else v
322
287
 
323
288
 
324
289
  class ECMWFSearch(PostJsonSearch):
@@ -333,7 +298,7 @@ class ECMWFSearch(PostJsonSearch):
333
298
  :param provider: An eodag providers configuration dictionary
334
299
  :param config: Search plugin configuration:
335
300
 
336
- * :attr:`~eodag.config.PluginConfig.remove_from_query` (``List[str]``): List of parameters
301
+ * :attr:`~eodag.config.PluginConfig.remove_from_query` (``list[str]``): List of parameters
337
302
  used to parse metadata but that must not be included to the query
338
303
  * :attr:`~eodag.config.PluginConfig.end_date_excluded` (``bool``): Set to `False` if
339
304
  provider does not include end date to search
@@ -350,59 +315,27 @@ class ECMWFSearch(PostJsonSearch):
350
315
  """
351
316
 
352
317
  def __init__(self, provider: str, config: PluginConfig) -> None:
353
- # cache fetching method
354
- self.fetch_data = functools.lru_cache()(self._fetch_data)
355
-
356
318
  config.metadata_mapping = {
357
- **keywords_to_mdt(ECMWF_KEYWORDS + COP_DS_KEYWORDS, "ecmwf"),
319
+ **ecmwf_mtd(),
320
+ **{
321
+ "id": "$.id",
322
+ "title": "$.id",
323
+ "storageStatus": OFFLINE_STATUS,
324
+ "downloadLink": "$.null",
325
+ "geometry": ["feature", "$.geometry"],
326
+ "defaultGeometry": "POLYGON((180 -90, 180 90, -180 90, -180 -90, 180 -90))",
327
+ },
358
328
  **config.metadata_mapping,
359
329
  }
360
330
 
361
331
  super().__init__(provider, config)
362
332
 
333
+ # ECMWF providers do not feature any api_endpoint or next_page_query_obj.
334
+ # Searched is faked by EODAG.
363
335
  self.config.__dict__.setdefault("api_endpoint", "")
364
-
365
- # needed by QueryStringSearch.build_query_string / format_free_text_search
366
- self.config.__dict__.setdefault("free_text_search_operations", {})
367
- # needed for compatibility
368
336
  self.config.pagination.setdefault("next_page_query_obj", "{{}}")
369
337
 
370
- # parse jsonpath on init: product type specific metadata-mapping
371
- for product_type in self.config.products.keys():
372
- if "metadata_mapping" in self.config.products[product_type].keys():
373
- self.config.products[product_type][
374
- "metadata_mapping"
375
- ] = mtd_cfg_as_conversion_and_querypath(
376
- self.config.products[product_type]["metadata_mapping"]
377
- )
378
- # Complete and ready to use product type specific metadata-mapping
379
- product_type_metadata_mapping = deepcopy(self.config.metadata_mapping)
380
-
381
- # update config using provider product type definition metadata_mapping
382
- # from another product
383
- other_product_for_mapping = cast(
384
- str,
385
- self.config.products[product_type].get(
386
- "metadata_mapping_from_product", ""
387
- ),
388
- )
389
- if other_product_for_mapping:
390
- other_product_type_def_params = self.get_product_type_def_params(
391
- other_product_for_mapping,
392
- )
393
- product_type_metadata_mapping.update(
394
- other_product_type_def_params.get("metadata_mapping", {})
395
- )
396
- # from current product
397
- product_type_metadata_mapping.update(
398
- self.config.products[product_type]["metadata_mapping"]
399
- )
400
-
401
- self.config.products[product_type][
402
- "metadata_mapping"
403
- ] = product_type_metadata_mapping
404
-
405
- def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
338
+ def do_search(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
406
339
  """Should perform the actual search request.
407
340
 
408
341
  :param args: arguments to be used in the search
@@ -416,7 +349,7 @@ class ECMWFSearch(PostJsonSearch):
416
349
  self,
417
350
  prep: PreparedSearch = PreparedSearch(),
418
351
  **kwargs: Any,
419
- ) -> Tuple[List[EOProduct], Optional[int]]:
352
+ ) -> tuple[list[EOProduct], Optional[int]]:
420
353
  """Build ready-to-download SearchResult
421
354
 
422
355
  :param prep: :class:`~eodag.plugins.search.PreparedSearch` object containing information needed for the search
@@ -426,7 +359,7 @@ class ECMWFSearch(PostJsonSearch):
426
359
  product_type = prep.product_type
427
360
  if not product_type:
428
361
  product_type = kwargs.get("productType", None)
429
- self._preprocess_search_params(kwargs, product_type)
362
+ kwargs = self._preprocess_search_params(kwargs, product_type)
430
363
  result, num_items = super().query(prep, **kwargs)
431
364
  if prep.count and not num_items:
432
365
  num_items = 1
@@ -438,34 +371,31 @@ class ECMWFSearch(PostJsonSearch):
438
371
  super().clear()
439
372
 
440
373
  def build_query_string(
441
- self, product_type: str, **kwargs: Any
442
- ) -> Tuple[Dict[str, Any], str]:
374
+ self, product_type: str, query_dict: dict[str, Any]
375
+ ) -> tuple[dict[str, Any], str]:
443
376
  """Build The query string using the search parameters
444
377
 
445
378
  :param product_type: product type id
446
- :param kwargs: keyword arguments to be used in the query string
379
+ :param query_dict: keyword arguments to be used in the query string
447
380
  :return: formatted query params and encode query string
448
381
  """
449
- # parse kwargs as properties as they might be needed to build the query
450
- parsed_properties = properties_from_json(
451
- kwargs,
452
- self.config.metadata_mapping,
453
- )
454
- available_properties = {
455
- # We strip values of superfluous quotes (added by mapping converter to_geojson).
456
- k: strip_quotes(v)
457
- for k, v in parsed_properties.items()
458
- if v not in [NOT_AVAILABLE, NOT_MAPPED]
459
- }
382
+ query_dict["_date"] = f"{query_dict.get(START)}/{query_dict.get(END)}"
383
+
384
+ # Reorder kwargs to make sure year/month/day/time if set overwrite default datetime.
385
+ priority_keys = [
386
+ START,
387
+ END,
388
+ ]
389
+ ordered_kwargs = {k: query_dict[k] for k in priority_keys if k in query_dict}
390
+ ordered_kwargs.update(query_dict)
460
391
 
461
- # build and return the query
462
392
  return super().build_query_string(
463
- product_type=product_type, **available_properties
393
+ product_type=product_type, query_dict=ordered_kwargs
464
394
  )
465
395
 
466
396
  def _preprocess_search_params(
467
- self, params: Dict[str, Any], product_type: Optional[str]
468
- ) -> None:
397
+ self, params: dict[str, Any], product_type: Optional[str]
398
+ ) -> dict[str, Any]:
469
399
  """Preprocess search parameters before making a request to the CDS API.
470
400
 
471
401
  This method is responsible for checking and updating the provided search parameters
@@ -481,28 +411,20 @@ class ECMWFSearch(PostJsonSearch):
481
411
  # if available, update search params using datacube query-string
482
412
  _dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
483
413
  if "/to/" in _dc_qp.get("date", ""):
484
- (
485
- params["startTimeFromAscendingNode"],
486
- params["completionTimeFromAscendingNode"],
487
- ) = _dc_qp["date"].split("/to/")
414
+ params[START], params[END] = _dc_qp["date"].split("/to/")
488
415
  elif "/" in _dc_qp.get("date", ""):
489
- (
490
- params["startTimeFromAscendingNode"],
491
- params["completionTimeFromAscendingNode"],
492
- ) = _dc_qp["date"].split("/")
416
+ (params[START], params[END],) = _dc_qp[
417
+ "date"
418
+ ].split("/")
493
419
  elif _dc_qp.get("date", None):
494
- params["startTimeFromAscendingNode"] = params[
495
- "completionTimeFromAscendingNode"
496
- ] = _dc_qp["date"]
420
+ params[START] = params[END] = _dc_qp["date"]
497
421
 
498
422
  if "/" in _dc_qp.get("area", ""):
499
423
  params["geometry"] = _dc_qp["area"].split("/")
500
424
 
501
- non_none_params = {k: v for k, v in params.items() if v}
502
-
503
- # productType
504
- dataset = params.get("ecmwf:dataset", None)
505
- params["productType"] = non_none_params.get("productType", dataset)
425
+ params = {
426
+ k.removeprefix(ECMWF_PREFIX): v for k, v in params.items() if v is not None
427
+ }
506
428
 
507
429
  # dates
508
430
  # check if default dates have to be added
@@ -510,25 +432,23 @@ class ECMWFSearch(PostJsonSearch):
510
432
  self._check_date_params(params, product_type)
511
433
 
512
434
  # adapt end date if it is midnight
513
- if "completionTimeFromAscendingNode" in params:
435
+ if END in params:
514
436
  end_date_excluded = getattr(self.config, "end_date_excluded", True)
515
437
  is_datetime = True
516
438
  try:
517
- end_date = datetime.strptime(
518
- params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%SZ"
519
- )
439
+ end_date = datetime.strptime(params[END], "%Y-%m-%dT%H:%M:%SZ")
520
440
  end_date = end_date.replace(tzinfo=tzutc())
521
441
  except ValueError:
522
442
  try:
523
443
  end_date = datetime.strptime(
524
- params["completionTimeFromAscendingNode"],
444
+ params[END],
525
445
  "%Y-%m-%dT%H:%M:%S.%fZ",
526
446
  )
527
447
  end_date = end_date.replace(tzinfo=tzutc())
528
448
  except ValueError:
529
- end_date = isoparse(params["completionTimeFromAscendingNode"])
449
+ end_date = isoparse(params[END])
530
450
  is_datetime = False
531
- start_date = isoparse(params["startTimeFromAscendingNode"])
451
+ start_date = isoparse(params[START])
532
452
  if (
533
453
  not end_date_excluded
534
454
  and is_datetime
@@ -537,15 +457,90 @@ class ECMWFSearch(PostJsonSearch):
537
457
  == end_date.replace(hour=0, minute=0, second=0, microsecond=0)
538
458
  ):
539
459
  end_date += timedelta(days=-1)
540
- params["completionTimeFromAscendingNode"] = end_date.isoformat()
460
+ params[END] = end_date.isoformat()
541
461
 
542
462
  # geometry
543
463
  if "geometry" in params:
544
464
  params["geometry"] = get_geometry_from_various(geometry=params["geometry"])
545
465
 
466
+ return params
467
+
468
+ def _check_date_params(
469
+ self, keywords: dict[str, Any], product_type: Optional[str]
470
+ ) -> None:
471
+ """checks if start and end date are present in the keywords and adds them if not"""
472
+
473
+ if START and END in keywords:
474
+ return
475
+
476
+ product_type_conf = getattr(self.config, "metadata_mapping", {})
477
+ if (
478
+ product_type
479
+ and product_type in self.config.products
480
+ and "metadata_mapping" in self.config.products[product_type]
481
+ ):
482
+ product_type_conf = self.config.products[product_type]["metadata_mapping"]
483
+
484
+ # start time given, end time missing
485
+ if START in keywords:
486
+ keywords[END] = (
487
+ keywords[START]
488
+ if END in product_type_conf
489
+ else self.get_product_type_cfg_value(
490
+ "missionEndDate", today().isoformat()
491
+ )
492
+ )
493
+ return
494
+
495
+ if END in product_type_conf:
496
+ mapping = product_type_conf[START]
497
+ if not isinstance(mapping, list):
498
+ mapping = product_type_conf[END]
499
+ if isinstance(mapping, list):
500
+ # get time parameters (date, year, month, ...) from metadata mapping
501
+ input_mapping = mapping[0].replace("{{", "").replace("}}", "")
502
+ time_params = [
503
+ values.split(":")[0].strip() for values in input_mapping.split(",")
504
+ ]
505
+ time_params = [
506
+ tp.replace('"', "").replace("'", "") for tp in time_params
507
+ ]
508
+ # if startTime is not given but other time params (e.g. year/month/(day)) are given,
509
+ # no default date is required
510
+ in_keywords = True
511
+ for tp in time_params:
512
+ if tp not in keywords:
513
+ in_keywords = False
514
+ break
515
+ if not in_keywords:
516
+ keywords[START] = self.get_product_type_cfg_value(
517
+ "missionStartDate", DEFAULT_MISSION_START_DATE
518
+ )
519
+ keywords[END] = (
520
+ keywords[START]
521
+ if END in product_type_conf
522
+ else self.get_product_type_cfg_value(
523
+ "missionEndDate", today().isoformat()
524
+ )
525
+ )
526
+
527
+ def _get_product_type_queryables(
528
+ self, product_type: Optional[str], alias: Optional[str], filters: dict[str, Any]
529
+ ) -> QueryablesDict:
530
+ """Override to set additional_properties to false."""
531
+ default_values: dict[str, Any] = deepcopy(
532
+ getattr(self.config, "products", {}).get(product_type, {})
533
+ )
534
+ default_values.pop("metadata_mapping", None)
535
+
536
+ filters["productType"] = product_type
537
+ queryables = self.discover_queryables(**{**default_values, **filters}) or {}
538
+
539
+ return QueryablesDict(additional_properties=False, **queryables)
540
+
546
541
  def discover_queryables(
547
542
  self, **kwargs: Any
548
- ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
543
+ ) -> Optional[dict[str, Annotated[Any, FieldInfo]]]:
549
544
  """Fetch queryables list from provider using its constraints file
550
545
 
551
546
  :param kwargs: additional filters for queryables (`productType` and other search
@@ -553,65 +548,64 @@ class ECMWFSearch(PostJsonSearch):
553
548
  :returns: fetched queryable parameters dict
554
549
  """
555
550
  product_type = kwargs.pop("productType")
556
- product_type_config = self.config.products.get(product_type, {})
557
- provider_product_type = (
558
- product_type_config.get("ecmwf:dataset", None)
559
- or product_type_config["productType"]
560
- )
561
- if "start" in kwargs:
562
- kwargs["startTimeFromAscendingNode"] = kwargs.pop("start")
563
- if "end" in kwargs:
564
- kwargs["completionTimeFromAscendingNode"] = kwargs.pop("end")
551
+
552
+ pt_config = self.get_product_type_def_params(product_type)
553
+
554
+ default_values = deepcopy(pt_config)
555
+ default_values.pop("metadata_mapping", None)
556
+ filters = {**default_values, **kwargs}
557
+
558
+ if "start" in filters:
559
+ filters[START] = filters.pop("start")
560
+ if "end" in filters:
561
+ filters[END] = filters.pop("end")
565
562
 
566
563
  # extract default datetime
567
- processed_kwargs = deepcopy(kwargs)
568
- self._preprocess_search_params(processed_kwargs, product_type)
564
+ processed_filters = self._preprocess_search_params(
565
+ deepcopy(filters), product_type
566
+ )
569
567
 
570
568
  constraints_url = format_metadata(
571
569
  getattr(self.config, "discover_queryables", {}).get("constraints_url", ""),
572
- **kwargs,
570
+ **filters,
573
571
  )
574
- constraints: List[Dict[str, Any]] = self.fetch_data(constraints_url)
572
+ constraints: list[dict[str, Any]] = self._fetch_data(constraints_url)
575
573
 
576
574
  form_url = format_metadata(
577
575
  getattr(self.config, "discover_queryables", {}).get("form_url", ""),
578
- **kwargs,
576
+ **filters,
579
577
  )
580
- form = self.fetch_data(form_url)
578
+ form: list[dict[str, Any]] = self._fetch_data(form_url)
581
579
 
582
- formated_kwargs = self.format_as_provider_keyword(
583
- product_type, processed_kwargs
580
+ formated_filters = self.format_as_provider_keyword(
581
+ product_type, processed_filters
584
582
  )
585
583
  # we re-apply kwargs input to consider override of year, month, day and time.
586
- for key in kwargs:
587
- if key.startswith("ecmwf:"):
588
- formated_kwargs[key.replace("ecmwf:", "")] = kwargs[key]
589
- elif key in (
590
- "startTimeFromAscendingNode",
591
- "completionTimeFromAscendingNode",
584
+ for k, v in {**default_values, **kwargs}.items():
585
+ key = k.removeprefix(ECMWF_PREFIX)
586
+
587
+ if key not in ALLOWED_KEYWORDS | {
588
+ START,
589
+ END,
592
590
  "geom",
593
- ):
594
- formated_kwargs[key] = kwargs[key]
595
- else:
591
+ "geometry",
592
+ }:
596
593
  raise ValidationError(
597
594
  f"{key} is not a queryable parameter for {self.provider}"
598
595
  )
599
596
 
600
- # we use non empty kwargs as default to integrate user inputs
597
+ formated_filters[key] = v
598
+
599
+ # we use non empty filters as default to integrate user inputs
601
600
  # it is needed because pydantic json schema does not represent "value"
602
601
  # but only "default"
603
- non_empty_formated: Dict[str, Any] = {
602
+ non_empty_formated: dict[str, Any] = {
604
603
  k: v
605
- for k, v in formated_kwargs.items()
606
- if v and (not isinstance(v, list) or all(v))
607
- }
608
- non_empty_kwargs: Dict[str, Any] = {
609
- k: v
610
- for k, v in processed_kwargs.items()
604
+ for k, v in formated_filters.items()
611
605
  if v and (not isinstance(v, list) or all(v))
612
606
  }
613
607
 
614
- required_keywords: Set[str] = set()
608
+ required_keywords: set[str] = set()
615
609
 
616
610
  # calculate available values
617
611
  if constraints:
@@ -625,33 +619,36 @@ class ECMWFSearch(PostJsonSearch):
625
619
  # Pre-compute the required keywords (present in all constraint dicts)
626
620
  # when form, required keywords are extracted directly from form
627
621
  if not form:
628
- required_keywords = set(constraints[0].keys())
629
- for constraint in constraints[1:]:
630
- required_keywords.intersection_update(constraint.keys())
622
+ required_keywords = set.intersection(
623
+ *(map(lambda d: set(d.keys()), constraints))
624
+ )
625
+
631
626
  else:
632
627
  values_url = getattr(self.config, "available_values_url", "")
633
628
  if not values_url:
634
629
  return self.queryables_from_metadata_mapping(product_type)
635
630
  if "{" in values_url:
636
- values_url = values_url.format(productType=provider_product_type)
637
- data = self.fetch_data(values_url)
631
+ values_url = values_url.format(**filters)
632
+ data = self._fetch_data(values_url)
638
633
  available_values = data["constraints"]
639
634
  required_keywords = data.get("required", [])
640
635
 
641
636
  # To check if all keywords are queryable parameters, we check if they are in the
642
637
  # available values or the product type config (available values calculated from the
643
638
  # constraints might not include all queryables)
644
- for keyword in kwargs:
639
+ for keyword in filters:
645
640
  if (
646
641
  keyword
647
642
  not in available_values.keys()
648
- | product_type_config.keys()
643
+ | pt_config.keys()
649
644
  | {
650
- "startTimeFromAscendingNode",
651
- "completionTimeFromAscendingNode",
645
+ START,
646
+ END,
652
647
  "geom",
653
648
  }
654
- and keyword.replace("ecmwf:", "") not in available_values
649
+ and keyword not in [f["name"] for f in form]
650
+ and keyword.removeprefix(ECMWF_PREFIX)
651
+ not in set(list(available_values.keys()) + [f["name"] for f in form])
655
652
  ):
656
653
  raise ValidationError(f"{keyword} is not a queryable parameter")
657
654
 
@@ -664,24 +661,24 @@ class ECMWFSearch(PostJsonSearch):
664
661
  )
665
662
  else:
666
663
  queryables = self.queryables_by_values(
667
- available_values, list(required_keywords), non_empty_kwargs
664
+ available_values, list(required_keywords), non_empty_formated
668
665
  )
669
666
 
670
667
  # ecmwf:date is replaced by start and end.
671
668
  # start and end filters are supported whenever combinations of "year", "month", "day" filters exist
672
669
  if (
673
- queryables.pop("ecmwf:date", None)
674
- or "ecmwf:year" in queryables
675
- or "ecmwf:hyear" in queryables
670
+ queryables.pop(f"{ECMWF_PREFIX}date", None)
671
+ or f"{ECMWF_PREFIX}year" in queryables
672
+ or f"{ECMWF_PREFIX}hyear" in queryables
676
673
  ):
677
674
  queryables.update(
678
675
  {
679
676
  "start": Queryables.get_with_default(
680
- "start", non_empty_kwargs.get("startTimeFromAscendingNode")
677
+ "start", processed_filters.get(START)
681
678
  ),
682
679
  "end": Queryables.get_with_default(
683
680
  "end",
684
- non_empty_kwargs.get("completionTimeFromAscendingNode"),
681
+ processed_filters.get(END),
685
682
  ),
686
683
  }
687
684
  )
@@ -689,7 +686,7 @@ class ECMWFSearch(PostJsonSearch):
689
686
  # area is geom in EODAG.
690
687
  if queryables.pop("area", None):
691
688
  queryables["geom"] = Annotated[
692
- Union[str, Dict[str, float], BaseGeometry],
689
+ Union[str, dict[str, float], BaseGeometry],
693
690
  Field(
694
691
  None,
695
692
  description="Read EODAG documentation for all supported geometry format.",
@@ -700,10 +697,10 @@ class ECMWFSearch(PostJsonSearch):
700
697
 
701
698
  def available_values_from_constraints(
702
699
  self,
703
- constraints: list[Dict[str, Any]],
704
- input_keywords: Dict[str, Any],
705
- form_keywords: List[str],
706
- ) -> Dict[str, List[str]]:
700
+ constraints: list[dict[str, Any]],
701
+ input_keywords: dict[str, Any],
702
+ form_keywords: list[str],
703
+ ) -> dict[str, list[str]]:
707
704
  """
708
705
  Filter constraints using input_keywords. Return list of available queryables.
709
706
  All constraint entries must have the same parameters.
@@ -727,9 +724,9 @@ class ECMWFSearch(PostJsonSearch):
727
724
  )
728
725
 
729
726
  # filter constraint entries matching input keyword values
730
- filtered_constraints: List[Dict[str, Any]]
727
+ filtered_constraints: list[dict[str, Any]]
731
728
 
732
- parsed_keywords: List[str] = []
729
+ parsed_keywords: list[str] = []
733
730
  for keyword in ordered_keywords:
734
731
  values = input_keywords.get(keyword)
735
732
 
@@ -742,14 +739,15 @@ class ECMWFSearch(PostJsonSearch):
742
739
  raise ValidationError(
743
740
  f"Parameter value as object is not supported: {keyword}={values}"
744
741
  )
745
- filter_v = values if isinstance(values, (list, tuple)) else [values]
746
742
 
747
743
  # We convert every single value to a list of string
744
+ filter_v = values if isinstance(values, (list, tuple)) else [values]
745
+
748
746
  # We strip values of superfluous quotes (added by mapping converter to_geojson).
749
747
  # ECMWF accept values with /to/. We need to split it to an array
750
748
  # ECMWF accept values in format val1/val2. We need to split it to an array
751
749
  sep = re.compile(r"/to/|/")
752
- filter_v = [i for v in filter_v for i in sep.split(strip_quotes(v))]
750
+ filter_v = [i for v in filter_v for i in sep.split(str(v))]
753
751
 
754
752
  # special handling for time 0000 converted to 0 by pre-formating with metadata_mapping
755
753
  if keyword.split(":")[-1] == "time":
@@ -768,7 +766,10 @@ class ECMWFSearch(PostJsonSearch):
768
766
  # we assume that if the first value is an interval, all values are intervals
769
767
  present_values = []
770
768
  if keyword == "date" and "/" in entry[keyword][0]:
771
- if any(is_range_in_range(x, values[0]) for x in entry[keyword]):
769
+ input_range = values
770
+ if isinstance(values, list):
771
+ input_range = values[0]
772
+ if any(is_range_in_range(x, input_range) for x in entry[keyword]):
772
773
  present_values = filter_v
773
774
  else:
774
775
  present_values = [
@@ -788,12 +789,12 @@ class ECMWFSearch(PostJsonSearch):
788
789
  {value for c in constraints for value in c.get(keyword, [])}
789
790
  )
790
791
  # restore ecmwf: prefix before raising error
791
- keyword = f"ecmwf:{keyword}"
792
+ keyword = ECMWF_PREFIX + keyword
792
793
 
793
794
  all_keywords_str = ""
794
795
  if len(parsed_keywords) > 1:
795
796
  keywords = [
796
- f"ecmwf:{k}={pk}"
797
+ f"{ECMWF_PREFIX + k}={pk}"
797
798
  for k in parsed_keywords
798
799
  if (pk := input_keywords.get(k))
799
800
  ]
@@ -808,7 +809,7 @@ class ECMWFSearch(PostJsonSearch):
808
809
  parsed_keywords.append(keyword)
809
810
  constraints = filtered_constraints
810
811
 
811
- available_values: Dict[str, Any] = {k: set() for k in ordered_keywords}
812
+ available_values: dict[str, Any] = {k: set() for k in ordered_keywords}
812
813
 
813
814
  # we aggregate the constraint entries left
814
815
  for entry in constraints:
@@ -819,10 +820,10 @@ class ECMWFSearch(PostJsonSearch):
819
820
 
820
821
  def queryables_by_form(
821
822
  self,
822
- form: List[Dict[str, Any]],
823
- available_values: Dict[str, List[str]],
824
- defaults: Dict[str, Any],
825
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
823
+ form: list[dict[str, Any]],
824
+ available_values: dict[str, list[str]],
825
+ defaults: dict[str, Any],
826
+ ) -> dict[str, Annotated[Any, FieldInfo]]:
826
827
  """
827
828
  Generate Annotated field definitions from form entries and available values
828
829
  Used by Copernicus services like cop_cds, cop_ads, cop_ewds.
@@ -832,9 +833,9 @@ class ECMWFSearch(PostJsonSearch):
832
833
  :param defaults: default values for the parameters
833
834
  :return: dict of annotated queryables
834
835
  """
835
- queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
836
+ queryables: dict[str, Annotated[Any, FieldInfo]] = {}
836
837
 
837
- required_list: List[str] = []
838
+ required_list: list[str] = []
838
839
  for element in form:
839
840
  name: str = element["name"]
840
841
 
@@ -842,6 +843,8 @@ class ECMWFSearch(PostJsonSearch):
842
843
  if name in ("area_group", "global", "warning", "licences"):
843
844
  continue
844
845
  if "type" not in element or element["type"] == "FreeEditionWidget":
846
+ # FreeEditionWidget used to select the whole available region
847
+ # and to provide comments for the dataset
845
848
  continue
846
849
 
847
850
  # ordering done by id -> set id to high value if not present -> element will be last
@@ -869,18 +872,11 @@ class ECMWFSearch(PostJsonSearch):
869
872
  if fields and (comment := fields[0].get("comment")):
870
873
  prop["description"] = comment
871
874
 
872
- if d := details.get("default"):
873
- default = default or (d[0] if fields else d)
874
-
875
875
  if name == "area" and isinstance(default, dict):
876
876
  default = list(default.values())
877
877
 
878
- if default:
879
- # We strip values of superfluous quotes (addded by mapping converter to_geojson).
880
- default = strip_quotes(default)
881
-
882
878
  # sometimes form returns default as array instead of string
883
- if default and prop["type"] == "string" and isinstance(default, list):
879
+ if default and prop.get("type") == "string" and isinstance(default, list):
884
880
  default = ",".join(default)
885
881
 
886
882
  is_required = bool(element.get("required"))
@@ -901,10 +897,10 @@ class ECMWFSearch(PostJsonSearch):
901
897
 
902
898
  def queryables_by_values(
903
899
  self,
904
- available_values: Dict[str, List[str]],
905
- required_keywords: List[str],
906
- defaults: Dict[str, Any],
907
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
900
+ available_values: dict[str, list[str]],
901
+ required_keywords: list[str],
902
+ defaults: dict[str, Any],
903
+ ) -> dict[str, Annotated[Any, FieldInfo]]:
908
904
  """
909
905
  Generate Annotated field definitions from available values.
910
906
  Used by ECMWF data providers like dedt_lumi.
@@ -918,19 +914,17 @@ class ECMWFSearch(PostJsonSearch):
918
914
  # Needed to map constraints like "xxxx" to eodag parameter "ecmwf:xxxx"
919
915
  required = [ecmwf_format(k) for k in required_keywords]
920
916
 
921
- queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
917
+ queryables: dict[str, Annotated[Any, FieldInfo]] = {}
922
918
  for name, values in available_values.items():
923
919
  # Rename keywords from form with metadata mapping.
924
920
  # Needed to map constraints like "xxxx" to eodag parameter "ecmwf:xxxx"
925
921
  key = ecmwf_format(name)
926
922
 
927
- default = defaults.get(key)
928
-
929
923
  queryables[key] = Annotated[
930
924
  get_args(
931
925
  json_field_definition_to_python(
932
926
  {"type": "string", "title": name, "enum": values},
933
- default_value=strip_quotes(default) if default else None,
927
+ default_value=defaults.get(name),
934
928
  required=bool(key in required),
935
929
  )
936
930
  )
@@ -939,24 +933,34 @@ class ECMWFSearch(PostJsonSearch):
939
933
  return queryables
940
934
 
941
935
  def format_as_provider_keyword(
942
- self, product_type: str, properties: Dict[str, Any]
943
- ) -> Dict[str, Any]:
936
+ self, product_type: str, properties: dict[str, Any]
937
+ ) -> dict[str, Any]:
944
938
  """Return provider equivalent keyword names from EODAG keywords.
945
939
 
946
940
  :param product_type: product type id
947
941
  :param properties: dict of properties to be formatted
948
942
  :return: dict of formatted properties
949
943
  """
950
- parsed_properties = properties_from_json(
951
- properties,
952
- self.config.metadata_mapping,
944
+ properties["productType"] = product_type
945
+
946
+ # provider product type specific conf
947
+ product_type_def_params = self.get_product_type_def_params(
948
+ product_type, format_variables=properties
953
949
  )
954
- available_properties = {
955
- k: v
956
- for k, v in parsed_properties.items()
957
- if v not in [NOT_AVAILABLE, NOT_MAPPED]
958
- }
959
- return format_query_params(product_type, self.config, available_properties)
950
+
951
+ # Add to the query, the queryable parameters set in the provider product type definition
952
+ properties.update(
953
+ {
954
+ k: v
955
+ for k, v in product_type_def_params.items()
956
+ if k not in properties.keys()
957
+ and k in self.config.metadata_mapping.keys()
958
+ and isinstance(self.config.metadata_mapping[k], list)
959
+ }
960
+ )
961
+ qp, _ = self.build_query_string(product_type, properties)
962
+
963
+ return qp
960
964
 
961
965
  def _fetch_data(self, url: str) -> Any:
962
966
  """
@@ -973,12 +977,12 @@ class ECMWFSearch(PostJsonSearch):
973
977
  if hasattr(self, "auth") and isinstance(self.auth, AuthBase)
974
978
  else None
975
979
  )
976
- timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
977
- return fetch_json(url, auth=auth, timeout=timeout)
980
+ timeout = getattr(self.config, "timeout", DEFAULT_SEARCH_TIMEOUT)
981
+ return functools.lru_cache()(fetch_json)(url, auth=auth, timeout=timeout)
978
982
 
979
983
  def normalize_results(
980
984
  self, results: RawSearchResult, **kwargs: Any
981
- ) -> List[EOProduct]:
985
+ ) -> list[EOProduct]:
982
986
  """Build :class:`~eodag.api.product._product.EOProduct` from provider result
983
987
 
984
988
  :param results: Raw provider result as single dict in list
@@ -1044,67 +1048,37 @@ class ECMWFSearch(PostJsonSearch):
1044
1048
  discovery_config=getattr(self.config, "discover_metadata", {}),
1045
1049
  )
1046
1050
 
1047
- if not product_type:
1048
- product_type = parsed_properties.get("productType", None)
1051
+ properties = {
1052
+ # use product_type_config as default properties
1053
+ **getattr(self.config, "product_type_config", {}),
1054
+ **{ecmwf_format(k): v for k, v in parsed_properties.items()},
1055
+ }
1056
+
1057
+ def slugify(date_str: str) -> str:
1058
+ return date_str.split("T")[0].replace("-", "")
1049
1059
 
1050
1060
  # build product id
1051
- id_prefix = (product_type or self.provider).upper()
1052
- if (
1053
- "startTimeFromAscendingNode" in parsed_properties
1054
- and parsed_properties["startTimeFromAscendingNode"] != "Not Available"
1055
- and "completionTimeFromAscendingNode" in parsed_properties
1056
- and parsed_properties["completionTimeFromAscendingNode"] != "Not Available"
1057
- ):
1058
- product_id = "%s_%s_%s_%s" % (
1059
- id_prefix,
1060
- parsed_properties["startTimeFromAscendingNode"]
1061
- .split("T")[0]
1062
- .replace("-", ""),
1063
- parsed_properties["completionTimeFromAscendingNode"]
1064
- .split("T")[0]
1065
- .replace("-", ""),
1066
- query_hash,
1067
- )
1068
- elif (
1069
- "startTimeFromAscendingNode" in parsed_properties
1070
- and parsed_properties["startTimeFromAscendingNode"] != "Not Available"
1071
- ):
1072
- product_id = "%s_%s_%s" % (
1073
- id_prefix,
1074
- parsed_properties["startTimeFromAscendingNode"]
1075
- .split("T")[0]
1076
- .replace("-", ""),
1077
- query_hash,
1078
- )
1079
- else:
1080
- product_id = f"{id_prefix}_{query_hash}"
1081
-
1082
- parsed_properties["id"] = parsed_properties["title"] = product_id
1083
-
1084
- # update downloadLink and orderLink
1085
- parsed_properties["_dc_qs"] = quote_plus(qs)
1086
- if parsed_properties["downloadLink"] != "Not Available":
1087
- parsed_properties["downloadLink"] += f"?{qs}"
1088
-
1089
- # parse metadata needing downloadLink
1090
- dl_path = Fields("downloadLink")
1091
- dl_path_from_root = Child(Root(), dl_path)
1092
- for param, mapping in self.config.metadata_mapping.items():
1093
- if dl_path in mapping or dl_path_from_root in mapping:
1094
- parsed_properties.update(
1095
- properties_from_json(parsed_properties, {param: mapping})
1096
- )
1061
+ product_id = (product_type or kwargs.get("dataset") or self.provider).upper()
1097
1062
 
1098
- # use product_type_config as default properties
1099
- parsed_properties = dict(
1100
- getattr(self.config, "product_type_config", {}),
1101
- **parsed_properties,
1102
- )
1063
+ start = properties.get(START, NOT_AVAILABLE)
1064
+ end = properties.get(END, NOT_AVAILABLE)
1065
+
1066
+ if start != NOT_AVAILABLE:
1067
+ product_id += f"_{slugify(start)}"
1068
+ if end != NOT_AVAILABLE:
1069
+ product_id += f"_{slugify(end)}"
1070
+
1071
+ product_id += f"_{query_hash}"
1072
+
1073
+ properties["id"] = properties["title"] = product_id
1074
+
1075
+ # used by server mode to generate downloadlink href
1076
+ properties["_dc_qs"] = quote_plus(qs)
1103
1077
 
1104
1078
  product = EOProduct(
1105
1079
  provider=self.provider,
1106
1080
  productType=product_type,
1107
- properties=parsed_properties,
1081
+ properties=properties,
1108
1082
  )
1109
1083
 
1110
1084
  return [
@@ -1150,7 +1124,7 @@ class MeteoblueSearch(ECMWFSearch):
1150
1124
  self,
1151
1125
  prep: PreparedSearch = PreparedSearch(),
1152
1126
  **kwargs: Any,
1153
- ) -> Tuple[List[str], int]:
1127
+ ) -> tuple[list[str], int]:
1154
1128
  """Wraps PostJsonSearch.collect_search_urls to force product count to 1
1155
1129
 
1156
1130
  :param prep: :class:`~eodag.plugins.search.PreparedSearch` object containing information for the search
@@ -1162,7 +1136,7 @@ class MeteoblueSearch(ECMWFSearch):
1162
1136
 
1163
1137
  def do_search(
1164
1138
  self, prep: PreparedSearch = PreparedSearch(items_per_page=None), **kwargs: Any
1165
- ) -> List[Dict[str, Any]]:
1139
+ ) -> list[dict[str, Any]]:
1166
1140
  """Perform the actual search request, and return result in a single element.
1167
1141
 
1168
1142
  :param prep: :class:`~eodag.plugins.search.PreparedSearch` object containing information for the search
@@ -1181,17 +1155,15 @@ class MeteoblueSearch(ECMWFSearch):
1181
1155
  return [response.json()]
1182
1156
 
1183
1157
  def build_query_string(
1184
- self, product_type: str, **kwargs: Any
1185
- ) -> Tuple[Dict[str, Any], str]:
1158
+ self, product_type: str, query_dict: dict[str, Any]
1159
+ ) -> tuple[dict[str, Any], str]:
1186
1160
  """Build The query string using the search parameters
1187
1161
 
1188
1162
  :param product_type: product type id
1189
- :param kwargs: keyword arguments to be used in the query string
1163
+ :param query_dict: keyword arguments to be used in the query string
1190
1164
  :return: formatted query params and encode query string
1191
1165
  """
1192
- return QueryStringSearch.build_query_string(
1193
- self, product_type=product_type, **kwargs
1194
- )
1166
+ return QueryStringSearch.build_query_string(self, product_type, query_dict)
1195
1167
 
1196
1168
 
1197
1169
  class WekeoECMWFSearch(ECMWFSearch):
@@ -1221,7 +1193,7 @@ class WekeoECMWFSearch(ECMWFSearch):
1221
1193
 
1222
1194
  def normalize_results(
1223
1195
  self, results: RawSearchResult, **kwargs: Any
1224
- ) -> List[EOProduct]:
1196
+ ) -> list[EOProduct]:
1225
1197
  """Build :class:`~eodag.api.product._product.EOProduct` from provider result
1226
1198
 
1227
1199
  :param results: Raw provider result as single dict in list
@@ -1247,7 +1219,7 @@ class WekeoECMWFSearch(ECMWFSearch):
1247
1219
 
1248
1220
  return normalized
1249
1221
 
1250
- def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
1222
+ def do_search(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
1251
1223
  """Should perform the actual search request.
1252
1224
 
1253
1225
  :param args: arguments to be used in the search
@@ -1255,25 +1227,3 @@ class WekeoECMWFSearch(ECMWFSearch):
1255
1227
  :return: list containing the results from the provider in json format
1256
1228
  """
1257
1229
  return QueryStringSearch.do_search(self, *args, **kwargs)
1258
-
1259
- def build_query_string(
1260
- self, product_type: str, **kwargs: Any
1261
- ) -> Tuple[Dict[str, Any], str]:
1262
- """Build The query string using the search parameters
1263
-
1264
- :param product_type: product type id
1265
- :param kwargs: keyword arguments to be used in the query string
1266
- :return: formatted query params and encode query string
1267
- """
1268
- # Reorder kwargs to make sure year/month/day/time if set overwrite default datetime.
1269
- # strip_quotes to remove duplicated quotes like "'1_1'" produced by convertors like to_geojson.
1270
- priority_keys = [
1271
- "startTimeFromAscendingNode",
1272
- "completionTimeFromAscendingNode",
1273
- ]
1274
- ordered_kwargs = {k: kwargs[k] for k in priority_keys if k in kwargs}
1275
- ordered_kwargs.update({k: strip_quotes(v) for k, v in kwargs.items()})
1276
-
1277
- return QueryStringSearch.build_query_string(
1278
- self, product_type=product_type, **ordered_kwargs
1279
- )