eodag 2.12.1__py3-none-any.whl → 3.0.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. eodag/api/core.py +440 -321
  2. eodag/api/product/__init__.py +5 -1
  3. eodag/api/product/_assets.py +57 -2
  4. eodag/api/product/_product.py +89 -68
  5. eodag/api/product/metadata_mapping.py +181 -66
  6. eodag/api/search_result.py +48 -1
  7. eodag/cli.py +20 -6
  8. eodag/config.py +95 -6
  9. eodag/plugins/apis/base.py +8 -165
  10. eodag/plugins/apis/ecmwf.py +36 -24
  11. eodag/plugins/apis/usgs.py +40 -24
  12. eodag/plugins/authentication/aws_auth.py +2 -2
  13. eodag/plugins/authentication/header.py +31 -6
  14. eodag/plugins/authentication/keycloak.py +13 -84
  15. eodag/plugins/authentication/oauth.py +3 -3
  16. eodag/plugins/authentication/openid_connect.py +256 -46
  17. eodag/plugins/authentication/qsauth.py +3 -0
  18. eodag/plugins/authentication/sas_auth.py +8 -1
  19. eodag/plugins/authentication/token.py +92 -46
  20. eodag/plugins/authentication/token_exchange.py +120 -0
  21. eodag/plugins/download/aws.py +86 -91
  22. eodag/plugins/download/base.py +72 -40
  23. eodag/plugins/download/http.py +607 -264
  24. eodag/plugins/download/s3rest.py +28 -15
  25. eodag/plugins/manager.py +74 -57
  26. eodag/plugins/search/__init__.py +36 -0
  27. eodag/plugins/search/base.py +225 -18
  28. eodag/plugins/search/build_search_result.py +389 -32
  29. eodag/plugins/search/cop_marine.py +378 -0
  30. eodag/plugins/search/creodias_s3.py +15 -14
  31. eodag/plugins/search/csw.py +5 -7
  32. eodag/plugins/search/data_request_search.py +44 -20
  33. eodag/plugins/search/qssearch.py +508 -203
  34. eodag/plugins/search/static_stac_search.py +99 -36
  35. eodag/resources/constraints/climate-dt.json +13 -0
  36. eodag/resources/constraints/extremes-dt.json +8 -0
  37. eodag/resources/ext_product_types.json +1 -1
  38. eodag/resources/product_types.yml +1897 -34
  39. eodag/resources/providers.yml +3539 -3277
  40. eodag/resources/stac.yml +48 -54
  41. eodag/resources/stac_api.yml +71 -25
  42. eodag/resources/stac_provider.yml +5 -0
  43. eodag/resources/user_conf_template.yml +51 -3
  44. eodag/rest/__init__.py +6 -0
  45. eodag/rest/cache.py +70 -0
  46. eodag/rest/config.py +68 -0
  47. eodag/rest/constants.py +27 -0
  48. eodag/rest/core.py +757 -0
  49. eodag/rest/server.py +397 -258
  50. eodag/rest/stac.py +438 -307
  51. eodag/rest/types/collections_search.py +44 -0
  52. eodag/rest/types/eodag_search.py +232 -43
  53. eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
  54. eodag/rest/types/stac_search.py +277 -0
  55. eodag/rest/utils/__init__.py +216 -0
  56. eodag/rest/utils/cql_evaluate.py +119 -0
  57. eodag/rest/utils/rfc3339.py +65 -0
  58. eodag/types/__init__.py +99 -9
  59. eodag/types/bbox.py +15 -14
  60. eodag/types/download_args.py +31 -0
  61. eodag/types/search_args.py +58 -7
  62. eodag/types/whoosh.py +81 -0
  63. eodag/utils/__init__.py +72 -9
  64. eodag/utils/constraints.py +37 -37
  65. eodag/utils/exceptions.py +23 -17
  66. eodag/utils/repr.py +113 -0
  67. eodag/utils/requests.py +138 -0
  68. eodag/utils/rest.py +104 -0
  69. eodag/utils/stac_reader.py +100 -16
  70. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/METADATA +65 -44
  71. eodag-3.0.0b2.dist-info/RECORD +110 -0
  72. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/WHEEL +1 -1
  73. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/entry_points.txt +6 -5
  74. eodag/plugins/apis/cds.py +0 -540
  75. eodag/rest/utils.py +0 -1133
  76. eodag-2.12.1.dist-info/RECORD +0 -94
  77. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/LICENSE +0 -0
  78. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/top_level.txt +0 -0
eodag/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
@@ -1338,8 +1445,12 @@ class EODataAccessGateway:
1338
1445
  start=start, end=end, geom=geom, locations=locations, **kwargs
1339
1446
  )
1340
1447
  for i, search_plugin in enumerate(search_plugins):
1341
- itp = items_per_page or search_plugin.config.pagination.get(
1342
- "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1448
+ itp = (
1449
+ items_per_page
1450
+ or getattr(search_plugin.config, "pagination", {}).get(
1451
+ "max_items_per_page"
1452
+ )
1453
+ or DEFAULT_MAX_ITEMS_PER_PAGE
1343
1454
  )
1344
1455
  logger.debug(
1345
1456
  "Searching for all the products with provider %s and a maximum of %s "
@@ -1385,7 +1496,7 @@ class EODataAccessGateway:
1385
1496
 
1386
1497
  def _search_by_id(
1387
1498
  self, uid: str, provider: Optional[str] = None, **kwargs: Any
1388
- ) -> Tuple[SearchResult, int]:
1499
+ ) -> SearchResult:
1389
1500
  """Internal method that enables searching a product by its id.
1390
1501
 
1391
1502
  Keeps requesting providers until a result matching the id is supplied. The
@@ -1405,9 +1516,8 @@ class EODataAccessGateway:
1405
1516
  :type provider: str
1406
1517
  :param kwargs: Search criteria to help finding the right product
1407
1518
  :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)
1519
+ :returns: A search result with one EO product or None at all
1520
+ :rtype: :class:`~eodag.api.search_result.SearchResult`
1411
1521
  """
1412
1522
  product_type = kwargs.get("productType", None)
1413
1523
  if product_type is not None:
@@ -1422,16 +1532,51 @@ class EODataAccessGateway:
1422
1532
  # datacube query string
1423
1533
  _dc_qs = kwargs.pop("_dc_qs", None)
1424
1534
 
1535
+ results = SearchResult([])
1536
+
1425
1537
  for plugin in search_plugins:
1426
1538
  logger.info(
1427
1539
  "Searching product with id '%s' on provider: %s", uid, plugin.provider
1428
1540
  )
1429
1541
  logger.debug("Using plugin class for search: %s", plugin.__class__.__name__)
1430
1542
  plugin.clear()
1543
+
1544
+ # adds maximal pagination to be able to do a search-all + crunch if more
1545
+ # than one result are returned
1546
+ items_per_page = plugin.config.pagination.get(
1547
+ "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1548
+ )
1549
+ kwargs.update(items_per_page=items_per_page)
1431
1550
  if isinstance(plugin, BuildPostSearchResult):
1432
- results, _ = self._do_search(plugin, id=uid, _dc_qs=_dc_qs, **kwargs)
1551
+ kwargs.update(
1552
+ items_per_page=items_per_page,
1553
+ _dc_qs=_dc_qs,
1554
+ )
1433
1555
  else:
1434
- results, _ = self._do_search(plugin, id=uid, **kwargs)
1556
+ kwargs.update(
1557
+ items_per_page=items_per_page,
1558
+ )
1559
+
1560
+ try:
1561
+ # if more than one results are found, try getting them all and then filter using crunch
1562
+ for page_results in self.search_iter_page_plugin(
1563
+ search_plugin=plugin,
1564
+ id=uid,
1565
+ **kwargs,
1566
+ ):
1567
+ results.data.extend(page_results.data)
1568
+ except Exception:
1569
+ if kwargs.get("raise_errors"):
1570
+ raise
1571
+ continue
1572
+
1573
+ # try using crunch to get unique result
1574
+ if (
1575
+ len(results) > 1
1576
+ and len(filtered := results.filter_property(id=uid)) == 1
1577
+ ):
1578
+ results = filtered
1579
+
1435
1580
  if len(results) == 1:
1436
1581
  if not results[0].product_type:
1437
1582
  # guess product type from properties
@@ -1439,20 +1584,36 @@ class EODataAccessGateway:
1439
1584
  results[0].product_type = guesses[0]
1440
1585
  # reset driver
1441
1586
  results[0].driver = results[0].get_driver()
1442
- return results, 1
1587
+ results.number_matched = 1
1588
+ return results
1443
1589
  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
1590
  logger.info(
1452
1591
  "Several products found for this id (%s). You may try searching using more selective criteria.",
1453
1592
  results,
1454
1593
  )
1455
- return SearchResult([]), 0
1594
+ return SearchResult([], 0)
1595
+
1596
+ def _fetch_external_product_type(self, provider: str, product_type: str):
1597
+ plugins = self._plugins_manager.get_search_plugins(provider=provider)
1598
+ plugin = next(plugins)
1599
+
1600
+ kwargs: Dict[str, Any] = {"productType": product_type}
1601
+
1602
+ # append auth if needed
1603
+ if getattr(plugin.config, "need_auth", False):
1604
+ auth_plugin = self._plugins_manager.get_auth_plugin(plugin.provider)
1605
+ if auth_plugin and callable(getattr(auth_plugin, "authenticate", None)):
1606
+ try:
1607
+ kwargs["auth"] = auth_plugin.authenticate()
1608
+ except (AuthenticationError, MisconfiguredError) as e:
1609
+ logger.warning(f"Could not authenticate on {provider}: {str(e)}")
1610
+ else:
1611
+ logger.warning(
1612
+ f"Could not authenticate on {provider} using {auth_plugin} plugin"
1613
+ )
1614
+
1615
+ product_type_config = plugin.discover_product_types(**kwargs)
1616
+ self.update_product_types_list({provider: product_type_config})
1456
1617
 
1457
1618
  def _prepare_search(
1458
1619
  self,
@@ -1532,7 +1693,7 @@ class EODataAccessGateway:
1532
1693
  try:
1533
1694
  product_type = self.get_product_type_from_alias(product_type)
1534
1695
  except NoMatchingProductType:
1535
- logger.warning("unknown product type " + product_type)
1696
+ logger.info("unknown product type " + product_type)
1536
1697
  kwargs["productType"] = product_type
1537
1698
 
1538
1699
  if start is not None:
@@ -1567,7 +1728,16 @@ class EODataAccessGateway:
1567
1728
  logger.debug(
1568
1729
  f"Fetching external product types sources to find {product_type} product type"
1569
1730
  )
1570
- self.fetch_product_types_list()
1731
+ if provider:
1732
+ # Try to get specific product type from external provider
1733
+ self._fetch_external_product_type(provider, product_type)
1734
+ if (
1735
+ not provider
1736
+ or product_type
1737
+ not in self._plugins_manager.product_type_to_provider_config_map.keys()
1738
+ ):
1739
+ # no provider or still not found -> fetch all external product types
1740
+ self.fetch_product_types_list()
1571
1741
 
1572
1742
  preferred_provider = self.get_preferred_provider()[0]
1573
1743
 
@@ -1617,8 +1787,7 @@ class EODataAccessGateway:
1617
1787
  for p in self.list_product_types(
1618
1788
  search_plugin.provider, fetch_providers=False
1619
1789
  )
1620
- if p["ID"] == product_type
1621
- or ("_id" in p and p["_id"] == product_type)
1790
+ if p["_id"] == product_type
1622
1791
  ][0],
1623
1792
  **{"productType": product_type},
1624
1793
  )
@@ -1638,10 +1807,10 @@ class EODataAccessGateway:
1638
1807
  def _do_search(
1639
1808
  self,
1640
1809
  search_plugin: Union[Search, Api],
1641
- count: bool = True,
1810
+ count: bool = False,
1642
1811
  raise_errors: bool = False,
1643
1812
  **kwargs: Any,
1644
- ) -> Tuple[SearchResult, Optional[int]]:
1813
+ ) -> SearchResult:
1645
1814
  """Internal method that performs a search on a given provider.
1646
1815
 
1647
1816
  :param search_plugin: A search plugin
@@ -1653,14 +1822,16 @@ class EODataAccessGateway:
1653
1822
  :type raise_errors: bool
1654
1823
  :param kwargs: Some other criteria that will be used to do the search
1655
1824
  :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
1825
+ :returns: A collection of EO products matching the criteria
1658
1826
  :rtype: tuple(:class:`~eodag.api.search_result.SearchResult`, int or None)
1659
1827
  """
1660
1828
  max_items_per_page = getattr(search_plugin.config, "pagination", {}).get(
1661
1829
  "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1662
1830
  )
1663
- if kwargs.get("items_per_page", DEFAULT_ITEMS_PER_PAGE) > max_items_per_page:
1831
+ if (
1832
+ kwargs.get("items_per_page", DEFAULT_ITEMS_PER_PAGE) > max_items_per_page
1833
+ and max_items_per_page > 0
1834
+ ):
1664
1835
  logger.warning(
1665
1836
  "EODAG believes that you might have asked for more products/items "
1666
1837
  "than the maximum allowed by '%s': %s > %s. Try to lower "
@@ -1676,51 +1847,18 @@ class EODataAccessGateway:
1676
1847
  can_authenticate = callable(getattr(auth_plugin, "authenticate", None))
1677
1848
 
1678
1849
  results: List[EOProduct] = []
1679
- total_results = 0
1850
+ total_results: Optional[int] = 0 if count else None
1680
1851
 
1681
1852
  try:
1853
+ prep = PreparedSearch(count=count)
1682
1854
  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
1855
+ prep.auth = auth_plugin.authenticate()
1856
+
1857
+ prep.auth_plugin = auth_plugin
1858
+ prep.page = kwargs.pop("page", None)
1859
+ prep.items_per_page = kwargs.pop("items_per_page", None)
1860
+
1861
+ res, nb_res = search_plugin.query(prep, **kwargs)
1724
1862
 
1725
1863
  if not isinstance(res, list):
1726
1864
  raise PluginImplementationError(
@@ -1744,7 +1882,6 @@ class EODataAccessGateway:
1744
1882
  try:
1745
1883
  guesses = self.guess_product_type(
1746
1884
  **{
1747
- # k:str(v) for k,v in eo_product.properties.items()
1748
1885
  k: pattern.sub("", str(v).upper())
1749
1886
  for k, v in eo_product.properties.items()
1750
1887
  if k
@@ -1779,8 +1916,12 @@ class EODataAccessGateway:
1779
1916
  eo_product.register_downloader(download_plugin, auth_plugin)
1780
1917
 
1781
1918
  results.extend(res)
1782
- total_results = None if nb_res is None else total_results + nb_res
1783
- if count:
1919
+ total_results = (
1920
+ None
1921
+ if (nb_res is None or total_results is None)
1922
+ else total_results + nb_res
1923
+ )
1924
+ if count and nb_res is not None:
1784
1925
  logger.info(
1785
1926
  "Found %s result(s) on provider '%s'",
1786
1927
  nb_res,
@@ -1805,6 +1946,9 @@ class EODataAccessGateway:
1805
1946
  if not raise_errors:
1806
1947
  log_msg += " Raise verbosity of log messages for details"
1807
1948
  logger.info(log_msg)
1949
+ # keep only the message from exception args
1950
+ if len(e.args) > 1:
1951
+ e.args = (e.args[0],)
1808
1952
  if raise_errors:
1809
1953
  # Raise the error, letting the application wrapping eodag know that
1810
1954
  # something went bad. This way it will be able to decide what to do next
@@ -1815,7 +1959,7 @@ class EODataAccessGateway:
1815
1959
  search_plugin.provider,
1816
1960
  )
1817
1961
  self.search_errors.add((search_plugin.provider, e))
1818
- return SearchResult(results), total_results
1962
+ return SearchResult(results, total_results)
1819
1963
 
1820
1964
  def crunch(self, results: SearchResult, **kwargs: Any) -> SearchResult:
1821
1965
  """Apply the filters given through the keyword arguments to the results
@@ -1865,7 +2009,7 @@ class EODataAccessGateway:
1865
2009
  progress_callback: Optional[ProgressCallback] = None,
1866
2010
  wait: int = DEFAULT_DOWNLOAD_WAIT,
1867
2011
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1868
- **kwargs: Any,
2012
+ **kwargs: Unpack[DownloadConf],
1869
2013
  ) -> List[str]:
1870
2014
  """Download all products resulting from a search.
1871
2015
 
@@ -1978,6 +2122,7 @@ class EODataAccessGateway:
1978
2122
  provider: Optional[str] = None,
1979
2123
  productType: Optional[str] = None,
1980
2124
  timeout: int = HTTP_REQ_TIMEOUT,
2125
+ ssl_verify: bool = True,
1981
2126
  **kwargs: Any,
1982
2127
  ) -> SearchResult:
1983
2128
  """Loads STAC items from a geojson file / STAC catalog or collection, and convert to SearchResult.
@@ -2011,6 +2156,7 @@ class EODataAccessGateway:
2011
2156
  recursive=recursive,
2012
2157
  max_connections=max_connections,
2013
2158
  timeout=timeout,
2159
+ ssl_verify=ssl_verify,
2014
2160
  )
2015
2161
  feature_collection = geojson.FeatureCollection(features)
2016
2162
 
@@ -2027,12 +2173,14 @@ class EODataAccessGateway:
2027
2173
  )
2028
2174
  )
2029
2175
 
2030
- products, _ = self.search(productType=productType, provider=provider, **kwargs)
2176
+ search_result = self.search(
2177
+ productType=productType, provider=provider, **kwargs
2178
+ )
2031
2179
 
2032
2180
  # restore plugin._request
2033
2181
  plugin._request = plugin_request
2034
2182
 
2035
- return products
2183
+ return search_result
2036
2184
 
2037
2185
  def download(
2038
2186
  self,
@@ -2040,7 +2188,7 @@ class EODataAccessGateway:
2040
2188
  progress_callback: Optional[ProgressCallback] = None,
2041
2189
  wait: int = DEFAULT_DOWNLOAD_WAIT,
2042
2190
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
2043
- **kwargs: Any,
2191
+ **kwargs: Unpack[DownloadConf],
2044
2192
  ) -> str:
2045
2193
  """Download a single product.
2046
2194
 
@@ -2120,9 +2268,7 @@ class EODataAccessGateway:
2120
2268
  return self._plugins_manager.get_crunch_plugin(name, **plugin_conf)
2121
2269
 
2122
2270
  def list_queryables(
2123
- self,
2124
- provider: Optional[str] = None,
2125
- **kwargs: Any,
2271
+ self, provider: Optional[str] = None, **kwargs: Any
2126
2272
  ) -> Dict[str, Annotated[Any, FieldInfo]]:
2127
2273
  """Fetch the queryable properties for a given product type and/or provider.
2128
2274
 
@@ -2131,113 +2277,53 @@ class EODataAccessGateway:
2131
2277
  :param kwargs: additional filters for queryables (`productType` or other search
2132
2278
  arguments)
2133
2279
  :type kwargs: Any
2280
+
2281
+ :raises UnsupportedProductType: If the specified product type is not available for the
2282
+ provider.
2283
+
2134
2284
  :returns: A dict containing the EODAG queryable properties, associating
2135
2285
  parameters to their annotated type
2136
2286
  :rtype: Dict[str, Annotated[Any, FieldInfo]]
2137
2287
  """
2138
- # unknown product type
2139
2288
  available_product_types = [
2140
- pt["ID"] for pt in self.list_product_types(fetch_providers=False)
2289
+ pt["ID"]
2290
+ for pt in self.list_product_types(provider=provider, fetch_providers=False)
2141
2291
  ]
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
- )
2292
+ product_type = kwargs.get("productType")
2160
2293
 
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()
2294
+ if product_type:
2295
+ try:
2296
+ kwargs["productType"] = product_type = self.get_product_type_from_alias(
2297
+ product_type
2168
2298
  )
2169
- return {
2170
- k: v
2171
- for k, v in providers_available_queryables.popitem()[1].items()
2172
- if k in queryables_keys
2173
- }
2299
+ except NoMatchingProductType as e:
2300
+ raise UnsupportedProductType(f"{product_type} is not available") from e
2174
2301
 
2175
- all_queryables = copy_deepcopy(
2176
- model_fields_to_annotated(Queryables.model_fields)
2177
- )
2302
+ if product_type and product_type not in available_product_types:
2303
+ self.fetch_product_types_list()
2178
2304
 
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
2305
+ if not provider and not product_type:
2185
2306
  return model_fields_to_annotated(CommonQueryables.model_fields)
2186
2307
 
2187
- providers_available_queryables[plugin.provider] = dict()
2308
+ providers_queryables: Dict[str, Dict[str, Annotated[Any, FieldInfo]]] = {}
2188
2309
 
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}"
2310
+ for plugin in self._plugins_manager.get_search_plugins(product_type, provider):
2311
+ if getattr(plugin.config, "need_auth", False) and (
2312
+ auth := self._plugins_manager.get_auth_plugin(plugin.provider)
2313
+ ):
2314
+ plugin.auth = auth.authenticate()
2315
+ providers_queryables[plugin.provider] = plugin.list_queryables(
2316
+ filters=kwargs, product_type=product_type
2199
2317
  )
2200
2318
 
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", {})
2319
+ queryable_keys: Set[str] = set.intersection( # type: ignore
2320
+ *[set(q.keys()) for q in providers_queryables.values()]
2208
2321
  )
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])
2322
+ queryables = {
2323
+ k: v
2324
+ for k, v in list(providers_queryables.values())[0].items()
2325
+ if k in queryable_keys
2326
+ }
2241
2327
 
2242
2328
  # always keep at least CommonQueryables
2243
2329
  common_queryables = copy_deepcopy(CommonQueryables.model_fields)
@@ -2245,6 +2331,39 @@ class EODataAccessGateway:
2245
2331
  if key in kwargs:
2246
2332
  queryable.default = kwargs[key]
2247
2333
 
2248
- provider_queryables.update(model_fields_to_annotated(common_queryables))
2334
+ queryables.update(model_fields_to_annotated(common_queryables))
2335
+
2336
+ return queryables
2337
+
2338
+ def available_sortables(self) -> Dict[str, Optional[ProviderSortables]]:
2339
+ """For each provider, gives its available sortable parameter(s) and its maximum
2340
+ number of them if it supports the sorting feature, otherwise gives None.
2249
2341
 
2250
- return provider_queryables
2342
+ :returns: A dictionnary with providers as keys and dictionnary of sortable parameter(s) and
2343
+ its (their) maximum number as value(s).
2344
+ :rtype: dict
2345
+ :raises: :class:`~eodag.utils.exceptions.UnsupportedProvider`
2346
+ """
2347
+ sortables: Dict[str, Optional[ProviderSortables]] = {}
2348
+ provider_search_plugins = self._plugins_manager.get_search_plugins()
2349
+ for provider_search_plugin in provider_search_plugins:
2350
+ provider = provider_search_plugin.provider
2351
+ if not hasattr(provider_search_plugin.config, "sort"):
2352
+ sortables[provider] = None
2353
+ continue
2354
+ sortable_params = list(
2355
+ provider_search_plugin.config.sort.get("sort_param_mapping", {}).keys()
2356
+ )
2357
+ if not provider_search_plugin.config.sort.get("max_sort_params"):
2358
+ sortables[provider] = {
2359
+ "sortables": sortable_params,
2360
+ "max_sort_params": None,
2361
+ }
2362
+ continue
2363
+ sortables[provider] = {
2364
+ "sortables": sortable_params,
2365
+ "max_sort_params": provider_search_plugin.config.sort[
2366
+ "max_sort_params"
2367
+ ],
2368
+ }
2369
+ return sortables