eodag 3.8.0__py3-none-any.whl → 3.9.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 (44) hide show
  1. eodag/api/core.py +3 -2
  2. eodag/api/product/drivers/generic.py +5 -1
  3. eodag/api/product/metadata_mapping.py +110 -9
  4. eodag/cli.py +36 -4
  5. eodag/config.py +5 -2
  6. eodag/plugins/apis/ecmwf.py +3 -1
  7. eodag/plugins/apis/usgs.py +2 -1
  8. eodag/plugins/authentication/aws_auth.py +228 -37
  9. eodag/plugins/authentication/base.py +12 -2
  10. eodag/plugins/authentication/oauth.py +5 -0
  11. eodag/plugins/authentication/sas_auth.py +15 -0
  12. eodag/plugins/base.py +3 -2
  13. eodag/plugins/download/aws.py +44 -285
  14. eodag/plugins/download/base.py +3 -2
  15. eodag/plugins/download/creodias_s3.py +1 -38
  16. eodag/plugins/download/http.py +111 -103
  17. eodag/plugins/download/s3rest.py +3 -1
  18. eodag/plugins/manager.py +2 -1
  19. eodag/plugins/search/__init__.py +2 -1
  20. eodag/plugins/search/base.py +2 -1
  21. eodag/plugins/search/build_search_result.py +2 -2
  22. eodag/plugins/search/creodias_s3.py +9 -1
  23. eodag/plugins/search/qssearch.py +3 -1
  24. eodag/resources/ext_product_types.json +1 -1
  25. eodag/resources/product_types.yml +220 -30
  26. eodag/resources/providers.yml +634 -89
  27. eodag/resources/stac_provider.yml +6 -3
  28. eodag/resources/user_conf_template.yml +0 -5
  29. eodag/rest/core.py +8 -0
  30. eodag/rest/errors.py +9 -0
  31. eodag/rest/server.py +8 -0
  32. eodag/rest/stac.py +8 -0
  33. eodag/rest/utils/__init__.py +2 -4
  34. eodag/rest/utils/rfc3339.py +1 -1
  35. eodag/utils/__init__.py +69 -54
  36. eodag/utils/dates.py +204 -0
  37. eodag/utils/s3.py +187 -168
  38. {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/METADATA +4 -3
  39. {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/RECORD +43 -43
  40. {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/entry_points.txt +1 -1
  41. eodag/utils/rest.py +0 -100
  42. {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/WHEEL +0 -0
  43. {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/licenses/LICENSE +0 -0
  44. {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/top_level.txt +0 -0
eodag/api/core.py CHANGED
@@ -74,6 +74,7 @@ from eodag.utils import (
74
74
  string_to_jsonpath,
75
75
  uri_to_path,
76
76
  )
77
+ from eodag.utils.dates import rfc3339_str_to_datetime
77
78
  from eodag.utils.env import is_env_var_true
78
79
  from eodag.utils.exceptions import (
79
80
  AuthenticationError,
@@ -84,7 +85,6 @@ from eodag.utils.exceptions import (
84
85
  UnsupportedProvider,
85
86
  )
86
87
  from eodag.utils.free_text_search import compile_free_text_query
87
- from eodag.utils.rest import rfc3339_str_to_datetime
88
88
  from eodag.utils.stac_reader import fetch_stac_items
89
89
 
90
90
  if TYPE_CHECKING:
@@ -1117,7 +1117,8 @@ class EODataAccessGateway:
1117
1117
  if not (max_start <= min_end):
1118
1118
  continue
1119
1119
 
1120
- guesses_with_score.append((pt_id, score))
1120
+ pt_alias = pt_dict.get("alias", pt_id)
1121
+ guesses_with_score.append((pt_alias, score))
1121
1122
 
1122
1123
  if guesses_with_score:
1123
1124
  # sort by score descending, then pt_id for stability
@@ -33,7 +33,11 @@ class GenericDriver(DatasetDriver):
33
33
  # data
34
34
  {
35
35
  "pattern": re.compile(
36
- r"^(?:.*[/\\])?([^/\\]+)(\.jp2|\.tiff?|\.dat|\.nc|\.grib2?)$",
36
+ (
37
+ r"^(?:.*[/\\])?([^/\\]+)"
38
+ r"(\.jp2|\.tiff?|\.dat|\.nc|\.grib2?|"
39
+ r"\.zarr|\.nat|\.covjson|\.parquet|\.zip|\.tar|\.gz)$"
40
+ ),
37
41
  re.IGNORECASE,
38
42
  ),
39
43
  "roles": ["data"],
@@ -47,7 +47,6 @@ from eodag.utils import (
47
47
  dict_items_recursive_apply,
48
48
  format_string,
49
49
  get_geometry_from_various,
50
- get_timestamp,
51
50
  items_recursive_apply,
52
51
  nested_pairs2dict,
53
52
  remove_str_array_quotes,
@@ -55,6 +54,7 @@ from eodag.utils import (
55
54
  string_to_jsonpath,
56
55
  update_nested_dict,
57
56
  )
57
+ from eodag.utils.dates import get_timestamp
58
58
  from eodag.utils.exceptions import ValidationError
59
59
 
60
60
  if TYPE_CHECKING:
@@ -174,6 +174,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
174
174
  - ``slice_str``: slice a string (equivalent to s[start, end, step])
175
175
  - ``to_lower``: Convert a string to lowercase
176
176
  - ``to_upper``: Convert a string to uppercase
177
+ - ``to_title``: Convert a string to title case
177
178
  - ``fake_l2a_title_from_l1c``: used to generate SAFE format metadata for data from AWS
178
179
  - ``s2msil2a_title_to_aws_productinfo``: used to generate SAFE format metadata for data from AWS
179
180
  - ``split_cop_dem_id``: get the bbox by splitting the product id
@@ -182,6 +183,8 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
182
183
  - ``get_ecmwf_time``: get the time of a datetime string in the ECMWF format
183
184
  - ``sanitize``: sanitize string
184
185
  - ``ceda_collection_name``: generate a CEDA collection name from a string
186
+ - ``convert_dict_filter_and_sub``: filter dict items using jsonpath and then apply recursive_sub_str
187
+ - ``convert_from_alternate``: update assets using given alternate
185
188
 
186
189
  :param search_param: The string to be formatted
187
190
  :param args: (optional) Additional arguments to use in the formatting process
@@ -523,12 +526,41 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
523
526
  value = MetadataFormatter.convert_to_geojson(value)
524
527
  elif not isinstance(value, str):
525
528
  raise TypeError(
526
- f"convert_replace_str expects a string or a dict (apply to_geojson). Got {type(value)}"
529
+ f"convert_replace_str expects a string or a dict (apply to_geojson). Got {type(value)}: {value}"
527
530
  )
528
531
 
529
532
  old, new = ast.literal_eval(args)
530
533
  return re.sub(old, new, value)
531
534
 
535
+ @staticmethod
536
+ def convert_replace_str_tuple(value: Any, args: str) -> str:
537
+ """
538
+ Apply multiple replacements on a string.
539
+ args should be a string representing a list/tuple of (old, new) pairs.
540
+ Example: '(("old1", "new1"), ("old2", "new2"))'
541
+ """
542
+ if isinstance(value, dict):
543
+ value = MetadataFormatter.convert_to_geojson(value)
544
+ elif not isinstance(value, str):
545
+ raise TypeError(
546
+ f"convert_replace_str_tuple expects a string or a dict (apply to_geojson). "
547
+ f"Got {type(value)}: {value}"
548
+ )
549
+
550
+ # args sera une chaîne représentant une liste/tuple de tuples
551
+ replacements = ast.literal_eval(args)
552
+
553
+ if not isinstance(replacements, (list, tuple)):
554
+ raise TypeError(
555
+ f"convert_replace_str_tuple expects a list/tuple of (old,new) pairs. "
556
+ f"Got {type(replacements)}: {replacements}"
557
+ )
558
+
559
+ for old, new in replacements:
560
+ value = re.sub(old, new, value)
561
+
562
+ return value
563
+
532
564
  @staticmethod
533
565
  def convert_ceda_collection_name(value: str) -> str:
534
566
  data_regex = re.compile(r"/data/(?P<name>.+?)/?$")
@@ -580,6 +612,45 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
580
612
  result[key] = match.value
581
613
  return result
582
614
 
615
+ @staticmethod
616
+ def convert_dict_filter_and_sub(
617
+ input_dict: dict[Any, Any], args: str
618
+ ) -> Union[dict[Any, Any], list[Any]]:
619
+ """Fitlers dict items using jsonpath and then apply recursive_sub_str"""
620
+ jsonpath_filter_str, old, new = ast.literal_eval(args)
621
+ filtered = MetadataFormatter.convert_dict_filter(
622
+ input_dict, jsonpath_filter_str
623
+ )
624
+ args_str = f"('{old}', '{new}')"
625
+ return MetadataFormatter.convert_recursive_sub_str(filtered, args_str)
626
+
627
+ @staticmethod
628
+ def convert_from_alternate(
629
+ input_obj: dict[str, Any], value: str
630
+ ) -> dict[str, Any]:
631
+ """
632
+ Update assets using given alternate.
633
+ """
634
+ result: dict[str, Any] = {}
635
+ for k, v in input_obj.items():
636
+ if not isinstance(v, dict):
637
+ continue
638
+
639
+ alt_dict = deepcopy(v).get("alternate")
640
+ if not isinstance(alt_dict, dict):
641
+ continue
642
+
643
+ value_entry = alt_dict.pop(value, None)
644
+ if not isinstance(value_entry, dict):
645
+ continue
646
+
647
+ result[k] = v | value_entry | {"alternate": alt_dict}
648
+
649
+ if len(result[k]["alternate"]) == 0:
650
+ del result[k]["alternate"]
651
+
652
+ return result
653
+
583
654
  @staticmethod
584
655
  def convert_slice_str(string: str, args: str) -> str:
585
656
  cmin, cmax, cstep = [
@@ -591,6 +662,8 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
591
662
  @staticmethod
592
663
  def convert_to_lower(string: str) -> str:
593
664
  """Convert a string to lowercase."""
665
+ if string == NOT_AVAILABLE:
666
+ return string
594
667
  return string.lower()
595
668
 
596
669
  @staticmethod
@@ -598,6 +671,13 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
598
671
  """Convert a string to uppercase."""
599
672
  return string.upper()
600
673
 
674
+ @staticmethod
675
+ def convert_to_title(string: str) -> str:
676
+ """Convert a string to title case."""
677
+ if string == NOT_AVAILABLE:
678
+ return string
679
+ return string.title()
680
+
601
681
  @staticmethod
602
682
  def convert_fake_l2a_title_from_l1c(string: str) -> str:
603
683
  id_regex = re.compile(
@@ -1362,17 +1442,30 @@ def format_query_params(
1362
1442
  error_context,
1363
1443
  )
1364
1444
 
1365
- for eodag_search_key, provider_search_key in queryables.items():
1445
+ for eodag_search_key, provider_search_param in queryables.items():
1366
1446
  user_input = query_dict[eodag_search_key]
1367
1447
 
1368
- if COMPLEX_QS_REGEX.match(provider_search_key):
1369
- parts = provider_search_key.split("=")
1448
+ if provider_search_param == user_input:
1449
+ # means the mapping is to be passed as is, in which case we
1450
+ # readily register it
1451
+ if (
1452
+ eodag_search_key in query_params
1453
+ and isinstance(query_params[eodag_search_key], dict)
1454
+ and isinstance(user_input, dict)
1455
+ ):
1456
+ query_params[eodag_search_key].update(user_input)
1457
+ else:
1458
+ query_params[eodag_search_key] = user_input
1459
+ continue
1460
+
1461
+ if COMPLEX_QS_REGEX.match(provider_search_param):
1462
+ parts = provider_search_param.split("=")
1370
1463
  if len(parts) == 1:
1371
1464
  formatted_query_param = format_metadata(
1372
- provider_search_key, product_type, **query_dict
1465
+ provider_search_param, product_type, **query_dict
1373
1466
  )
1374
1467
  formatted_query_param = formatted_query_param.replace("'", '"')
1375
- if "{{" in provider_search_key:
1468
+ if "{{" in provider_search_param:
1376
1469
  # retrieve values from hashes where keys are given in the param
1377
1470
  if "}[" in formatted_query_param:
1378
1471
  formatted_query_param = _resolve_hashes(formatted_query_param)
@@ -1396,7 +1489,7 @@ def format_query_params(
1396
1489
  provider_value, product_type, **query_dict
1397
1490
  )
1398
1491
  else:
1399
- query_params[provider_search_key] = user_input
1492
+ query_params[provider_search_param] = user_input
1400
1493
  # Now get all the literal search params (i.e params to be passed "as is"
1401
1494
  # in the search request)
1402
1495
  # ignore additional_params if it isn't a dictionary
@@ -1527,7 +1620,15 @@ def _get_queryables(
1527
1620
  config.discover_metadata.get("metadata_pattern", "")
1528
1621
  )
1529
1622
  search_param_cfg = config.discover_metadata.get("search_param", "")
1530
- if pattern.match(eodag_search_key) and isinstance(
1623
+ search_param_unparsed_cfg = config.discover_metadata.get(
1624
+ "search_param_unparsed", []
1625
+ )
1626
+ if (
1627
+ search_param_unparsed_cfg
1628
+ and eodag_search_key in search_param_unparsed_cfg
1629
+ ):
1630
+ queryables[eodag_search_key] = user_input
1631
+ elif pattern.match(eodag_search_key) and isinstance(
1531
1632
  search_param_cfg, str
1532
1633
  ):
1533
1634
  search_param = search_param_cfg.format(metadata=eodag_search_key)
eodag/cli.py CHANGED
@@ -42,13 +42,14 @@ Commands:
42
42
 
43
43
  from __future__ import annotations
44
44
 
45
+ import functools
45
46
  import json
46
47
  import os
47
48
  import shutil
48
49
  import sys
49
50
  import textwrap
50
51
  from importlib.metadata import metadata
51
- from typing import TYPE_CHECKING, Any, Mapping
52
+ from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional
52
53
  from urllib.parse import parse_qs
53
54
 
54
55
  import click
@@ -118,6 +119,22 @@ class MutuallyExclusiveOption(click.Option):
118
119
  return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args)
119
120
 
120
121
 
122
+ def _deprecated_cli(message: str, version: Optional[str] = None) -> Callable[..., Any]:
123
+ """Decorator to mark a CLI command as deprecated and print a bold yellow warning."""
124
+ version_msg = f" -- Deprecated since v{version}" if version else ""
125
+
126
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
127
+ @functools.wraps(func)
128
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
129
+ full_message = f"DEPRECATED: {message}{version_msg}"
130
+ click.echo(click.style(full_message, fg="yellow", bold=True), err=True)
131
+ return func(*args, **kwargs)
132
+
133
+ return wrapper
134
+
135
+ return decorator
136
+
137
+
121
138
  @click.group(chain=True)
122
139
  @click.option(
123
140
  "-v",
@@ -631,9 +648,17 @@ def download(ctx: Context, **kwargs: Any) -> None:
631
648
 
632
649
 
633
650
  @eodag.command(
634
- help="Start eodag HTTP server\n\n"
635
- "Set EODAG_CORS_ALLOWED_ORIGINS environment variable to configure Cross-Origin Resource Sharing allowed origins as "
636
- "comma-separated URLs (e.g. 'http://somewhere,htttp://somewhere.else')."
651
+ help="(deprecated) Start eodag HTTP server\n\n"
652
+ + (
653
+ click.style(
654
+ "Running a web server from the CLI is deprecated and will be removed in a future version.\n"
655
+ "This feature has been moved to its own repository: https://github.com/CS-SI/stac-fastapi-eodag\n\n",
656
+ fg="yellow",
657
+ bold=True,
658
+ )
659
+ + "Set EODAG_CORS_ALLOWED_ORIGINS environment variable to configure Cross-Origin Resource Sharing allowed "
660
+ "origins as comma-separated URLs (e.g. 'http://somewhere,http://somewhere.else')."
661
+ )
637
662
  )
638
663
  @click.option(
639
664
  "-f",
@@ -676,6 +701,13 @@ def download(ctx: Context, **kwargs: Any) -> None:
676
701
  help="Run in debug mode (for development purpose)",
677
702
  )
678
703
  @click.pass_context
704
+ @_deprecated_cli(
705
+ message=(
706
+ "Running a web server from the CLI is deprecated and will be removed in a future version. "
707
+ "This feature has been moved to its own repository: https://github.com/CS-SI/stac-fastapi-eodag"
708
+ ),
709
+ version="3.9.0",
710
+ )
679
711
  def serve_rest(
680
712
  ctx: Context,
681
713
  daemon: bool,
eodag/config.py CHANGED
@@ -272,6 +272,8 @@ class PluginConfig(yaml.YAMLObject):
272
272
  search_param: str | dict[str, Any]
273
273
  #: Path to the metadata in search result
274
274
  metadata_path: str
275
+ #: list search parameters to send as is to the provider
276
+ search_param_unparsed: list[str]
275
277
  #: Whether an error must be raised when using a search parameter which is not queryable or not
276
278
  raise_mtd_discovery_error: bool
277
279
 
@@ -543,8 +545,6 @@ class PluginConfig(yaml.YAMLObject):
543
545
  #: :class:`~eodag.plugins.download.s3rest.S3RestDownload`
544
546
  #: At which level of the path part of the url the bucket can be found
545
547
  bucket_path_level: int
546
- #: :class:`~eodag.plugins.download.aws.AwsDownload` Whether download is done from a requester-pays bucket or not
547
- requester_pays: bool
548
548
  #: :class:`~eodag.plugins.download.aws.AwsDownload` S3 endpoint
549
549
  s3_endpoint: str
550
550
 
@@ -571,6 +571,9 @@ class PluginConfig(yaml.YAMLObject):
571
571
  #: :class:`~eodag.plugins.authentication.base.Authentication` Part of the search or download plugin configuration
572
572
  #: that needs authentication
573
573
  matching_conf: dict[str, Any]
574
+ #: :class:`~eodag.plugins.authentication.aws_auth.AwsAuth`
575
+ #: Whether download is done from a requester-pays bucket or not
576
+ requester_pays: bool
574
577
  #: :class:`~eodag.plugins.authentication.openid_connect.OIDCRefreshTokenBase`
575
578
  #: How the token should be used in the request
576
579
  token_provision: str
@@ -46,6 +46,7 @@ from eodag.utils.logging import get_logging_verbose
46
46
  if TYPE_CHECKING:
47
47
  from typing import Any, Optional, Union
48
48
 
49
+ from mypy_boto3_s3 import S3ServiceResource
49
50
  from requests.auth import AuthBase
50
51
 
51
52
  from eodag.api.product import EOProduct
@@ -55,6 +56,7 @@ if TYPE_CHECKING:
55
56
  from eodag.types.download_args import DownloadConf
56
57
  from eodag.utils import DownloadedCallback, ProgressCallback, Unpack
57
58
 
59
+
58
60
  logger = logging.getLogger("eodag.apis.ecmwf")
59
61
 
60
62
  ECMWF_MARS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
@@ -171,7 +173,7 @@ class EcmwfApi(Api, ECMWFSearch):
171
173
  def download(
172
174
  self,
173
175
  product: EOProduct,
174
- auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
176
+ auth: Optional[Union[AuthBase, S3SessionKwargs, S3ServiceResource]] = None,
175
177
  progress_callback: Optional[ProgressCallback] = None,
176
178
  wait: float = DEFAULT_DOWNLOAD_WAIT,
177
179
  timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
@@ -57,6 +57,7 @@ from eodag.utils.exceptions import (
57
57
  )
58
58
 
59
59
  if TYPE_CHECKING:
60
+ from mypy_boto3_s3 import S3ServiceResource
60
61
  from requests.auth import AuthBase
61
62
 
62
63
  from eodag.api.search_result import SearchResult
@@ -296,7 +297,7 @@ class UsgsApi(Api):
296
297
  def download(
297
298
  self,
298
299
  product: EOProduct,
299
- auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
300
+ auth: Optional[Union[AuthBase, S3SessionKwargs, S3ServiceResource]] = None,
300
301
  progress_callback: Optional[ProgressCallback] = None,
301
302
  wait: float = DEFAULT_DOWNLOAD_WAIT,
302
303
  timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,