eodag 4.0.0a3__py3-none-any.whl → 4.0.0a5__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.
eodag/api/core.py CHANGED
@@ -18,6 +18,7 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import datetime
21
+ import itertools
21
22
  import logging
22
23
  import os
23
24
  import re
@@ -25,19 +26,18 @@ import shutil
25
26
  import tempfile
26
27
  import warnings
27
28
  from collections import deque
29
+ from copy import deepcopy
28
30
  from importlib.metadata import version
29
31
  from importlib.resources import files as res_files
30
32
  from operator import attrgetter, itemgetter
31
- from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
33
+ from typing import TYPE_CHECKING, Any, Iterator, Optional, Union, cast
32
34
 
33
35
  import geojson
34
36
  import yaml
35
37
 
36
38
  from eodag.api.collection import Collection, CollectionsDict, CollectionsList
37
- from eodag.api.product.metadata_mapping import (
38
- NOT_AVAILABLE,
39
- mtd_cfg_as_conversion_and_querypath,
40
- )
39
+ from eodag.api.product.metadata_mapping import mtd_cfg_as_conversion_and_querypath
40
+ from eodag.api.provider import Provider, ProvidersDict
41
41
  from eodag.api.search_result import SearchResult
42
42
  from eodag.config import (
43
43
  PLUGINS_TOPICS_KEYS,
@@ -46,13 +46,7 @@ from eodag.config import (
46
46
  credentials_in_auth,
47
47
  get_ext_collections_conf,
48
48
  load_default_config,
49
- load_stac_provider_config,
50
49
  load_yml_config,
51
- override_config_from_env,
52
- override_config_from_file,
53
- override_config_from_mapping,
54
- provider_config_init,
55
- share_credentials,
56
50
  )
57
51
  from eodag.plugins.manager import PluginManager
58
52
  from eodag.plugins.search import PreparedSearch
@@ -93,6 +87,7 @@ from eodag.utils.free_text_search import compile_free_text_query
93
87
  from eodag.utils.stac_reader import fetch_stac_items
94
88
 
95
89
  if TYPE_CHECKING:
90
+ from concurrent.futures import ThreadPoolExecutor
96
91
  from shapely.geometry.base import BaseGeometry
97
92
 
98
93
  from eodag.api.product import EOProduct
@@ -124,7 +119,8 @@ class EODataAccessGateway:
124
119
  )
125
120
  collections_config_dict = SimpleYamlProxyConfig(collections_config_path).source
126
121
  self.collections_config = self._collections_config_init(collections_config_dict)
127
- self.providers_config = load_default_config()
122
+
123
+ self._providers = ProvidersDict.from_configs(load_default_config())
128
124
 
129
125
  env_var_cfg_dir = "EODAG_CFG_DIR"
130
126
  self.conf_dir = os.getenv(
@@ -148,9 +144,8 @@ class EODataAccessGateway:
148
144
  self.conf_dir = tmp_conf_dir
149
145
  makedirs(self.conf_dir)
150
146
 
151
- self._plugins_manager = PluginManager(self.providers_config)
152
- # use updated providers_config
153
- self.providers_config = self._plugins_manager.providers_config
147
+ self._plugins_manager = PluginManager(self._providers)
148
+ self._providers = self._plugins_manager.providers
154
149
 
155
150
  # First level override: From a user configuration file
156
151
  if user_conf_file_path is None:
@@ -166,67 +161,29 @@ class EODataAccessGateway:
166
161
  ),
167
162
  standard_configuration_path,
168
163
  )
169
- override_config_from_file(self.providers_config, user_conf_file_path)
164
+ self._providers.update_from_config_file(user_conf_file_path)
170
165
 
171
166
  # Second level override: From environment variables
172
- override_config_from_env(self.providers_config)
173
-
174
- # share credentials between updated plugins confs
175
- share_credentials(self.providers_config)
167
+ self._providers.update_from_env()
176
168
 
177
169
  # init updated providers conf
178
170
  strict_mode = is_env_var_true("EODAG_STRICT_COLLECTIONS")
179
- available_collections = set(self.collections_config.keys())
180
171
 
181
- for provider in self.providers_config.keys():
182
- provider_config_init(
183
- self.providers_config[provider],
184
- load_stac_provider_config(),
185
- )
186
-
187
- self._sync_provider_collections(
188
- provider, available_collections, strict_mode
189
- )
172
+ for provider in self._providers.values():
173
+ provider.sync_collections(self, strict_mode)
190
174
 
191
175
  # re-build _plugins_manager using up-to-date providers_config
192
- self._plugins_manager.rebuild(self.providers_config)
176
+ self._plugins_manager.rebuild(self._providers)
193
177
 
194
178
  # store pruned providers configs
195
179
  self._pruned_providers_config: dict[str, Any] = {}
180
+
196
181
  # filter out providers needing auth that have no credentials set
197
182
  self._prune_providers_list()
198
183
 
199
184
  # Sort providers taking into account of possible new priority orders
200
185
  self._plugins_manager.sort_providers()
201
186
 
202
- # set locations configuration
203
- if locations_conf_path is None:
204
- locations_conf_path = os.getenv("EODAG_LOCS_CFG_FILE")
205
- if locations_conf_path is None:
206
- locations_conf_path = os.path.join(self.conf_dir, "locations.yml")
207
- if not os.path.isfile(locations_conf_path):
208
- # copy locations conf file and replace path example
209
- locations_conf_template = str(
210
- res_files("eodag") / "resources" / "locations_conf_template.yml"
211
- )
212
- with (
213
- open(locations_conf_template) as infile,
214
- open(locations_conf_path, "w") as outfile,
215
- ):
216
- # The template contains paths in the form of:
217
- # /path/to/locations/file.shp
218
- path_template = "/path/to/locations/"
219
- for line in infile:
220
- line = line.replace(
221
- path_template,
222
- os.path.join(self.conf_dir, "shp") + os.path.sep,
223
- )
224
- outfile.write(line)
225
- # copy sample shapefile dir
226
- shutil.copytree(
227
- str(res_files("eodag") / "resources" / "shp"),
228
- os.path.join(self.conf_dir, "shp"),
229
- )
230
187
  self.set_locations_conf(locations_conf_path)
231
188
 
232
189
  def _collections_config_init(
@@ -243,57 +200,19 @@ class EODataAccessGateway:
243
200
  ]
244
201
  return CollectionsDict(collections)
245
202
 
246
- def _sync_provider_collections(
247
- self,
248
- provider: str,
249
- available_collections: set[str],
250
- strict_mode: bool,
251
- ) -> None:
252
- """
253
- Synchronize collections for a provider based on strict or permissive mode.
254
-
255
- In strict mode, removes collections not in available_collections.
256
- In permissive mode, adds empty collection configs for missing types.
257
-
258
- :param provider: The provider name whose collections should be synchronized.
259
- :param available_collections: The set of available collection IDs.
260
- :param strict_mode: If True, remove unknown collections; if False, add empty configs for them.
261
- :returns: None
262
- """
263
- provider_products = self.providers_config[provider].products
264
- products_to_remove: list[str] = []
265
- products_to_add: list[str] = []
266
-
267
- for product_id in provider_products:
268
- if product_id == GENERIC_COLLECTION:
269
- continue
270
-
271
- if product_id not in available_collections:
272
- if strict_mode:
273
- products_to_remove.append(product_id)
274
- continue
275
-
276
- empty_product = Collection.create_with_dag(
277
- self, id=product_id, title=product_id, description=NOT_AVAILABLE
278
- )
279
- self.collections_config[product_id] = empty_product
280
- products_to_add.append(product_id)
281
-
282
- if products_to_add:
283
- logger.debug(
284
- "Collections permissive mode, %s added (provider %s)",
285
- ", ".join(products_to_add),
286
- provider,
203
+ @property
204
+ def providers(self) -> ProvidersDict:
205
+ """Providers of eodag configuration sorted by priority in descending order and by name in ascending order."""
206
+ providers = deepcopy(self._providers)
207
+ # Sort: priority descending, then name ascending
208
+ providers.data = {
209
+ k: v
210
+ for k, v in sorted(
211
+ providers.data.items(), key=lambda item: (-item[1].priority, item[0])
287
212
  )
213
+ }
288
214
 
289
- if products_to_remove:
290
- logger.debug(
291
- "Collections strict mode, ignoring %s (provider %s)",
292
- ", ".join(products_to_remove),
293
- provider,
294
- )
295
- for id in products_to_remove:
296
- del self.providers_config[provider].products[id]
215
+ return providers
297
216
 
298
217
  def get_version(self) -> str:
299
218
  """Get eodag package version"""
@@ -305,10 +224,11 @@ class EODataAccessGateway:
305
224
  :param provider: The name of the provider that should be considered as the
306
225
  preferred provider to be used for this instance
307
226
  """
308
- if provider not in self.available_providers():
227
+ if provider not in self.providers.names:
309
228
  raise UnsupportedProvider(
310
229
  f"This provider is not recognised by eodag: {provider}"
311
230
  )
231
+
312
232
  preferred_provider, max_priority = self.get_preferred_provider()
313
233
  if preferred_provider != provider:
314
234
  new_priority = max_priority + 1
@@ -320,12 +240,7 @@ class EODataAccessGateway:
320
240
 
321
241
  :returns: The provider with the maximum priority and its priority
322
242
  """
323
- providers_with_priority = [
324
- (provider, conf.priority)
325
- for provider, conf in self.providers_config.items()
326
- ]
327
- preferred, priority = max(providers_with_priority, key=itemgetter(1))
328
- return preferred, priority
243
+ return max(self._providers.priorities.items(), key=itemgetter(1))
329
244
 
330
245
  def update_providers_config(
331
246
  self,
@@ -347,27 +262,17 @@ class EODataAccessGateway:
347
262
  return None
348
263
 
349
264
  # restore the pruned configuration
350
- for provider in list(self._pruned_providers_config.keys()):
351
- if provider in conf_update:
265
+ for name in list(self._pruned_providers_config):
266
+ config = self._pruned_providers_config[name]
267
+ if name in conf_update:
352
268
  logger.info(
353
- "%s: provider restored from the pruned configurations",
354
- provider,
269
+ "%s: provider restored from the pruned configurations", name
355
270
  )
356
- self.providers_config[provider] = self._pruned_providers_config.pop(
357
- provider
358
- )
359
-
360
- override_config_from_mapping(self.providers_config, conf_update)
271
+ self._providers[name] = Provider(config)
272
+ self._pruned_providers_config.pop(name)
361
273
 
362
- # share credentials between updated plugins confs
363
- share_credentials(self.providers_config)
274
+ self._providers.update_from_configs(conf_update)
364
275
 
365
- for provider in conf_update.keys():
366
- provider_config_init(
367
- self.providers_config[provider],
368
- load_stac_provider_config(),
369
- )
370
- setattr(self.providers_config[provider], "collections_fetched", False)
371
276
  # re-create _plugins_manager using up-to-date providers_config
372
277
  self._plugins_manager.build_collection_to_provider_config_map()
373
278
 
@@ -398,10 +303,11 @@ class EODataAccessGateway:
398
303
  :param search: Search :class:`~eodag.config.PluginConfig` mapping
399
304
  :param products: Provider collections mapping
400
305
  :param download: Download :class:`~eodag.config.PluginConfig` mapping
401
- :param kwargs: Additional :class:`~eodag.config.ProviderConfig` mapping
306
+ :param kwargs: Additional :class:`~eodag.api.provider.ProviderConfig` mapping
402
307
  """
403
308
  conf_dict: dict[str, Any] = {
404
309
  name: {
310
+ "name": name,
405
311
  "url": url,
406
312
  "search": {"type": "StacSearch", **search},
407
313
  "products": {
@@ -440,8 +346,10 @@ class EODataAccessGateway:
440
346
  def _prune_providers_list(self) -> None:
441
347
  """Removes from config providers needing auth that have no credentials set."""
442
348
  update_needed = False
443
- for provider in list(self.providers_config.keys()):
444
- conf = self.providers_config[provider]
349
+
350
+ # loop over a copy to allow popping items
351
+ for name, provider in list(self._providers.items()):
352
+ conf = provider.config
445
353
 
446
354
  # remove providers using skipped plugins
447
355
  if [
@@ -450,7 +358,7 @@ class EODataAccessGateway:
450
358
  if isinstance(v, PluginConfig)
451
359
  and getattr(v, "type", None) in self._plugins_manager.skipped_plugins
452
360
  ]:
453
- self.providers_config.pop(provider)
361
+ del self._providers[provider.name]
454
362
  logger.debug(
455
363
  f"{provider}: provider needing unavailable plugin has been removed"
456
364
  )
@@ -461,26 +369,28 @@ class EODataAccessGateway:
461
369
  credentials_exist = credentials_in_auth(conf.api)
462
370
  if not credentials_exist:
463
371
  # credentials needed but not found
464
- self._pruned_providers_config[provider] = self.providers_config.pop(
465
- provider
466
- )
372
+ self._pruned_providers_config[provider.name] = conf
373
+ del self._providers[provider.name]
374
+
467
375
  update_needed = True
468
376
  logger.info(
469
377
  "%s: provider needing auth for search has been pruned because no credentials could be found",
470
378
  provider,
471
379
  )
380
+
472
381
  elif hasattr(conf, "search") and getattr(conf.search, "need_auth", False):
473
382
  if not hasattr(conf, "auth") and not hasattr(conf, "search_auth"):
474
383
  # credentials needed but no auth plugin was found
475
- self._pruned_providers_config[provider] = self.providers_config.pop(
476
- provider
477
- )
384
+ self._pruned_providers_config[provider.name] = conf
385
+ del self._providers[provider.name]
386
+
478
387
  update_needed = True
479
388
  logger.info(
480
389
  "%s: provider needing auth for search has been pruned because no auth plugin could be found",
481
390
  provider,
482
391
  )
483
392
  continue
393
+
484
394
  credentials_exist = (
485
395
  hasattr(conf, "search_auth")
486
396
  and credentials_in_auth(conf.search_auth)
@@ -491,30 +401,31 @@ class EODataAccessGateway:
491
401
  )
492
402
  if not credentials_exist:
493
403
  # credentials needed but not found
494
- self._pruned_providers_config[provider] = self.providers_config.pop(
495
- provider
496
- )
404
+ self._pruned_providers_config[provider.name] = conf
405
+ del self._providers[provider.name]
406
+
497
407
  update_needed = True
498
408
  logger.info(
499
409
  "%s: provider needing auth for search has been pruned because no credentials could be found",
500
410
  provider,
501
411
  )
412
+
502
413
  elif not hasattr(conf, "api") and not hasattr(conf, "search"):
503
414
  # provider should have at least an api or search plugin
504
- self._pruned_providers_config[provider] = self.providers_config.pop(
505
- provider
506
- )
415
+ self._pruned_providers_config[provider.name] = conf
416
+ del self._providers[provider.name]
417
+
418
+ update_needed = True
507
419
  logger.info(
508
420
  "%s: provider has been pruned because no api or search plugin could be found",
509
421
  provider,
510
422
  )
511
- update_needed = True
512
423
 
513
424
  if update_needed:
514
425
  # rebuild _plugins_manager with updated providers list
515
- self._plugins_manager.rebuild(self.providers_config)
426
+ self._plugins_manager.rebuild(self._providers)
516
427
 
517
- def set_locations_conf(self, locations_conf_path: str) -> None:
428
+ def set_locations_conf(self, locations_conf_path: Optional[str]) -> None:
518
429
  """Set locations configuration.
519
430
  This configuration (YML format) will contain a shapefile list associated
520
431
  to a name and attribute parameters needed to identify the needed geometry.
@@ -537,6 +448,37 @@ class EODataAccessGateway:
537
448
 
538
449
  :param locations_conf_path: Path to the locations configuration file
539
450
  """
451
+ if locations_conf_path is None:
452
+ locations_conf_path = os.getenv("EODAG_LOCS_CFG_FILE")
453
+ if locations_conf_path is None:
454
+ locations_conf_path = os.path.join(self.conf_dir, "locations.yml")
455
+ if not os.path.isfile(locations_conf_path):
456
+ # Ensure the directory exists
457
+ os.makedirs(os.path.dirname(locations_conf_path), exist_ok=True)
458
+
459
+ # copy locations conf file and replace path example
460
+ locations_conf_template = str(
461
+ res_files("eodag") / "resources" / "locations_conf_template.yml"
462
+ )
463
+ with (
464
+ open(locations_conf_template) as infile,
465
+ open(locations_conf_path, "w") as outfile,
466
+ ):
467
+ # The template contains paths in the form of:
468
+ # /path/to/locations/file.shp
469
+ path_template = "/path/to/locations/"
470
+ for line in infile:
471
+ line = line.replace(
472
+ path_template,
473
+ os.path.join(self.conf_dir, "shp") + os.path.sep,
474
+ )
475
+ outfile.write(line)
476
+ # copy sample shapefile dir
477
+ shutil.copytree(
478
+ str(res_files("eodag") / "resources" / "shp"),
479
+ os.path.join(self.conf_dir, "shp"),
480
+ )
481
+
540
482
  if os.path.isfile(locations_conf_path):
541
483
  locations_config = load_yml_config(locations_conf_path)
542
484
 
@@ -567,17 +509,11 @@ class EODataAccessGateway:
567
509
  # First, update collections list if possible
568
510
  self.fetch_collections_list(provider=provider)
569
511
 
570
- providers_configs = (
571
- list(self.providers_config.values())
572
- if not provider
573
- else [
574
- p
575
- for p in self.providers_config.values()
576
- if provider in [p.name, getattr(p, "group", None)]
577
- ]
512
+ providers_iter, providers_check = itertools.tee(
513
+ self._providers.filter_by_name_or_group(provider)
578
514
  )
579
515
 
580
- if provider and not providers_configs:
516
+ if provider and not any(providers_check):
581
517
  raise UnsupportedProvider(
582
518
  f"The requested provider is not (yet) supported: {provider}"
583
519
  )
@@ -585,8 +521,8 @@ class EODataAccessGateway:
585
521
  # unique collection ids from providers configs
586
522
  collection_ids = {
587
523
  collection_id
588
- for p in providers_configs
589
- for collection_id in p.products
524
+ for p in providers_iter
525
+ for collection_id in p.collections_config
590
526
  if collection_id != GENERIC_COLLECTION
591
527
  }
592
528
 
@@ -611,42 +547,18 @@ class EODataAccessGateway:
611
547
  if strict_mode:
612
548
  return
613
549
 
614
- providers_to_fetch = list(self.providers_config.keys())
615
- # check if some providers are grouped under a group name which is not a provider name
616
- if provider is not None and provider not in self.providers_config:
617
- providers_to_fetch = [
618
- p
619
- for p, pconf in self.providers_config.items()
620
- if provider == getattr(pconf, "group", None)
621
- ]
622
- if providers_to_fetch:
623
- logger.info(
624
- f"Fetch collections for {provider} group: {', '.join(providers_to_fetch)}"
625
- )
626
- else:
627
- return None
628
- elif provider is not None:
629
- providers_to_fetch = [provider]
630
-
631
550
  # providers discovery confs that are fetchable
632
- providers_discovery_configs_fetchable: dict[str, Any] = {}
551
+ providers_discovery_configs_fetchable: dict[
552
+ str, PluginConfig.DiscoverCollections
553
+ ] = {}
633
554
  # check if any provider has not already been fetched for collections
634
555
  already_fetched = True
635
- for provider_to_fetch in providers_to_fetch:
636
- provider_config = self.providers_config[provider_to_fetch]
637
- # get discovery conf
638
- if hasattr(provider_config, "search"):
639
- provider_search_config = provider_config.search
640
- elif hasattr(provider_config, "api"):
641
- provider_search_config = provider_config.api
642
- else:
643
- continue
644
- discovery_conf = getattr(provider_search_config, "discover_collections", {})
645
- if discovery_conf.get("fetch_url"):
556
+ for provider_to_fetch in self._providers.filter_by_name_or_group(provider):
557
+ if provider_to_fetch.fetchable and provider_to_fetch.search_config:
646
558
  providers_discovery_configs_fetchable[
647
- provider_to_fetch
648
- ] = discovery_conf
649
- if not getattr(provider_config, "collections_fetched", False):
559
+ provider_to_fetch.name
560
+ ] = provider_to_fetch.search_config.discover_collections
561
+ if not provider_to_fetch.collections_fetched:
650
562
  already_fetched = False
651
563
 
652
564
  if not already_fetched:
@@ -672,54 +584,55 @@ class EODataAccessGateway:
672
584
  # and collections list would need to be fetched
673
585
 
674
586
  # get ext_collections conf for user modified providers
675
- default_providers_config = load_default_config()
587
+ default_providers = ProvidersDict.from_configs(load_default_config())
676
588
  for (
677
589
  provider,
678
590
  user_discovery_conf,
679
591
  ) in providers_discovery_configs_fetchable.items():
680
592
  # default discover_collections conf
681
- if provider in default_providers_config:
682
- default_provider_config = default_providers_config[provider]
683
- if hasattr(default_provider_config, "search"):
684
- default_provider_search_config = default_provider_config.search
685
- elif hasattr(default_provider_config, "api"):
686
- default_provider_search_config = default_provider_config.api
687
- else:
593
+ if provider in default_providers:
594
+ default_provider = default_providers[provider]
595
+ if not default_provider.search_config:
688
596
  continue
689
- default_discovery_conf = getattr(
690
- default_provider_search_config, "discover_collections", {}
597
+
598
+ default_discovery_conf = (
599
+ default_provider.search_config.discover_collections
691
600
  )
601
+
692
602
  # compare confs
693
603
  if default_discovery_conf["result_type"] == "json" and isinstance(
694
604
  default_discovery_conf["results_entry"], str
695
605
  ):
696
- default_discovery_conf_parsed = dict(
697
- default_discovery_conf,
698
- **{
699
- "results_entry": string_to_jsonpath(
700
- default_discovery_conf["results_entry"], force=True
701
- )
702
- },
703
- **mtd_cfg_as_conversion_and_querypath(
704
- dict(
705
- generic_collection_id=default_discovery_conf[
706
- "generic_collection_id"
707
- ]
708
- )
709
- ),
710
- **dict(
711
- generic_collection_parsable_properties=mtd_cfg_as_conversion_and_querypath(
712
- default_discovery_conf[
713
- "generic_collection_parsable_properties"
714
- ]
715
- )
716
- ),
717
- **dict(
718
- generic_collection_parsable_metadata=mtd_cfg_as_conversion_and_querypath(
719
- default_discovery_conf[
720
- "generic_collection_parsable_metadata"
721
- ]
722
- )
606
+ default_discovery_conf_parsed = cast(
607
+ PluginConfig.DiscoverCollections,
608
+ dict(
609
+ default_discovery_conf,
610
+ **{
611
+ "results_entry": string_to_jsonpath(
612
+ default_discovery_conf["results_entry"], force=True
613
+ )
614
+ },
615
+ **mtd_cfg_as_conversion_and_querypath(
616
+ dict(
617
+ generic_collection_id=default_discovery_conf[
618
+ "generic_collection_id"
619
+ ]
620
+ )
621
+ ),
622
+ **dict(
623
+ generic_collection_parsable_properties=mtd_cfg_as_conversion_and_querypath(
624
+ default_discovery_conf[
625
+ "generic_collection_parsable_properties"
626
+ ]
627
+ )
628
+ ),
629
+ **dict(
630
+ generic_collection_parsable_metadata=mtd_cfg_as_conversion_and_querypath(
631
+ default_discovery_conf[
632
+ "generic_collection_parsable_metadata"
633
+ ]
634
+ )
635
+ ),
723
636
  ),
724
637
  )
725
638
  else:
@@ -757,51 +670,34 @@ class EODataAccessGateway:
757
670
  all providers (None value).
758
671
  :returns: external collections configuration
759
672
  """
760
- grouped_providers = [
761
- p
762
- for p, provider_config in self.providers_config.items()
763
- if provider == getattr(provider_config, "group", None)
764
- ]
765
- if provider and provider not in self.providers_config and grouped_providers:
766
- logger.info(
767
- f"Discover collections for {provider} group: {', '.join(grouped_providers)}"
768
- )
769
- elif provider and provider not in self.providers_config:
673
+
674
+ providers_iter, providers_check = itertools.tee(
675
+ self.providers.filter_by_name_or_group(provider)
676
+ )
677
+
678
+ if provider and not any(providers_check):
770
679
  raise UnsupportedProvider(
771
680
  f"The requested provider is not (yet) supported: {provider}"
772
681
  )
682
+
773
683
  ext_collections_conf: dict[str, Any] = {}
774
- providers_to_fetch = [
775
- p
776
- for p in (
777
- [
778
- p
779
- for p in self.providers_config
780
- if p in grouped_providers + [provider]
781
- ]
782
- if provider
783
- else self.available_providers()
784
- )
785
- ]
684
+
786
685
  kwargs: dict[str, Any] = {}
787
- for provider in providers_to_fetch:
788
- if hasattr(self.providers_config[provider], "search"):
789
- search_plugin_config = self.providers_config[provider].search
790
- elif hasattr(self.providers_config[provider], "api"):
791
- search_plugin_config = self.providers_config[provider].api
792
- else:
686
+ for p in providers_iter:
687
+ if not p.search_config:
793
688
  return None
794
- if getattr(search_plugin_config, "discover_collections", {}).get(
795
- "fetch_url", None
796
- ):
689
+
690
+ if p.fetchable:
797
691
  search_plugin: Union[Search, Api] = next(
798
- self._plugins_manager.get_search_plugins(provider=provider)
692
+ self._plugins_manager.get_search_plugins(provider=p.name)
799
693
  )
694
+
800
695
  # check after plugin init if still fetchable
801
696
  if not getattr(search_plugin.config, "discover_collections", {}).get(
802
697
  "fetch_url"
803
698
  ):
804
699
  continue
700
+
805
701
  # append auth to search plugin if needed
806
702
  if getattr(search_plugin.config, "need_auth", False):
807
703
  if auth := self._plugins_manager.get_auth(
@@ -812,12 +708,12 @@ class EODataAccessGateway:
812
708
  kwargs["auth"] = auth
813
709
  else:
814
710
  logger.debug(
815
- f"Could not authenticate on {provider} for collections discovery"
711
+ f"Could not authenticate on {p} for collections discovery"
816
712
  )
817
- ext_collections_conf[provider] = None
713
+ ext_collections_conf[p.name] = None
818
714
  continue
819
715
 
820
- ext_collections_conf[provider] = search_plugin.discover_collections(
716
+ ext_collections_conf[p.name] = search_plugin.discover_collections(
821
717
  **kwargs
822
718
  )
823
719
 
@@ -831,20 +727,15 @@ class EODataAccessGateway:
831
727
  :param ext_collections_conf: external collections configuration
832
728
  """
833
729
  for provider, new_collections_conf in ext_collections_conf.items():
834
- if new_collections_conf and provider in self.providers_config:
730
+ if new_collections_conf and provider in self._providers:
835
731
  try:
836
- search_plugin_config = getattr(
837
- self.providers_config[provider], "search", None
838
- ) or getattr(self.providers_config[provider], "api", None)
839
- if search_plugin_config is None:
840
- continue
841
- if not getattr(
842
- search_plugin_config, "discover_collections", {}
843
- ).get("fetch_url"):
732
+ fetchable = self._providers[provider].fetchable
733
+ if not fetchable:
844
734
  # conf has been updated and provider collections are no more discoverable
845
735
  continue
736
+
846
737
  provider_products_config = (
847
- self.providers_config[provider].products or {}
738
+ self._providers[provider].collections_config or {}
848
739
  )
849
740
  except UnsupportedProvider:
850
741
  logger.debug(
@@ -852,6 +743,7 @@ class EODataAccessGateway:
852
743
  provider,
853
744
  )
854
745
  continue
746
+
855
747
  new_collections: list[str] = []
856
748
  bad_formatted_col_count = 0
857
749
  for (
@@ -861,11 +753,10 @@ class EODataAccessGateway:
861
753
  if new_collection not in provider_products_config:
862
754
  for existing_collection in provider_products_config.copy():
863
755
  # compare parsed extracted conf (without metadata_mapping entry)
864
- unparsable_keys = (
865
- search_plugin_config.discover_collections.get(
866
- "generic_collection_unparsable_properties", {}
867
- ).keys()
868
- )
756
+ unparsable_keys = self._providers[
757
+ provider
758
+ ].unparsable_properties
759
+
869
760
  new_parsed_collections_conf = {
870
761
  k: v
871
762
  for k, v in new_collection_conf.items()
@@ -930,19 +821,27 @@ class EODataAccessGateway:
930
821
  provider,
931
822
  )
932
823
 
933
- elif provider not in self.providers_config:
824
+ elif provider not in self._providers:
934
825
  # unknown provider
935
826
  continue
936
- self.providers_config[provider].collections_fetched = True
827
+
828
+ self._providers[provider].collections_fetched = True
937
829
 
938
830
  # re-create _plugins_manager using up-to-date providers_config
939
831
  self._plugins_manager.build_collection_to_provider_config_map()
940
832
 
833
+ @_deprecated(
834
+ reason="Please use 'EODataAccessGateway.providers' instead",
835
+ version="4.0.0",
836
+ )
941
837
  def available_providers(
942
838
  self, collection: Optional[str] = None, by_group: bool = False
943
839
  ) -> list[str]:
944
840
  """Gives the sorted list of the available providers or groups
945
841
 
842
+ .. deprecated:: v4.0.0
843
+ Please use :attr:`eodag.api.core.EODataAccessGateway.providers` instead.
844
+
946
845
  The providers or groups are sorted first by their priority level in descending order,
947
846
  and then alphabetically in ascending order for providers or groups with the same
948
847
  priority level.
@@ -952,32 +851,26 @@ class EODataAccessGateway:
952
851
  of providers, mixed with other providers
953
852
  :returns: the sorted list of the available providers or groups
954
853
  """
854
+ candidates = []
955
855
 
956
- if collection:
957
- providers = [
958
- (v.group if by_group and hasattr(v, "group") else k, v.priority)
959
- for k, v in self.providers_config.items()
960
- if collection in getattr(v, "products", {}).keys()
961
- ]
962
- else:
963
- providers = [
964
- (v.group if by_group and hasattr(v, "group") else k, v.priority)
965
- for k, v in self.providers_config.items()
966
- ]
856
+ # use "providers" property to get sorted providers
857
+ for key, provider in self.providers.items():
858
+ if collection and collection not in provider.collections_config:
859
+ continue
967
860
 
968
- # If by_group is True, keep only the highest priority for each group
969
- if by_group:
970
- group_priority: dict[str, int] = {}
971
- for name, priority in providers:
972
- if name not in group_priority or priority > group_priority[name]:
973
- group_priority[name] = priority
974
- providers = list(group_priority.items())
861
+ group = getattr(provider.config, "group", None)
862
+ name = group if by_group and group else key
863
+ candidates.append((name, provider.priority))
975
864
 
976
- # Sort by priority (descending) and then by name (ascending)
977
- providers.sort(key=lambda x: (-x[1], x[0]))
865
+ if by_group:
866
+ # Keep only the highest-priority entry per group
867
+ grouped: dict[str, int] = {}
868
+ for name, priority in candidates:
869
+ if name not in grouped or priority > grouped[name]:
870
+ grouped[name] = priority
871
+ candidates = list(grouped.items())
978
872
 
979
- # Return only the names of the providers or groups
980
- return [name for name, _ in providers]
873
+ return [name for name, _ in candidates]
981
874
 
982
875
  def get_collection_from_alias(self, alias_or_id: str) -> str:
983
876
  """Return the id of a collection by either its id or alias
@@ -1253,7 +1146,7 @@ class EODataAccessGateway:
1253
1146
  warnings.warn(
1254
1147
  "Usage of deprecated search parameter 'page' "
1255
1148
  "(Please use 'SearchResult.next_page()' instead)"
1256
- " -- Deprecated since v3.9.0",
1149
+ " -- Deprecated since v4.0.0",
1257
1150
  DeprecationWarning,
1258
1151
  stacklevel=2,
1259
1152
  )
@@ -1323,7 +1216,7 @@ class EODataAccessGateway:
1323
1216
 
1324
1217
  @_deprecated(
1325
1218
  reason="Please use 'SearchResult.next_page()' instead",
1326
- version="v3.9.0",
1219
+ version="4.0.0",
1327
1220
  )
1328
1221
  def search_iter_page(
1329
1222
  self,
@@ -1336,7 +1229,7 @@ class EODataAccessGateway:
1336
1229
  ) -> Iterator[SearchResult]:
1337
1230
  """Iterate over the pages of a products search.
1338
1231
 
1339
- .. deprecated:: v3.9.0
1232
+ .. deprecated:: v4.0.0
1340
1233
  Please use :meth:`eodag.api.search_result.SearchResult.next_page` instead.
1341
1234
 
1342
1235
  :param items_per_page: (optional) The number of results requested per page
@@ -1391,7 +1284,7 @@ class EODataAccessGateway:
1391
1284
 
1392
1285
  @_deprecated(
1393
1286
  reason="Please use 'SearchResult.next_page()' instead",
1394
- version="v3.9.0",
1287
+ version="4.0.0",
1395
1288
  )
1396
1289
  def search_iter_page_plugin(
1397
1290
  self,
@@ -1401,7 +1294,7 @@ class EODataAccessGateway:
1401
1294
  ) -> Iterator[SearchResult]:
1402
1295
  """Iterate over the pages of a products search using a given search plugin.
1403
1296
 
1404
- .. deprecated:: v3.9.0
1297
+ .. deprecated:: v4.0.0
1405
1298
  Please use :meth:`eodag.api.search_result.SearchResult.next_page` instead.
1406
1299
 
1407
1300
  :param items_per_page: (optional) The number of results requested per page
@@ -2030,6 +1923,7 @@ class EODataAccessGateway:
2030
1923
  search_result: SearchResult,
2031
1924
  downloaded_callback: Optional[DownloadedCallback] = None,
2032
1925
  progress_callback: Optional[ProgressCallback] = None,
1926
+ executor: Optional[ThreadPoolExecutor] = None,
2033
1927
  wait: float = DEFAULT_DOWNLOAD_WAIT,
2034
1928
  timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
2035
1929
  **kwargs: Unpack[DownloadConf],
@@ -2047,6 +1941,8 @@ class EODataAccessGateway:
2047
1941
  size as inputs and handle progress bar
2048
1942
  creation and update to give the user a
2049
1943
  feedback on the download progress
1944
+ :param executor: (optional) An executor to download EO products of ``search_result`` in parallel
1945
+ which will also be reused to download assets of these products in parallel.
2050
1946
  :param wait: (optional) If download fails, wait time in minutes between
2051
1947
  two download tries of the same product
2052
1948
  :param timeout: (optional) If download fails, maximum time in minutes
@@ -2067,8 +1963,7 @@ class EODataAccessGateway:
2067
1963
  paths = []
2068
1964
  if search_result:
2069
1965
  logger.info("Downloading %s products", len(search_result))
2070
- # Get download plugin using first product assuming product from several provider
2071
- # aren't mixed into a search result
1966
+ # Get download plugin using first product assuming all plugins use base.Download.download_all
2072
1967
  download_plugin = self._plugins_manager.get_download_plugin(
2073
1968
  search_result[0]
2074
1969
  )
@@ -2076,6 +1971,7 @@ class EODataAccessGateway:
2076
1971
  search_result,
2077
1972
  downloaded_callback=downloaded_callback,
2078
1973
  progress_callback=progress_callback,
1974
+ executor=executor,
2079
1975
  wait=wait,
2080
1976
  timeout=timeout,
2081
1977
  **kwargs,
@@ -2137,6 +2033,7 @@ class EODataAccessGateway:
2137
2033
  self,
2138
2034
  product: EOProduct,
2139
2035
  progress_callback: Optional[ProgressCallback] = None,
2036
+ executor: Optional[ThreadPoolExecutor] = None,
2140
2037
  wait: float = DEFAULT_DOWNLOAD_WAIT,
2141
2038
  timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
2142
2039
  **kwargs: Unpack[DownloadConf],
@@ -2167,6 +2064,8 @@ class EODataAccessGateway:
2167
2064
  size as inputs and handle progress bar
2168
2065
  creation and update to give the user a
2169
2066
  feedback on the download progress
2067
+ :param executor: (optional) An executor to download assets of ``product`` in parallel if it has any. If ``None``
2068
+ , a default executor will be created
2170
2069
  :param wait: (optional) If download fails, wait time in minutes between
2171
2070
  two download tries
2172
2071
  :param timeout: (optional) If download fails, maximum time in minutes
@@ -2191,7 +2090,11 @@ class EODataAccessGateway:
2191
2090
  return uri_to_path(product.location)
2192
2091
  self._setup_downloader(product)
2193
2092
  path = product.download(
2194
- progress_callback=progress_callback, wait=wait, timeout=timeout, **kwargs
2093
+ progress_callback=progress_callback,
2094
+ executor=executor,
2095
+ wait=wait,
2096
+ timeout=timeout,
2097
+ **kwargs,
2195
2098
  )
2196
2099
 
2197
2100
  return path