eodag 3.0.1__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 (87) hide show
  1. eodag/api/core.py +174 -138
  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 +117 -90
  10. eodag/api/search_result.py +13 -23
  11. eodag/cli.py +26 -5
  12. eodag/config.py +86 -92
  13. eodag/plugins/apis/base.py +1 -1
  14. eodag/plugins/apis/ecmwf.py +42 -22
  15. eodag/plugins/apis/usgs.py +17 -16
  16. eodag/plugins/authentication/aws_auth.py +16 -13
  17. eodag/plugins/authentication/base.py +5 -3
  18. eodag/plugins/authentication/header.py +3 -3
  19. eodag/plugins/authentication/keycloak.py +4 -4
  20. eodag/plugins/authentication/oauth.py +7 -3
  21. eodag/plugins/authentication/openid_connect.py +22 -16
  22. eodag/plugins/authentication/sas_auth.py +4 -4
  23. eodag/plugins/authentication/token.py +41 -10
  24. eodag/plugins/authentication/token_exchange.py +1 -1
  25. eodag/plugins/base.py +4 -4
  26. eodag/plugins/crunch/base.py +4 -4
  27. eodag/plugins/crunch/filter_date.py +4 -4
  28. eodag/plugins/crunch/filter_latest_intersect.py +6 -6
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
  30. eodag/plugins/crunch/filter_overlap.py +4 -4
  31. eodag/plugins/crunch/filter_property.py +6 -7
  32. eodag/plugins/download/aws.py +146 -87
  33. eodag/plugins/download/base.py +38 -56
  34. eodag/plugins/download/creodias_s3.py +29 -0
  35. eodag/plugins/download/http.py +173 -183
  36. eodag/plugins/download/s3rest.py +10 -11
  37. eodag/plugins/manager.py +10 -20
  38. eodag/plugins/search/__init__.py +6 -5
  39. eodag/plugins/search/base.py +90 -46
  40. eodag/plugins/search/build_search_result.py +1048 -361
  41. eodag/plugins/search/cop_marine.py +22 -12
  42. eodag/plugins/search/creodias_s3.py +9 -73
  43. eodag/plugins/search/csw.py +11 -11
  44. eodag/plugins/search/data_request_search.py +19 -18
  45. eodag/plugins/search/qssearch.py +99 -258
  46. eodag/plugins/search/stac_list_assets.py +85 -0
  47. eodag/plugins/search/static_stac_search.py +4 -4
  48. eodag/resources/ext_product_types.json +1 -1
  49. eodag/resources/product_types.yml +1134 -325
  50. eodag/resources/providers.yml +906 -2006
  51. eodag/resources/stac_api.yml +2 -2
  52. eodag/resources/user_conf_template.yml +10 -9
  53. eodag/rest/cache.py +2 -2
  54. eodag/rest/config.py +3 -3
  55. eodag/rest/core.py +112 -82
  56. eodag/rest/errors.py +5 -5
  57. eodag/rest/server.py +33 -14
  58. eodag/rest/stac.py +41 -38
  59. eodag/rest/types/collections_search.py +3 -3
  60. eodag/rest/types/eodag_search.py +29 -23
  61. eodag/rest/types/queryables.py +42 -31
  62. eodag/rest/types/stac_search.py +15 -25
  63. eodag/rest/utils/__init__.py +14 -21
  64. eodag/rest/utils/cql_evaluate.py +6 -6
  65. eodag/rest/utils/rfc3339.py +2 -2
  66. eodag/types/__init__.py +141 -32
  67. eodag/types/bbox.py +2 -2
  68. eodag/types/download_args.py +3 -3
  69. eodag/types/queryables.py +183 -72
  70. eodag/types/search_args.py +4 -4
  71. eodag/types/whoosh.py +127 -3
  72. eodag/utils/__init__.py +153 -51
  73. eodag/utils/exceptions.py +28 -21
  74. eodag/utils/import_system.py +2 -2
  75. eodag/utils/repr.py +65 -6
  76. eodag/utils/requests.py +13 -13
  77. eodag/utils/rest.py +2 -2
  78. eodag/utils/s3.py +231 -0
  79. eodag/utils/stac_reader.py +10 -10
  80. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/METADATA +77 -76
  81. eodag-3.1.0.dist-info/RECORD +113 -0
  82. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  83. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +4 -2
  84. eodag/utils/constraints.py +0 -244
  85. eodag-3.0.1.dist-info/RECORD +0 -109
  86. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  87. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
eodag/utils/__init__.py CHANGED
@@ -54,14 +54,10 @@ from typing import (
54
54
  TYPE_CHECKING,
55
55
  Any,
56
56
  Callable,
57
- Dict,
58
57
  Iterable,
59
58
  Iterator,
60
- List,
61
59
  Mapping,
62
60
  Optional,
63
- Tuple,
64
- Type,
65
61
  Union,
66
62
  cast,
67
63
  )
@@ -84,6 +80,7 @@ if sys.version_info >= (3, 12):
84
80
  else:
85
81
  from typing_extensions import Unpack # noqa
86
82
 
83
+
87
84
  import click
88
85
  import orjson
89
86
  import shapefile
@@ -118,6 +115,7 @@ eodag_version = metadata("eodag")["Version"]
118
115
  USER_AGENT = {"User-Agent": f"eodag/{eodag_version}"}
119
116
 
120
117
  HTTP_REQ_TIMEOUT = 5 # in seconds
118
+ DEFAULT_SEARCH_TIMEOUT = 20 # in seconds
121
119
  DEFAULT_STREAM_REQUESTS_TIMEOUT = 60 # in seconds
122
120
 
123
121
  REQ_RETRY_TOTAL = 3
@@ -125,8 +123,8 @@ REQ_RETRY_BACKOFF_FACTOR = 2
125
123
  REQ_RETRY_STATUS_FORCELIST = [401, 429, 500, 502, 503, 504]
126
124
 
127
125
  # default wait times in minutes
128
- DEFAULT_DOWNLOAD_WAIT = 2 # in minutes
129
- DEFAULT_DOWNLOAD_TIMEOUT = 20 # in minutes
126
+ DEFAULT_DOWNLOAD_WAIT = 0.2 # in minutes
127
+ DEFAULT_DOWNLOAD_TIMEOUT = 10 # in minutes
130
128
 
131
129
  JSONPATH_MATCH = re.compile(r"^[\{\(]*\$(\..*)*$")
132
130
  WORKABLE_JSONPATH_MATCH = re.compile(r"^\$(\.[a-zA-Z0-9-_:\.\[\]\"\(\)=\?\*]+)*$")
@@ -142,6 +140,13 @@ DEFAULT_MAX_ITEMS_PER_PAGE = 50
142
140
  # default product-types start date
143
141
  DEFAULT_MISSION_START_DATE = "2015-01-01T00:00:00Z"
144
142
 
143
+ # update missing mimetypes
144
+ mimetypes.add_type("text/xml", ".xsd")
145
+ mimetypes.add_type("application/x-grib", ".grib")
146
+ mimetypes.add_type("application/x-grib2", ".grib2")
147
+ # jp2 is missing on windows
148
+ mimetypes.add_type("image/jp2", ".jp2")
149
+
145
150
 
146
151
  def _deprecated(reason: str = "", version: Optional[str] = None) -> Callable[..., Any]:
147
152
  """Simple decorator to mark functions/methods/classes as deprecated.
@@ -314,7 +319,7 @@ def path_to_uri(path: str) -> str:
314
319
  return Path(path).as_uri()
315
320
 
316
321
 
317
- def mutate_dict_in_place(func: Callable[[Any], Any], mapping: Dict[Any, Any]) -> None:
322
+ def mutate_dict_in_place(func: Callable[[Any], Any], mapping: dict[Any, Any]) -> None:
318
323
  """Apply func to values of mapping.
319
324
 
320
325
  The mapping object's values are modified in-place. The function is recursive,
@@ -332,7 +337,7 @@ def mutate_dict_in_place(func: Callable[[Any], Any], mapping: Dict[Any, Any]) ->
332
337
  mapping[key] = func(value)
333
338
 
334
339
 
335
- def merge_mappings(mapping1: Dict[Any, Any], mapping2: Dict[Any, Any]) -> None:
340
+ def merge_mappings(mapping1: dict[Any, Any], mapping2: dict[Any, Any]) -> None:
336
341
  """Merge two mappings with string keys, values from ``mapping2`` overriding values
337
342
  from ``mapping1``.
338
343
 
@@ -430,6 +435,33 @@ def datetime_range(start: dt, end: dt) -> Iterator[dt]:
430
435
  yield start + datetime.timedelta(days=nday)
431
436
 
432
437
 
438
+ def is_range_in_range(valid_range: str, check_range: str) -> bool:
439
+ """Check if the check_range is completely within the valid_range.
440
+
441
+ This function checks if both the start and end dates of the check_range
442
+ are within the start and end dates of the valid_range.
443
+
444
+ :param valid_range: The valid date range in the format 'YYYY-MM-DD/YYYY-MM-DD'.
445
+ :param check_range: The date range to check in the format 'YYYY-MM-DD/YYYY-MM-DD'.
446
+ :returns: True if check_range is within valid_range, otherwise False.
447
+ """
448
+ if "/" not in valid_range or "/" not in check_range:
449
+ return False
450
+
451
+ # Split the date ranges into start and end dates
452
+ start_valid, end_valid = valid_range.split("/")
453
+ start_check, end_check = check_range.split("/")
454
+
455
+ # Convert the strings to datetime objects using fromisoformat
456
+ start_valid_dt = datetime.datetime.fromisoformat(start_valid)
457
+ end_valid_dt = datetime.datetime.fromisoformat(end_valid)
458
+ start_check_dt = datetime.datetime.fromisoformat(start_check)
459
+ end_check_dt = datetime.datetime.fromisoformat(end_check)
460
+
461
+ # Check if check_range is within valid_range
462
+ return start_valid_dt <= start_check_dt and end_valid_dt >= end_check_dt
463
+
464
+
433
465
  class DownloadedCallback:
434
466
  """Example class for callback after each download in :meth:`~eodag.api.core.EODataAccessGateway.download_all`"""
435
467
 
@@ -570,9 +602,49 @@ def rename_subfolder(dirpath: str, name: str) -> None:
570
602
  )
571
603
 
572
604
 
605
+ def rename_with_version(file_path: str, suffix: str = "old") -> str:
606
+ """
607
+ Renames a file by appending and incrementing a version number if a conflict exists.
608
+
609
+ :param file_path: full path of the file to rename
610
+ :param suffix: suffix preceding version number in case of name conflict
611
+ :returns: new file path with the version appended or incremented
612
+
613
+ Example:
614
+
615
+ >>> import tempfile
616
+ >>> from pathlib import Path
617
+ >>> with tempfile.TemporaryDirectory() as tmpdir:
618
+ ... file_path = (Path(tmpdir) / "foo.txt")
619
+ ... file_path.touch()
620
+ ... (Path(tmpdir) / "foo_old1.txt").touch()
621
+ ... expected = str(Path(tmpdir) / "foo_old2.txt")
622
+ ... assert expected == rename_with_version(str(file_path))
623
+
624
+ """
625
+ if not os.path.isfile(file_path):
626
+ raise FileNotFoundError(f"The file '{file_path}' does not exist.")
627
+
628
+ dir_path, file_name = os.path.split(file_path)
629
+ file_base, file_ext = os.path.splitext(file_name)
630
+
631
+ new_file_path = file_path
632
+
633
+ # loop and iterate on conflicting existing files
634
+ version = 0
635
+ while os.path.exists(new_file_path):
636
+ version += 1
637
+ new_file_name = f"{file_base}_{suffix}{version}{file_ext}"
638
+ new_file_path = os.path.join(dir_path, new_file_name)
639
+
640
+ # Rename the file
641
+ os.rename(file_path, new_file_path)
642
+ return new_file_path
643
+
644
+
573
645
  def format_dict_items(
574
- config_dict: Dict[str, Any], **format_variables: Any
575
- ) -> Dict[Any, Any]:
646
+ config_dict: dict[str, Any], **format_variables: Any
647
+ ) -> dict[Any, Any]:
576
648
  r"""Recursively apply :meth:`str.format` to ``**format_variables`` on ``config_dict`` values
577
649
 
578
650
  >>> format_dict_items(
@@ -589,8 +661,8 @@ def format_dict_items(
589
661
 
590
662
 
591
663
  def jsonpath_parse_dict_items(
592
- jsonpath_dict: Dict[str, Any], values_dict: Dict[str, Any]
593
- ) -> Dict[Any, Any]:
664
+ jsonpath_dict: dict[str, Any], values_dict: dict[str, Any]
665
+ ) -> dict[Any, Any]:
594
666
  """Recursively parse :class:`jsonpath_ng.JSONPath` elements in dict
595
667
 
596
668
  >>> import jsonpath_ng.ext as jsonpath
@@ -608,12 +680,12 @@ def jsonpath_parse_dict_items(
608
680
 
609
681
 
610
682
  def update_nested_dict(
611
- old_dict: Dict[Any, Any],
612
- new_dict: Dict[Any, Any],
683
+ old_dict: dict[Any, Any],
684
+ new_dict: dict[Any, Any],
613
685
  extend_list_values: bool = False,
614
686
  allow_empty_values: bool = False,
615
687
  allow_extend_duplicates: bool = True,
616
- ) -> Dict[Any, Any]:
688
+ ) -> dict[Any, Any]:
617
689
  """Update recursively ``old_dict`` items with ``new_dict`` ones
618
690
 
619
691
  >>> update_nested_dict(
@@ -693,10 +765,10 @@ def update_nested_dict(
693
765
 
694
766
 
695
767
  def items_recursive_apply(
696
- input_obj: Union[Dict[Any, Any], List[Any]],
768
+ input_obj: Union[dict[Any, Any], list[Any]],
697
769
  apply_method: Callable[..., Any],
698
770
  **apply_method_parameters: Any,
699
- ) -> Union[Dict[Any, Any], List[Any]]:
771
+ ) -> Union[dict[Any, Any], list[Any]]:
700
772
  """Recursive apply method to items contained in input object (dict or list)
701
773
 
702
774
  >>> items_recursive_apply(
@@ -734,10 +806,10 @@ def items_recursive_apply(
734
806
 
735
807
 
736
808
  def dict_items_recursive_apply(
737
- config_dict: Dict[Any, Any],
809
+ config_dict: dict[Any, Any],
738
810
  apply_method: Callable[..., Any],
739
811
  **apply_method_parameters: Any,
740
- ) -> Dict[Any, Any]:
812
+ ) -> dict[Any, Any]:
741
813
  """Recursive apply method to dict elements
742
814
 
743
815
  >>> dict_items_recursive_apply(
@@ -751,7 +823,7 @@ def dict_items_recursive_apply(
751
823
  :param apply_method_parameters: Optional parameters passed to the method
752
824
  :returns: Updated dict
753
825
  """
754
- result_dict: Dict[Any, Any] = deepcopy(config_dict)
826
+ result_dict: dict[Any, Any] = deepcopy(config_dict)
755
827
  for dict_k, dict_v in result_dict.items():
756
828
  if isinstance(dict_v, dict):
757
829
  result_dict[dict_k] = dict_items_recursive_apply(
@@ -759,7 +831,7 @@ def dict_items_recursive_apply(
759
831
  )
760
832
  elif any(isinstance(dict_v, t) for t in (list, tuple)):
761
833
  result_dict[dict_k] = list_items_recursive_apply(
762
- dict_v, apply_method, **apply_method_parameters
834
+ list(dict_v), apply_method, **apply_method_parameters
763
835
  )
764
836
  else:
765
837
  result_dict[dict_k] = apply_method(
@@ -770,10 +842,10 @@ def dict_items_recursive_apply(
770
842
 
771
843
 
772
844
  def list_items_recursive_apply(
773
- config_list: List[Any],
845
+ config_list: list[Any],
774
846
  apply_method: Callable[..., Any],
775
847
  **apply_method_parameters: Any,
776
- ) -> List[Any]:
848
+ ) -> list[Any]:
777
849
  """Recursive apply method to list elements
778
850
 
779
851
  >>> list_items_recursive_apply(
@@ -806,8 +878,8 @@ def list_items_recursive_apply(
806
878
 
807
879
 
808
880
  def items_recursive_sort(
809
- input_obj: Union[List[Any], Dict[Any, Any]],
810
- ) -> Union[List[Any], Dict[Any, Any]]:
881
+ input_obj: Union[list[Any], dict[Any, Any]],
882
+ ) -> Union[list[Any], dict[Any, Any]]:
811
883
  """Recursive sort dict items contained in input object (dict or list)
812
884
 
813
885
  >>> items_recursive_sort(
@@ -831,7 +903,7 @@ def items_recursive_sort(
831
903
  return input_obj
832
904
 
833
905
 
834
- def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
906
+ def dict_items_recursive_sort(config_dict: dict[Any, Any]) -> dict[Any, Any]:
835
907
  """Recursive sort dict elements
836
908
 
837
909
  >>> dict_items_recursive_sort(
@@ -842,7 +914,7 @@ def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
842
914
  :param config_dict: Input nested dictionary
843
915
  :returns: Updated dict
844
916
  """
845
- result_dict: Dict[Any, Any] = deepcopy(config_dict)
917
+ result_dict: dict[Any, Any] = deepcopy(config_dict)
846
918
  for dict_k, dict_v in result_dict.items():
847
919
  if isinstance(dict_v, dict):
848
920
  result_dict[dict_k] = dict_items_recursive_sort(dict_v)
@@ -854,7 +926,7 @@ def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
854
926
  return dict(sorted(result_dict.items()))
855
927
 
856
928
 
857
- def list_items_recursive_sort(config_list: List[Any]) -> List[Any]:
929
+ def list_items_recursive_sort(config_list: list[Any]) -> list[Any]:
858
930
  """Recursive sort dicts in list elements
859
931
 
860
932
  >>> list_items_recursive_sort(["b", {2: 0, 0: 1, 1: 2}])
@@ -863,7 +935,7 @@ def list_items_recursive_sort(config_list: List[Any]) -> List[Any]:
863
935
  :param config_list: Input list containing nested lists/dicts
864
936
  :returns: Updated list
865
937
  """
866
- result_list: List[Any] = deepcopy(config_list)
938
+ result_list: list[Any] = deepcopy(config_list)
867
939
  for list_idx, list_v in enumerate(result_list):
868
940
  if isinstance(list_v, dict):
869
941
  result_list[list_idx] = dict_items_recursive_sort(list_v)
@@ -976,7 +1048,7 @@ def format_string(key: str, str_to_format: Any, **format_variables: Any) -> Any:
976
1048
  # defaultdict usage will return "" for missing keys in format_args
977
1049
  try:
978
1050
  result = str_to_format.format_map(defaultdict(str, **format_variables))
979
- except TypeError as e:
1051
+ except (ValueError, TypeError) as e:
980
1052
  raise MisconfiguredError(
981
1053
  f"Unable to format str={str_to_format} using {str(format_variables)}: {str(e)}"
982
1054
  )
@@ -989,7 +1061,7 @@ def format_string(key: str, str_to_format: Any, **format_variables: Any) -> Any:
989
1061
 
990
1062
 
991
1063
  def parse_jsonpath(
992
- key: str, jsonpath_obj: Union[str, jsonpath.Child], **values_dict: Dict[str, Any]
1064
+ key: str, jsonpath_obj: Union[str, jsonpath.Child], **values_dict: dict[str, Any]
993
1065
  ) -> Optional[str]:
994
1066
  """Parse jsonpah in ``jsonpath_obj`` using ``values_dict``
995
1067
 
@@ -1009,7 +1081,7 @@ def parse_jsonpath(
1009
1081
  return jsonpath_obj
1010
1082
 
1011
1083
 
1012
- def nested_pairs2dict(pairs: Union[List[Any], Any]) -> Union[Any, Dict[Any, Any]]:
1084
+ def nested_pairs2dict(pairs: Union[list[Any], Any]) -> Union[Any, dict[Any, Any]]:
1013
1085
  """Create a dict using nested pairs
1014
1086
 
1015
1087
  >>> nested_pairs2dict([["foo", [["bar", "baz"]]]])
@@ -1031,7 +1103,7 @@ def nested_pairs2dict(pairs: Union[List[Any], Any]) -> Union[Any, Dict[Any, Any]
1031
1103
 
1032
1104
 
1033
1105
  def get_geometry_from_various(
1034
- locations_config: List[Dict[str, Any]] = [], **query_args: Any
1106
+ locations_config: list[dict[str, Any]] = [], **query_args: Any
1035
1107
  ) -> BaseGeometry:
1036
1108
  """Creates a ``shapely.geometry`` using given query kwargs arguments
1037
1109
 
@@ -1120,7 +1192,7 @@ def get_geometry_from_various(
1120
1192
  class MockResponse:
1121
1193
  """Fake requests response"""
1122
1194
 
1123
- def __init__(self, json_data: Any, status_code: int) -> None:
1195
+ def __init__(self, json_data: Any = None, status_code: int = 200) -> None:
1124
1196
  self.json_data = json_data
1125
1197
  self.status_code = status_code
1126
1198
  self.content = json_data
@@ -1129,10 +1201,21 @@ class MockResponse:
1129
1201
  """Return json data"""
1130
1202
  return self.json_data
1131
1203
 
1204
+ def __iter__(self):
1205
+ yield self
1206
+
1207
+ def __enter__(self):
1208
+ return self
1209
+
1210
+ def __exit__(self, exc_type, exc_val, exc_tb):
1211
+ pass
1212
+
1132
1213
  def raise_for_status(self) -> None:
1133
1214
  """raises an exception when the status is not ok"""
1134
1215
  if self.status_code != 200:
1135
- raise HTTPError(response=Response())
1216
+ response = Response()
1217
+ response.status_code = self.status_code
1218
+ raise HTTPError(response=response)
1136
1219
 
1137
1220
 
1138
1221
  def md5sum(file_path: str) -> str:
@@ -1196,7 +1279,7 @@ def _mutable_cached_yaml_load(config_path: str) -> Any:
1196
1279
  return yaml.load(fh, Loader=yaml.SafeLoader)
1197
1280
 
1198
1281
 
1199
- def cached_yaml_load(config_path: str) -> Dict[str, Any]:
1282
+ def cached_yaml_load(config_path: str) -> dict[str, Any]:
1200
1283
  """Cached :func:`yaml.load`
1201
1284
 
1202
1285
  :param config_path: path to the yaml configuration file
@@ -1206,12 +1289,12 @@ def cached_yaml_load(config_path: str) -> Dict[str, Any]:
1206
1289
 
1207
1290
 
1208
1291
  @functools.lru_cache()
1209
- def _mutable_cached_yaml_load_all(config_path: str) -> List[Any]:
1292
+ def _mutable_cached_yaml_load_all(config_path: str) -> list[Any]:
1210
1293
  with open(config_path, "r") as fh:
1211
1294
  return list(yaml.load_all(fh, Loader=yaml.Loader))
1212
1295
 
1213
1296
 
1214
- def cached_yaml_load_all(config_path: str) -> List[Any]:
1297
+ def cached_yaml_load_all(config_path: str) -> list[Any]:
1215
1298
  """Cached :func:`yaml.load_all`
1216
1299
 
1217
1300
  Load all configurations stored in the configuration file as separated yaml documents
@@ -1224,7 +1307,7 @@ def cached_yaml_load_all(config_path: str) -> List[Any]:
1224
1307
 
1225
1308
  def get_bucket_name_and_prefix(
1226
1309
  url: str, bucket_path_level: Optional[int] = None
1227
- ) -> Tuple[Optional[str], Optional[str]]:
1310
+ ) -> tuple[Optional[str], Optional[str]]:
1228
1311
  """Extract bucket name and prefix from URL
1229
1312
 
1230
1313
  :param url: (optional) URL to use as product.location
@@ -1237,7 +1320,9 @@ def get_bucket_name_and_prefix(
1237
1320
  subdomain = netloc.split(".")[0]
1238
1321
  path = path.strip("/")
1239
1322
 
1240
- if scheme and bucket_path_level is None:
1323
+ if "/" in path and scheme and subdomain == "s3" and bucket_path_level is None:
1324
+ bucket, prefix = path.split("/", 1)
1325
+ elif scheme and bucket_path_level is None:
1241
1326
  bucket = subdomain
1242
1327
  prefix = path
1243
1328
  elif not scheme and bucket_path_level is None:
@@ -1283,10 +1368,10 @@ def deepcopy(sth: Any) -> Any:
1283
1368
  :param sth: Object to copy
1284
1369
  :returns: Copied object
1285
1370
  """
1286
- _dispatcher: Dict[Type[Any], Callable[..., Any]] = {}
1371
+ _dispatcher: dict[type[Any], Callable[..., Any]] = {}
1287
1372
 
1288
1373
  def _copy_list(
1289
- input_list: List[Any], dispatch: Dict[Type[Any], Callable[..., Any]]
1374
+ input_list: list[Any], dispatch: dict[type[Any], Callable[..., Any]]
1290
1375
  ):
1291
1376
  ret = input_list.copy()
1292
1377
  for idx, item in enumerate(ret):
@@ -1296,7 +1381,7 @@ def deepcopy(sth: Any) -> Any:
1296
1381
  return ret
1297
1382
 
1298
1383
  def _copy_dict(
1299
- input_dict: Dict[Any, Any], dispatch: Dict[Type[Any], Callable[..., Any]]
1384
+ input_dict: dict[Any, Any], dispatch: dict[type[Any], Callable[..., Any]]
1300
1385
  ):
1301
1386
  ret = input_dict.copy()
1302
1387
  for key, value in ret.items():
@@ -1373,17 +1458,34 @@ class StreamResponse:
1373
1458
 
1374
1459
 
1375
1460
  def guess_file_type(file: str) -> Optional[str]:
1376
- """guess the mime type of a file or URL based on its extension"""
1377
- mimetypes.add_type("text/xml", ".xsd")
1378
- mimetypes.add_type("application/x-grib", ".grib")
1461
+ """Guess the mime type of a file or URL based on its extension,
1462
+ using eodag extended mimetypes definition
1463
+
1464
+ >>> guess_file_type('foo.tiff')
1465
+ 'image/tiff'
1466
+ >>> guess_file_type('foo.grib')
1467
+ 'application/x-grib'
1468
+
1469
+ :param file: file url or path
1470
+ :returns: guessed mime type
1471
+ """
1379
1472
  mime_type, _ = mimetypes.guess_type(file, False)
1473
+ if mime_type == "text/xml":
1474
+ return "application/xml"
1380
1475
  return mime_type
1381
1476
 
1382
1477
 
1383
1478
  def guess_extension(type: str) -> Optional[str]:
1384
- """guess extension from mime type"""
1385
- mimetypes.add_type("text/xml", ".xsd")
1386
- mimetypes.add_type("application/x-grib", ".grib")
1479
+ """Guess extension from mime type, using eodag extended mimetypes definition
1480
+
1481
+ >>> guess_extension('image/tiff')
1482
+ '.tiff'
1483
+ >>> guess_extension('application/x-grib')
1484
+ '.grib'
1485
+
1486
+ :param type: mime type
1487
+ :returns: guessed file extension
1488
+ """
1387
1489
  return mimetypes.guess_extension(type, strict=False)
1388
1490
 
1389
1491
 
@@ -1404,7 +1506,7 @@ def get_ssl_context(ssl_verify: bool) -> ssl.SSLContext:
1404
1506
  return ctx
1405
1507
 
1406
1508
 
1407
- def sort_dict(input_dict: Dict[str, Any]) -> Dict[str, Any]:
1509
+ def sort_dict(input_dict: dict[str, Any]) -> dict[str, Any]:
1408
1510
  """
1409
1511
  Recursively sorts a dict by keys.
1410
1512
 
@@ -1420,7 +1522,7 @@ def sort_dict(input_dict: Dict[str, Any]) -> Dict[str, Any]:
1420
1522
  }
1421
1523
 
1422
1524
 
1423
- def dict_md5sum(input_dict: Dict[str, Any]) -> str:
1525
+ def dict_md5sum(input_dict: dict[str, Any]) -> str:
1424
1526
  """
1425
1527
  Hash nested dictionary
1426
1528
 
eodag/utils/exceptions.py CHANGED
@@ -20,23 +20,15 @@ from __future__ import annotations
20
20
  from typing import TYPE_CHECKING, Annotated
21
21
 
22
22
  if TYPE_CHECKING:
23
- from typing import Optional, Set
23
+ from typing import Optional
24
24
 
25
- from typing_extensions import Doc
25
+ from typing_extensions import Doc, Self
26
26
 
27
27
 
28
28
  class EodagError(Exception):
29
29
  """General EODAG error"""
30
30
 
31
31
 
32
- class ValidationError(EodagError):
33
- """Error validating data"""
34
-
35
- def __init__(self, message: str, parameters: Set[str] = set()) -> None:
36
- self.message = message
37
- self.parameters = parameters
38
-
39
-
40
32
  class PluginNotFoundError(EodagError):
41
33
  """Error when looking for a plugin class that was not defined"""
42
34
 
@@ -74,14 +66,19 @@ class AuthenticationError(EodagError):
74
66
  authenticating a user"""
75
67
 
76
68
 
77
- class DownloadError(EodagError):
78
- """An error indicating something wrong with the download process"""
79
-
80
-
81
69
  class NotAvailableError(EodagError):
82
70
  """An error indicating that the product is not available for download"""
83
71
 
84
72
 
73
+ class NoMatchingProductType(EodagError):
74
+ """An error indicating that eodag was unable to derive a product type from a set
75
+ of search parameters"""
76
+
77
+
78
+ class STACOpenerError(EodagError):
79
+ """An error indicating that a STAC file could not be opened"""
80
+
81
+
85
82
  class RequestError(EodagError):
86
83
  """An error indicating that a request has failed. Usually eodag functions
87
84
  and methods should catch and skip this"""
@@ -89,7 +86,7 @@ class RequestError(EodagError):
89
86
  status_code: Annotated[Optional[int], Doc("HTTP status code")] = None
90
87
 
91
88
  @classmethod
92
- def from_error(cls, error: Exception, msg: Optional[str] = None):
89
+ def from_error(cls, error: Exception, msg: Optional[str] = None) -> Self:
93
90
  """Generate a RequestError from an Exception"""
94
91
  status_code = getattr(error, "code", None)
95
92
  text = getattr(error, "msg", None)
@@ -99,7 +96,7 @@ class RequestError(EodagError):
99
96
  # have a status code other than 200
100
97
  if response is not None:
101
98
  status_code = response.status_code
102
- text = response.text
99
+ text = " ".join([text or "", response.text])
103
100
 
104
101
  text = text or str(error)
105
102
 
@@ -108,13 +105,23 @@ class RequestError(EodagError):
108
105
  return e
109
106
 
110
107
 
111
- class NoMatchingProductType(EodagError):
112
- """An error indicating that eodag was unable to derive a product type from a set
113
- of search parameters"""
108
+ class ValidationError(RequestError):
109
+ """Error validating data"""
114
110
 
111
+ def __init__(self, message: str, parameters: set[str] = set()) -> None:
112
+ self.message = message
113
+ self.parameters = parameters
115
114
 
116
- class STACOpenerError(EodagError):
117
- """An error indicating that a STAC file could not be opened"""
115
+ @classmethod
116
+ def from_error(cls, error: Exception, msg: Optional[str] = None) -> Self:
117
+ """Override parent from_error to handle ValidationError specificities."""
118
+ setattr(error, "msg", msg)
119
+ validation_error = super().from_error(error)
120
+ return validation_error
121
+
122
+
123
+ class DownloadError(RequestError):
124
+ """An error indicating something wrong with the download process"""
118
125
 
119
126
 
120
127
  class TimeOutError(RequestError):
@@ -21,14 +21,14 @@ import importlib
21
21
  import pkgutil
22
22
  from contextlib import contextmanager
23
23
  from functools import partial
24
- from typing import TYPE_CHECKING, Any, Generator, Tuple
24
+ from typing import TYPE_CHECKING, Any, Generator
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from types import ModuleType
28
28
 
29
29
 
30
30
  def import_all_modules(
31
- base_package: ModuleType, depth: int = 1, exclude: Tuple[str, ...] = ()
31
+ base_package: ModuleType, depth: int = 1, exclude: tuple[str, ...] = ()
32
32
  ) -> None:
33
33
  """Import all modules in base_package, including modules in the sub-packages up to `depth` and excluding modules in
34
34
  `exclude`.