eodag 3.0.0b3__py3-none-any.whl → 3.1.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 +292 -198
  2. eodag/api/product/_assets.py +6 -6
  3. eodag/api/product/_product.py +18 -18
  4. eodag/api/product/metadata_mapping.py +51 -14
  5. eodag/api/search_result.py +29 -3
  6. eodag/cli.py +57 -20
  7. eodag/config.py +413 -117
  8. eodag/plugins/apis/base.py +10 -4
  9. eodag/plugins/apis/ecmwf.py +49 -16
  10. eodag/plugins/apis/usgs.py +30 -7
  11. eodag/plugins/authentication/aws_auth.py +14 -5
  12. eodag/plugins/authentication/base.py +10 -1
  13. eodag/plugins/authentication/generic.py +14 -3
  14. eodag/plugins/authentication/header.py +12 -4
  15. eodag/plugins/authentication/keycloak.py +41 -22
  16. eodag/plugins/authentication/oauth.py +11 -1
  17. eodag/plugins/authentication/openid_connect.py +178 -163
  18. eodag/plugins/authentication/qsauth.py +12 -4
  19. eodag/plugins/authentication/sas_auth.py +19 -2
  20. eodag/plugins/authentication/token.py +93 -15
  21. eodag/plugins/authentication/token_exchange.py +19 -19
  22. eodag/plugins/crunch/base.py +4 -1
  23. eodag/plugins/crunch/filter_date.py +5 -2
  24. eodag/plugins/crunch/filter_latest_intersect.py +5 -4
  25. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  26. eodag/plugins/crunch/filter_overlap.py +5 -7
  27. eodag/plugins/crunch/filter_property.py +6 -6
  28. eodag/plugins/download/aws.py +50 -34
  29. eodag/plugins/download/base.py +41 -50
  30. eodag/plugins/download/creodias_s3.py +40 -2
  31. eodag/plugins/download/http.py +221 -195
  32. eodag/plugins/download/s3rest.py +25 -25
  33. eodag/plugins/manager.py +168 -23
  34. eodag/plugins/search/base.py +106 -39
  35. eodag/plugins/search/build_search_result.py +1065 -324
  36. eodag/plugins/search/cop_marine.py +112 -29
  37. eodag/plugins/search/creodias_s3.py +45 -24
  38. eodag/plugins/search/csw.py +41 -1
  39. eodag/plugins/search/data_request_search.py +109 -9
  40. eodag/plugins/search/qssearch.py +549 -257
  41. eodag/plugins/search/static_stac_search.py +20 -21
  42. eodag/resources/ext_product_types.json +1 -1
  43. eodag/resources/product_types.yml +577 -87
  44. eodag/resources/providers.yml +1619 -2776
  45. eodag/resources/stac.yml +3 -163
  46. eodag/resources/user_conf_template.yml +112 -97
  47. eodag/rest/config.py +1 -2
  48. eodag/rest/constants.py +0 -1
  49. eodag/rest/core.py +138 -98
  50. eodag/rest/errors.py +181 -0
  51. eodag/rest/server.py +55 -329
  52. eodag/rest/stac.py +93 -544
  53. eodag/rest/types/eodag_search.py +19 -8
  54. eodag/rest/types/queryables.py +6 -8
  55. eodag/rest/types/stac_search.py +11 -2
  56. eodag/rest/utils/__init__.py +3 -0
  57. eodag/types/__init__.py +71 -18
  58. eodag/types/download_args.py +3 -3
  59. eodag/types/queryables.py +180 -73
  60. eodag/types/search_args.py +3 -3
  61. eodag/types/whoosh.py +126 -0
  62. eodag/utils/__init__.py +147 -66
  63. eodag/utils/exceptions.py +47 -26
  64. eodag/utils/logging.py +37 -77
  65. eodag/utils/repr.py +65 -6
  66. eodag/utils/requests.py +11 -13
  67. eodag/utils/stac_reader.py +1 -1
  68. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/METADATA +80 -81
  69. eodag-3.1.0b1.dist-info/RECORD +108 -0
  70. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
  71. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +4 -2
  72. eodag/resources/constraints/climate-dt.json +0 -13
  73. eodag/resources/constraints/extremes-dt.json +0 -8
  74. eodag/utils/constraints.py +0 -244
  75. eodag-3.0.0b3.dist-info/RECORD +0 -110
  76. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/LICENSE +0 -0
  77. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
eodag/api/core.py CHANGED
@@ -17,29 +17,34 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
+ import datetime
20
21
  import logging
21
22
  import os
22
23
  import re
23
24
  import shutil
24
25
  import tempfile
25
26
  from operator import itemgetter
26
- from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Tuple, Union
27
+ from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Tuple, Union
27
28
 
28
29
  import geojson
29
30
  import pkg_resources
30
31
  import yaml.parser
31
32
  from pkg_resources import resource_filename
32
- from pydantic.fields import FieldInfo
33
33
  from whoosh import analysis, fields
34
34
  from whoosh.fields import Schema
35
- from whoosh.index import create_in, exists_in, open_dir
35
+ from whoosh.index import exists_in, open_dir
36
36
  from whoosh.qparser import QueryParser
37
37
 
38
- from eodag.api.product.metadata_mapping import mtd_cfg_as_conversion_and_querypath
38
+ from eodag.api.product.metadata_mapping import (
39
+ ONLINE_STATUS,
40
+ mtd_cfg_as_conversion_and_querypath,
41
+ )
39
42
  from eodag.api.search_result import SearchResult
40
43
  from eodag.config import (
44
+ PLUGINS_TOPICS_KEYS,
41
45
  PluginConfig,
42
46
  SimpleYamlProxyConfig,
47
+ credentials_in_auth,
43
48
  get_ext_product_types_conf,
44
49
  load_default_config,
45
50
  load_stac_provider_config,
@@ -48,13 +53,15 @@ from eodag.config import (
48
53
  override_config_from_file,
49
54
  override_config_from_mapping,
50
55
  provider_config_init,
56
+ share_credentials,
51
57
  )
52
58
  from eodag.plugins.manager import PluginManager
53
59
  from eodag.plugins.search import PreparedSearch
54
- from eodag.plugins.search.build_search_result import BuildPostSearchResult
60
+ from eodag.plugins.search.build_search_result import MeteoblueSearch
61
+ from eodag.plugins.search.qssearch import PostJsonSearch
55
62
  from eodag.types import model_fields_to_annotated
56
- from eodag.types.queryables import CommonQueryables
57
- from eodag.types.whoosh import EODAGQueryParser
63
+ from eodag.types.queryables import CommonQueryables, QueryablesDict
64
+ from eodag.types.whoosh import EODAGQueryParser, create_in
58
65
  from eodag.utils import (
59
66
  DEFAULT_DOWNLOAD_TIMEOUT,
60
67
  DEFAULT_DOWNLOAD_WAIT,
@@ -65,7 +72,6 @@ from eodag.utils import (
65
72
  HTTP_REQ_TIMEOUT,
66
73
  MockResponse,
67
74
  _deprecated,
68
- copy_deepcopy,
69
75
  get_geometry_from_various,
70
76
  makedirs,
71
77
  obj_md5sum,
@@ -76,7 +82,6 @@ from eodag.utils import (
76
82
  from eodag.utils.exceptions import (
77
83
  AuthenticationError,
78
84
  EodagError,
79
- MisconfiguredError,
80
85
  NoMatchingProductType,
81
86
  PluginImplementationError,
82
87
  RequestError,
@@ -96,7 +101,7 @@ if TYPE_CHECKING:
96
101
  from eodag.plugins.search.base import Search
97
102
  from eodag.types import ProviderSortables
98
103
  from eodag.types.download_args import DownloadConf
99
- from eodag.utils import Annotated, DownloadedCallback, ProgressCallback, Unpack
104
+ from eodag.utils import DownloadedCallback, ProgressCallback, Unpack
100
105
 
101
106
  logger = logging.getLogger("eodag.core")
102
107
 
@@ -166,10 +171,15 @@ class EODataAccessGateway:
166
171
  # Second level override: From environment variables
167
172
  override_config_from_env(self.providers_config)
168
173
 
174
+ # share credentials between updated plugins confs
175
+ share_credentials(self.providers_config)
176
+
169
177
  # init updated providers conf
170
- stac_provider_config = load_stac_provider_config()
171
178
  for provider in self.providers_config.keys():
172
- provider_config_init(self.providers_config[provider], stac_provider_config)
179
+ provider_config_init(
180
+ self.providers_config[provider],
181
+ load_stac_provider_config(),
182
+ )
173
183
 
174
184
  # re-build _plugins_manager using up-to-date providers_config
175
185
  self._plugins_manager.rebuild(self.providers_config)
@@ -197,9 +207,10 @@ class EODataAccessGateway:
197
207
  "eodag",
198
208
  os.path.join("resources", "locations_conf_template.yml"),
199
209
  )
200
- with open(locations_conf_template) as infile, open(
201
- locations_conf_path, "w"
202
- ) as outfile:
210
+ with (
211
+ open(locations_conf_template) as infile,
212
+ open(locations_conf_path, "w") as outfile,
213
+ ):
203
214
  # The template contains paths in the form of:
204
215
  # /path/to/locations/file.shp
205
216
  path_template = "/path/to/locations/"
@@ -215,7 +226,6 @@ class EODataAccessGateway:
215
226
  os.path.join(self.conf_dir, "shp"),
216
227
  )
217
228
  self.set_locations_conf(locations_conf_path)
218
- self.search_errors: Set = set()
219
229
 
220
230
  def get_version(self) -> str:
221
231
  """Get eodag package version"""
@@ -296,13 +306,18 @@ class EODataAccessGateway:
296
306
  product_type, **{"md5": self.product_types_config_md5}
297
307
  )
298
308
  # add to index
299
- ix_writer.add_document(
300
- **{
301
- k: v
302
- for k, v in versioned_product_type.items()
303
- if k in product_types_schema.names()
304
- }
305
- )
309
+ try:
310
+ ix_writer.add_document(
311
+ **{
312
+ k: v
313
+ for k, v in versioned_product_type.items()
314
+ if k in product_types_schema.names()
315
+ }
316
+ )
317
+ except TypeError as e:
318
+ logger.error(
319
+ f"Cannot write product type {product_type['ID']} into index. e={e} product_type={product_type}"
320
+ )
306
321
  ix_writer.commit()
307
322
 
308
323
  def set_preferred_provider(self, provider: str) -> None:
@@ -333,14 +348,24 @@ class EODataAccessGateway:
333
348
  preferred, priority = max(providers_with_priority, key=itemgetter(1))
334
349
  return preferred, priority
335
350
 
336
- def update_providers_config(self, yaml_conf: str) -> None:
351
+ def update_providers_config(
352
+ self,
353
+ yaml_conf: Optional[str] = None,
354
+ dict_conf: Optional[Dict[str, Any]] = None,
355
+ ) -> None:
337
356
  """Update providers configuration with given input.
338
357
  Can be used to add a provider to existing configuration or update
339
358
  an existing one.
340
359
 
341
360
  :param yaml_conf: YAML formated provider configuration
361
+ :param dict_conf: provider configuration as dictionary in place of ``yaml_conf``
342
362
  """
343
- conf_update = yaml.safe_load(yaml_conf)
363
+ if dict_conf is not None:
364
+ conf_update = dict_conf
365
+ elif yaml_conf is not None:
366
+ conf_update = yaml.safe_load(yaml_conf)
367
+ else:
368
+ return None
344
369
 
345
370
  # restore the pruned configuration
346
371
  for provider in list(self._pruned_providers_config.keys()):
@@ -355,9 +380,14 @@ class EODataAccessGateway:
355
380
 
356
381
  override_config_from_mapping(self.providers_config, conf_update)
357
382
 
358
- stac_provider_config = load_stac_provider_config()
383
+ # share credentials between updated plugins confs
384
+ share_credentials(self.providers_config)
385
+
359
386
  for provider in conf_update.keys():
360
- provider_config_init(self.providers_config[provider], stac_provider_config)
387
+ provider_config_init(
388
+ self.providers_config[provider],
389
+ load_stac_provider_config(),
390
+ )
361
391
  setattr(self.providers_config[provider], "product_types_fetched", False)
362
392
  # re-create _plugins_manager using up-to-date providers_config
363
393
  self._plugins_manager.build_product_type_to_provider_config_map()
@@ -419,14 +449,11 @@ class EODataAccessGateway:
419
449
 
420
450
  # api plugin usage: remove unneeded search/download/auth plugin conf
421
451
  if conf_dict[name].get("api"):
422
- conf_dict[name].pop("search", None)
423
- conf_dict[name].pop("download", None)
424
- conf_dict[name].pop("auth", None)
452
+ for k in PLUGINS_TOPICS_KEYS:
453
+ if k != "api":
454
+ conf_dict[name].pop(k, None)
425
455
 
426
- override_config_from_mapping(self.providers_config, conf_dict)
427
- provider_config_init(self.providers_config[name], load_stac_provider_config())
428
- setattr(self.providers_config[name], "product_types_fetched", False)
429
- self._plugins_manager.build_product_type_to_provider_config_map()
456
+ self.update_providers_config(dict_conf=conf_dict)
430
457
 
431
458
  if priority is None:
432
459
  self.set_preferred_provider(name)
@@ -452,12 +479,7 @@ class EODataAccessGateway:
452
479
 
453
480
  # check authentication
454
481
  if hasattr(conf, "api") and getattr(conf.api, "need_auth", False):
455
- credentials_exist = any(
456
- [
457
- cred is not None
458
- for cred in getattr(conf.api, "credentials", {}).values()
459
- ]
460
- )
482
+ credentials_exist = credentials_in_auth(conf.api)
461
483
  if not credentials_exist:
462
484
  # credentials needed but not found
463
485
  self._pruned_providers_config[provider] = self.providers_config.pop(
@@ -469,7 +491,7 @@ class EODataAccessGateway:
469
491
  provider,
470
492
  )
471
493
  elif hasattr(conf, "search") and getattr(conf.search, "need_auth", False):
472
- if not hasattr(conf, "auth"):
494
+ if not hasattr(conf, "auth") and not hasattr(conf, "search_auth"):
473
495
  # credentials needed but no auth plugin was found
474
496
  self._pruned_providers_config[provider] = self.providers_config.pop(
475
497
  provider
@@ -480,11 +502,13 @@ class EODataAccessGateway:
480
502
  provider,
481
503
  )
482
504
  continue
483
- credentials_exist = any(
484
- [
485
- cred is not None
486
- for cred in getattr(conf.auth, "credentials", {}).values()
487
- ]
505
+ credentials_exist = (
506
+ hasattr(conf, "search_auth")
507
+ and credentials_in_auth(conf.search_auth)
508
+ ) or (
509
+ not hasattr(conf, "search_auth")
510
+ and hasattr(conf, "auth")
511
+ and credentials_in_auth(conf.auth)
488
512
  )
489
513
  if not credentials_exist:
490
514
  # credentials needed but not found
@@ -599,21 +623,32 @@ class EODataAccessGateway:
599
623
  def fetch_product_types_list(self, provider: Optional[str] = None) -> None:
600
624
  """Fetch product types list and update if needed
601
625
 
602
- :param provider: (optional) The name of a provider for which product types list
603
- should be updated. Defaults to all providers (None value).
626
+ :param provider: The name of a provider or provider-group for which product types
627
+ list should be updated. Defaults to all providers (None value).
604
628
  """
629
+ providers_to_fetch = list(self.providers_config.keys())
630
+ # check if some providers are grouped under a group name which is not a provider name
605
631
  if provider is not None and provider not in self.providers_config:
606
- return
632
+ providers_to_fetch = [
633
+ p
634
+ for p, pconf in self.providers_config.items()
635
+ if provider == getattr(pconf, "group", None)
636
+ ]
637
+ if providers_to_fetch:
638
+ logger.info(
639
+ f"Fetch product types for {provider} group: {', '.join(providers_to_fetch)}"
640
+ )
641
+ else:
642
+ return None
643
+ elif provider is not None:
644
+ providers_to_fetch = [provider]
607
645
 
608
646
  # providers discovery confs that are fetchable
609
647
  providers_discovery_configs_fetchable: Dict[str, Any] = {}
610
648
  # check if any provider has not already been fetched for product types
611
649
  already_fetched = True
612
- for provider_to_fetch, provider_config in (
613
- {provider: self.providers_config[provider]}.items()
614
- if provider
615
- else self.providers_config.items()
616
- ):
650
+ for provider_to_fetch in providers_to_fetch:
651
+ provider_config = self.providers_config[provider_to_fetch]
617
652
  # get discovery conf
618
653
  if hasattr(provider_config, "search"):
619
654
  provider_search_config = provider_config.search
@@ -735,11 +770,20 @@ class EODataAccessGateway:
735
770
  ) -> Optional[Dict[str, Any]]:
736
771
  """Fetch providers for product types
737
772
 
738
- :param provider: (optional) The name of a provider to fetch. Defaults to all
739
- providers (None value).
773
+ :param provider: The name of a provider or provider-group to fetch. Defaults to
774
+ all providers (None value).
740
775
  :returns: external product types configuration
741
776
  """
742
- if provider and provider not in self.providers_config:
777
+ grouped_providers = [
778
+ p
779
+ for p, provider_config in self.providers_config.items()
780
+ if provider == getattr(provider_config, "group", None)
781
+ ]
782
+ if provider and provider not in self.providers_config and grouped_providers:
783
+ logger.info(
784
+ f"Discover product types for {provider} group: {', '.join(grouped_providers)}"
785
+ )
786
+ elif provider and provider not in self.providers_config:
743
787
  raise UnsupportedProvider(
744
788
  f"The requested provider is not (yet) supported: {provider}"
745
789
  )
@@ -748,7 +792,9 @@ class EODataAccessGateway:
748
792
  p
749
793
  for p in (
750
794
  [
751
- provider,
795
+ p
796
+ for p in self.providers_config
797
+ if p in grouped_providers + [provider]
752
798
  ]
753
799
  if provider
754
800
  else self.available_providers()
@@ -762,7 +808,9 @@ class EODataAccessGateway:
762
808
  search_plugin_config = self.providers_config[provider].api
763
809
  else:
764
810
  return None
765
- if getattr(search_plugin_config, "discover_product_types", None):
811
+ if getattr(search_plugin_config, "discover_product_types", {}).get(
812
+ "fetch_url", None
813
+ ):
766
814
  search_plugin: Union[Search, Api] = next(
767
815
  self._plugins_manager.get_search_plugins(provider=provider)
768
816
  )
@@ -773,23 +821,15 @@ class EODataAccessGateway:
773
821
  continue
774
822
  # append auth to search plugin if needed
775
823
  if getattr(search_plugin.config, "need_auth", False):
776
- auth_plugin = self._plugins_manager.get_auth_plugin(
777
- search_plugin.provider
778
- )
779
- if auth_plugin and callable(
780
- getattr(auth_plugin, "authenticate", None)
824
+ if auth := self._plugins_manager.get_auth(
825
+ search_plugin.provider,
826
+ getattr(search_plugin.config, "api_endpoint", None),
827
+ search_plugin.config,
781
828
  ):
782
- try:
783
- kwargs["auth"] = auth_plugin.authenticate()
784
- except (AuthenticationError, MisconfiguredError) as e:
785
- logger.warning(
786
- f"Could not authenticate on {provider}: {str(e)}"
787
- )
788
- ext_product_types_conf[provider] = None
789
- continue
829
+ kwargs["auth"] = auth
790
830
  else:
791
- logger.warning(
792
- f"Could not authenticate on {provider} using {auth_plugin} plugin"
831
+ logger.debug(
832
+ f"Could not authenticate on {provider} for product types discovery"
793
833
  )
794
834
  ext_product_types_conf[provider] = None
795
835
  continue
@@ -815,7 +855,9 @@ class EODataAccessGateway:
815
855
  ) or getattr(self.providers_config[provider], "api", None)
816
856
  if search_plugin_config is None:
817
857
  continue
818
- if not hasattr(search_plugin_config, "discover_product_types"):
858
+ if not getattr(
859
+ search_plugin_config, "discover_product_types", {}
860
+ ).get("fetch_url", None):
819
861
  # conf has been updated and provider product types are no more discoverable
820
862
  continue
821
863
  provider_products_config = (
@@ -874,7 +916,7 @@ class EODataAccessGateway:
874
916
  new_product_types.append(new_product_type)
875
917
  if new_product_types:
876
918
  logger.debug(
877
- f"Added product types {str(new_product_types)} for {provider}"
919
+ f"Added {len(new_product_types)} product types for {provider}"
878
920
  )
879
921
 
880
922
  elif provider not in self.providers_config:
@@ -1046,20 +1088,28 @@ class EODataAccessGateway:
1046
1088
 
1047
1089
  # datetime filtering
1048
1090
  if missionStartDate or missionEndDate:
1091
+ min_aware = datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
1092
+ max_aware = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
1049
1093
  guesses = [
1050
1094
  g
1051
1095
  for g in guesses
1052
1096
  if (
1053
- not missionEndDate
1054
- or g.get("missionStartDate")
1055
- and rfc3339_str_to_datetime(g["missionStartDate"])
1056
- <= rfc3339_str_to_datetime(missionEndDate)
1057
- )
1058
- and (
1059
- not missionStartDate
1060
- or g.get("missionEndDate")
1061
- and rfc3339_str_to_datetime(g["missionEndDate"])
1062
- >= rfc3339_str_to_datetime(missionStartDate)
1097
+ max(
1098
+ rfc3339_str_to_datetime(missionStartDate)
1099
+ if missionStartDate
1100
+ else min_aware,
1101
+ rfc3339_str_to_datetime(g["missionStartDate"])
1102
+ if g.get("missionStartDate")
1103
+ else min_aware,
1104
+ )
1105
+ <= min(
1106
+ rfc3339_str_to_datetime(missionEndDate)
1107
+ if missionEndDate
1108
+ else max_aware,
1109
+ rfc3339_str_to_datetime(g["missionEndDate"])
1110
+ if g.get("missionEndDate")
1111
+ else max_aware,
1112
+ )
1063
1113
  )
1064
1114
  ]
1065
1115
 
@@ -1155,7 +1205,7 @@ class EODataAccessGateway:
1155
1205
  items_per_page=items_per_page,
1156
1206
  )
1157
1207
 
1158
- self.search_errors = set()
1208
+ errors: List[Tuple[str, Exception]] = []
1159
1209
  # Loop over available providers and return the first non-empty results
1160
1210
  for i, search_plugin in enumerate(search_plugins):
1161
1211
  search_plugin.clear()
@@ -1165,17 +1215,19 @@ class EODataAccessGateway:
1165
1215
  raise_errors=raise_errors,
1166
1216
  **search_kwargs,
1167
1217
  )
1218
+ errors.extend(search_results.errors)
1168
1219
  if len(search_results) == 0 and i < len(search_plugins) - 1:
1169
1220
  logger.warning(
1170
1221
  f"No result could be obtained from provider {search_plugin.provider}, "
1171
1222
  "we will try to get the data from another provider",
1172
1223
  )
1173
1224
  elif len(search_results) > 0:
1225
+ search_results.errors = errors
1174
1226
  return search_results
1175
1227
 
1176
1228
  if i > 1:
1177
1229
  logger.error("No result could be obtained from any available provider")
1178
- return SearchResult([], 0) if count else SearchResult([])
1230
+ return SearchResult([], 0, errors) if count else SearchResult([], errors=errors)
1179
1231
 
1180
1232
  def search_iter_page(
1181
1233
  self,
@@ -1435,7 +1487,7 @@ class EODataAccessGateway:
1435
1487
  )
1436
1488
  or DEFAULT_MAX_ITEMS_PER_PAGE
1437
1489
  )
1438
- logger.debug(
1490
+ logger.info(
1439
1491
  "Searching for all the products with provider %s and a maximum of %s "
1440
1492
  "items per page.",
1441
1493
  search_plugin.provider,
@@ -1503,7 +1555,7 @@ class EODataAccessGateway:
1503
1555
  try:
1504
1556
  product_type = self.get_product_type_from_alias(product_type)
1505
1557
  except NoMatchingProductType:
1506
- logger.warning("product type %s not found", product_type)
1558
+ logger.debug("product type %s not found", product_type)
1507
1559
  get_search_plugins_kwargs = dict(provider=provider, product_type=product_type)
1508
1560
  search_plugins = self._plugins_manager.get_search_plugins(
1509
1561
  **get_search_plugins_kwargs
@@ -1526,7 +1578,7 @@ class EODataAccessGateway:
1526
1578
  "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1527
1579
  )
1528
1580
  kwargs.update(items_per_page=items_per_page)
1529
- if isinstance(plugin, BuildPostSearchResult):
1581
+ if isinstance(plugin, PostJsonSearch):
1530
1582
  kwargs.update(
1531
1583
  items_per_page=items_per_page,
1532
1584
  _dc_qs=_dc_qs,
@@ -1544,9 +1596,10 @@ class EODataAccessGateway:
1544
1596
  **kwargs,
1545
1597
  ):
1546
1598
  results.data.extend(page_results.data)
1547
- except Exception:
1599
+ except Exception as e:
1548
1600
  if kwargs.get("raise_errors"):
1549
1601
  raise
1602
+ logger.warning(e)
1550
1603
  continue
1551
1604
 
1552
1605
  # try using crunch to get unique result
@@ -1584,16 +1637,12 @@ class EODataAccessGateway:
1584
1637
 
1585
1638
  # append auth if needed
1586
1639
  if getattr(plugin.config, "need_auth", False):
1587
- auth_plugin = self._plugins_manager.get_auth_plugin(plugin.provider)
1588
- if auth_plugin and callable(getattr(auth_plugin, "authenticate", None)):
1589
- try:
1590
- kwargs["auth"] = auth_plugin.authenticate()
1591
- except (AuthenticationError, MisconfiguredError) as e:
1592
- logger.warning(f"Could not authenticate on {provider}: {str(e)}")
1593
- else:
1594
- logger.warning(
1595
- f"Could not authenticate on {provider} using {auth_plugin} plugin"
1596
- )
1640
+ if auth := self._plugins_manager.get_auth(
1641
+ plugin.provider,
1642
+ getattr(plugin.config, "api_endpoint", None),
1643
+ plugin.config,
1644
+ ):
1645
+ kwargs["auth"] = auth
1597
1646
 
1598
1647
  product_type_config = plugin.discover_product_types(**kwargs)
1599
1648
  self.update_product_types_list({provider: product_type_config})
@@ -1718,12 +1767,12 @@ class EODataAccessGateway:
1718
1767
  for plugin in self._plugins_manager.get_search_plugins(
1719
1768
  product_type=product_type, provider=provider
1720
1769
  ):
1721
- # exclude BuildPostSearchResult plugins from search fallback for unknow product_type
1770
+ # exclude MeteoblueSearch plugins from search fallback for unknown product_type
1722
1771
  if (
1723
1772
  provider != plugin.provider
1724
1773
  and preferred_provider != plugin.provider
1725
1774
  and product_type not in self.product_types_config
1726
- and isinstance(plugin, BuildPostSearchResult)
1775
+ and isinstance(plugin, MeteoblueSearch)
1727
1776
  ):
1728
1777
  continue
1729
1778
  search_plugins.append(plugin)
@@ -1732,12 +1781,10 @@ class EODataAccessGateway:
1732
1781
  provider = preferred_provider
1733
1782
  providers = [plugin.provider for plugin in search_plugins]
1734
1783
  if provider not in providers:
1735
- logger.warning(
1736
- "Product type '%s' is not available with provider '%s'. "
1737
- "Searching it on provider '%s' instead.",
1784
+ logger.debug(
1785
+ "Product type '%s' is not available with preferred provider '%s'.",
1738
1786
  product_type,
1739
1787
  provider,
1740
- search_plugins[0].provider,
1741
1788
  )
1742
1789
  else:
1743
1790
  provider_plugin = list(
@@ -1745,35 +1792,10 @@ class EODataAccessGateway:
1745
1792
  )[0]
1746
1793
  search_plugins.remove(provider_plugin)
1747
1794
  search_plugins.insert(0, provider_plugin)
1748
- logger.info(
1749
- "Searching product type '%s' on provider: %s",
1750
- product_type,
1751
- search_plugins[0].provider,
1752
- )
1753
1795
  # Add product_types_config to plugin config. This dict contains product
1754
1796
  # type metadata that will also be stored in each product's properties.
1755
1797
  for search_plugin in search_plugins:
1756
- try:
1757
- search_plugin.config.product_type_config = dict(
1758
- [
1759
- p
1760
- for p in self.list_product_types(
1761
- search_plugin.provider, fetch_providers=False
1762
- )
1763
- if p["_id"] == product_type
1764
- ][0],
1765
- **{"productType": product_type},
1766
- )
1767
- # If the product isn't in the catalog, it's a generic product type.
1768
- except IndexError:
1769
- # Construct the GENERIC_PRODUCT_TYPE metadata
1770
- search_plugin.config.product_type_config = dict(
1771
- ID=GENERIC_PRODUCT_TYPE,
1772
- **self.product_types_config[GENERIC_PRODUCT_TYPE],
1773
- productType=product_type,
1774
- )
1775
- # Remove the ID since this is equal to productType.
1776
- search_plugin.config.product_type_config.pop("ID", None)
1798
+ self._attach_product_type_config(search_plugin, product_type)
1777
1799
 
1778
1800
  return search_plugins, kwargs
1779
1801
 
@@ -1793,6 +1815,7 @@ class EODataAccessGateway:
1793
1815
  :param kwargs: Some other criteria that will be used to do the search
1794
1816
  :returns: A collection of EO products matching the criteria
1795
1817
  """
1818
+ logger.info("Searching on provider %s", search_plugin.provider)
1796
1819
  max_items_per_page = getattr(search_plugin.config, "pagination", {}).get(
1797
1820
  "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1798
1821
  )
@@ -1810,19 +1833,23 @@ class EODataAccessGateway:
1810
1833
  max_items_per_page,
1811
1834
  )
1812
1835
 
1813
- need_auth = getattr(search_plugin.config, "need_auth", False)
1814
- auth_plugin = self._plugins_manager.get_auth_plugin(search_plugin.provider)
1815
- can_authenticate = callable(getattr(auth_plugin, "authenticate", None))
1816
-
1817
1836
  results: List[EOProduct] = []
1818
1837
  total_results: Optional[int] = 0 if count else None
1819
1838
 
1839
+ errors: List[Tuple[str, Exception]] = []
1840
+
1820
1841
  try:
1821
1842
  prep = PreparedSearch(count=count)
1822
- if need_auth and auth_plugin and can_authenticate:
1823
- prep.auth = auth_plugin.authenticate()
1824
1843
 
1825
- prep.auth_plugin = auth_plugin
1844
+ # append auth if needed
1845
+ if getattr(search_plugin.config, "need_auth", False):
1846
+ if auth := self._plugins_manager.get_auth(
1847
+ search_plugin.provider,
1848
+ getattr(search_plugin.config, "api_endpoint", None),
1849
+ search_plugin.config,
1850
+ ):
1851
+ prep.auth = auth
1852
+
1826
1853
  prep.page = kwargs.pop("page", None)
1827
1854
  prep.items_per_page = kwargs.pop("items_per_page", None)
1828
1855
 
@@ -1876,12 +1903,31 @@ class EODataAccessGateway:
1876
1903
  eo_product.product_type
1877
1904
  )
1878
1905
  except NoMatchingProductType:
1879
- logger.warning("product type %s not found", eo_product.product_type)
1906
+ logger.debug("product type %s not found", eo_product.product_type)
1880
1907
 
1881
1908
  if eo_product.search_intersection is not None:
1882
1909
  download_plugin = self._plugins_manager.get_download_plugin(
1883
1910
  eo_product
1884
1911
  )
1912
+ if len(eo_product.assets) > 0:
1913
+ matching_url = next(iter(eo_product.assets.values()))["href"]
1914
+ elif eo_product.properties.get("storageStatus") != ONLINE_STATUS:
1915
+ matching_url = eo_product.properties.get(
1916
+ "orderLink"
1917
+ ) or eo_product.properties.get("downloadLink")
1918
+ else:
1919
+ matching_url = eo_product.properties.get("downloadLink")
1920
+
1921
+ try:
1922
+ auth_plugin = next(
1923
+ self._plugins_manager.get_auth_plugins(
1924
+ search_plugin.provider,
1925
+ matching_url=matching_url,
1926
+ matching_conf=download_plugin.config,
1927
+ )
1928
+ )
1929
+ except StopIteration:
1930
+ auth_plugin = None
1885
1931
  eo_product.register_downloader(download_plugin, auth_plugin)
1886
1932
 
1887
1933
  results.extend(res)
@@ -1911,13 +1957,6 @@ class EODataAccessGateway:
1911
1957
  "the total number of products matching the search criteria"
1912
1958
  )
1913
1959
  except Exception as e:
1914
- log_msg = f"No result from provider '{search_plugin.provider}' due to an error during search."
1915
- if not raise_errors:
1916
- log_msg += " Raise verbosity of log messages for details"
1917
- logger.info(log_msg)
1918
- # keep only the message from exception args
1919
- if len(e.args) > 1:
1920
- e.args = (e.args[0],)
1921
1960
  if raise_errors:
1922
1961
  # Raise the error, letting the application wrapping eodag know that
1923
1962
  # something went bad. This way it will be able to decide what to do next
@@ -1927,8 +1966,8 @@ class EODataAccessGateway:
1927
1966
  "Error while searching on provider %s (ignored):",
1928
1967
  search_plugin.provider,
1929
1968
  )
1930
- self.search_errors.add((search_plugin.provider, e))
1931
- return SearchResult(results, total_results)
1969
+ errors.append((search_plugin.provider, e))
1970
+ return SearchResult(results, total_results, errors)
1932
1971
 
1933
1972
  def crunch(self, results: SearchResult, **kwargs: Any) -> SearchResult:
1934
1973
  """Apply the filters given through the keyword arguments to the results
@@ -1973,8 +2012,8 @@ class EODataAccessGateway:
1973
2012
  search_result: SearchResult,
1974
2013
  downloaded_callback: Optional[DownloadedCallback] = None,
1975
2014
  progress_callback: Optional[ProgressCallback] = None,
1976
- wait: int = DEFAULT_DOWNLOAD_WAIT,
1977
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
2015
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
2016
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
1978
2017
  **kwargs: Unpack[DownloadConf],
1979
2018
  ) -> List[str]:
1980
2019
  """Download all products resulting from a search.
@@ -1982,7 +2021,7 @@ class EODataAccessGateway:
1982
2021
  :param search_result: A collection of EO products resulting from a search
1983
2022
  :param downloaded_callback: (optional) A method or a callable object which takes
1984
2023
  as parameter the ``product``. You can use the base class
1985
- :class:`~eodag.api.product.DownloadedCallback` and override
2024
+ :class:`~eodag.utils.DownloadedCallback` and override
1986
2025
  its ``__call__`` method. Will be called each time a product
1987
2026
  finishes downloading
1988
2027
  :param progress_callback: (optional) A method or a callable object
@@ -2061,12 +2100,12 @@ class EODataAccessGateway:
2061
2100
  products = self.deserialize(filename)
2062
2101
  for i, product in enumerate(products):
2063
2102
  if product.downloader is None:
2103
+ downloader = self._plugins_manager.get_download_plugin(product)
2064
2104
  auth = product.downloader_auth
2065
2105
  if auth is None:
2066
- auth = self._plugins_manager.get_auth_plugin(product.provider)
2067
- products[i].register_downloader(
2068
- self._plugins_manager.get_download_plugin(product), auth
2069
- )
2106
+ auth = self._plugins_manager.get_auth_plugin(downloader, product)
2107
+ products[i].register_downloader(downloader, auth)
2108
+
2070
2109
  return products
2071
2110
 
2072
2111
  @_deprecated(
@@ -2090,7 +2129,7 @@ class EODataAccessGateway:
2090
2129
 
2091
2130
  :param filename: A filename containing features encoded as a geojson
2092
2131
  :param recursive: (optional) Browse recursively in child nodes if True
2093
- :param max_connections: (optional) Maximum number of connections for HTTP requests
2132
+ :param max_connections: (optional) Maximum number of connections for concurrent HTTP requests
2094
2133
  :param provider: (optional) Data provider
2095
2134
  :param productType: (optional) Data product type
2096
2135
  :param timeout: (optional) Timeout in seconds for each internal HTTP request
@@ -2136,8 +2175,8 @@ class EODataAccessGateway:
2136
2175
  self,
2137
2176
  product: EOProduct,
2138
2177
  progress_callback: Optional[ProgressCallback] = None,
2139
- wait: int = DEFAULT_DOWNLOAD_WAIT,
2140
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
2178
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
2179
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
2141
2180
  **kwargs: Unpack[DownloadConf],
2142
2181
  ) -> str:
2143
2182
  """Download a single product.
@@ -2197,12 +2236,11 @@ class EODataAccessGateway:
2197
2236
 
2198
2237
  def _setup_downloader(self, product: EOProduct) -> None:
2199
2238
  if product.downloader is None:
2239
+ downloader = self._plugins_manager.get_download_plugin(product)
2200
2240
  auth = product.downloader_auth
2201
2241
  if auth is None:
2202
- auth = self._plugins_manager.get_auth_plugin(product.provider)
2203
- product.register_downloader(
2204
- self._plugins_manager.get_download_plugin(product), auth
2205
- )
2242
+ auth = self._plugins_manager.get_auth_plugin(downloader, product)
2243
+ product.register_downloader(downloader, auth)
2206
2244
 
2207
2245
  def get_cruncher(self, name: str, **options: Any) -> Crunch:
2208
2246
  """Build a crunch plugin from a configuration
@@ -2216,67 +2254,96 @@ class EODataAccessGateway:
2216
2254
  return self._plugins_manager.get_crunch_plugin(name, **plugin_conf)
2217
2255
 
2218
2256
  def list_queryables(
2219
- self, provider: Optional[str] = None, **kwargs: Any
2220
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
2257
+ self,
2258
+ provider: Optional[str] = None,
2259
+ fetch_providers: bool = True,
2260
+ **kwargs: Any,
2261
+ ) -> QueryablesDict:
2221
2262
  """Fetch the queryable properties for a given product type and/or provider.
2222
2263
 
2223
2264
  :param provider: (optional) The provider.
2265
+ :param fetch_providers: If new product types should be fetched from the providers; default: True
2224
2266
  :param kwargs: additional filters for queryables (`productType` or other search
2225
2267
  arguments)
2226
2268
 
2227
2269
  :raises UnsupportedProductType: If the specified product type is not available for the
2228
2270
  provider.
2229
2271
 
2230
- :returns: A dict containing the EODAG queryable properties, associating
2231
- parameters to their annotated type
2272
+ :returns: A :class:`~eodag.api.product.queryables.QuerybalesDict` containing the EODAG queryable
2273
+ properties, associating parameters to their annotated type, and a additional_properties attribute
2232
2274
  """
2275
+ # only fetch providers if product type is not found
2233
2276
  available_product_types = [
2234
2277
  pt["ID"]
2235
2278
  for pt in self.list_product_types(provider=provider, fetch_providers=False)
2236
2279
  ]
2237
- product_type = kwargs.get("productType")
2280
+ product_type: Optional[str] = kwargs.get("productType")
2281
+ pt_alias: Optional[str] = product_type
2238
2282
 
2239
2283
  if product_type:
2284
+ if product_type not in available_product_types:
2285
+ if fetch_providers:
2286
+ # fetch providers and try again
2287
+ available_product_types = [
2288
+ pt["ID"]
2289
+ for pt in self.list_product_types(
2290
+ provider=provider, fetch_providers=True
2291
+ )
2292
+ ]
2293
+ raise UnsupportedProductType(f"{product_type} is not available.")
2240
2294
  try:
2241
2295
  kwargs["productType"] = product_type = self.get_product_type_from_alias(
2242
2296
  product_type
2243
2297
  )
2244
2298
  except NoMatchingProductType as e:
2245
- raise UnsupportedProductType(f"{product_type} is not available") from e
2246
-
2247
- if product_type and product_type not in available_product_types:
2248
- self.fetch_product_types_list()
2299
+ raise UnsupportedProductType(f"{product_type} is not available.") from e
2249
2300
 
2250
2301
  if not provider and not product_type:
2251
- return model_fields_to_annotated(CommonQueryables.model_fields)
2302
+ return QueryablesDict(
2303
+ additional_properties=True,
2304
+ **model_fields_to_annotated(CommonQueryables.model_fields),
2305
+ )
2252
2306
 
2253
- providers_queryables: Dict[str, Dict[str, Annotated[Any, FieldInfo]]] = {}
2307
+ queryables: QueryablesDict = QueryablesDict(
2308
+ additional_properties=True, additional_information="", **{}
2309
+ )
2254
2310
 
2255
2311
  for plugin in self._plugins_manager.get_search_plugins(product_type, provider):
2312
+ # attach product type config
2313
+ product_type_configs = {}
2314
+ if product_type:
2315
+ self._attach_product_type_config(plugin, product_type)
2316
+ product_type_configs[product_type] = plugin.config.product_type_config
2317
+ else:
2318
+ for pt in available_product_types:
2319
+ self._attach_product_type_config(plugin, pt)
2320
+ product_type_configs[pt] = plugin.config.product_type_config
2321
+
2322
+ # authenticate if required
2256
2323
  if getattr(plugin.config, "need_auth", False) and (
2257
- auth := self._plugins_manager.get_auth_plugin(plugin.provider)
2324
+ auth := self._plugins_manager.get_auth_plugin(plugin)
2258
2325
  ):
2259
- plugin.auth = auth.authenticate()
2260
- providers_queryables[plugin.provider] = plugin.list_queryables(
2261
- filters=kwargs, product_type=product_type
2326
+ try:
2327
+ plugin.auth = auth.authenticate()
2328
+ except AuthenticationError:
2329
+ logger.debug(
2330
+ "queryables from provider %s could not be fetched due to an authentication error",
2331
+ plugin.provider,
2332
+ )
2333
+ plugin_queryables = plugin.list_queryables(
2334
+ kwargs,
2335
+ available_product_types,
2336
+ product_type_configs,
2337
+ product_type,
2338
+ pt_alias,
2262
2339
  )
2263
-
2264
- queryable_keys: Set[str] = set.intersection( # type: ignore
2265
- *[set(q.keys()) for q in providers_queryables.values()]
2266
- )
2267
- queryables = {
2268
- k: v
2269
- for k, v in list(providers_queryables.values())[0].items()
2270
- if k in queryable_keys
2271
- }
2272
-
2273
- # always keep at least CommonQueryables
2274
- common_queryables = copy_deepcopy(CommonQueryables.model_fields)
2275
- for key, queryable in common_queryables.items():
2276
- if key in kwargs:
2277
- queryable.default = kwargs[key]
2278
-
2279
- queryables.update(model_fields_to_annotated(common_queryables))
2340
+ queryables.update(plugin_queryables)
2341
+ if not plugin_queryables.additional_properties:
2342
+ queryables.additional_properties = False
2343
+ if plugin_queryables.additional_information:
2344
+ queryables.additional_information += (
2345
+ f"{provider}: {plugin_queryables.additional_information}"
2346
+ )
2280
2347
 
2281
2348
  return queryables
2282
2349
 
@@ -2284,7 +2351,7 @@ class EODataAccessGateway:
2284
2351
  """For each provider, gives its available sortable parameter(s) and its maximum
2285
2352
  number of them if it supports the sorting feature, otherwise gives None.
2286
2353
 
2287
- :returns: A dictionnary with providers as keys and dictionnary of sortable parameter(s) and
2354
+ :returns: A dictionary with providers as keys and dictionary of sortable parameter(s) and
2288
2355
  its (their) maximum number as value(s).
2289
2356
  :raises: :class:`~eodag.utils.exceptions.UnsupportedProvider`
2290
2357
  """
@@ -2311,3 +2378,30 @@ class EODataAccessGateway:
2311
2378
  ],
2312
2379
  }
2313
2380
  return sortables
2381
+
2382
+ def _attach_product_type_config(self, plugin: Search, product_type: str) -> None:
2383
+ """
2384
+ Attach product_types_config to plugin config. This dict contains product
2385
+ type metadata that will also be stored in each product's properties.
2386
+ """
2387
+ try:
2388
+ plugin.config.product_type_config = dict(
2389
+ [
2390
+ p
2391
+ for p in self.list_product_types(
2392
+ plugin.provider, fetch_providers=False
2393
+ )
2394
+ if p["_id"] == product_type
2395
+ ][0],
2396
+ **{"productType": product_type},
2397
+ )
2398
+ # If the product isn't in the catalog, it's a generic product type.
2399
+ except IndexError:
2400
+ # Construct the GENERIC_PRODUCT_TYPE metadata
2401
+ plugin.config.product_type_config = dict(
2402
+ ID=GENERIC_PRODUCT_TYPE,
2403
+ **self.product_types_config[GENERIC_PRODUCT_TYPE],
2404
+ productType=product_type,
2405
+ )
2406
+ # Remove the ID since this is equal to productType.
2407
+ plugin.config.product_type_config.pop("ID", None)