eodag 2.12.0__py3-none-any.whl → 3.0.0b1__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 (77) hide show
  1. eodag/api/core.py +434 -319
  2. eodag/api/product/__init__.py +5 -1
  3. eodag/api/product/_assets.py +7 -2
  4. eodag/api/product/_product.py +46 -68
  5. eodag/api/product/metadata_mapping.py +181 -66
  6. eodag/api/search_result.py +21 -1
  7. eodag/cli.py +20 -6
  8. eodag/config.py +95 -6
  9. eodag/plugins/apis/base.py +8 -162
  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 +73 -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/requests.py +138 -0
  67. eodag/utils/rest.py +104 -0
  68. eodag/utils/stac_reader.py +100 -16
  69. {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/METADATA +64 -44
  70. eodag-3.0.0b1.dist-info/RECORD +109 -0
  71. {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/WHEEL +1 -1
  72. {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/entry_points.txt +6 -5
  73. eodag/plugins/apis/cds.py +0 -540
  74. eodag/rest/utils.py +0 -1133
  75. eodag-2.12.0.dist-info/RECORD +0 -94
  76. {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/LICENSE +0 -0
  77. {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/top_level.txt +0 -0
eodag/api/core.py CHANGED
@@ -23,18 +23,7 @@ import re
23
23
  import shutil
24
24
  import tempfile
25
25
  from operator import itemgetter
26
- from typing import (
27
- TYPE_CHECKING,
28
- AbstractSet,
29
- Any,
30
- Dict,
31
- Iterator,
32
- List,
33
- Optional,
34
- Set,
35
- Tuple,
36
- Union,
37
- )
26
+ from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Tuple, Union
38
27
 
39
28
  import geojson
40
29
  import pkg_resources
@@ -46,12 +35,10 @@ from whoosh.fields import Schema
46
35
  from whoosh.index import create_in, exists_in, open_dir
47
36
  from whoosh.qparser import QueryParser
48
37
 
49
- from eodag.api.product.metadata_mapping import (
50
- NOT_MAPPED,
51
- mtd_cfg_as_conversion_and_querypath,
52
- )
38
+ from eodag.api.product.metadata_mapping import mtd_cfg_as_conversion_and_querypath
53
39
  from eodag.api.search_result import SearchResult
54
40
  from eodag.config import (
41
+ PluginConfig,
55
42
  SimpleYamlProxyConfig,
56
43
  get_ext_product_types_conf,
57
44
  load_default_config,
@@ -63,9 +50,11 @@ from eodag.config import (
63
50
  provider_config_init,
64
51
  )
65
52
  from eodag.plugins.manager import PluginManager
53
+ from eodag.plugins.search import PreparedSearch
66
54
  from eodag.plugins.search.build_search_result import BuildPostSearchResult
67
55
  from eodag.types import model_fields_to_annotated
68
- from eodag.types.queryables import CommonQueryables, Queryables
56
+ from eodag.types.queryables import CommonQueryables
57
+ from eodag.types.whoosh import EODAGQueryParser
69
58
  from eodag.utils import (
70
59
  DEFAULT_DOWNLOAD_TIMEOUT,
71
60
  DEFAULT_DOWNLOAD_WAIT,
@@ -78,7 +67,6 @@ from eodag.utils import (
78
67
  _deprecated,
79
68
  copy_deepcopy,
80
69
  deepcopy,
81
- get_args,
82
70
  get_geometry_from_various,
83
71
  makedirs,
84
72
  obj_md5sum,
@@ -87,6 +75,7 @@ from eodag.utils import (
87
75
  )
88
76
  from eodag.utils.exceptions import (
89
77
  AuthenticationError,
78
+ EodagError,
90
79
  MisconfiguredError,
91
80
  NoMatchingProductType,
92
81
  PluginImplementationError,
@@ -94,6 +83,7 @@ from eodag.utils.exceptions import (
94
83
  UnsupportedProductType,
95
84
  UnsupportedProvider,
96
85
  )
86
+ from eodag.utils.rest import rfc3339_str_to_datetime
97
87
  from eodag.utils.stac_reader import fetch_stac_items
98
88
 
99
89
  if TYPE_CHECKING:
@@ -104,7 +94,9 @@ if TYPE_CHECKING:
104
94
  from eodag.plugins.apis.base import Api
105
95
  from eodag.plugins.crunch.base import Crunch
106
96
  from eodag.plugins.search.base import Search
107
- from eodag.utils import Annotated, DownloadedCallback, ProgressCallback
97
+ from eodag.types import ProviderSortables
98
+ from eodag.types.download_args import DownloadConf
99
+ from eodag.utils import Annotated, DownloadedCallback, ProgressCallback, Unpack
108
100
 
109
101
  logger = logging.getLogger("eodag.core")
110
102
 
@@ -247,7 +239,6 @@ class EODataAccessGateway:
247
239
  if "unsupported pickle protocol" in str(ve):
248
240
  logger.debug("Need to recreate whoosh .index: '%s'", ve)
249
241
  create_index = True
250
- shutil.rmtree(index_dir)
251
242
  # Unexpected error
252
243
  else:
253
244
  logger.error(
@@ -261,13 +252,14 @@ class EODataAccessGateway:
261
252
  if self._product_types_index is None:
262
253
  logger.debug("Opening product types index in %s", index_dir)
263
254
  self._product_types_index = open_dir(index_dir)
264
- try:
265
- self.guess_product_type(md5=self.product_types_config_md5)
266
- except NoMatchingProductType:
267
- create_index = True
268
- finally:
269
- if create_index:
270
- shutil.rmtree(index_dir)
255
+
256
+ with self._product_types_index.searcher() as searcher:
257
+ p = QueryParser("md5", self._product_types_index.schema, plugins=[])
258
+ query = p.parse(self.product_types_config_md5)
259
+ results = searcher.search(query, limit=1)
260
+
261
+ if not results:
262
+ create_index = True
271
263
  logger.debug(
272
264
  "Out-of-date product types index removed from %s", index_dir
273
265
  )
@@ -284,9 +276,8 @@ class EODataAccessGateway:
284
276
  )
285
277
 
286
278
  product_types_schema = Schema(
287
- ID=fields.STORED,
288
- alias=fields.ID,
289
- abstract=fields.STORED,
279
+ ID=fields.ID(stored=True),
280
+ abstract=fields.TEXT,
290
281
  instrument=fields.IDLIST,
291
282
  platform=fields.ID,
292
283
  platformSerialIdentifier=fields.IDLIST,
@@ -294,10 +285,11 @@ class EODataAccessGateway:
294
285
  sensorType=fields.ID,
295
286
  md5=fields.ID,
296
287
  license=fields.ID,
297
- title=fields.ID,
298
- missionStartDate=fields.ID,
299
- missionEndDate=fields.ID,
288
+ title=fields.TEXT,
289
+ missionStartDate=fields.STORED,
290
+ missionEndDate=fields.STORED,
300
291
  keywords=fields.KEYWORD(analyzer=kw_analyzer),
292
+ stacCollection=fields.STORED,
301
293
  )
302
294
  self._product_types_index = create_in(index_dir, product_types_schema)
303
295
  ix_writer = self._product_types_index.writer()
@@ -404,6 +396,7 @@ class EODataAccessGateway:
404
396
  stac_provider_config = load_stac_provider_config()
405
397
  for provider in conf_update.keys():
406
398
  provider_config_init(self.providers_config[provider], stac_provider_config)
399
+ setattr(self.providers_config[provider], "product_types_fetched", False)
407
400
  # re-create _plugins_manager using up-to-date providers_config
408
401
  self._plugins_manager.build_product_type_to_provider_config_map()
409
402
 
@@ -413,6 +406,20 @@ class EODataAccessGateway:
413
406
  for provider in list(self.providers_config.keys()):
414
407
  conf = self.providers_config[provider]
415
408
 
409
+ # remove providers using skipped plugins
410
+ if [
411
+ v
412
+ for v in conf.__dict__.values()
413
+ if isinstance(v, PluginConfig)
414
+ and getattr(v, "type", None) in self._plugins_manager.skipped_plugins
415
+ ]:
416
+ self.providers_config.pop(provider)
417
+ logger.debug(
418
+ f"{provider}: provider needing unavailable plugin has been removed"
419
+ )
420
+ continue
421
+
422
+ # check authentication
416
423
  if hasattr(conf, "api") and getattr(conf.api, "need_auth", False):
417
424
  credentials_exist = any(
418
425
  [
@@ -427,7 +434,7 @@ class EODataAccessGateway:
427
434
  )
428
435
  update_needed = True
429
436
  logger.info(
430
- "%s: provider needing auth for search has been pruned because no crendentials could be found",
437
+ "%s: provider needing auth for search has been pruned because no credentials could be found",
431
438
  provider,
432
439
  )
433
440
  elif hasattr(conf, "search") and getattr(conf.search, "need_auth", False):
@@ -455,7 +462,7 @@ class EODataAccessGateway:
455
462
  )
456
463
  update_needed = True
457
464
  logger.info(
458
- "%s: provider needing auth for search has been pruned because no crendentials could be found",
465
+ "%s: provider needing auth for search has been pruned because no credentials could be found",
459
466
  provider,
460
467
  )
461
468
  elif not hasattr(conf, "api") and not hasattr(conf, "search"):
@@ -501,10 +508,10 @@ class EODataAccessGateway:
501
508
  locations_config = load_yml_config(locations_conf_path)
502
509
 
503
510
  main_key = next(iter(locations_config))
504
- locations_config = locations_config[main_key]
511
+ main_locations_config = locations_config[main_key]
505
512
 
506
513
  logger.info("Locations configuration loaded from %s" % locations_conf_path)
507
- self.locations_config: List[Dict[str, Any]] = locations_config
514
+ self.locations_config: List[Dict[str, Any]] = main_locations_config
508
515
  else:
509
516
  logger.info(
510
517
  "Could not load locations configuration from %s" % locations_conf_path
@@ -531,35 +538,34 @@ class EODataAccessGateway:
531
538
  self.fetch_product_types_list(provider=provider)
532
539
 
533
540
  product_types: List[Dict[str, Any]] = []
534
- if provider is not None:
535
- if provider in self.providers_config:
536
- provider_supported_products = self.providers_config[provider].products
537
- for product_type_id in provider_supported_products:
538
- if product_type_id == GENERIC_PRODUCT_TYPE:
539
- continue
540
- config = self.product_types_config[product_type_id]
541
- if "alias" in config:
542
- config["_id"] = product_type_id
543
- product_type_id = config["alias"]
544
- product_type = dict(ID=product_type_id, **config)
545
- if product_type_id not in product_types:
546
- product_types.append(product_type)
547
- return sorted(product_types, key=itemgetter("ID"))
541
+
542
+ providers_configs = (
543
+ list(self.providers_config.values())
544
+ if not provider
545
+ else [
546
+ p
547
+ for p in self.providers_config.values()
548
+ if provider in [p.name, getattr(p, "group", None)]
549
+ ]
550
+ )
551
+
552
+ if provider and not providers_configs:
548
553
  raise UnsupportedProvider(
549
554
  f"The requested provider is not (yet) supported: {provider}"
550
555
  )
551
- # Only get the product types supported by the available providers
552
- for provider in self.available_providers():
553
- current_product_type_ids = [pt["ID"] for pt in product_types]
554
- product_types.extend(
555
- [
556
- pt
557
- for pt in self.list_product_types(
558
- provider=provider, fetch_providers=False
559
- )
560
- if pt["ID"] not in current_product_type_ids
561
- ]
562
- )
556
+
557
+ for p in providers_configs:
558
+ for product_type_id in p.products: # type: ignore
559
+ if product_type_id == GENERIC_PRODUCT_TYPE:
560
+ continue
561
+ config = self.product_types_config[product_type_id]
562
+ config["_id"] = product_type_id
563
+ if "alias" in config:
564
+ product_type_id = config["alias"]
565
+ product_type = {"ID": product_type_id, **config}
566
+ if product_type not in product_types:
567
+ product_types.append(product_type)
568
+
563
569
  # Return the product_types sorted in lexicographic order of their ID
564
570
  return sorted(product_types, key=itemgetter("ID"))
565
571
 
@@ -611,9 +617,8 @@ class EODataAccessGateway:
611
617
 
612
618
  if not ext_product_types_conf:
613
619
  # empty ext_product_types conf
614
- discover_kwargs = dict(provider=provider) if provider else {}
615
- ext_product_types_conf = self.discover_product_types(
616
- **discover_kwargs
620
+ ext_product_types_conf = (
621
+ self.discover_product_types(provider=provider) or {}
617
622
  )
618
623
 
619
624
  # update eodag product types list with new conf
@@ -691,13 +696,13 @@ class EODataAccessGateway:
691
696
  # providers not skipped here should be user-modified
692
697
  # or not in ext_product_types_conf (if eodag system conf != eodag conf used for ext_product_types_conf)
693
698
 
694
- # discover product types for user configured provider
695
- provider_ext_product_types_conf = self.discover_product_types(
696
- provider=provider
697
- )
698
-
699
- # update eodag product types list with new conf
700
- self.update_product_types_list(provider_ext_product_types_conf)
699
+ if not already_fetched:
700
+ # discover product types for user configured provider
701
+ provider_ext_product_types_conf = (
702
+ self.discover_product_types(provider=provider) or {}
703
+ )
704
+ # update eodag product types list with new conf
705
+ self.update_product_types_list(provider_ext_product_types_conf)
701
706
 
702
707
  def discover_product_types(
703
708
  self, provider: Optional[str] = None
@@ -710,6 +715,10 @@ class EODataAccessGateway:
710
715
  :returns: external product types configuration
711
716
  :rtype: dict
712
717
  """
718
+ if provider and provider not in self.providers_config:
719
+ raise UnsupportedProvider(
720
+ f"The requested provider is not (yet) supported: {provider}"
721
+ )
713
722
  ext_product_types_conf: Dict[str, Any] = {}
714
723
  providers_to_fetch = [
715
724
  p
@@ -721,6 +730,7 @@ class EODataAccessGateway:
721
730
  else self.available_providers()
722
731
  )
723
732
  ]
733
+ kwargs: Dict[str, Any] = {}
724
734
  for provider in providers_to_fetch:
725
735
  if hasattr(self.providers_config[provider], "search"):
726
736
  search_plugin_config = self.providers_config[provider].search
@@ -737,9 +747,11 @@ class EODataAccessGateway:
737
747
  auth_plugin = self._plugins_manager.get_auth_plugin(
738
748
  search_plugin.provider
739
749
  )
740
- if callable(getattr(auth_plugin, "authenticate", None)):
750
+ if auth_plugin and callable(
751
+ getattr(auth_plugin, "authenticate", None)
752
+ ):
741
753
  try:
742
- search_plugin.auth = auth_plugin.authenticate()
754
+ kwargs["auth"] = auth_plugin.authenticate()
743
755
  except (AuthenticationError, MisconfiguredError) as e:
744
756
  logger.warning(
745
757
  f"Could not authenticate on {provider}: {str(e)}"
@@ -753,9 +765,9 @@ class EODataAccessGateway:
753
765
  ext_product_types_conf[provider] = None
754
766
  continue
755
767
 
756
- ext_product_types_conf[
757
- provider
758
- ] = search_plugin.discover_product_types()
768
+ ext_product_types_conf[provider] = search_plugin.discover_product_types(
769
+ **kwargs
770
+ )
759
771
 
760
772
  return ext_product_types_conf
761
773
 
@@ -848,23 +860,49 @@ class EODataAccessGateway:
848
860
  # rebuild index after product types list update
849
861
  self.build_index()
850
862
 
851
- def available_providers(self, product_type: Optional[str] = None) -> List[str]:
852
- """Gives the sorted list of the available providers
863
+ def available_providers(
864
+ self, product_type: Optional[str] = None, by_group: bool = False
865
+ ) -> List[str]:
866
+ """Gives the sorted list of the available providers or groups
867
+
868
+ The providers or groups are sorted first by their priority level in descending order,
869
+ and then alphabetically in ascending order for providers or groups with the same
870
+ priority level.
853
871
 
854
872
  :param product_type: (optional) Only list providers configured for this product_type
855
- :type product_type: str
856
- :returns: the sorted list of the available providers
857
- :rtype: list
873
+ :type product_type: Optional[str]
874
+ :param by_group: (optional) If set to True, list groups when available instead
875
+ of providers, mixed with other providers
876
+ :type by_group: bool
877
+ :returns: the sorted list of the available providers or groups
878
+ :rtype: List[str]
858
879
  """
859
880
 
860
881
  if product_type:
861
- return sorted(
862
- k
882
+ providers = [
883
+ (v.group if by_group and hasattr(v, "group") else k, v.priority)
863
884
  for k, v in self.providers_config.items()
864
885
  if product_type in getattr(v, "products", {}).keys()
865
- )
886
+ ]
866
887
  else:
867
- return sorted(tuple(self.providers_config.keys()))
888
+ providers = [
889
+ (v.group if by_group and hasattr(v, "group") else k, v.priority)
890
+ for k, v in self.providers_config.items()
891
+ ]
892
+
893
+ # If by_group is True, keep only the highest priority for each group
894
+ if by_group:
895
+ group_priority: Dict[str, int] = {}
896
+ for name, priority in providers:
897
+ if name not in group_priority or priority > group_priority[name]:
898
+ group_priority[name] = priority
899
+ providers = list(group_priority.items())
900
+
901
+ # Sort by priority (descending) and then by name (ascending)
902
+ providers.sort(key=lambda x: (-x[1], x[0]))
903
+
904
+ # Return only the names of the providers or groups
905
+ return [name for name, _ in providers]
868
906
 
869
907
  def get_product_type_from_alias(self, alias_or_id: str) -> str:
870
908
  """Return the ID of a product type by either its ID or alias
@@ -910,47 +948,116 @@ class EODataAccessGateway:
910
948
 
911
949
  return self.product_types_config[product_type].get("alias", product_type)
912
950
 
913
- def guess_product_type(self, **kwargs: Any) -> List[str]:
914
- """Find eodag product types codes that best match a set of search params
915
-
916
- :param kwargs: A set of search parameters as keywords arguments
917
- :returns: The best match for the given parameters
918
- :rtype: list[str]
951
+ def guess_product_type(
952
+ self,
953
+ free_text: Optional[str] = None,
954
+ intersect: bool = False,
955
+ instrument: Optional[str] = None,
956
+ platform: Optional[str] = None,
957
+ platformSerialIdentifier: Optional[str] = None,
958
+ processingLevel: Optional[str] = None,
959
+ sensorType: Optional[str] = None,
960
+ keywords: Optional[str] = None,
961
+ abstract: Optional[str] = None,
962
+ title: Optional[str] = None,
963
+ missionStartDate: Optional[str] = None,
964
+ missionEndDate: Optional[str] = None,
965
+ **kwargs: Any,
966
+ ) -> List[str]:
967
+ """
968
+ Find EODAG product type IDs that best match a set of search parameters.
969
+
970
+ See https://whoosh.readthedocs.io/en/latest/querylang.html#the-default-query-language
971
+ for syntax.
972
+
973
+ :param free_text: Whoosh-compatible free text search filter used to search
974
+ accross all the following parameters
975
+ :type free_text: Optional[str]
976
+ :param intersect: Join results for each parameter using INTERSECT instead of UNION.
977
+ :type intersect: bool
978
+ :param instrument: Instrument parameter.
979
+ :type instrument: Optional[str]
980
+ :param platform: Platform parameter.
981
+ :type platform: Optional[str]
982
+ :param platformSerialIdentifier: Platform serial identifier parameter.
983
+ :type platformSerialIdentifier: Optional[str]
984
+ :param processingLevel: Processing level parameter.
985
+ :type processingLevel: Optional[str]
986
+ :param sensorType: Sensor type parameter.
987
+ :type sensorType: Optional[str]
988
+ :param keywords: Keywords parameter.
989
+ :type keywords: Optional[str]
990
+ :param abstract: Abstract parameter.
991
+ :type abstract: Optional[str]
992
+ :param title: Title parameter.
993
+ :type title: Optional[str]
994
+ :param missionStartDate: start date for datetime filtering. Not used by free_text
995
+ :type missionStartDate: Optional[str]
996
+ :param missionEndDate: end date for datetime filtering. Not used by free_text
997
+ :type missionEndDate: Optional[str]
998
+ :returns: The best match for the given parameters.
999
+ :rtype: List[str]
919
1000
  :raises: :class:`~eodag.utils.exceptions.NoMatchingProductType`
920
1001
  """
921
- if kwargs.get("productType", None):
922
- return [kwargs["productType"]]
923
- supported_params = {
924
- param
925
- for param in (
926
- "instrument",
927
- "platform",
928
- "platformSerialIdentifier",
929
- "processingLevel",
930
- "sensorType",
931
- "keywords",
932
- "md5",
933
- )
934
- if kwargs.get(param, None) is not None
1002
+ if productType := kwargs.get("productType"):
1003
+ return [productType]
1004
+
1005
+ if not self._product_types_index:
1006
+ raise EodagError("Missing product types index")
1007
+
1008
+ filters = {
1009
+ "instrument": instrument,
1010
+ "platform": platform,
1011
+ "platformSerialIdentifier": platformSerialIdentifier,
1012
+ "processingLevel": processingLevel,
1013
+ "sensorType": sensorType,
1014
+ "keywords": keywords,
1015
+ "abstract": abstract,
1016
+ "title": title,
935
1017
  }
1018
+ joint = " AND " if intersect else " OR "
1019
+ filters_text = joint.join(
1020
+ [f"{k}:({v})" for k, v in filters.items() if v is not None]
1021
+ )
1022
+
1023
+ text = f"({free_text})" if free_text else ""
1024
+ if free_text and filters_text:
1025
+ text += joint
1026
+ if filters_text:
1027
+ text += f"({filters_text})"
1028
+
1029
+ if not text and (missionStartDate or missionEndDate):
1030
+ text = "*"
1031
+
936
1032
  with self._product_types_index.searcher() as searcher:
937
- results = None
938
- # For each search key, do a guess and then upgrade the result (i.e. when
939
- # merging results, if a hit appears in both results, its position is raised
940
- # to the top. This way, the top most result will be the hit that best
941
- # matches the given queries. Put another way, this best guess is the one
942
- # that crosses the highest number of search params from the given queries
943
- for search_key in supported_params:
944
- query = QueryParser(search_key, self._product_types_index.schema).parse(
945
- kwargs[search_key]
1033
+ p = EODAGQueryParser(list(filters.keys()), self._product_types_index.schema)
1034
+ query = p.parse(text)
1035
+ results = searcher.search(query, limit=None)
1036
+
1037
+ guesses: List[Dict[str, str]] = [dict(r) for r in results or []]
1038
+
1039
+ # datetime filtering
1040
+ if missionStartDate or missionEndDate:
1041
+ guesses = [
1042
+ g
1043
+ for g in guesses
1044
+ if (
1045
+ not missionEndDate
1046
+ or g.get("missionStartDate")
1047
+ and rfc3339_str_to_datetime(g["missionStartDate"])
1048
+ <= rfc3339_str_to_datetime(missionEndDate)
946
1049
  )
947
- if results is None:
948
- results = searcher.search(query, limit=None)
949
- else:
950
- results.upgrade_and_extend(searcher.search(query, limit=None))
951
- guesses: List[str] = [r["ID"] for r in results or []]
1050
+ and (
1051
+ not missionStartDate
1052
+ or g.get("missionEndDate")
1053
+ and rfc3339_str_to_datetime(g["missionEndDate"])
1054
+ >= rfc3339_str_to_datetime(missionStartDate)
1055
+ )
1056
+ ]
1057
+
952
1058
  if guesses:
953
- return guesses
1059
+ return [g["ID"] for g in guesses or []]
1060
+
954
1061
  raise NoMatchingProductType()
955
1062
 
956
1063
  def search(
@@ -963,8 +1070,9 @@ class EODataAccessGateway:
963
1070
  geom: Optional[Union[str, Dict[str, float], BaseGeometry]] = None,
964
1071
  locations: Optional[Dict[str, str]] = None,
965
1072
  provider: Optional[str] = None,
1073
+ count: bool = False,
966
1074
  **kwargs: Any,
967
- ) -> Tuple[SearchResult, int]:
1075
+ ) -> SearchResult:
968
1076
  """Look for products matching criteria on known providers.
969
1077
 
970
1078
  The default behaviour is to look for products on the provider with the
@@ -1006,16 +1114,17 @@ class EODataAccessGateway:
1006
1114
  'PA' such as Panama and Pakistan in the shapefile configured with
1007
1115
  name=country and attr=ISO3
1008
1116
  :type locations: dict
1009
- :param kwargs: Some other criteria that will be used to do the search,
1010
- using paramaters compatibles with the provider
1011
1117
  :param provider: (optional) the provider to be used. If set, search fallback will be disabled.
1012
1118
  If not set, the configured preferred provider will be used at first
1013
1119
  before trying others until finding results.
1014
1120
  :type provider: str
1121
+ :param count: (optional) Whether to run a query with a count request or not
1122
+ :type count: bool
1123
+ :param kwargs: Some other criteria that will be used to do the search,
1124
+ using paramaters compatibles with the provider
1015
1125
  :type kwargs: Union[int, str, bool, dict]
1016
- :returns: A collection of EO products matching the criteria and the total
1017
- number of results found
1018
- :rtype: tuple(:class:`~eodag.api.search_result.SearchResult`, int)
1126
+ :returns: A collection of EO products matching the criteria
1127
+ :rtype: :class:`~eodag.api.search_result.SearchResult`
1019
1128
 
1020
1129
  .. note::
1021
1130
  The search interfaces, which are implemented as plugins, are required to
@@ -1030,16 +1139,12 @@ class EODataAccessGateway:
1030
1139
  provider=provider,
1031
1140
  **kwargs,
1032
1141
  )
1033
-
1034
1142
  if search_kwargs.get("id"):
1035
- # adds minimal pagination to be able to check only 1 product is returned
1036
- search_kwargs.update(
1037
- page=1,
1038
- items_per_page=2,
1039
- raise_errors=raise_errors,
1040
- )
1041
1143
  return self._search_by_id(
1042
- search_kwargs.pop("id"), provider=provider, **search_kwargs
1144
+ search_kwargs.pop("id"),
1145
+ provider=provider,
1146
+ raise_errors=raise_errors,
1147
+ **search_kwargs,
1043
1148
  )
1044
1149
  # remove datacube query string from kwargs which was only needed for search-by-id
1045
1150
  search_kwargs.pop("_dc_qs", None)
@@ -1053,9 +1158,9 @@ class EODataAccessGateway:
1053
1158
  # Loop over available providers and return the first non-empty results
1054
1159
  for i, search_plugin in enumerate(search_plugins):
1055
1160
  search_plugin.clear()
1056
- search_results, total_results = self._do_search(
1161
+ search_results = self._do_search(
1057
1162
  search_plugin,
1058
- count=True,
1163
+ count=count,
1059
1164
  raise_errors=raise_errors,
1060
1165
  **search_kwargs,
1061
1166
  )
@@ -1065,10 +1170,11 @@ class EODataAccessGateway:
1065
1170
  "we will try to get the data from another provider",
1066
1171
  )
1067
1172
  elif len(search_results) > 0:
1068
- return search_results, total_results
1173
+ return search_results
1069
1174
 
1070
- logger.error("No result could be obtained from any available provider")
1071
- return SearchResult([]), 0
1175
+ if i > 1:
1176
+ logger.error("No result could be obtained from any available provider")
1177
+ return SearchResult([], 0) if count else SearchResult([])
1072
1178
 
1073
1179
  def search_iter_page(
1074
1180
  self,
@@ -1181,9 +1287,10 @@ class EODataAccessGateway:
1181
1287
  pagination_config["next_page_query_obj"] = next_page_query_obj
1182
1288
  logger.info("Iterate search over multiple pages: page #%s", iteration)
1183
1289
  try:
1184
- if "raise_errors" in kwargs:
1185
- kwargs.pop("raise_errors")
1186
- products, _ = self._do_search(
1290
+ # remove unwanted kwargs for _do_search
1291
+ kwargs.pop("count", None)
1292
+ kwargs.pop("raise_errors", None)
1293
+ search_result = self._do_search(
1187
1294
  search_plugin, count=False, raise_errors=True, **kwargs
1188
1295
  )
1189
1296
  except Exception:
@@ -1222,12 +1329,12 @@ class EODataAccessGateway:
1222
1329
  else:
1223
1330
  search_plugin.next_page_query_obj = next_page_query_obj
1224
1331
 
1225
- if len(products) > 0:
1332
+ if len(search_result) > 0:
1226
1333
  # The first products between two iterations are compared. If they
1227
1334
  # are actually the same product, it means the iteration failed at
1228
1335
  # progressing for some reason. This is implemented as a workaround
1229
1336
  # to some search plugins/providers not handling pagination.
1230
- product = products[0]
1337
+ product = search_result[0]
1231
1338
  if (
1232
1339
  prev_product
1233
1340
  and product.properties["id"] == prev_product.properties["id"]
@@ -1240,11 +1347,11 @@ class EODataAccessGateway:
1240
1347
  )
1241
1348
  last_page_with_products = iteration - 1
1242
1349
  break
1243
- yield products
1350
+ yield search_result
1244
1351
  prev_product = product
1245
1352
  # Prevent a last search if the current one returned less than the
1246
1353
  # maximum number of items asked for.
1247
- if len(products) < items_per_page:
1354
+ if len(search_result) < items_per_page:
1248
1355
  last_page_with_products = iteration
1249
1356
  break
1250
1357
  else:
@@ -1319,7 +1426,7 @@ class EODataAccessGateway:
1319
1426
  # of items_per_page if defined for the provider used.
1320
1427
  try:
1321
1428
  product_type = self.get_product_type_from_alias(
1322
- kwargs.get("productType", None) or self.guess_product_type(**kwargs)[0]
1429
+ self.guess_product_type(**kwargs)[0]
1323
1430
  )
1324
1431
  except NoMatchingProductType:
1325
1432
  product_type = GENERIC_PRODUCT_TYPE
@@ -1385,7 +1492,7 @@ class EODataAccessGateway:
1385
1492
 
1386
1493
  def _search_by_id(
1387
1494
  self, uid: str, provider: Optional[str] = None, **kwargs: Any
1388
- ) -> Tuple[SearchResult, int]:
1495
+ ) -> SearchResult:
1389
1496
  """Internal method that enables searching a product by its id.
1390
1497
 
1391
1498
  Keeps requesting providers until a result matching the id is supplied. The
@@ -1405,9 +1512,8 @@ class EODataAccessGateway:
1405
1512
  :type provider: str
1406
1513
  :param kwargs: Search criteria to help finding the right product
1407
1514
  :type kwargs: Any
1408
- :returns: A search result with one EO product or None at all, and the number
1409
- of EO products retrieved (0 or 1)
1410
- :rtype: tuple(:class:`~eodag.api.search_result.SearchResult`, int)
1515
+ :returns: A search result with one EO product or None at all
1516
+ :rtype: :class:`~eodag.api.search_result.SearchResult`
1411
1517
  """
1412
1518
  product_type = kwargs.get("productType", None)
1413
1519
  if product_type is not None:
@@ -1422,16 +1528,51 @@ class EODataAccessGateway:
1422
1528
  # datacube query string
1423
1529
  _dc_qs = kwargs.pop("_dc_qs", None)
1424
1530
 
1531
+ results = SearchResult([])
1532
+
1425
1533
  for plugin in search_plugins:
1426
1534
  logger.info(
1427
1535
  "Searching product with id '%s' on provider: %s", uid, plugin.provider
1428
1536
  )
1429
1537
  logger.debug("Using plugin class for search: %s", plugin.__class__.__name__)
1430
1538
  plugin.clear()
1539
+
1540
+ # adds maximal pagination to be able to do a search-all + crunch if more
1541
+ # than one result are returned
1542
+ items_per_page = plugin.config.pagination.get(
1543
+ "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1544
+ )
1545
+ kwargs.update(items_per_page=items_per_page)
1431
1546
  if isinstance(plugin, BuildPostSearchResult):
1432
- results, _ = self._do_search(plugin, id=uid, _dc_qs=_dc_qs, **kwargs)
1547
+ kwargs.update(
1548
+ items_per_page=items_per_page,
1549
+ _dc_qs=_dc_qs,
1550
+ )
1433
1551
  else:
1434
- results, _ = self._do_search(plugin, id=uid, **kwargs)
1552
+ kwargs.update(
1553
+ items_per_page=items_per_page,
1554
+ )
1555
+
1556
+ try:
1557
+ # if more than one results are found, try getting them all and then filter using crunch
1558
+ for page_results in self.search_iter_page_plugin(
1559
+ search_plugin=plugin,
1560
+ id=uid,
1561
+ **kwargs,
1562
+ ):
1563
+ results.data.extend(page_results.data)
1564
+ except Exception:
1565
+ if kwargs.get("raise_errors"):
1566
+ raise
1567
+ continue
1568
+
1569
+ # try using crunch to get unique result
1570
+ if (
1571
+ len(results) > 1
1572
+ and len(filtered := results.filter_property(id=uid)) == 1
1573
+ ):
1574
+ results = filtered
1575
+
1435
1576
  if len(results) == 1:
1436
1577
  if not results[0].product_type:
1437
1578
  # guess product type from properties
@@ -1439,20 +1580,36 @@ class EODataAccessGateway:
1439
1580
  results[0].product_type = guesses[0]
1440
1581
  # reset driver
1441
1582
  results[0].driver = results[0].get_driver()
1442
- return results, 1
1583
+ results.number_matched = 1
1584
+ return results
1443
1585
  elif len(results) > 1:
1444
- if getattr(plugin.config, "two_passes_id_search", False):
1445
- # check if id of one product exactly matches id that was searched for
1446
- # required if provider does not offer search by id and therefore other
1447
- # parameters which might not given an exact result are used
1448
- for result in results:
1449
- if result.properties["id"] == uid.split(".")[0]:
1450
- return [results[0]], 1
1451
1586
  logger.info(
1452
1587
  "Several products found for this id (%s). You may try searching using more selective criteria.",
1453
1588
  results,
1454
1589
  )
1455
- return SearchResult([]), 0
1590
+ return SearchResult([], 0)
1591
+
1592
+ def _fetch_external_product_type(self, provider: str, product_type: str):
1593
+ plugins = self._plugins_manager.get_search_plugins(provider=provider)
1594
+ plugin = next(plugins)
1595
+
1596
+ kwargs: Dict[str, Any] = {"productType": product_type}
1597
+
1598
+ # append auth if needed
1599
+ if getattr(plugin.config, "need_auth", False):
1600
+ auth_plugin = self._plugins_manager.get_auth_plugin(plugin.provider)
1601
+ if auth_plugin and callable(getattr(auth_plugin, "authenticate", None)):
1602
+ try:
1603
+ kwargs["auth"] = auth_plugin.authenticate()
1604
+ except (AuthenticationError, MisconfiguredError) as e:
1605
+ logger.warning(f"Could not authenticate on {provider}: {str(e)}")
1606
+ else:
1607
+ logger.warning(
1608
+ f"Could not authenticate on {provider} using {auth_plugin} plugin"
1609
+ )
1610
+
1611
+ product_type_config = plugin.discover_product_types(**kwargs)
1612
+ self.update_product_types_list({provider: product_type_config})
1456
1613
 
1457
1614
  def _prepare_search(
1458
1615
  self,
@@ -1532,7 +1689,7 @@ class EODataAccessGateway:
1532
1689
  try:
1533
1690
  product_type = self.get_product_type_from_alias(product_type)
1534
1691
  except NoMatchingProductType:
1535
- logger.warning("unknown product type " + product_type)
1692
+ logger.info("unknown product type " + product_type)
1536
1693
  kwargs["productType"] = product_type
1537
1694
 
1538
1695
  if start is not None:
@@ -1567,7 +1724,16 @@ class EODataAccessGateway:
1567
1724
  logger.debug(
1568
1725
  f"Fetching external product types sources to find {product_type} product type"
1569
1726
  )
1570
- self.fetch_product_types_list()
1727
+ if provider:
1728
+ # Try to get specific product type from external provider
1729
+ self._fetch_external_product_type(provider, product_type)
1730
+ if (
1731
+ not provider
1732
+ or product_type
1733
+ not in self._plugins_manager.product_type_to_provider_config_map.keys()
1734
+ ):
1735
+ # no provider or still not found -> fetch all external product types
1736
+ self.fetch_product_types_list()
1571
1737
 
1572
1738
  preferred_provider = self.get_preferred_provider()[0]
1573
1739
 
@@ -1617,8 +1783,7 @@ class EODataAccessGateway:
1617
1783
  for p in self.list_product_types(
1618
1784
  search_plugin.provider, fetch_providers=False
1619
1785
  )
1620
- if p["ID"] == product_type
1621
- or ("_id" in p and p["_id"] == product_type)
1786
+ if p["_id"] == product_type
1622
1787
  ][0],
1623
1788
  **{"productType": product_type},
1624
1789
  )
@@ -1638,10 +1803,10 @@ class EODataAccessGateway:
1638
1803
  def _do_search(
1639
1804
  self,
1640
1805
  search_plugin: Union[Search, Api],
1641
- count: bool = True,
1806
+ count: bool = False,
1642
1807
  raise_errors: bool = False,
1643
1808
  **kwargs: Any,
1644
- ) -> Tuple[SearchResult, Optional[int]]:
1809
+ ) -> SearchResult:
1645
1810
  """Internal method that performs a search on a given provider.
1646
1811
 
1647
1812
  :param search_plugin: A search plugin
@@ -1653,14 +1818,16 @@ class EODataAccessGateway:
1653
1818
  :type raise_errors: bool
1654
1819
  :param kwargs: Some other criteria that will be used to do the search
1655
1820
  :type kwargs: Any
1656
- :returns: A collection of EO products matching the criteria and the total
1657
- number of results found if count is True else None
1821
+ :returns: A collection of EO products matching the criteria
1658
1822
  :rtype: tuple(:class:`~eodag.api.search_result.SearchResult`, int or None)
1659
1823
  """
1660
1824
  max_items_per_page = getattr(search_plugin.config, "pagination", {}).get(
1661
1825
  "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1662
1826
  )
1663
- if kwargs.get("items_per_page", DEFAULT_ITEMS_PER_PAGE) > max_items_per_page:
1827
+ if (
1828
+ kwargs.get("items_per_page", DEFAULT_ITEMS_PER_PAGE) > max_items_per_page
1829
+ and max_items_per_page > 0
1830
+ ):
1664
1831
  logger.warning(
1665
1832
  "EODAG believes that you might have asked for more products/items "
1666
1833
  "than the maximum allowed by '%s': %s > %s. Try to lower "
@@ -1676,51 +1843,18 @@ class EODataAccessGateway:
1676
1843
  can_authenticate = callable(getattr(auth_plugin, "authenticate", None))
1677
1844
 
1678
1845
  results: List[EOProduct] = []
1679
- total_results = 0
1846
+ total_results: Optional[int] = 0 if count else None
1680
1847
 
1681
1848
  try:
1849
+ prep = PreparedSearch(count=count)
1682
1850
  if need_auth and auth_plugin and can_authenticate:
1683
- search_plugin.auth = auth_plugin.authenticate()
1684
-
1685
- res, nb_res = search_plugin.query(count=count, auth=auth_plugin, **kwargs)
1686
-
1687
- # Only do the pagination computations when it makes sense. For example,
1688
- # for a search by id, we can reasonably guess that the provider will return
1689
- # At most 1 product, so we don't need such a thing as pagination
1690
- page = kwargs.get("page")
1691
- items_per_page = kwargs.get("items_per_page")
1692
- if page and items_per_page and count:
1693
- # Take into account the fact that a provider may not return the count of
1694
- # products (in that case, fallback to using the length of the results it
1695
- # returned and the page requested. As an example, check the result of
1696
- # the following request (look for the value of properties.totalResults)
1697
- # https://theia-landsat.cnes.fr/resto/api/collections/Landsat/search.json?
1698
- # maxRecords=1&page=1
1699
- if not nb_res:
1700
- nb_res = len(res) * page
1701
-
1702
- # Attempt to ensure a little bit more coherence. Some providers return
1703
- # a fuzzy number of total results, meaning that you have to keep
1704
- # requesting it until it has returned everything it has to know exactly
1705
- # how many EO products they have in their stock. In that case, we need
1706
- # to replace the returned number of results with the sum of the number
1707
- # of items that were skipped so far and the length of the currently
1708
- # retrieved items. We know there is an incoherence when the number of
1709
- # skipped items is greater than the total number of items returned by
1710
- # the plugin
1711
- nb_skipped_items = items_per_page * (page - 1)
1712
- nb_current_items = len(res)
1713
- if nb_skipped_items > nb_res:
1714
- if nb_res != 0:
1715
- nb_res = nb_skipped_items + nb_current_items
1716
- # This is for when the returned results is an empty list and the
1717
- # number of results returned is incoherent with the observations.
1718
- # In that case, we assume the total number of results is the number
1719
- # of skipped results. By requesting a lower page than the current
1720
- # one, a user can iteratively reach the last page of results for
1721
- # these criteria on the provider.
1722
- else:
1723
- nb_res = nb_skipped_items
1851
+ prep.auth = auth_plugin.authenticate()
1852
+
1853
+ prep.auth_plugin = auth_plugin
1854
+ prep.page = kwargs.pop("page", None)
1855
+ prep.items_per_page = kwargs.pop("items_per_page", None)
1856
+
1857
+ res, nb_res = search_plugin.query(prep, **kwargs)
1724
1858
 
1725
1859
  if not isinstance(res, list):
1726
1860
  raise PluginImplementationError(
@@ -1744,7 +1878,6 @@ class EODataAccessGateway:
1744
1878
  try:
1745
1879
  guesses = self.guess_product_type(
1746
1880
  **{
1747
- # k:str(v) for k,v in eo_product.properties.items()
1748
1881
  k: pattern.sub("", str(v).upper())
1749
1882
  for k, v in eo_product.properties.items()
1750
1883
  if k
@@ -1779,8 +1912,12 @@ class EODataAccessGateway:
1779
1912
  eo_product.register_downloader(download_plugin, auth_plugin)
1780
1913
 
1781
1914
  results.extend(res)
1782
- total_results = None if nb_res is None else total_results + nb_res
1783
- if count:
1915
+ total_results = (
1916
+ None
1917
+ if (nb_res is None or total_results is None)
1918
+ else total_results + nb_res
1919
+ )
1920
+ if count and nb_res is not None:
1784
1921
  logger.info(
1785
1922
  "Found %s result(s) on provider '%s'",
1786
1923
  nb_res,
@@ -1805,6 +1942,9 @@ class EODataAccessGateway:
1805
1942
  if not raise_errors:
1806
1943
  log_msg += " Raise verbosity of log messages for details"
1807
1944
  logger.info(log_msg)
1945
+ # keep only the message from exception args
1946
+ if len(e.args) > 1:
1947
+ e.args = (e.args[0],)
1808
1948
  if raise_errors:
1809
1949
  # Raise the error, letting the application wrapping eodag know that
1810
1950
  # something went bad. This way it will be able to decide what to do next
@@ -1815,7 +1955,7 @@ class EODataAccessGateway:
1815
1955
  search_plugin.provider,
1816
1956
  )
1817
1957
  self.search_errors.add((search_plugin.provider, e))
1818
- return SearchResult(results), total_results
1958
+ return SearchResult(results, total_results)
1819
1959
 
1820
1960
  def crunch(self, results: SearchResult, **kwargs: Any) -> SearchResult:
1821
1961
  """Apply the filters given through the keyword arguments to the results
@@ -1865,7 +2005,7 @@ class EODataAccessGateway:
1865
2005
  progress_callback: Optional[ProgressCallback] = None,
1866
2006
  wait: int = DEFAULT_DOWNLOAD_WAIT,
1867
2007
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1868
- **kwargs: Any,
2008
+ **kwargs: Unpack[DownloadConf],
1869
2009
  ) -> List[str]:
1870
2010
  """Download all products resulting from a search.
1871
2011
 
@@ -1978,6 +2118,7 @@ class EODataAccessGateway:
1978
2118
  provider: Optional[str] = None,
1979
2119
  productType: Optional[str] = None,
1980
2120
  timeout: int = HTTP_REQ_TIMEOUT,
2121
+ ssl_verify: bool = True,
1981
2122
  **kwargs: Any,
1982
2123
  ) -> SearchResult:
1983
2124
  """Loads STAC items from a geojson file / STAC catalog or collection, and convert to SearchResult.
@@ -2011,6 +2152,7 @@ class EODataAccessGateway:
2011
2152
  recursive=recursive,
2012
2153
  max_connections=max_connections,
2013
2154
  timeout=timeout,
2155
+ ssl_verify=ssl_verify,
2014
2156
  )
2015
2157
  feature_collection = geojson.FeatureCollection(features)
2016
2158
 
@@ -2027,12 +2169,14 @@ class EODataAccessGateway:
2027
2169
  )
2028
2170
  )
2029
2171
 
2030
- products, _ = self.search(productType=productType, provider=provider, **kwargs)
2172
+ search_result = self.search(
2173
+ productType=productType, provider=provider, **kwargs
2174
+ )
2031
2175
 
2032
2176
  # restore plugin._request
2033
2177
  plugin._request = plugin_request
2034
2178
 
2035
- return products
2179
+ return search_result
2036
2180
 
2037
2181
  def download(
2038
2182
  self,
@@ -2040,7 +2184,7 @@ class EODataAccessGateway:
2040
2184
  progress_callback: Optional[ProgressCallback] = None,
2041
2185
  wait: int = DEFAULT_DOWNLOAD_WAIT,
2042
2186
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
2043
- **kwargs: Any,
2187
+ **kwargs: Unpack[DownloadConf],
2044
2188
  ) -> str:
2045
2189
  """Download a single product.
2046
2190
 
@@ -2120,9 +2264,7 @@ class EODataAccessGateway:
2120
2264
  return self._plugins_manager.get_crunch_plugin(name, **plugin_conf)
2121
2265
 
2122
2266
  def list_queryables(
2123
- self,
2124
- provider: Optional[str] = None,
2125
- **kwargs: Any,
2267
+ self, provider: Optional[str] = None, **kwargs: Any
2126
2268
  ) -> Dict[str, Annotated[Any, FieldInfo]]:
2127
2269
  """Fetch the queryable properties for a given product type and/or provider.
2128
2270
 
@@ -2131,113 +2273,53 @@ class EODataAccessGateway:
2131
2273
  :param kwargs: additional filters for queryables (`productType` or other search
2132
2274
  arguments)
2133
2275
  :type kwargs: Any
2276
+
2277
+ :raises UnsupportedProductType: If the specified product type is not available for the
2278
+ provider.
2279
+
2134
2280
  :returns: A dict containing the EODAG queryable properties, associating
2135
2281
  parameters to their annotated type
2136
2282
  :rtype: Dict[str, Annotated[Any, FieldInfo]]
2137
2283
  """
2138
- # unknown product type
2139
2284
  available_product_types = [
2140
- pt["ID"] for pt in self.list_product_types(fetch_providers=False)
2285
+ pt["ID"]
2286
+ for pt in self.list_product_types(provider=provider, fetch_providers=False)
2141
2287
  ]
2142
- product_type = kwargs.get("productType", None)
2143
- if product_type is not None and product_type not in available_product_types:
2144
- self.fetch_product_types_list()
2145
-
2146
- # dictionary of the queryable properties of the providers supporting the given product type
2147
- providers_available_queryables: Dict[
2148
- str, Dict[str, Annotated[Any, FieldInfo]]
2149
- ] = dict()
2150
-
2151
- if provider is None and product_type is None:
2152
- return model_fields_to_annotated(CommonQueryables.model_fields)
2153
- elif provider is None:
2154
- for plugin in self._plugins_manager.get_search_plugins(
2155
- product_type, provider
2156
- ):
2157
- providers_available_queryables[plugin.provider] = self.list_queryables(
2158
- provider=plugin.provider, **kwargs
2159
- )
2288
+ product_type = kwargs.get("productType")
2160
2289
 
2161
- # return providers queryables intersection
2162
- queryables_keys: AbstractSet[str] = set()
2163
- for queryables in providers_available_queryables.values():
2164
- queryables_keys = (
2165
- queryables_keys & queryables.keys()
2166
- if queryables_keys
2167
- else queryables.keys()
2290
+ if product_type:
2291
+ try:
2292
+ kwargs["productType"] = product_type = self.get_product_type_from_alias(
2293
+ product_type
2168
2294
  )
2169
- return {
2170
- k: v
2171
- for k, v in providers_available_queryables.popitem()[1].items()
2172
- if k in queryables_keys
2173
- }
2295
+ except NoMatchingProductType as e:
2296
+ raise UnsupportedProductType(f"{product_type} is not available") from e
2174
2297
 
2175
- all_queryables = copy_deepcopy(
2176
- model_fields_to_annotated(Queryables.model_fields)
2177
- )
2298
+ if product_type and product_type not in available_product_types:
2299
+ self.fetch_product_types_list()
2178
2300
 
2179
- try:
2180
- plugin = next(
2181
- self._plugins_manager.get_search_plugins(product_type, provider)
2182
- )
2183
- except StopIteration:
2184
- # return default queryables if no plugin is found
2301
+ if not provider and not product_type:
2185
2302
  return model_fields_to_annotated(CommonQueryables.model_fields)
2186
2303
 
2187
- providers_available_queryables[plugin.provider] = dict()
2304
+ providers_queryables: Dict[str, Dict[str, Annotated[Any, FieldInfo]]] = {}
2188
2305
 
2189
- # unknown product type: try again after fetch_product_types_list()
2190
- if (
2191
- product_type
2192
- and product_type not in plugin.config.products.keys()
2193
- and provider is None
2194
- ):
2195
- raise UnsupportedProductType(product_type)
2196
- elif product_type and product_type not in plugin.config.products.keys():
2197
- raise UnsupportedProductType(
2198
- f"{product_type} is not available for provider {provider}"
2306
+ for plugin in self._plugins_manager.get_search_plugins(product_type, provider):
2307
+ if getattr(plugin.config, "need_auth", False) and (
2308
+ auth := self._plugins_manager.get_auth_plugin(plugin.provider)
2309
+ ):
2310
+ plugin.auth = auth.authenticate()
2311
+ providers_queryables[plugin.provider] = plugin.list_queryables(
2312
+ filters=kwargs, product_type=product_type
2199
2313
  )
2200
2314
 
2201
- metadata_mapping = deepcopy(getattr(plugin.config, "metadata_mapping", {}))
2202
-
2203
- # product_type-specific metadata-mapping
2204
- metadata_mapping.update(
2205
- getattr(plugin.config, "products", {})
2206
- .get(product_type, {})
2207
- .get("metadata_mapping", {})
2315
+ queryable_keys: Set[str] = set.intersection( # type: ignore
2316
+ *[set(q.keys()) for q in providers_queryables.values()]
2208
2317
  )
2209
-
2210
- # default values
2211
- default_values = deepcopy(
2212
- getattr(plugin.config, "products", {}).get(product_type, {})
2213
- )
2214
- default_values.pop("metadata_mapping", None)
2215
- kwargs = dict(default_values, **kwargs)
2216
-
2217
- # remove not mapped parameters or non-queryables
2218
- for param in list(metadata_mapping.keys()):
2219
- if NOT_MAPPED in metadata_mapping[param] or not isinstance(
2220
- metadata_mapping[param], list
2221
- ):
2222
- del metadata_mapping[param]
2223
-
2224
- for key, value in all_queryables.items():
2225
- annotated_args = get_args(value)
2226
- if len(annotated_args) < 1:
2227
- continue
2228
- field_info = annotated_args[1]
2229
- if not isinstance(field_info, FieldInfo):
2230
- continue
2231
- if key in kwargs:
2232
- field_info.default = kwargs[key]
2233
- if field_info.is_required() or (
2234
- (field_info.alias or key) in metadata_mapping
2235
- ):
2236
- providers_available_queryables[plugin.provider][key] = value
2237
-
2238
- provider_queryables = plugin.discover_queryables(**kwargs) or dict()
2239
- # use EODAG configured queryables by default
2240
- provider_queryables.update(providers_available_queryables[provider])
2318
+ queryables = {
2319
+ k: v
2320
+ for k, v in list(providers_queryables.values())[0].items()
2321
+ if k in queryable_keys
2322
+ }
2241
2323
 
2242
2324
  # always keep at least CommonQueryables
2243
2325
  common_queryables = copy_deepcopy(CommonQueryables.model_fields)
@@ -2245,6 +2327,39 @@ class EODataAccessGateway:
2245
2327
  if key in kwargs:
2246
2328
  queryable.default = kwargs[key]
2247
2329
 
2248
- provider_queryables.update(model_fields_to_annotated(common_queryables))
2330
+ queryables.update(model_fields_to_annotated(common_queryables))
2331
+
2332
+ return queryables
2333
+
2334
+ def available_sortables(self) -> Dict[str, Optional[ProviderSortables]]:
2335
+ """For each provider, gives its available sortable parameter(s) and its maximum
2336
+ number of them if it supports the sorting feature, otherwise gives None.
2249
2337
 
2250
- return provider_queryables
2338
+ :returns: A dictionnary with providers as keys and dictionnary of sortable parameter(s) and
2339
+ its (their) maximum number as value(s).
2340
+ :rtype: dict
2341
+ :raises: :class:`~eodag.utils.exceptions.UnsupportedProvider`
2342
+ """
2343
+ sortables: Dict[str, Optional[ProviderSortables]] = {}
2344
+ provider_search_plugins = self._plugins_manager.get_search_plugins()
2345
+ for provider_search_plugin in provider_search_plugins:
2346
+ provider = provider_search_plugin.provider
2347
+ if not hasattr(provider_search_plugin.config, "sort"):
2348
+ sortables[provider] = None
2349
+ continue
2350
+ sortable_params = list(
2351
+ provider_search_plugin.config.sort.get("sort_param_mapping", {}).keys()
2352
+ )
2353
+ if not provider_search_plugin.config.sort.get("max_sort_params"):
2354
+ sortables[provider] = {
2355
+ "sortables": sortable_params,
2356
+ "max_sort_params": None,
2357
+ }
2358
+ continue
2359
+ sortables[provider] = {
2360
+ "sortables": sortable_params,
2361
+ "max_sort_params": provider_search_plugin.config.sort[
2362
+ "max_sort_params"
2363
+ ],
2364
+ }
2365
+ return sortables