eodag 4.0.0a3__py3-none-any.whl → 4.0.0a4__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
@@ -124,7 +118,8 @@ class EODataAccessGateway:
124
118
  )
125
119
  collections_config_dict = SimpleYamlProxyConfig(collections_config_path).source
126
120
  self.collections_config = self._collections_config_init(collections_config_dict)
127
- self.providers_config = load_default_config()
121
+
122
+ self._providers = ProvidersDict.from_configs(load_default_config())
128
123
 
129
124
  env_var_cfg_dir = "EODAG_CFG_DIR"
130
125
  self.conf_dir = os.getenv(
@@ -148,9 +143,8 @@ class EODataAccessGateway:
148
143
  self.conf_dir = tmp_conf_dir
149
144
  makedirs(self.conf_dir)
150
145
 
151
- self._plugins_manager = PluginManager(self.providers_config)
152
- # use updated providers_config
153
- self.providers_config = self._plugins_manager.providers_config
146
+ self._plugins_manager = PluginManager(self._providers)
147
+ self._providers = self._plugins_manager.providers
154
148
 
155
149
  # First level override: From a user configuration file
156
150
  if user_conf_file_path is None:
@@ -166,67 +160,29 @@ class EODataAccessGateway:
166
160
  ),
167
161
  standard_configuration_path,
168
162
  )
169
- override_config_from_file(self.providers_config, user_conf_file_path)
163
+ self._providers.update_from_config_file(user_conf_file_path)
170
164
 
171
165
  # 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)
166
+ self._providers.update_from_env()
176
167
 
177
168
  # init updated providers conf
178
169
  strict_mode = is_env_var_true("EODAG_STRICT_COLLECTIONS")
179
- available_collections = set(self.collections_config.keys())
180
170
 
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
- )
171
+ for provider in self._providers.values():
172
+ provider.sync_collections(self, strict_mode)
190
173
 
191
174
  # re-build _plugins_manager using up-to-date providers_config
192
- self._plugins_manager.rebuild(self.providers_config)
175
+ self._plugins_manager.rebuild(self._providers)
193
176
 
194
177
  # store pruned providers configs
195
178
  self._pruned_providers_config: dict[str, Any] = {}
179
+
196
180
  # filter out providers needing auth that have no credentials set
197
181
  self._prune_providers_list()
198
182
 
199
183
  # Sort providers taking into account of possible new priority orders
200
184
  self._plugins_manager.sort_providers()
201
185
 
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
186
  self.set_locations_conf(locations_conf_path)
231
187
 
232
188
  def _collections_config_init(
@@ -243,57 +199,19 @@ class EODataAccessGateway:
243
199
  ]
244
200
  return CollectionsDict(collections)
245
201
 
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,
202
+ @property
203
+ def providers(self) -> ProvidersDict:
204
+ """Providers of eodag configuration sorted by priority in descending order and by name in ascending order."""
205
+ providers = deepcopy(self._providers)
206
+ # Sort: priority descending, then name ascending
207
+ providers.data = {
208
+ k: v
209
+ for k, v in sorted(
210
+ providers.data.items(), key=lambda item: (-item[1].priority, item[0])
287
211
  )
212
+ }
288
213
 
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]
214
+ return providers
297
215
 
298
216
  def get_version(self) -> str:
299
217
  """Get eodag package version"""
@@ -305,10 +223,11 @@ class EODataAccessGateway:
305
223
  :param provider: The name of the provider that should be considered as the
306
224
  preferred provider to be used for this instance
307
225
  """
308
- if provider not in self.available_providers():
226
+ if provider not in self.providers.names:
309
227
  raise UnsupportedProvider(
310
228
  f"This provider is not recognised by eodag: {provider}"
311
229
  )
230
+
312
231
  preferred_provider, max_priority = self.get_preferred_provider()
313
232
  if preferred_provider != provider:
314
233
  new_priority = max_priority + 1
@@ -320,12 +239,7 @@ class EODataAccessGateway:
320
239
 
321
240
  :returns: The provider with the maximum priority and its priority
322
241
  """
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
242
+ return max(self._providers.priorities.items(), key=itemgetter(1))
329
243
 
330
244
  def update_providers_config(
331
245
  self,
@@ -347,27 +261,17 @@ class EODataAccessGateway:
347
261
  return None
348
262
 
349
263
  # restore the pruned configuration
350
- for provider in list(self._pruned_providers_config.keys()):
351
- if provider in conf_update:
264
+ for name in list(self._pruned_providers_config):
265
+ config = self._pruned_providers_config[name]
266
+ if name in conf_update:
352
267
  logger.info(
353
- "%s: provider restored from the pruned configurations",
354
- provider,
355
- )
356
- self.providers_config[provider] = self._pruned_providers_config.pop(
357
- provider
268
+ "%s: provider restored from the pruned configurations", name
358
269
  )
270
+ self._providers[name] = Provider(config)
271
+ self._pruned_providers_config.pop(name)
359
272
 
360
- override_config_from_mapping(self.providers_config, conf_update)
361
-
362
- # share credentials between updated plugins confs
363
- share_credentials(self.providers_config)
273
+ self._providers.update_from_configs(conf_update)
364
274
 
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
275
  # re-create _plugins_manager using up-to-date providers_config
372
276
  self._plugins_manager.build_collection_to_provider_config_map()
373
277
 
@@ -398,10 +302,11 @@ class EODataAccessGateway:
398
302
  :param search: Search :class:`~eodag.config.PluginConfig` mapping
399
303
  :param products: Provider collections mapping
400
304
  :param download: Download :class:`~eodag.config.PluginConfig` mapping
401
- :param kwargs: Additional :class:`~eodag.config.ProviderConfig` mapping
305
+ :param kwargs: Additional :class:`~eodag.api.provider.ProviderConfig` mapping
402
306
  """
403
307
  conf_dict: dict[str, Any] = {
404
308
  name: {
309
+ "name": name,
405
310
  "url": url,
406
311
  "search": {"type": "StacSearch", **search},
407
312
  "products": {
@@ -440,8 +345,10 @@ class EODataAccessGateway:
440
345
  def _prune_providers_list(self) -> None:
441
346
  """Removes from config providers needing auth that have no credentials set."""
442
347
  update_needed = False
443
- for provider in list(self.providers_config.keys()):
444
- conf = self.providers_config[provider]
348
+
349
+ # loop over a copy to allow popping items
350
+ for name, provider in list(self._providers.items()):
351
+ conf = provider.config
445
352
 
446
353
  # remove providers using skipped plugins
447
354
  if [
@@ -450,7 +357,7 @@ class EODataAccessGateway:
450
357
  if isinstance(v, PluginConfig)
451
358
  and getattr(v, "type", None) in self._plugins_manager.skipped_plugins
452
359
  ]:
453
- self.providers_config.pop(provider)
360
+ del self._providers[provider.name]
454
361
  logger.debug(
455
362
  f"{provider}: provider needing unavailable plugin has been removed"
456
363
  )
@@ -461,26 +368,28 @@ class EODataAccessGateway:
461
368
  credentials_exist = credentials_in_auth(conf.api)
462
369
  if not credentials_exist:
463
370
  # credentials needed but not found
464
- self._pruned_providers_config[provider] = self.providers_config.pop(
465
- provider
466
- )
371
+ self._pruned_providers_config[provider.name] = conf
372
+ del self._providers[provider.name]
373
+
467
374
  update_needed = True
468
375
  logger.info(
469
376
  "%s: provider needing auth for search has been pruned because no credentials could be found",
470
377
  provider,
471
378
  )
379
+
472
380
  elif hasattr(conf, "search") and getattr(conf.search, "need_auth", False):
473
381
  if not hasattr(conf, "auth") and not hasattr(conf, "search_auth"):
474
382
  # credentials needed but no auth plugin was found
475
- self._pruned_providers_config[provider] = self.providers_config.pop(
476
- provider
477
- )
383
+ self._pruned_providers_config[provider.name] = conf
384
+ del self._providers[provider.name]
385
+
478
386
  update_needed = True
479
387
  logger.info(
480
388
  "%s: provider needing auth for search has been pruned because no auth plugin could be found",
481
389
  provider,
482
390
  )
483
391
  continue
392
+
484
393
  credentials_exist = (
485
394
  hasattr(conf, "search_auth")
486
395
  and credentials_in_auth(conf.search_auth)
@@ -491,30 +400,31 @@ class EODataAccessGateway:
491
400
  )
492
401
  if not credentials_exist:
493
402
  # credentials needed but not found
494
- self._pruned_providers_config[provider] = self.providers_config.pop(
495
- provider
496
- )
403
+ self._pruned_providers_config[provider.name] = conf
404
+ del self._providers[provider.name]
405
+
497
406
  update_needed = True
498
407
  logger.info(
499
408
  "%s: provider needing auth for search has been pruned because no credentials could be found",
500
409
  provider,
501
410
  )
411
+
502
412
  elif not hasattr(conf, "api") and not hasattr(conf, "search"):
503
413
  # provider should have at least an api or search plugin
504
- self._pruned_providers_config[provider] = self.providers_config.pop(
505
- provider
506
- )
414
+ self._pruned_providers_config[provider.name] = conf
415
+ del self._providers[provider.name]
416
+
417
+ update_needed = True
507
418
  logger.info(
508
419
  "%s: provider has been pruned because no api or search plugin could be found",
509
420
  provider,
510
421
  )
511
- update_needed = True
512
422
 
513
423
  if update_needed:
514
424
  # rebuild _plugins_manager with updated providers list
515
- self._plugins_manager.rebuild(self.providers_config)
425
+ self._plugins_manager.rebuild(self._providers)
516
426
 
517
- def set_locations_conf(self, locations_conf_path: str) -> None:
427
+ def set_locations_conf(self, locations_conf_path: Optional[str]) -> None:
518
428
  """Set locations configuration.
519
429
  This configuration (YML format) will contain a shapefile list associated
520
430
  to a name and attribute parameters needed to identify the needed geometry.
@@ -537,6 +447,34 @@ class EODataAccessGateway:
537
447
 
538
448
  :param locations_conf_path: Path to the locations configuration file
539
449
  """
450
+ if locations_conf_path is None:
451
+ locations_conf_path = os.getenv("EODAG_LOCS_CFG_FILE")
452
+ if locations_conf_path is None:
453
+ locations_conf_path = os.path.join(self.conf_dir, "locations.yml")
454
+ if not os.path.isfile(locations_conf_path):
455
+ # copy locations conf file and replace path example
456
+ locations_conf_template = str(
457
+ res_files("eodag") / "resources" / "locations_conf_template.yml"
458
+ )
459
+ with (
460
+ open(locations_conf_template) as infile,
461
+ open(locations_conf_path, "w") as outfile,
462
+ ):
463
+ # The template contains paths in the form of:
464
+ # /path/to/locations/file.shp
465
+ path_template = "/path/to/locations/"
466
+ for line in infile:
467
+ line = line.replace(
468
+ path_template,
469
+ os.path.join(self.conf_dir, "shp") + os.path.sep,
470
+ )
471
+ outfile.write(line)
472
+ # copy sample shapefile dir
473
+ shutil.copytree(
474
+ str(res_files("eodag") / "resources" / "shp"),
475
+ os.path.join(self.conf_dir, "shp"),
476
+ )
477
+
540
478
  if os.path.isfile(locations_conf_path):
541
479
  locations_config = load_yml_config(locations_conf_path)
542
480
 
@@ -567,17 +505,11 @@ class EODataAccessGateway:
567
505
  # First, update collections list if possible
568
506
  self.fetch_collections_list(provider=provider)
569
507
 
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
- ]
508
+ providers_iter, providers_check = itertools.tee(
509
+ self._providers.filter_by_name_or_group(provider)
578
510
  )
579
511
 
580
- if provider and not providers_configs:
512
+ if provider and not any(providers_check):
581
513
  raise UnsupportedProvider(
582
514
  f"The requested provider is not (yet) supported: {provider}"
583
515
  )
@@ -585,8 +517,8 @@ class EODataAccessGateway:
585
517
  # unique collection ids from providers configs
586
518
  collection_ids = {
587
519
  collection_id
588
- for p in providers_configs
589
- for collection_id in p.products
520
+ for p in providers_iter
521
+ for collection_id in p.collections_config
590
522
  if collection_id != GENERIC_COLLECTION
591
523
  }
592
524
 
@@ -611,42 +543,18 @@ class EODataAccessGateway:
611
543
  if strict_mode:
612
544
  return
613
545
 
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
546
  # providers discovery confs that are fetchable
632
- providers_discovery_configs_fetchable: dict[str, Any] = {}
547
+ providers_discovery_configs_fetchable: dict[
548
+ str, PluginConfig.DiscoverCollections
549
+ ] = {}
633
550
  # check if any provider has not already been fetched for collections
634
551
  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"):
552
+ for provider_to_fetch in self._providers.filter_by_name_or_group(provider):
553
+ if provider_to_fetch.fetchable and provider_to_fetch.search_config:
646
554
  providers_discovery_configs_fetchable[
647
- provider_to_fetch
648
- ] = discovery_conf
649
- if not getattr(provider_config, "collections_fetched", False):
555
+ provider_to_fetch.name
556
+ ] = provider_to_fetch.search_config.discover_collections
557
+ if not provider_to_fetch.collections_fetched:
650
558
  already_fetched = False
651
559
 
652
560
  if not already_fetched:
@@ -672,54 +580,55 @@ class EODataAccessGateway:
672
580
  # and collections list would need to be fetched
673
581
 
674
582
  # get ext_collections conf for user modified providers
675
- default_providers_config = load_default_config()
583
+ default_providers = ProvidersDict.from_configs(load_default_config())
676
584
  for (
677
585
  provider,
678
586
  user_discovery_conf,
679
587
  ) in providers_discovery_configs_fetchable.items():
680
588
  # 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:
589
+ if provider in default_providers:
590
+ default_provider = default_providers[provider]
591
+ if not default_provider.search_config:
688
592
  continue
689
- default_discovery_conf = getattr(
690
- default_provider_search_config, "discover_collections", {}
593
+
594
+ default_discovery_conf = (
595
+ default_provider.search_config.discover_collections
691
596
  )
597
+
692
598
  # compare confs
693
599
  if default_discovery_conf["result_type"] == "json" and isinstance(
694
600
  default_discovery_conf["results_entry"], str
695
601
  ):
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
- )
602
+ default_discovery_conf_parsed = cast(
603
+ PluginConfig.DiscoverCollections,
604
+ dict(
605
+ default_discovery_conf,
606
+ **{
607
+ "results_entry": string_to_jsonpath(
608
+ default_discovery_conf["results_entry"], force=True
609
+ )
610
+ },
611
+ **mtd_cfg_as_conversion_and_querypath(
612
+ dict(
613
+ generic_collection_id=default_discovery_conf[
614
+ "generic_collection_id"
615
+ ]
616
+ )
617
+ ),
618
+ **dict(
619
+ generic_collection_parsable_properties=mtd_cfg_as_conversion_and_querypath(
620
+ default_discovery_conf[
621
+ "generic_collection_parsable_properties"
622
+ ]
623
+ )
624
+ ),
625
+ **dict(
626
+ generic_collection_parsable_metadata=mtd_cfg_as_conversion_and_querypath(
627
+ default_discovery_conf[
628
+ "generic_collection_parsable_metadata"
629
+ ]
630
+ )
631
+ ),
723
632
  ),
724
633
  )
725
634
  else:
@@ -757,51 +666,34 @@ class EODataAccessGateway:
757
666
  all providers (None value).
758
667
  :returns: external collections configuration
759
668
  """
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:
669
+
670
+ providers_iter, providers_check = itertools.tee(
671
+ self.providers.filter_by_name_or_group(provider)
672
+ )
673
+
674
+ if provider and not any(providers_check):
770
675
  raise UnsupportedProvider(
771
676
  f"The requested provider is not (yet) supported: {provider}"
772
677
  )
678
+
773
679
  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
- ]
680
+
786
681
  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:
682
+ for p in providers_iter:
683
+ if not p.search_config:
793
684
  return None
794
- if getattr(search_plugin_config, "discover_collections", {}).get(
795
- "fetch_url", None
796
- ):
685
+
686
+ if p.fetchable:
797
687
  search_plugin: Union[Search, Api] = next(
798
- self._plugins_manager.get_search_plugins(provider=provider)
688
+ self._plugins_manager.get_search_plugins(provider=p.name)
799
689
  )
690
+
800
691
  # check after plugin init if still fetchable
801
692
  if not getattr(search_plugin.config, "discover_collections", {}).get(
802
693
  "fetch_url"
803
694
  ):
804
695
  continue
696
+
805
697
  # append auth to search plugin if needed
806
698
  if getattr(search_plugin.config, "need_auth", False):
807
699
  if auth := self._plugins_manager.get_auth(
@@ -812,12 +704,12 @@ class EODataAccessGateway:
812
704
  kwargs["auth"] = auth
813
705
  else:
814
706
  logger.debug(
815
- f"Could not authenticate on {provider} for collections discovery"
707
+ f"Could not authenticate on {p} for collections discovery"
816
708
  )
817
- ext_collections_conf[provider] = None
709
+ ext_collections_conf[p.name] = None
818
710
  continue
819
711
 
820
- ext_collections_conf[provider] = search_plugin.discover_collections(
712
+ ext_collections_conf[p.name] = search_plugin.discover_collections(
821
713
  **kwargs
822
714
  )
823
715
 
@@ -831,20 +723,15 @@ class EODataAccessGateway:
831
723
  :param ext_collections_conf: external collections configuration
832
724
  """
833
725
  for provider, new_collections_conf in ext_collections_conf.items():
834
- if new_collections_conf and provider in self.providers_config:
726
+ if new_collections_conf and provider in self._providers:
835
727
  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"):
728
+ fetchable = self._providers[provider].fetchable
729
+ if not fetchable:
844
730
  # conf has been updated and provider collections are no more discoverable
845
731
  continue
732
+
846
733
  provider_products_config = (
847
- self.providers_config[provider].products or {}
734
+ self._providers[provider].collections_config or {}
848
735
  )
849
736
  except UnsupportedProvider:
850
737
  logger.debug(
@@ -852,6 +739,7 @@ class EODataAccessGateway:
852
739
  provider,
853
740
  )
854
741
  continue
742
+
855
743
  new_collections: list[str] = []
856
744
  bad_formatted_col_count = 0
857
745
  for (
@@ -861,11 +749,10 @@ class EODataAccessGateway:
861
749
  if new_collection not in provider_products_config:
862
750
  for existing_collection in provider_products_config.copy():
863
751
  # 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
- )
752
+ unparsable_keys = self._providers[
753
+ provider
754
+ ].unparsable_properties
755
+
869
756
  new_parsed_collections_conf = {
870
757
  k: v
871
758
  for k, v in new_collection_conf.items()
@@ -930,19 +817,27 @@ class EODataAccessGateway:
930
817
  provider,
931
818
  )
932
819
 
933
- elif provider not in self.providers_config:
820
+ elif provider not in self._providers:
934
821
  # unknown provider
935
822
  continue
936
- self.providers_config[provider].collections_fetched = True
823
+
824
+ self._providers[provider].collections_fetched = True
937
825
 
938
826
  # re-create _plugins_manager using up-to-date providers_config
939
827
  self._plugins_manager.build_collection_to_provider_config_map()
940
828
 
829
+ @_deprecated(
830
+ reason="Please use 'EODataAccessGateway.providers' instead",
831
+ version="4.0.0",
832
+ )
941
833
  def available_providers(
942
834
  self, collection: Optional[str] = None, by_group: bool = False
943
835
  ) -> list[str]:
944
836
  """Gives the sorted list of the available providers or groups
945
837
 
838
+ .. deprecated:: v4.0.0
839
+ Please use :attr:`eodag.api.core.EODataAccessGateway.providers` instead.
840
+
946
841
  The providers or groups are sorted first by their priority level in descending order,
947
842
  and then alphabetically in ascending order for providers or groups with the same
948
843
  priority level.
@@ -952,32 +847,26 @@ class EODataAccessGateway:
952
847
  of providers, mixed with other providers
953
848
  :returns: the sorted list of the available providers or groups
954
849
  """
850
+ candidates = []
955
851
 
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
- ]
852
+ # use "providers" property to get sorted providers
853
+ for key, provider in self.providers.items():
854
+ if collection and collection not in provider.collections_config:
855
+ continue
967
856
 
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())
857
+ group = getattr(provider.config, "group", None)
858
+ name = group if by_group and group else key
859
+ candidates.append((name, provider.priority))
975
860
 
976
- # Sort by priority (descending) and then by name (ascending)
977
- providers.sort(key=lambda x: (-x[1], x[0]))
861
+ if by_group:
862
+ # Keep only the highest-priority entry per group
863
+ grouped: dict[str, int] = {}
864
+ for name, priority in candidates:
865
+ if name not in grouped or priority > grouped[name]:
866
+ grouped[name] = priority
867
+ candidates = list(grouped.items())
978
868
 
979
- # Return only the names of the providers or groups
980
- return [name for name, _ in providers]
869
+ return [name for name, _ in candidates]
981
870
 
982
871
  def get_collection_from_alias(self, alias_or_id: str) -> str:
983
872
  """Return the id of a collection by either its id or alias
@@ -1253,7 +1142,7 @@ class EODataAccessGateway:
1253
1142
  warnings.warn(
1254
1143
  "Usage of deprecated search parameter 'page' "
1255
1144
  "(Please use 'SearchResult.next_page()' instead)"
1256
- " -- Deprecated since v3.9.0",
1145
+ " -- Deprecated since v4.0.0",
1257
1146
  DeprecationWarning,
1258
1147
  stacklevel=2,
1259
1148
  )
@@ -1323,7 +1212,7 @@ class EODataAccessGateway:
1323
1212
 
1324
1213
  @_deprecated(
1325
1214
  reason="Please use 'SearchResult.next_page()' instead",
1326
- version="v3.9.0",
1215
+ version="4.0.0",
1327
1216
  )
1328
1217
  def search_iter_page(
1329
1218
  self,
@@ -1336,7 +1225,7 @@ class EODataAccessGateway:
1336
1225
  ) -> Iterator[SearchResult]:
1337
1226
  """Iterate over the pages of a products search.
1338
1227
 
1339
- .. deprecated:: v3.9.0
1228
+ .. deprecated:: v4.0.0
1340
1229
  Please use :meth:`eodag.api.search_result.SearchResult.next_page` instead.
1341
1230
 
1342
1231
  :param items_per_page: (optional) The number of results requested per page
@@ -1391,7 +1280,7 @@ class EODataAccessGateway:
1391
1280
 
1392
1281
  @_deprecated(
1393
1282
  reason="Please use 'SearchResult.next_page()' instead",
1394
- version="v3.9.0",
1283
+ version="4.0.0",
1395
1284
  )
1396
1285
  def search_iter_page_plugin(
1397
1286
  self,
@@ -1401,7 +1290,7 @@ class EODataAccessGateway:
1401
1290
  ) -> Iterator[SearchResult]:
1402
1291
  """Iterate over the pages of a products search using a given search plugin.
1403
1292
 
1404
- .. deprecated:: v3.9.0
1293
+ .. deprecated:: v4.0.0
1405
1294
  Please use :meth:`eodag.api.search_result.SearchResult.next_page` instead.
1406
1295
 
1407
1296
  :param items_per_page: (optional) The number of results requested per page