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.
- eodag/api/core.py +174 -138
- eodag/api/product/_assets.py +44 -15
- eodag/api/product/_product.py +58 -47
- eodag/api/product/drivers/__init__.py +81 -4
- eodag/api/product/drivers/base.py +65 -4
- eodag/api/product/drivers/generic.py +65 -0
- eodag/api/product/drivers/sentinel1.py +97 -0
- eodag/api/product/drivers/sentinel2.py +95 -0
- eodag/api/product/metadata_mapping.py +117 -90
- eodag/api/search_result.py +13 -23
- eodag/cli.py +26 -5
- eodag/config.py +86 -92
- eodag/plugins/apis/base.py +1 -1
- eodag/plugins/apis/ecmwf.py +42 -22
- eodag/plugins/apis/usgs.py +17 -16
- eodag/plugins/authentication/aws_auth.py +16 -13
- eodag/plugins/authentication/base.py +5 -3
- eodag/plugins/authentication/header.py +3 -3
- eodag/plugins/authentication/keycloak.py +4 -4
- eodag/plugins/authentication/oauth.py +7 -3
- eodag/plugins/authentication/openid_connect.py +22 -16
- eodag/plugins/authentication/sas_auth.py +4 -4
- eodag/plugins/authentication/token.py +41 -10
- eodag/plugins/authentication/token_exchange.py +1 -1
- eodag/plugins/base.py +4 -4
- eodag/plugins/crunch/base.py +4 -4
- eodag/plugins/crunch/filter_date.py +4 -4
- eodag/plugins/crunch/filter_latest_intersect.py +6 -6
- eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
- eodag/plugins/crunch/filter_overlap.py +4 -4
- eodag/plugins/crunch/filter_property.py +6 -7
- eodag/plugins/download/aws.py +146 -87
- eodag/plugins/download/base.py +38 -56
- eodag/plugins/download/creodias_s3.py +29 -0
- eodag/plugins/download/http.py +173 -183
- eodag/plugins/download/s3rest.py +10 -11
- eodag/plugins/manager.py +10 -20
- eodag/plugins/search/__init__.py +6 -5
- eodag/plugins/search/base.py +90 -46
- eodag/plugins/search/build_search_result.py +1048 -361
- eodag/plugins/search/cop_marine.py +22 -12
- eodag/plugins/search/creodias_s3.py +9 -73
- eodag/plugins/search/csw.py +11 -11
- eodag/plugins/search/data_request_search.py +19 -18
- eodag/plugins/search/qssearch.py +99 -258
- eodag/plugins/search/stac_list_assets.py +85 -0
- eodag/plugins/search/static_stac_search.py +4 -4
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1134 -325
- eodag/resources/providers.yml +906 -2006
- eodag/resources/stac_api.yml +2 -2
- eodag/resources/user_conf_template.yml +10 -9
- eodag/rest/cache.py +2 -2
- eodag/rest/config.py +3 -3
- eodag/rest/core.py +112 -82
- eodag/rest/errors.py +5 -5
- eodag/rest/server.py +33 -14
- eodag/rest/stac.py +41 -38
- eodag/rest/types/collections_search.py +3 -3
- eodag/rest/types/eodag_search.py +29 -23
- eodag/rest/types/queryables.py +42 -31
- eodag/rest/types/stac_search.py +15 -25
- eodag/rest/utils/__init__.py +14 -21
- eodag/rest/utils/cql_evaluate.py +6 -6
- eodag/rest/utils/rfc3339.py +2 -2
- eodag/types/__init__.py +141 -32
- eodag/types/bbox.py +2 -2
- eodag/types/download_args.py +3 -3
- eodag/types/queryables.py +183 -72
- eodag/types/search_args.py +4 -4
- eodag/types/whoosh.py +127 -3
- eodag/utils/__init__.py +153 -51
- eodag/utils/exceptions.py +28 -21
- eodag/utils/import_system.py +2 -2
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +13 -13
- eodag/utils/rest.py +2 -2
- eodag/utils/s3.py +231 -0
- eodag/utils/stac_reader.py +10 -10
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/METADATA +77 -76
- eodag-3.1.0.dist-info/RECORD +113 -0
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +4 -2
- eodag/utils/constraints.py +0 -244
- eodag-3.0.1.dist-info/RECORD +0 -109
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
- {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 =
|
|
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:
|
|
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:
|
|
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:
|
|
575
|
-
) ->
|
|
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:
|
|
593
|
-
) ->
|
|
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:
|
|
612
|
-
new_dict:
|
|
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
|
-
) ->
|
|
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[
|
|
768
|
+
input_obj: Union[dict[Any, Any], list[Any]],
|
|
697
769
|
apply_method: Callable[..., Any],
|
|
698
770
|
**apply_method_parameters: Any,
|
|
699
|
-
) -> Union[
|
|
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:
|
|
809
|
+
config_dict: dict[Any, Any],
|
|
738
810
|
apply_method: Callable[..., Any],
|
|
739
811
|
**apply_method_parameters: Any,
|
|
740
|
-
) ->
|
|
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:
|
|
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:
|
|
845
|
+
config_list: list[Any],
|
|
774
846
|
apply_method: Callable[..., Any],
|
|
775
847
|
**apply_method_parameters: Any,
|
|
776
|
-
) ->
|
|
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[
|
|
810
|
-
) -> Union[
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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[
|
|
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:
|
|
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
|
-
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
-
) ->
|
|
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:
|
|
1371
|
+
_dispatcher: dict[type[Any], Callable[..., Any]] = {}
|
|
1287
1372
|
|
|
1288
1373
|
def _copy_list(
|
|
1289
|
-
input_list:
|
|
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:
|
|
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
|
-
"""
|
|
1377
|
-
mimetypes
|
|
1378
|
-
|
|
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
|
-
"""
|
|
1385
|
-
|
|
1386
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
112
|
-
"""
|
|
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
|
-
|
|
117
|
-
|
|
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):
|
eodag/utils/import_system.py
CHANGED
|
@@ -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
|
|
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:
|
|
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`.
|