eodag 3.0.0b3__py3-none-any.whl → 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. eodag/api/core.py +347 -247
  2. eodag/api/product/_assets.py +44 -15
  3. eodag/api/product/_product.py +58 -47
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +129 -93
  10. eodag/api/search_result.py +28 -12
  11. eodag/cli.py +61 -24
  12. eodag/config.py +457 -167
  13. eodag/plugins/apis/base.py +10 -4
  14. eodag/plugins/apis/ecmwf.py +53 -23
  15. eodag/plugins/apis/usgs.py +41 -17
  16. eodag/plugins/authentication/aws_auth.py +30 -18
  17. eodag/plugins/authentication/base.py +14 -3
  18. eodag/plugins/authentication/generic.py +14 -3
  19. eodag/plugins/authentication/header.py +14 -6
  20. eodag/plugins/authentication/keycloak.py +44 -25
  21. eodag/plugins/authentication/oauth.py +18 -4
  22. eodag/plugins/authentication/openid_connect.py +192 -171
  23. eodag/plugins/authentication/qsauth.py +12 -4
  24. eodag/plugins/authentication/sas_auth.py +22 -5
  25. eodag/plugins/authentication/token.py +95 -17
  26. eodag/plugins/authentication/token_exchange.py +19 -19
  27. eodag/plugins/base.py +4 -4
  28. eodag/plugins/crunch/base.py +8 -5
  29. eodag/plugins/crunch/filter_date.py +9 -6
  30. eodag/plugins/crunch/filter_latest_intersect.py +9 -8
  31. eodag/plugins/crunch/filter_latest_tpl_name.py +8 -8
  32. eodag/plugins/crunch/filter_overlap.py +9 -11
  33. eodag/plugins/crunch/filter_property.py +10 -10
  34. eodag/plugins/download/aws.py +181 -105
  35. eodag/plugins/download/base.py +49 -67
  36. eodag/plugins/download/creodias_s3.py +40 -2
  37. eodag/plugins/download/http.py +247 -223
  38. eodag/plugins/download/s3rest.py +29 -28
  39. eodag/plugins/manager.py +176 -41
  40. eodag/plugins/search/__init__.py +6 -5
  41. eodag/plugins/search/base.py +123 -60
  42. eodag/plugins/search/build_search_result.py +1046 -355
  43. eodag/plugins/search/cop_marine.py +132 -39
  44. eodag/plugins/search/creodias_s3.py +19 -68
  45. eodag/plugins/search/csw.py +48 -8
  46. eodag/plugins/search/data_request_search.py +124 -23
  47. eodag/plugins/search/qssearch.py +531 -310
  48. eodag/plugins/search/stac_list_assets.py +85 -0
  49. eodag/plugins/search/static_stac_search.py +23 -24
  50. eodag/resources/ext_product_types.json +1 -1
  51. eodag/resources/product_types.yml +1295 -355
  52. eodag/resources/providers.yml +1819 -3010
  53. eodag/resources/stac.yml +3 -163
  54. eodag/resources/stac_api.yml +2 -2
  55. eodag/resources/user_conf_template.yml +115 -99
  56. eodag/rest/cache.py +2 -2
  57. eodag/rest/config.py +3 -4
  58. eodag/rest/constants.py +0 -1
  59. eodag/rest/core.py +157 -117
  60. eodag/rest/errors.py +181 -0
  61. eodag/rest/server.py +57 -339
  62. eodag/rest/stac.py +133 -581
  63. eodag/rest/types/collections_search.py +3 -3
  64. eodag/rest/types/eodag_search.py +41 -30
  65. eodag/rest/types/queryables.py +42 -32
  66. eodag/rest/types/stac_search.py +15 -16
  67. eodag/rest/utils/__init__.py +14 -21
  68. eodag/rest/utils/cql_evaluate.py +6 -6
  69. eodag/rest/utils/rfc3339.py +2 -2
  70. eodag/types/__init__.py +153 -32
  71. eodag/types/bbox.py +2 -2
  72. eodag/types/download_args.py +4 -4
  73. eodag/types/queryables.py +183 -73
  74. eodag/types/search_args.py +6 -6
  75. eodag/types/whoosh.py +127 -3
  76. eodag/utils/__init__.py +228 -106
  77. eodag/utils/exceptions.py +47 -26
  78. eodag/utils/import_system.py +2 -2
  79. eodag/utils/logging.py +37 -77
  80. eodag/utils/repr.py +65 -6
  81. eodag/utils/requests.py +13 -15
  82. eodag/utils/rest.py +2 -2
  83. eodag/utils/s3.py +231 -0
  84. eodag/utils/stac_reader.py +11 -11
  85. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/METADATA +81 -81
  86. eodag-3.1.0.dist-info/RECORD +113 -0
  87. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  88. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +5 -2
  89. eodag/resources/constraints/climate-dt.json +0 -13
  90. eodag/resources/constraints/extremes-dt.json +0 -8
  91. eodag/utils/constraints.py +0 -244
  92. eodag-3.0.0b3.dist-info/RECORD +0 -110
  93. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  94. {eodag-3.0.0b3.dist-info → eodag-3.1.0.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
26
+ from importlib.metadata import version
27
+ from importlib.resources import files as res_files
25
28
  from operator import itemgetter
26
- from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Tuple, Union
29
+ from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
27
30
 
28
31
  import geojson
29
- import pkg_resources
30
32
  import yaml.parser
31
- 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
 
@@ -114,8 +119,8 @@ class EODataAccessGateway:
114
119
  user_conf_file_path: Optional[str] = None,
115
120
  locations_conf_path: Optional[str] = None,
116
121
  ) -> None:
117
- product_types_config_path = resource_filename(
118
- "eodag", os.path.join("resources/", "product_types.yml")
122
+ product_types_config_path = os.getenv("EODAG_PRODUCT_TYPES_CFG_FILE") or str(
123
+ res_files("eodag") / "resources" / "product_types.yml"
119
124
  )
120
125
  self.product_types_config = SimpleYamlProxyConfig(product_types_config_path)
121
126
  self.product_types_config_md5 = obj_md5sum(self.product_types_config.source)
@@ -156,8 +161,8 @@ class EODataAccessGateway:
156
161
  user_conf_file_path = standard_configuration_path
157
162
  if not os.path.isfile(standard_configuration_path):
158
163
  shutil.copy(
159
- resource_filename(
160
- "eodag", os.path.join("resources", "user_conf_template.yml")
164
+ str(
165
+ res_files("eodag") / "resources" / "user_conf_template.yml"
161
166
  ),
162
167
  standard_configuration_path,
163
168
  )
@@ -166,16 +171,21 @@ 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)
176
186
 
177
187
  # store pruned providers configs
178
- self._pruned_providers_config: Dict[str, Any] = {}
188
+ self._pruned_providers_config: dict[str, Any] = {}
179
189
  # filter out providers needing auth that have no credentials set
180
190
  self._prune_providers_list()
181
191
 
@@ -193,13 +203,13 @@ class EODataAccessGateway:
193
203
  locations_conf_path = os.path.join(self.conf_dir, "locations.yml")
194
204
  if not os.path.isfile(locations_conf_path):
195
205
  # copy locations conf file and replace path example
196
- locations_conf_template = resource_filename(
197
- "eodag",
198
- os.path.join("resources", "locations_conf_template.yml"),
206
+ locations_conf_template = str(
207
+ res_files("eodag") / "resources" / "locations_conf_template.yml"
199
208
  )
200
- with open(locations_conf_template) as infile, open(
201
- locations_conf_path, "w"
202
- ) as outfile:
209
+ with (
210
+ open(locations_conf_template) as infile,
211
+ open(locations_conf_path, "w") as outfile,
212
+ ):
203
213
  # The template contains paths in the form of:
204
214
  # /path/to/locations/file.shp
205
215
  path_template = "/path/to/locations/"
@@ -211,15 +221,14 @@ class EODataAccessGateway:
211
221
  outfile.write(line)
212
222
  # copy sample shapefile dir
213
223
  shutil.copytree(
214
- resource_filename("eodag", os.path.join("resources", "shp")),
224
+ str(res_files("eodag") / "resources" / "shp"),
215
225
  os.path.join(self.conf_dir, "shp"),
216
226
  )
217
227
  self.set_locations_conf(locations_conf_path)
218
- self.search_errors: Set = set()
219
228
 
220
229
  def get_version(self) -> str:
221
230
  """Get eodag package version"""
222
- return pkg_resources.get_distribution("eodag").version
231
+ return version("eodag")
223
232
 
224
233
  def build_index(self) -> None:
225
234
  """Build a `Whoosh <https://whoosh.readthedocs.io/en/latest/index.html>`_
@@ -296,13 +305,18 @@ class EODataAccessGateway:
296
305
  product_type, **{"md5": self.product_types_config_md5}
297
306
  )
298
307
  # 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
- )
308
+ try:
309
+ ix_writer.add_document(
310
+ **{
311
+ k: v
312
+ for k, v in versioned_product_type.items()
313
+ if k in product_types_schema.names()
314
+ }
315
+ )
316
+ except TypeError as e:
317
+ logger.error(
318
+ f"Cannot write product type {product_type['ID']} into index. e={e} product_type={product_type}"
319
+ )
306
320
  ix_writer.commit()
307
321
 
308
322
  def set_preferred_provider(self, provider: str) -> None:
@@ -320,7 +334,7 @@ class EODataAccessGateway:
320
334
  new_priority = max_priority + 1
321
335
  self._plugins_manager.set_priority(provider, new_priority)
322
336
 
323
- def get_preferred_provider(self) -> Tuple[str, int]:
337
+ def get_preferred_provider(self) -> tuple[str, int]:
324
338
  """Get the provider currently set as the preferred one for searching
325
339
  products, along with its priority.
326
340
 
@@ -333,14 +347,24 @@ class EODataAccessGateway:
333
347
  preferred, priority = max(providers_with_priority, key=itemgetter(1))
334
348
  return preferred, priority
335
349
 
336
- def update_providers_config(self, yaml_conf: str) -> None:
350
+ def update_providers_config(
351
+ self,
352
+ yaml_conf: Optional[str] = None,
353
+ dict_conf: Optional[dict[str, Any]] = None,
354
+ ) -> None:
337
355
  """Update providers configuration with given input.
338
356
  Can be used to add a provider to existing configuration or update
339
357
  an existing one.
340
358
 
341
359
  :param yaml_conf: YAML formated provider configuration
360
+ :param dict_conf: provider configuration as dictionary in place of ``yaml_conf``
342
361
  """
343
- conf_update = yaml.safe_load(yaml_conf)
362
+ if dict_conf is not None:
363
+ conf_update = dict_conf
364
+ elif yaml_conf is not None:
365
+ conf_update = yaml.safe_load(yaml_conf)
366
+ else:
367
+ return None
344
368
 
345
369
  # restore the pruned configuration
346
370
  for provider in list(self._pruned_providers_config.keys()):
@@ -355,9 +379,14 @@ class EODataAccessGateway:
355
379
 
356
380
  override_config_from_mapping(self.providers_config, conf_update)
357
381
 
358
- stac_provider_config = load_stac_provider_config()
382
+ # share credentials between updated plugins confs
383
+ share_credentials(self.providers_config)
384
+
359
385
  for provider in conf_update.keys():
360
- provider_config_init(self.providers_config[provider], stac_provider_config)
386
+ provider_config_init(
387
+ self.providers_config[provider],
388
+ load_stac_provider_config(),
389
+ )
361
390
  setattr(self.providers_config[provider], "product_types_fetched", False)
362
391
  # re-create _plugins_manager using up-to-date providers_config
363
392
  self._plugins_manager.build_product_type_to_provider_config_map()
@@ -367,12 +396,12 @@ class EODataAccessGateway:
367
396
  name: str,
368
397
  url: Optional[str] = None,
369
398
  priority: Optional[int] = None,
370
- search: Dict[str, Any] = {"type": "StacSearch"},
371
- products: Dict[str, Any] = {
399
+ search: dict[str, Any] = {"type": "StacSearch"},
400
+ products: dict[str, Any] = {
372
401
  GENERIC_PRODUCT_TYPE: {"productType": "{productType}"}
373
402
  },
374
- download: Dict[str, Any] = {"type": "HTTPDownload", "auth_error_code": 401},
375
- **kwargs: Dict[str, Any],
403
+ download: dict[str, Any] = {"type": "HTTPDownload", "auth_error_code": 401},
404
+ **kwargs: dict[str, Any],
376
405
  ):
377
406
  """Adds a new provider.
378
407
 
@@ -391,7 +420,7 @@ class EODataAccessGateway:
391
420
  :param download: Download :class:`~eodag.config.PluginConfig` mapping
392
421
  :param kwargs: Additional :class:`~eodag.config.ProviderConfig` mapping
393
422
  """
394
- conf_dict: Dict[str, Any] = {
423
+ conf_dict: dict[str, Any] = {
395
424
  name: {
396
425
  "url": url,
397
426
  "search": {"type": "StacSearch", **search},
@@ -419,14 +448,11 @@ class EODataAccessGateway:
419
448
 
420
449
  # api plugin usage: remove unneeded search/download/auth plugin conf
421
450
  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)
451
+ for k in PLUGINS_TOPICS_KEYS:
452
+ if k != "api":
453
+ conf_dict[name].pop(k, None)
425
454
 
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()
455
+ self.update_providers_config(dict_conf=conf_dict)
430
456
 
431
457
  if priority is None:
432
458
  self.set_preferred_provider(name)
@@ -452,12 +478,7 @@ class EODataAccessGateway:
452
478
 
453
479
  # check authentication
454
480
  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
- )
481
+ credentials_exist = credentials_in_auth(conf.api)
461
482
  if not credentials_exist:
462
483
  # credentials needed but not found
463
484
  self._pruned_providers_config[provider] = self.providers_config.pop(
@@ -469,7 +490,7 @@ class EODataAccessGateway:
469
490
  provider,
470
491
  )
471
492
  elif hasattr(conf, "search") and getattr(conf.search, "need_auth", False):
472
- if not hasattr(conf, "auth"):
493
+ if not hasattr(conf, "auth") and not hasattr(conf, "search_auth"):
473
494
  # credentials needed but no auth plugin was found
474
495
  self._pruned_providers_config[provider] = self.providers_config.pop(
475
496
  provider
@@ -480,11 +501,13 @@ class EODataAccessGateway:
480
501
  provider,
481
502
  )
482
503
  continue
483
- credentials_exist = any(
484
- [
485
- cred is not None
486
- for cred in getattr(conf.auth, "credentials", {}).values()
487
- ]
504
+ credentials_exist = (
505
+ hasattr(conf, "search_auth")
506
+ and credentials_in_auth(conf.search_auth)
507
+ ) or (
508
+ not hasattr(conf, "search_auth")
509
+ and hasattr(conf, "auth")
510
+ and credentials_in_auth(conf.auth)
488
511
  )
489
512
  if not credentials_exist:
490
513
  # credentials needed but not found
@@ -541,7 +564,7 @@ class EODataAccessGateway:
541
564
  main_locations_config = locations_config[main_key]
542
565
 
543
566
  logger.info("Locations configuration loaded from %s" % locations_conf_path)
544
- self.locations_config: List[Dict[str, Any]] = main_locations_config
567
+ self.locations_config: list[dict[str, Any]] = main_locations_config
545
568
  else:
546
569
  logger.info(
547
570
  "Could not load locations configuration from %s" % locations_conf_path
@@ -550,7 +573,7 @@ class EODataAccessGateway:
550
573
 
551
574
  def list_product_types(
552
575
  self, provider: Optional[str] = None, fetch_providers: bool = True
553
- ) -> List[Dict[str, Any]]:
576
+ ) -> list[dict[str, Any]]:
554
577
  """Lists supported product types.
555
578
 
556
579
  :param provider: (optional) The name of a provider that must support the product
@@ -564,7 +587,7 @@ class EODataAccessGateway:
564
587
  # First, update product types list if possible
565
588
  self.fetch_product_types_list(provider=provider)
566
589
 
567
- product_types: List[Dict[str, Any]] = []
590
+ product_types: list[dict[str, Any]] = []
568
591
 
569
592
  providers_configs = (
570
593
  list(self.providers_config.values())
@@ -599,21 +622,32 @@ class EODataAccessGateway:
599
622
  def fetch_product_types_list(self, provider: Optional[str] = None) -> None:
600
623
  """Fetch product types list and update if needed
601
624
 
602
- :param provider: (optional) The name of a provider for which product types list
603
- should be updated. Defaults to all providers (None value).
625
+ :param provider: The name of a provider or provider-group for which product types
626
+ list should be updated. Defaults to all providers (None value).
604
627
  """
628
+ providers_to_fetch = list(self.providers_config.keys())
629
+ # check if some providers are grouped under a group name which is not a provider name
605
630
  if provider is not None and provider not in self.providers_config:
606
- return
631
+ providers_to_fetch = [
632
+ p
633
+ for p, pconf in self.providers_config.items()
634
+ if provider == getattr(pconf, "group", None)
635
+ ]
636
+ if providers_to_fetch:
637
+ logger.info(
638
+ f"Fetch product types for {provider} group: {', '.join(providers_to_fetch)}"
639
+ )
640
+ else:
641
+ return None
642
+ elif provider is not None:
643
+ providers_to_fetch = [provider]
607
644
 
608
645
  # providers discovery confs that are fetchable
609
- providers_discovery_configs_fetchable: Dict[str, Any] = {}
646
+ providers_discovery_configs_fetchable: dict[str, Any] = {}
610
647
  # check if any provider has not already been fetched for product types
611
648
  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
- ):
649
+ for provider_to_fetch in providers_to_fetch:
650
+ provider_config = self.providers_config[provider_to_fetch]
617
651
  # get discovery conf
618
652
  if hasattr(provider_config, "search"):
619
653
  provider_search_config = provider_config.search
@@ -732,29 +766,40 @@ class EODataAccessGateway:
732
766
 
733
767
  def discover_product_types(
734
768
  self, provider: Optional[str] = None
735
- ) -> Optional[Dict[str, Any]]:
769
+ ) -> Optional[dict[str, Any]]:
736
770
  """Fetch providers for product types
737
771
 
738
- :param provider: (optional) The name of a provider to fetch. Defaults to all
739
- providers (None value).
772
+ :param provider: The name of a provider or provider-group to fetch. Defaults to
773
+ all providers (None value).
740
774
  :returns: external product types configuration
741
775
  """
742
- if provider and provider not in self.providers_config:
776
+ grouped_providers = [
777
+ p
778
+ for p, provider_config in self.providers_config.items()
779
+ if provider == getattr(provider_config, "group", None)
780
+ ]
781
+ if provider and provider not in self.providers_config and grouped_providers:
782
+ logger.info(
783
+ f"Discover product types for {provider} group: {', '.join(grouped_providers)}"
784
+ )
785
+ elif provider and provider not in self.providers_config:
743
786
  raise UnsupportedProvider(
744
787
  f"The requested provider is not (yet) supported: {provider}"
745
788
  )
746
- ext_product_types_conf: Dict[str, Any] = {}
789
+ ext_product_types_conf: dict[str, Any] = {}
747
790
  providers_to_fetch = [
748
791
  p
749
792
  for p in (
750
793
  [
751
- provider,
794
+ p
795
+ for p in self.providers_config
796
+ if p in grouped_providers + [provider]
752
797
  ]
753
798
  if provider
754
799
  else self.available_providers()
755
800
  )
756
801
  ]
757
- kwargs: Dict[str, Any] = {}
802
+ kwargs: dict[str, Any] = {}
758
803
  for provider in providers_to_fetch:
759
804
  if hasattr(self.providers_config[provider], "search"):
760
805
  search_plugin_config = self.providers_config[provider].search
@@ -762,7 +807,9 @@ class EODataAccessGateway:
762
807
  search_plugin_config = self.providers_config[provider].api
763
808
  else:
764
809
  return None
765
- if getattr(search_plugin_config, "discover_product_types", None):
810
+ if getattr(search_plugin_config, "discover_product_types", {}).get(
811
+ "fetch_url", None
812
+ ):
766
813
  search_plugin: Union[Search, Api] = next(
767
814
  self._plugins_manager.get_search_plugins(provider=provider)
768
815
  )
@@ -773,23 +820,15 @@ class EODataAccessGateway:
773
820
  continue
774
821
  # append auth to search plugin if needed
775
822
  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)
823
+ if auth := self._plugins_manager.get_auth(
824
+ search_plugin.provider,
825
+ getattr(search_plugin.config, "api_endpoint", None),
826
+ search_plugin.config,
781
827
  ):
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
828
+ kwargs["auth"] = auth
790
829
  else:
791
- logger.warning(
792
- f"Could not authenticate on {provider} using {auth_plugin} plugin"
830
+ logger.debug(
831
+ f"Could not authenticate on {provider} for product types discovery"
793
832
  )
794
833
  ext_product_types_conf[provider] = None
795
834
  continue
@@ -801,7 +840,7 @@ class EODataAccessGateway:
801
840
  return sort_dict(ext_product_types_conf)
802
841
 
803
842
  def update_product_types_list(
804
- self, ext_product_types_conf: Dict[str, Optional[Dict[str, Dict[str, Any]]]]
843
+ self, ext_product_types_conf: dict[str, Optional[dict[str, dict[str, Any]]]]
805
844
  ) -> None:
806
845
  """Update eodag product types list
807
846
 
@@ -815,7 +854,9 @@ class EODataAccessGateway:
815
854
  ) or getattr(self.providers_config[provider], "api", None)
816
855
  if search_plugin_config is None:
817
856
  continue
818
- if not hasattr(search_plugin_config, "discover_product_types"):
857
+ if not getattr(
858
+ search_plugin_config, "discover_product_types", {}
859
+ ).get("fetch_url", None):
819
860
  # conf has been updated and provider product types are no more discoverable
820
861
  continue
821
862
  provider_products_config = (
@@ -827,7 +868,7 @@ class EODataAccessGateway:
827
868
  provider,
828
869
  )
829
870
  continue
830
- new_product_types: List[str] = []
871
+ new_product_types: list[str] = []
831
872
  for (
832
873
  new_product_type,
833
874
  new_product_type_conf,
@@ -874,7 +915,7 @@ class EODataAccessGateway:
874
915
  new_product_types.append(new_product_type)
875
916
  if new_product_types:
876
917
  logger.debug(
877
- f"Added product types {str(new_product_types)} for {provider}"
918
+ f"Added {len(new_product_types)} product types for {provider}"
878
919
  )
879
920
 
880
921
  elif provider not in self.providers_config:
@@ -890,7 +931,7 @@ class EODataAccessGateway:
890
931
 
891
932
  def available_providers(
892
933
  self, product_type: Optional[str] = None, by_group: bool = False
893
- ) -> List[str]:
934
+ ) -> list[str]:
894
935
  """Gives the sorted list of the available providers or groups
895
936
 
896
937
  The providers or groups are sorted first by their priority level in descending order,
@@ -917,7 +958,7 @@ class EODataAccessGateway:
917
958
 
918
959
  # If by_group is True, keep only the highest priority for each group
919
960
  if by_group:
920
- group_priority: Dict[str, int] = {}
961
+ group_priority: dict[str, int] = {}
921
962
  for name, priority in providers:
922
963
  if name not in group_priority or priority > group_priority[name]:
923
964
  group_priority[name] = priority
@@ -984,7 +1025,7 @@ class EODataAccessGateway:
984
1025
  missionStartDate: Optional[str] = None,
985
1026
  missionEndDate: Optional[str] = None,
986
1027
  **kwargs: Any,
987
- ) -> List[str]:
1028
+ ) -> list[str]:
988
1029
  """
989
1030
  Find EODAG product type IDs that best match a set of search parameters.
990
1031
 
@@ -1042,24 +1083,32 @@ class EODataAccessGateway:
1042
1083
  query = p.parse(text)
1043
1084
  results = searcher.search(query, limit=None)
1044
1085
 
1045
- guesses: List[Dict[str, str]] = [dict(r) for r in results or []]
1086
+ guesses: list[dict[str, str]] = [dict(r) for r in results or []]
1046
1087
 
1047
1088
  # datetime filtering
1048
1089
  if missionStartDate or missionEndDate:
1090
+ min_aware = datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
1091
+ max_aware = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
1049
1092
  guesses = [
1050
1093
  g
1051
1094
  for g in guesses
1052
1095
  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)
1096
+ max(
1097
+ rfc3339_str_to_datetime(missionStartDate)
1098
+ if missionStartDate
1099
+ else min_aware,
1100
+ rfc3339_str_to_datetime(g["missionStartDate"])
1101
+ if g.get("missionStartDate")
1102
+ else min_aware,
1103
+ )
1104
+ <= min(
1105
+ rfc3339_str_to_datetime(missionEndDate)
1106
+ if missionEndDate
1107
+ else max_aware,
1108
+ rfc3339_str_to_datetime(g["missionEndDate"])
1109
+ if g.get("missionEndDate")
1110
+ else max_aware,
1111
+ )
1063
1112
  )
1064
1113
  ]
1065
1114
 
@@ -1075,8 +1124,8 @@ class EODataAccessGateway:
1075
1124
  raise_errors: bool = False,
1076
1125
  start: Optional[str] = None,
1077
1126
  end: Optional[str] = None,
1078
- geom: Optional[Union[str, Dict[str, float], BaseGeometry]] = None,
1079
- locations: Optional[Dict[str, str]] = None,
1127
+ geom: Optional[Union[str, dict[str, float], BaseGeometry]] = None,
1128
+ locations: Optional[dict[str, str]] = None,
1080
1129
  provider: Optional[str] = None,
1081
1130
  count: bool = False,
1082
1131
  **kwargs: Any,
@@ -1155,7 +1204,7 @@ class EODataAccessGateway:
1155
1204
  items_per_page=items_per_page,
1156
1205
  )
1157
1206
 
1158
- self.search_errors = set()
1207
+ errors: list[tuple[str, Exception]] = []
1159
1208
  # Loop over available providers and return the first non-empty results
1160
1209
  for i, search_plugin in enumerate(search_plugins):
1161
1210
  search_plugin.clear()
@@ -1165,25 +1214,27 @@ class EODataAccessGateway:
1165
1214
  raise_errors=raise_errors,
1166
1215
  **search_kwargs,
1167
1216
  )
1217
+ errors.extend(search_results.errors)
1168
1218
  if len(search_results) == 0 and i < len(search_plugins) - 1:
1169
1219
  logger.warning(
1170
1220
  f"No result could be obtained from provider {search_plugin.provider}, "
1171
1221
  "we will try to get the data from another provider",
1172
1222
  )
1173
1223
  elif len(search_results) > 0:
1224
+ search_results.errors = errors
1174
1225
  return search_results
1175
1226
 
1176
1227
  if i > 1:
1177
1228
  logger.error("No result could be obtained from any available provider")
1178
- return SearchResult([], 0) if count else SearchResult([])
1229
+ return SearchResult([], 0, errors) if count else SearchResult([], errors=errors)
1179
1230
 
1180
1231
  def search_iter_page(
1181
1232
  self,
1182
1233
  items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
1183
1234
  start: Optional[str] = None,
1184
1235
  end: Optional[str] = None,
1185
- geom: Optional[Union[str, Dict[str, float], BaseGeometry]] = None,
1186
- locations: Optional[Dict[str, str]] = None,
1236
+ geom: Optional[Union[str, dict[str, float], BaseGeometry]] = None,
1237
+ locations: Optional[dict[str, str]] = None,
1187
1238
  **kwargs: Any,
1188
1239
  ) -> Iterator[SearchResult]:
1189
1240
  """Iterate over the pages of a products search.
@@ -1359,8 +1410,8 @@ class EODataAccessGateway:
1359
1410
  items_per_page: Optional[int] = None,
1360
1411
  start: Optional[str] = None,
1361
1412
  end: Optional[str] = None,
1362
- geom: Optional[Union[str, Dict[str, float], BaseGeometry]] = None,
1363
- locations: Optional[Dict[str, str]] = None,
1413
+ geom: Optional[Union[str, dict[str, float], BaseGeometry]] = None,
1414
+ locations: Optional[dict[str, str]] = None,
1364
1415
  **kwargs: Any,
1365
1416
  ) -> SearchResult:
1366
1417
  """Search and return all the products matching the search criteria.
@@ -1435,7 +1486,7 @@ class EODataAccessGateway:
1435
1486
  )
1436
1487
  or DEFAULT_MAX_ITEMS_PER_PAGE
1437
1488
  )
1438
- logger.debug(
1489
+ logger.info(
1439
1490
  "Searching for all the products with provider %s and a maximum of %s "
1440
1491
  "items per page.",
1441
1492
  search_plugin.provider,
@@ -1503,7 +1554,7 @@ class EODataAccessGateway:
1503
1554
  try:
1504
1555
  product_type = self.get_product_type_from_alias(product_type)
1505
1556
  except NoMatchingProductType:
1506
- logger.warning("product type %s not found", product_type)
1557
+ logger.debug("product type %s not found", product_type)
1507
1558
  get_search_plugins_kwargs = dict(provider=provider, product_type=product_type)
1508
1559
  search_plugins = self._plugins_manager.get_search_plugins(
1509
1560
  **get_search_plugins_kwargs
@@ -1526,7 +1577,7 @@ class EODataAccessGateway:
1526
1577
  "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1527
1578
  )
1528
1579
  kwargs.update(items_per_page=items_per_page)
1529
- if isinstance(plugin, BuildPostSearchResult):
1580
+ if isinstance(plugin, PostJsonSearch):
1530
1581
  kwargs.update(
1531
1582
  items_per_page=items_per_page,
1532
1583
  _dc_qs=_dc_qs,
@@ -1544,9 +1595,10 @@ class EODataAccessGateway:
1544
1595
  **kwargs,
1545
1596
  ):
1546
1597
  results.data.extend(page_results.data)
1547
- except Exception:
1598
+ except Exception as e:
1548
1599
  if kwargs.get("raise_errors"):
1549
1600
  raise
1601
+ logger.warning(e)
1550
1602
  continue
1551
1603
 
1552
1604
  # try using crunch to get unique result
@@ -1580,20 +1632,16 @@ class EODataAccessGateway:
1580
1632
  if not getattr(plugin.config, "discover_product_types", {}).get("fetch_url"):
1581
1633
  return None
1582
1634
 
1583
- kwargs: Dict[str, Any] = {"productType": product_type}
1635
+ kwargs: dict[str, Any] = {"productType": product_type}
1584
1636
 
1585
1637
  # append auth if needed
1586
1638
  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
- )
1639
+ if auth := self._plugins_manager.get_auth(
1640
+ plugin.provider,
1641
+ getattr(plugin.config, "api_endpoint", None),
1642
+ plugin.config,
1643
+ ):
1644
+ kwargs["auth"] = auth
1597
1645
 
1598
1646
  product_type_config = plugin.discover_product_types(**kwargs)
1599
1647
  self.update_product_types_list({provider: product_type_config})
@@ -1602,11 +1650,11 @@ class EODataAccessGateway:
1602
1650
  self,
1603
1651
  start: Optional[str] = None,
1604
1652
  end: Optional[str] = None,
1605
- geom: Optional[Union[str, Dict[str, float], BaseGeometry]] = None,
1606
- locations: Optional[Dict[str, str]] = None,
1653
+ geom: Optional[Union[str, dict[str, float], BaseGeometry]] = None,
1654
+ locations: Optional[dict[str, str]] = None,
1607
1655
  provider: Optional[str] = None,
1608
1656
  **kwargs: Any,
1609
- ) -> Tuple[List[Union[Search, Api]], Dict[str, Any]]:
1657
+ ) -> tuple[list[Union[Search, Api]], dict[str, Any]]:
1610
1658
  """Internal method to prepare the search kwargs and get the search plugins.
1611
1659
 
1612
1660
  Product query:
@@ -1714,16 +1762,16 @@ class EODataAccessGateway:
1714
1762
 
1715
1763
  preferred_provider = self.get_preferred_provider()[0]
1716
1764
 
1717
- search_plugins: List[Union[Search, Api]] = []
1765
+ search_plugins: list[Union[Search, Api]] = []
1718
1766
  for plugin in self._plugins_manager.get_search_plugins(
1719
1767
  product_type=product_type, provider=provider
1720
1768
  ):
1721
- # exclude BuildPostSearchResult plugins from search fallback for unknow product_type
1769
+ # exclude MeteoblueSearch plugins from search fallback for unknown product_type
1722
1770
  if (
1723
1771
  provider != plugin.provider
1724
1772
  and preferred_provider != plugin.provider
1725
1773
  and product_type not in self.product_types_config
1726
- and isinstance(plugin, BuildPostSearchResult)
1774
+ and isinstance(plugin, MeteoblueSearch)
1727
1775
  ):
1728
1776
  continue
1729
1777
  search_plugins.append(plugin)
@@ -1732,12 +1780,10 @@ class EODataAccessGateway:
1732
1780
  provider = preferred_provider
1733
1781
  providers = [plugin.provider for plugin in search_plugins]
1734
1782
  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.",
1783
+ logger.debug(
1784
+ "Product type '%s' is not available with preferred provider '%s'.",
1738
1785
  product_type,
1739
1786
  provider,
1740
- search_plugins[0].provider,
1741
1787
  )
1742
1788
  else:
1743
1789
  provider_plugin = list(
@@ -1745,35 +1791,10 @@ class EODataAccessGateway:
1745
1791
  )[0]
1746
1792
  search_plugins.remove(provider_plugin)
1747
1793
  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
1794
  # Add product_types_config to plugin config. This dict contains product
1754
1795
  # type metadata that will also be stored in each product's properties.
1755
1796
  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)
1797
+ self._attach_product_type_config(search_plugin, product_type)
1777
1798
 
1778
1799
  return search_plugins, kwargs
1779
1800
 
@@ -1793,6 +1814,7 @@ class EODataAccessGateway:
1793
1814
  :param kwargs: Some other criteria that will be used to do the search
1794
1815
  :returns: A collection of EO products matching the criteria
1795
1816
  """
1817
+ logger.info("Searching on provider %s", search_plugin.provider)
1796
1818
  max_items_per_page = getattr(search_plugin.config, "pagination", {}).get(
1797
1819
  "max_items_per_page", DEFAULT_MAX_ITEMS_PER_PAGE
1798
1820
  )
@@ -1810,19 +1832,23 @@ class EODataAccessGateway:
1810
1832
  max_items_per_page,
1811
1833
  )
1812
1834
 
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
- results: List[EOProduct] = []
1835
+ results: list[EOProduct] = []
1818
1836
  total_results: Optional[int] = 0 if count else None
1819
1837
 
1838
+ errors: list[tuple[str, Exception]] = []
1839
+
1820
1840
  try:
1821
1841
  prep = PreparedSearch(count=count)
1822
- if need_auth and auth_plugin and can_authenticate:
1823
- prep.auth = auth_plugin.authenticate()
1824
1842
 
1825
- prep.auth_plugin = auth_plugin
1843
+ # append auth if needed
1844
+ if getattr(search_plugin.config, "need_auth", False):
1845
+ if auth := self._plugins_manager.get_auth(
1846
+ search_plugin.provider,
1847
+ getattr(search_plugin.config, "api_endpoint", None),
1848
+ search_plugin.config,
1849
+ ):
1850
+ prep.auth = auth
1851
+
1826
1852
  prep.page = kwargs.pop("page", None)
1827
1853
  prep.items_per_page = kwargs.pop("items_per_page", None)
1828
1854
 
@@ -1876,12 +1902,31 @@ class EODataAccessGateway:
1876
1902
  eo_product.product_type
1877
1903
  )
1878
1904
  except NoMatchingProductType:
1879
- logger.warning("product type %s not found", eo_product.product_type)
1905
+ logger.debug("product type %s not found", eo_product.product_type)
1880
1906
 
1881
1907
  if eo_product.search_intersection is not None:
1882
1908
  download_plugin = self._plugins_manager.get_download_plugin(
1883
1909
  eo_product
1884
1910
  )
1911
+ if len(eo_product.assets) > 0:
1912
+ matching_url = next(iter(eo_product.assets.values()))["href"]
1913
+ elif eo_product.properties.get("storageStatus") != ONLINE_STATUS:
1914
+ matching_url = eo_product.properties.get(
1915
+ "orderLink"
1916
+ ) or eo_product.properties.get("downloadLink")
1917
+ else:
1918
+ matching_url = eo_product.properties.get("downloadLink")
1919
+
1920
+ try:
1921
+ auth_plugin = next(
1922
+ self._plugins_manager.get_auth_plugins(
1923
+ search_plugin.provider,
1924
+ matching_url=matching_url,
1925
+ matching_conf=download_plugin.config,
1926
+ )
1927
+ )
1928
+ except StopIteration:
1929
+ auth_plugin = None
1885
1930
  eo_product.register_downloader(download_plugin, auth_plugin)
1886
1931
 
1887
1932
  results.extend(res)
@@ -1911,13 +1956,6 @@ class EODataAccessGateway:
1911
1956
  "the total number of products matching the search criteria"
1912
1957
  )
1913
1958
  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
1959
  if raise_errors:
1922
1960
  # Raise the error, letting the application wrapping eodag know that
1923
1961
  # something went bad. This way it will be able to decide what to do next
@@ -1927,8 +1965,8 @@ class EODataAccessGateway:
1927
1965
  "Error while searching on provider %s (ignored):",
1928
1966
  search_plugin.provider,
1929
1967
  )
1930
- self.search_errors.add((search_plugin.provider, e))
1931
- return SearchResult(results, total_results)
1968
+ errors.append((search_plugin.provider, e))
1969
+ return SearchResult(results, total_results, errors)
1932
1970
 
1933
1971
  def crunch(self, results: SearchResult, **kwargs: Any) -> SearchResult:
1934
1972
  """Apply the filters given through the keyword arguments to the results
@@ -1945,7 +1983,7 @@ class EODataAccessGateway:
1945
1983
  return results
1946
1984
 
1947
1985
  @staticmethod
1948
- def group_by_extent(searches: List[SearchResult]) -> List[SearchResult]:
1986
+ def group_by_extent(searches: list[SearchResult]) -> list[SearchResult]:
1949
1987
  """Combines multiple SearchResults and return a list of SearchResults grouped
1950
1988
  by extent (i.e. bounding box).
1951
1989
 
@@ -1954,7 +1992,7 @@ class EODataAccessGateway:
1954
1992
  """
1955
1993
  # Dict with extents as keys, each extent being defined by a str
1956
1994
  # "{minx}{miny}{maxx}{maxy}" (each float rounded to 2 dec).
1957
- products_grouped_by_extent: Dict[str, Any] = {}
1995
+ products_grouped_by_extent: dict[str, Any] = {}
1958
1996
 
1959
1997
  for search in searches:
1960
1998
  for product in search:
@@ -1973,16 +2011,16 @@ class EODataAccessGateway:
1973
2011
  search_result: SearchResult,
1974
2012
  downloaded_callback: Optional[DownloadedCallback] = None,
1975
2013
  progress_callback: Optional[ProgressCallback] = None,
1976
- wait: int = DEFAULT_DOWNLOAD_WAIT,
1977
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
2014
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
2015
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
1978
2016
  **kwargs: Unpack[DownloadConf],
1979
- ) -> List[str]:
2017
+ ) -> list[str]:
1980
2018
  """Download all products resulting from a search.
1981
2019
 
1982
2020
  :param search_result: A collection of EO products resulting from a search
1983
2021
  :param downloaded_callback: (optional) A method or a callable object which takes
1984
2022
  as parameter the ``product``. You can use the base class
1985
- :class:`~eodag.api.product.DownloadedCallback` and override
2023
+ :class:`~eodag.utils.DownloadedCallback` and override
1986
2024
  its ``__call__`` method. Will be called each time a product
1987
2025
  finishes downloading
1988
2026
  :param progress_callback: (optional) A method or a callable object
@@ -2061,12 +2099,12 @@ class EODataAccessGateway:
2061
2099
  products = self.deserialize(filename)
2062
2100
  for i, product in enumerate(products):
2063
2101
  if product.downloader is None:
2102
+ downloader = self._plugins_manager.get_download_plugin(product)
2064
2103
  auth = product.downloader_auth
2065
2104
  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
- )
2105
+ auth = self._plugins_manager.get_auth_plugin(downloader, product)
2106
+ products[i].register_downloader(downloader, auth)
2107
+
2070
2108
  return products
2071
2109
 
2072
2110
  @_deprecated(
@@ -2090,7 +2128,7 @@ class EODataAccessGateway:
2090
2128
 
2091
2129
  :param filename: A filename containing features encoded as a geojson
2092
2130
  :param recursive: (optional) Browse recursively in child nodes if True
2093
- :param max_connections: (optional) Maximum number of connections for HTTP requests
2131
+ :param max_connections: (optional) Maximum number of connections for concurrent HTTP requests
2094
2132
  :param provider: (optional) Data provider
2095
2133
  :param productType: (optional) Data product type
2096
2134
  :param timeout: (optional) Timeout in seconds for each internal HTTP request
@@ -2136,8 +2174,8 @@ class EODataAccessGateway:
2136
2174
  self,
2137
2175
  product: EOProduct,
2138
2176
  progress_callback: Optional[ProgressCallback] = None,
2139
- wait: int = DEFAULT_DOWNLOAD_WAIT,
2140
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
2177
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
2178
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
2141
2179
  **kwargs: Unpack[DownloadConf],
2142
2180
  ) -> str:
2143
2181
  """Download a single product.
@@ -2197,12 +2235,11 @@ class EODataAccessGateway:
2197
2235
 
2198
2236
  def _setup_downloader(self, product: EOProduct) -> None:
2199
2237
  if product.downloader is None:
2238
+ downloader = self._plugins_manager.get_download_plugin(product)
2200
2239
  auth = product.downloader_auth
2201
2240
  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
- )
2241
+ auth = self._plugins_manager.get_auth_plugin(downloader, product)
2242
+ product.register_downloader(downloader, auth)
2206
2243
 
2207
2244
  def get_cruncher(self, name: str, **options: Any) -> Crunch:
2208
2245
  """Build a crunch plugin from a configuration
@@ -2216,79 +2253,115 @@ class EODataAccessGateway:
2216
2253
  return self._plugins_manager.get_crunch_plugin(name, **plugin_conf)
2217
2254
 
2218
2255
  def list_queryables(
2219
- self, provider: Optional[str] = None, **kwargs: Any
2220
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
2256
+ self,
2257
+ provider: Optional[str] = None,
2258
+ fetch_providers: bool = True,
2259
+ **kwargs: Any,
2260
+ ) -> QueryablesDict:
2221
2261
  """Fetch the queryable properties for a given product type and/or provider.
2222
2262
 
2223
2263
  :param provider: (optional) The provider.
2264
+ :param fetch_providers: If new product types should be fetched from the providers; default: True
2224
2265
  :param kwargs: additional filters for queryables (`productType` or other search
2225
2266
  arguments)
2226
2267
 
2227
2268
  :raises UnsupportedProductType: If the specified product type is not available for the
2228
2269
  provider.
2229
2270
 
2230
- :returns: A dict containing the EODAG queryable properties, associating
2231
- parameters to their annotated type
2271
+ :returns: A :class:`~eodag.api.product.queryables.QuerybalesDict` containing the EODAG queryable
2272
+ properties, associating parameters to their annotated type, and a additional_properties attribute
2232
2273
  """
2233
- available_product_types = [
2274
+ # only fetch providers if product type is not found
2275
+ available_product_types: list[str] = [
2234
2276
  pt["ID"]
2235
2277
  for pt in self.list_product_types(provider=provider, fetch_providers=False)
2236
2278
  ]
2237
- product_type = kwargs.get("productType")
2279
+ product_type: Optional[str] = kwargs.get("productType")
2280
+ pt_alias: Optional[str] = product_type
2238
2281
 
2239
2282
  if product_type:
2283
+ if product_type not in available_product_types:
2284
+ if fetch_providers:
2285
+ # fetch providers and try again
2286
+ available_product_types = [
2287
+ pt["ID"]
2288
+ for pt in self.list_product_types(
2289
+ provider=provider, fetch_providers=True
2290
+ )
2291
+ ]
2292
+ raise UnsupportedProductType(f"{product_type} is not available.")
2240
2293
  try:
2241
2294
  kwargs["productType"] = product_type = self.get_product_type_from_alias(
2242
2295
  product_type
2243
2296
  )
2244
2297
  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()
2298
+ raise UnsupportedProductType(f"{product_type} is not available.") from e
2249
2299
 
2250
2300
  if not provider and not product_type:
2251
- return model_fields_to_annotated(CommonQueryables.model_fields)
2301
+ return QueryablesDict(
2302
+ additional_properties=True,
2303
+ **model_fields_to_annotated(CommonQueryables.model_fields),
2304
+ )
2252
2305
 
2253
- providers_queryables: Dict[str, Dict[str, Annotated[Any, FieldInfo]]] = {}
2306
+ additional_properties = False
2307
+ additional_information = []
2308
+ queryable_properties: dict[str, Any] = {}
2254
2309
 
2255
2310
  for plugin in self._plugins_manager.get_search_plugins(product_type, provider):
2311
+ # attach product type config
2312
+ product_type_configs: dict[str, Any] = {}
2313
+ if product_type:
2314
+ self._attach_product_type_config(plugin, product_type)
2315
+ product_type_configs[product_type] = plugin.config.product_type_config
2316
+ else:
2317
+ for pt in available_product_types:
2318
+ self._attach_product_type_config(plugin, pt)
2319
+ product_type_configs[pt] = plugin.config.product_type_config
2320
+
2321
+ # authenticate if required
2256
2322
  if getattr(plugin.config, "need_auth", False) and (
2257
- auth := self._plugins_manager.get_auth_plugin(plugin.provider)
2323
+ auth := self._plugins_manager.get_auth_plugin(plugin)
2258
2324
  ):
2259
- plugin.auth = auth.authenticate()
2260
- providers_queryables[plugin.provider] = plugin.list_queryables(
2261
- filters=kwargs, product_type=product_type
2262
- )
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
- }
2325
+ try:
2326
+ plugin.auth = auth.authenticate()
2327
+ except AuthenticationError:
2328
+ logger.debug(
2329
+ "queryables from provider %s could not be fetched due to an authentication error",
2330
+ plugin.provider,
2331
+ )
2272
2332
 
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]
2333
+ plugin_queryables = plugin.list_queryables(
2334
+ kwargs,
2335
+ available_product_types,
2336
+ product_type_configs,
2337
+ product_type,
2338
+ pt_alias,
2339
+ )
2278
2340
 
2279
- queryables.update(model_fields_to_annotated(common_queryables))
2341
+ if plugin_queryables.additional_information:
2342
+ additional_information.append(
2343
+ f"{plugin.provider}: {plugin_queryables.additional_information}"
2344
+ )
2345
+ queryable_properties = {**plugin_queryables, **queryable_properties}
2346
+ additional_properties = (
2347
+ additional_properties or plugin_queryables.additional_properties
2348
+ )
2280
2349
 
2281
- return queryables
2350
+ return QueryablesDict(
2351
+ additional_properties=additional_properties,
2352
+ additional_information=" | ".join(additional_information),
2353
+ **queryable_properties,
2354
+ )
2282
2355
 
2283
- def available_sortables(self) -> Dict[str, Optional[ProviderSortables]]:
2356
+ def available_sortables(self) -> dict[str, Optional[ProviderSortables]]:
2284
2357
  """For each provider, gives its available sortable parameter(s) and its maximum
2285
2358
  number of them if it supports the sorting feature, otherwise gives None.
2286
2359
 
2287
- :returns: A dictionnary with providers as keys and dictionnary of sortable parameter(s) and
2360
+ :returns: A dictionary with providers as keys and dictionary of sortable parameter(s) and
2288
2361
  its (their) maximum number as value(s).
2289
2362
  :raises: :class:`~eodag.utils.exceptions.UnsupportedProvider`
2290
2363
  """
2291
- sortables: Dict[str, Optional[ProviderSortables]] = {}
2364
+ sortables: dict[str, Optional[ProviderSortables]] = {}
2292
2365
  provider_search_plugins = self._plugins_manager.get_search_plugins()
2293
2366
  for provider_search_plugin in provider_search_plugins:
2294
2367
  provider = provider_search_plugin.provider
@@ -2311,3 +2384,30 @@ class EODataAccessGateway:
2311
2384
  ],
2312
2385
  }
2313
2386
  return sortables
2387
+
2388
+ def _attach_product_type_config(self, plugin: Search, product_type: str) -> None:
2389
+ """
2390
+ Attach product_types_config to plugin config. This dict contains product
2391
+ type metadata that will also be stored in each product's properties.
2392
+ """
2393
+ try:
2394
+ plugin.config.product_type_config = dict(
2395
+ [
2396
+ p
2397
+ for p in self.list_product_types(
2398
+ plugin.provider, fetch_providers=False
2399
+ )
2400
+ if p["_id"] == product_type
2401
+ ][0],
2402
+ **{"productType": product_type},
2403
+ )
2404
+ # If the product isn't in the catalog, it's a generic product type.
2405
+ except IndexError:
2406
+ # Construct the GENERIC_PRODUCT_TYPE metadata
2407
+ plugin.config.product_type_config = dict(
2408
+ ID=GENERIC_PRODUCT_TYPE,
2409
+ **self.product_types_config[GENERIC_PRODUCT_TYPE],
2410
+ productType=product_type,
2411
+ )
2412
+ # Remove the ID since this is equal to productType.
2413
+ plugin.config.product_type_config.pop("ID", None)