eodag 3.1.0b1__py3-none-any.whl → 3.1.0b2__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 +59 -52
- eodag/api/product/_assets.py +5 -5
- eodag/api/product/_product.py +27 -12
- 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 +62 -74
- eodag/api/search_result.py +13 -23
- eodag/cli.py +4 -4
- eodag/config.py +66 -69
- eodag/plugins/apis/base.py +1 -1
- eodag/plugins/apis/ecmwf.py +10 -9
- eodag/plugins/apis/usgs.py +11 -10
- 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 +14 -14
- eodag/plugins/authentication/sas_auth.py +4 -4
- eodag/plugins/authentication/token.py +7 -7
- 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 +4 -4
- eodag/plugins/download/aws.py +47 -66
- eodag/plugins/download/base.py +8 -17
- eodag/plugins/download/creodias_s3.py +2 -2
- eodag/plugins/download/http.py +30 -32
- eodag/plugins/download/s3rest.py +5 -4
- eodag/plugins/manager.py +10 -20
- eodag/plugins/search/__init__.py +6 -5
- eodag/plugins/search/base.py +35 -40
- eodag/plugins/search/build_search_result.py +69 -68
- eodag/plugins/search/cop_marine.py +22 -12
- eodag/plugins/search/creodias_s3.py +8 -78
- eodag/plugins/search/csw.py +11 -11
- eodag/plugins/search/data_request_search.py +16 -15
- eodag/plugins/search/qssearch.py +56 -52
- eodag/plugins/search/stac_list_assets.py +85 -0
- eodag/plugins/search/static_stac_search.py +3 -3
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +288 -288
- eodag/resources/providers.yml +146 -6
- eodag/resources/stac_api.yml +2 -2
- eodag/resources/user_conf_template.yml +11 -0
- eodag/rest/cache.py +2 -2
- eodag/rest/config.py +3 -3
- eodag/rest/core.py +24 -24
- eodag/rest/errors.py +5 -5
- eodag/rest/server.py +3 -11
- eodag/rest/stac.py +40 -38
- eodag/rest/types/collections_search.py +3 -3
- eodag/rest/types/eodag_search.py +23 -23
- eodag/rest/types/queryables.py +13 -13
- eodag/rest/types/stac_search.py +15 -25
- eodag/rest/utils/__init__.py +11 -21
- eodag/rest/utils/cql_evaluate.py +6 -6
- eodag/rest/utils/rfc3339.py +2 -2
- eodag/types/__init__.py +24 -18
- eodag/types/bbox.py +2 -2
- eodag/types/download_args.py +2 -2
- eodag/types/queryables.py +5 -2
- eodag/types/search_args.py +4 -4
- eodag/types/whoosh.py +1 -3
- eodag/utils/__init__.py +81 -40
- eodag/utils/exceptions.py +2 -2
- eodag/utils/import_system.py +2 -2
- eodag/utils/requests.py +2 -2
- eodag/utils/rest.py +2 -2
- eodag/utils/s3.py +208 -0
- eodag/utils/stac_reader.py +10 -10
- {eodag-3.1.0b1.dist-info → eodag-3.1.0b2.dist-info}/METADATA +5 -4
- eodag-3.1.0b2.dist-info/RECORD +113 -0
- {eodag-3.1.0b1.dist-info → eodag-3.1.0b2.dist-info}/entry_points.txt +1 -0
- eodag-3.1.0b1.dist-info/RECORD +0 -108
- {eodag-3.1.0b1.dist-info → eodag-3.1.0b2.dist-info}/LICENSE +0 -0
- {eodag-3.1.0b1.dist-info → eodag-3.1.0b2.dist-info}/WHEEL +0 -0
- {eodag-3.1.0b1.dist-info → eodag-3.1.0b2.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
|
)
|
|
@@ -119,6 +115,7 @@ eodag_version = metadata("eodag")["Version"]
|
|
|
119
115
|
USER_AGENT = {"User-Agent": f"eodag/{eodag_version}"}
|
|
120
116
|
|
|
121
117
|
HTTP_REQ_TIMEOUT = 5 # in seconds
|
|
118
|
+
DEFAULT_SEARCH_TIMEOUT = 20 # in seconds
|
|
122
119
|
DEFAULT_STREAM_REQUESTS_TIMEOUT = 60 # in seconds
|
|
123
120
|
|
|
124
121
|
REQ_RETRY_TOTAL = 3
|
|
@@ -322,7 +319,7 @@ def path_to_uri(path: str) -> str:
|
|
|
322
319
|
return Path(path).as_uri()
|
|
323
320
|
|
|
324
321
|
|
|
325
|
-
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:
|
|
326
323
|
"""Apply func to values of mapping.
|
|
327
324
|
|
|
328
325
|
The mapping object's values are modified in-place. The function is recursive,
|
|
@@ -340,7 +337,7 @@ def mutate_dict_in_place(func: Callable[[Any], Any], mapping: Dict[Any, Any]) ->
|
|
|
340
337
|
mapping[key] = func(value)
|
|
341
338
|
|
|
342
339
|
|
|
343
|
-
def merge_mappings(mapping1:
|
|
340
|
+
def merge_mappings(mapping1: dict[Any, Any], mapping2: dict[Any, Any]) -> None:
|
|
344
341
|
"""Merge two mappings with string keys, values from ``mapping2`` overriding values
|
|
345
342
|
from ``mapping1``.
|
|
346
343
|
|
|
@@ -605,9 +602,49 @@ def rename_subfolder(dirpath: str, name: str) -> None:
|
|
|
605
602
|
)
|
|
606
603
|
|
|
607
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
|
+
|
|
608
645
|
def format_dict_items(
|
|
609
|
-
config_dict:
|
|
610
|
-
) ->
|
|
646
|
+
config_dict: dict[str, Any], **format_variables: Any
|
|
647
|
+
) -> dict[Any, Any]:
|
|
611
648
|
r"""Recursively apply :meth:`str.format` to ``**format_variables`` on ``config_dict`` values
|
|
612
649
|
|
|
613
650
|
>>> format_dict_items(
|
|
@@ -624,8 +661,8 @@ def format_dict_items(
|
|
|
624
661
|
|
|
625
662
|
|
|
626
663
|
def jsonpath_parse_dict_items(
|
|
627
|
-
jsonpath_dict:
|
|
628
|
-
) ->
|
|
664
|
+
jsonpath_dict: dict[str, Any], values_dict: dict[str, Any]
|
|
665
|
+
) -> dict[Any, Any]:
|
|
629
666
|
"""Recursively parse :class:`jsonpath_ng.JSONPath` elements in dict
|
|
630
667
|
|
|
631
668
|
>>> import jsonpath_ng.ext as jsonpath
|
|
@@ -643,12 +680,12 @@ def jsonpath_parse_dict_items(
|
|
|
643
680
|
|
|
644
681
|
|
|
645
682
|
def update_nested_dict(
|
|
646
|
-
old_dict:
|
|
647
|
-
new_dict:
|
|
683
|
+
old_dict: dict[Any, Any],
|
|
684
|
+
new_dict: dict[Any, Any],
|
|
648
685
|
extend_list_values: bool = False,
|
|
649
686
|
allow_empty_values: bool = False,
|
|
650
687
|
allow_extend_duplicates: bool = True,
|
|
651
|
-
) ->
|
|
688
|
+
) -> dict[Any, Any]:
|
|
652
689
|
"""Update recursively ``old_dict`` items with ``new_dict`` ones
|
|
653
690
|
|
|
654
691
|
>>> update_nested_dict(
|
|
@@ -728,10 +765,10 @@ def update_nested_dict(
|
|
|
728
765
|
|
|
729
766
|
|
|
730
767
|
def items_recursive_apply(
|
|
731
|
-
input_obj: Union[
|
|
768
|
+
input_obj: Union[dict[Any, Any], list[Any]],
|
|
732
769
|
apply_method: Callable[..., Any],
|
|
733
770
|
**apply_method_parameters: Any,
|
|
734
|
-
) -> Union[
|
|
771
|
+
) -> Union[dict[Any, Any], list[Any]]:
|
|
735
772
|
"""Recursive apply method to items contained in input object (dict or list)
|
|
736
773
|
|
|
737
774
|
>>> items_recursive_apply(
|
|
@@ -769,10 +806,10 @@ def items_recursive_apply(
|
|
|
769
806
|
|
|
770
807
|
|
|
771
808
|
def dict_items_recursive_apply(
|
|
772
|
-
config_dict:
|
|
809
|
+
config_dict: dict[Any, Any],
|
|
773
810
|
apply_method: Callable[..., Any],
|
|
774
811
|
**apply_method_parameters: Any,
|
|
775
|
-
) ->
|
|
812
|
+
) -> dict[Any, Any]:
|
|
776
813
|
"""Recursive apply method to dict elements
|
|
777
814
|
|
|
778
815
|
>>> dict_items_recursive_apply(
|
|
@@ -786,7 +823,7 @@ def dict_items_recursive_apply(
|
|
|
786
823
|
:param apply_method_parameters: Optional parameters passed to the method
|
|
787
824
|
:returns: Updated dict
|
|
788
825
|
"""
|
|
789
|
-
result_dict:
|
|
826
|
+
result_dict: dict[Any, Any] = deepcopy(config_dict)
|
|
790
827
|
for dict_k, dict_v in result_dict.items():
|
|
791
828
|
if isinstance(dict_v, dict):
|
|
792
829
|
result_dict[dict_k] = dict_items_recursive_apply(
|
|
@@ -794,7 +831,7 @@ def dict_items_recursive_apply(
|
|
|
794
831
|
)
|
|
795
832
|
elif any(isinstance(dict_v, t) for t in (list, tuple)):
|
|
796
833
|
result_dict[dict_k] = list_items_recursive_apply(
|
|
797
|
-
dict_v, apply_method, **apply_method_parameters
|
|
834
|
+
list(dict_v), apply_method, **apply_method_parameters
|
|
798
835
|
)
|
|
799
836
|
else:
|
|
800
837
|
result_dict[dict_k] = apply_method(
|
|
@@ -805,10 +842,10 @@ def dict_items_recursive_apply(
|
|
|
805
842
|
|
|
806
843
|
|
|
807
844
|
def list_items_recursive_apply(
|
|
808
|
-
config_list:
|
|
845
|
+
config_list: list[Any],
|
|
809
846
|
apply_method: Callable[..., Any],
|
|
810
847
|
**apply_method_parameters: Any,
|
|
811
|
-
) ->
|
|
848
|
+
) -> list[Any]:
|
|
812
849
|
"""Recursive apply method to list elements
|
|
813
850
|
|
|
814
851
|
>>> list_items_recursive_apply(
|
|
@@ -841,8 +878,8 @@ def list_items_recursive_apply(
|
|
|
841
878
|
|
|
842
879
|
|
|
843
880
|
def items_recursive_sort(
|
|
844
|
-
input_obj: Union[
|
|
845
|
-
) -> Union[
|
|
881
|
+
input_obj: Union[list[Any], dict[Any, Any]],
|
|
882
|
+
) -> Union[list[Any], dict[Any, Any]]:
|
|
846
883
|
"""Recursive sort dict items contained in input object (dict or list)
|
|
847
884
|
|
|
848
885
|
>>> items_recursive_sort(
|
|
@@ -866,7 +903,7 @@ def items_recursive_sort(
|
|
|
866
903
|
return input_obj
|
|
867
904
|
|
|
868
905
|
|
|
869
|
-
def dict_items_recursive_sort(config_dict:
|
|
906
|
+
def dict_items_recursive_sort(config_dict: dict[Any, Any]) -> dict[Any, Any]:
|
|
870
907
|
"""Recursive sort dict elements
|
|
871
908
|
|
|
872
909
|
>>> dict_items_recursive_sort(
|
|
@@ -877,7 +914,7 @@ def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
|
|
|
877
914
|
:param config_dict: Input nested dictionary
|
|
878
915
|
:returns: Updated dict
|
|
879
916
|
"""
|
|
880
|
-
result_dict:
|
|
917
|
+
result_dict: dict[Any, Any] = deepcopy(config_dict)
|
|
881
918
|
for dict_k, dict_v in result_dict.items():
|
|
882
919
|
if isinstance(dict_v, dict):
|
|
883
920
|
result_dict[dict_k] = dict_items_recursive_sort(dict_v)
|
|
@@ -889,7 +926,7 @@ def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
|
|
|
889
926
|
return dict(sorted(result_dict.items()))
|
|
890
927
|
|
|
891
928
|
|
|
892
|
-
def list_items_recursive_sort(config_list:
|
|
929
|
+
def list_items_recursive_sort(config_list: list[Any]) -> list[Any]:
|
|
893
930
|
"""Recursive sort dicts in list elements
|
|
894
931
|
|
|
895
932
|
>>> list_items_recursive_sort(["b", {2: 0, 0: 1, 1: 2}])
|
|
@@ -898,7 +935,7 @@ def list_items_recursive_sort(config_list: List[Any]) -> List[Any]:
|
|
|
898
935
|
:param config_list: Input list containing nested lists/dicts
|
|
899
936
|
:returns: Updated list
|
|
900
937
|
"""
|
|
901
|
-
result_list:
|
|
938
|
+
result_list: list[Any] = deepcopy(config_list)
|
|
902
939
|
for list_idx, list_v in enumerate(result_list):
|
|
903
940
|
if isinstance(list_v, dict):
|
|
904
941
|
result_list[list_idx] = dict_items_recursive_sort(list_v)
|
|
@@ -1024,7 +1061,7 @@ def format_string(key: str, str_to_format: Any, **format_variables: Any) -> Any:
|
|
|
1024
1061
|
|
|
1025
1062
|
|
|
1026
1063
|
def parse_jsonpath(
|
|
1027
|
-
key: str, jsonpath_obj: Union[str, jsonpath.Child], **values_dict:
|
|
1064
|
+
key: str, jsonpath_obj: Union[str, jsonpath.Child], **values_dict: dict[str, Any]
|
|
1028
1065
|
) -> Optional[str]:
|
|
1029
1066
|
"""Parse jsonpah in ``jsonpath_obj`` using ``values_dict``
|
|
1030
1067
|
|
|
@@ -1044,7 +1081,7 @@ def parse_jsonpath(
|
|
|
1044
1081
|
return jsonpath_obj
|
|
1045
1082
|
|
|
1046
1083
|
|
|
1047
|
-
def nested_pairs2dict(pairs: Union[
|
|
1084
|
+
def nested_pairs2dict(pairs: Union[list[Any], Any]) -> Union[Any, dict[Any, Any]]:
|
|
1048
1085
|
"""Create a dict using nested pairs
|
|
1049
1086
|
|
|
1050
1087
|
>>> nested_pairs2dict([["foo", [["bar", "baz"]]]])
|
|
@@ -1066,7 +1103,7 @@ def nested_pairs2dict(pairs: Union[List[Any], Any]) -> Union[Any, Dict[Any, Any]
|
|
|
1066
1103
|
|
|
1067
1104
|
|
|
1068
1105
|
def get_geometry_from_various(
|
|
1069
|
-
locations_config:
|
|
1106
|
+
locations_config: list[dict[str, Any]] = [], **query_args: Any
|
|
1070
1107
|
) -> BaseGeometry:
|
|
1071
1108
|
"""Creates a ``shapely.geometry`` using given query kwargs arguments
|
|
1072
1109
|
|
|
@@ -1242,7 +1279,7 @@ def _mutable_cached_yaml_load(config_path: str) -> Any:
|
|
|
1242
1279
|
return yaml.load(fh, Loader=yaml.SafeLoader)
|
|
1243
1280
|
|
|
1244
1281
|
|
|
1245
|
-
def cached_yaml_load(config_path: str) ->
|
|
1282
|
+
def cached_yaml_load(config_path: str) -> dict[str, Any]:
|
|
1246
1283
|
"""Cached :func:`yaml.load`
|
|
1247
1284
|
|
|
1248
1285
|
:param config_path: path to the yaml configuration file
|
|
@@ -1252,12 +1289,12 @@ def cached_yaml_load(config_path: str) -> Dict[str, Any]:
|
|
|
1252
1289
|
|
|
1253
1290
|
|
|
1254
1291
|
@functools.lru_cache()
|
|
1255
|
-
def _mutable_cached_yaml_load_all(config_path: str) ->
|
|
1292
|
+
def _mutable_cached_yaml_load_all(config_path: str) -> list[Any]:
|
|
1256
1293
|
with open(config_path, "r") as fh:
|
|
1257
1294
|
return list(yaml.load_all(fh, Loader=yaml.Loader))
|
|
1258
1295
|
|
|
1259
1296
|
|
|
1260
|
-
def cached_yaml_load_all(config_path: str) ->
|
|
1297
|
+
def cached_yaml_load_all(config_path: str) -> list[Any]:
|
|
1261
1298
|
"""Cached :func:`yaml.load_all`
|
|
1262
1299
|
|
|
1263
1300
|
Load all configurations stored in the configuration file as separated yaml documents
|
|
@@ -1270,7 +1307,7 @@ def cached_yaml_load_all(config_path: str) -> List[Any]:
|
|
|
1270
1307
|
|
|
1271
1308
|
def get_bucket_name_and_prefix(
|
|
1272
1309
|
url: str, bucket_path_level: Optional[int] = None
|
|
1273
|
-
) ->
|
|
1310
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
1274
1311
|
"""Extract bucket name and prefix from URL
|
|
1275
1312
|
|
|
1276
1313
|
:param url: (optional) URL to use as product.location
|
|
@@ -1283,7 +1320,9 @@ def get_bucket_name_and_prefix(
|
|
|
1283
1320
|
subdomain = netloc.split(".")[0]
|
|
1284
1321
|
path = path.strip("/")
|
|
1285
1322
|
|
|
1286
|
-
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:
|
|
1287
1326
|
bucket = subdomain
|
|
1288
1327
|
prefix = path
|
|
1289
1328
|
elif not scheme and bucket_path_level is None:
|
|
@@ -1329,10 +1368,10 @@ def deepcopy(sth: Any) -> Any:
|
|
|
1329
1368
|
:param sth: Object to copy
|
|
1330
1369
|
:returns: Copied object
|
|
1331
1370
|
"""
|
|
1332
|
-
_dispatcher:
|
|
1371
|
+
_dispatcher: dict[type[Any], Callable[..., Any]] = {}
|
|
1333
1372
|
|
|
1334
1373
|
def _copy_list(
|
|
1335
|
-
input_list:
|
|
1374
|
+
input_list: list[Any], dispatch: dict[type[Any], Callable[..., Any]]
|
|
1336
1375
|
):
|
|
1337
1376
|
ret = input_list.copy()
|
|
1338
1377
|
for idx, item in enumerate(ret):
|
|
@@ -1342,7 +1381,7 @@ def deepcopy(sth: Any) -> Any:
|
|
|
1342
1381
|
return ret
|
|
1343
1382
|
|
|
1344
1383
|
def _copy_dict(
|
|
1345
|
-
input_dict:
|
|
1384
|
+
input_dict: dict[Any, Any], dispatch: dict[type[Any], Callable[..., Any]]
|
|
1346
1385
|
):
|
|
1347
1386
|
ret = input_dict.copy()
|
|
1348
1387
|
for key, value in ret.items():
|
|
@@ -1431,6 +1470,8 @@ def guess_file_type(file: str) -> Optional[str]:
|
|
|
1431
1470
|
:returns: guessed mime type
|
|
1432
1471
|
"""
|
|
1433
1472
|
mime_type, _ = mimetypes.guess_type(file, False)
|
|
1473
|
+
if mime_type == "text/xml":
|
|
1474
|
+
return "application/xml"
|
|
1434
1475
|
return mime_type
|
|
1435
1476
|
|
|
1436
1477
|
|
|
@@ -1465,7 +1506,7 @@ def get_ssl_context(ssl_verify: bool) -> ssl.SSLContext:
|
|
|
1465
1506
|
return ctx
|
|
1466
1507
|
|
|
1467
1508
|
|
|
1468
|
-
def sort_dict(input_dict:
|
|
1509
|
+
def sort_dict(input_dict: dict[str, Any]) -> dict[str, Any]:
|
|
1469
1510
|
"""
|
|
1470
1511
|
Recursively sorts a dict by keys.
|
|
1471
1512
|
|
|
@@ -1481,7 +1522,7 @@ def sort_dict(input_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1481
1522
|
}
|
|
1482
1523
|
|
|
1483
1524
|
|
|
1484
|
-
def dict_md5sum(input_dict:
|
|
1525
|
+
def dict_md5sum(input_dict: dict[str, Any]) -> str:
|
|
1485
1526
|
"""
|
|
1486
1527
|
Hash nested dictionary
|
|
1487
1528
|
|
eodag/utils/exceptions.py
CHANGED
|
@@ -20,7 +20,7 @@ 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
25
|
from typing_extensions import Doc, Self
|
|
26
26
|
|
|
@@ -108,7 +108,7 @@ class RequestError(EodagError):
|
|
|
108
108
|
class ValidationError(RequestError):
|
|
109
109
|
"""Error validating data"""
|
|
110
110
|
|
|
111
|
-
def __init__(self, message: str, parameters:
|
|
111
|
+
def __init__(self, message: str, parameters: set[str] = set()) -> None:
|
|
112
112
|
self.message = message
|
|
113
113
|
self.parameters = parameters
|
|
114
114
|
|
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`.
|
eodag/utils/requests.py
CHANGED
|
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
|
-
from typing import Any, Optional
|
|
22
|
+
from typing import Any, Optional
|
|
23
23
|
|
|
24
24
|
import requests
|
|
25
25
|
|
|
@@ -75,7 +75,7 @@ class LocalFileAdapter(requests.adapters.BaseAdapter):
|
|
|
75
75
|
"""
|
|
76
76
|
|
|
77
77
|
@staticmethod
|
|
78
|
-
def _chkpath(method: str, path: str) ->
|
|
78
|
+
def _chkpath(method: str, path: str) -> tuple[int, str]:
|
|
79
79
|
"""Return an HTTP status for the given filesystem path.
|
|
80
80
|
|
|
81
81
|
:param method: method of the request
|
eodag/utils/rest.py
CHANGED
|
@@ -21,7 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import datetime
|
|
23
23
|
import re
|
|
24
|
-
from typing import Any,
|
|
24
|
+
from typing import Any, Optional
|
|
25
25
|
|
|
26
26
|
import dateutil.parser
|
|
27
27
|
from dateutil import tz
|
|
@@ -35,7 +35,7 @@ RFC3339_PATTERN = (
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def get_datetime(arguments:
|
|
38
|
+
def get_datetime(arguments: dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
|
|
39
39
|
"""Get start and end dates from a dict containing `/` separated dates in `datetime` item
|
|
40
40
|
|
|
41
41
|
:param arguments: dict containing a single date or `/` separated dates in `datetime` item
|
eodag/utils/s3.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
|
|
3
|
+
#
|
|
4
|
+
# This file is part of EODAG project
|
|
5
|
+
# https://www.github.com/CS-SI/EODAG
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import io
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import zipfile
|
|
24
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
25
|
+
from urllib.parse import urlparse
|
|
26
|
+
|
|
27
|
+
import boto3
|
|
28
|
+
import botocore
|
|
29
|
+
|
|
30
|
+
from eodag.plugins.authentication.aws_auth import AwsAuth
|
|
31
|
+
from eodag.utils import get_bucket_name_and_prefix, guess_file_type
|
|
32
|
+
from eodag.utils.exceptions import (
|
|
33
|
+
AuthenticationError,
|
|
34
|
+
MisconfiguredError,
|
|
35
|
+
NotAvailableError,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from zipfile import ZipInfo
|
|
40
|
+
|
|
41
|
+
from mypy_boto3_s3.client import S3Client
|
|
42
|
+
|
|
43
|
+
from eodag.api.product import EOProduct # type: ignore
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger("eodag.utils.s3")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fetch(
|
|
49
|
+
bucket_name: str, key_name: str, start: int, len: int, client_s3: S3Client
|
|
50
|
+
) -> bytes:
|
|
51
|
+
"""
|
|
52
|
+
Range-fetches a S3 key.
|
|
53
|
+
|
|
54
|
+
:param bucket_name: Bucket name of the object to fetch
|
|
55
|
+
:param key_name: Key name of the object to fetch
|
|
56
|
+
:param start: Bucket name to fetch
|
|
57
|
+
:param len: Bucket name to fetch
|
|
58
|
+
:param client_s3: s3 client used to fetch the object
|
|
59
|
+
:returns: Object bytes
|
|
60
|
+
"""
|
|
61
|
+
end = start + len - 1
|
|
62
|
+
s3_object = client_s3.get_object(
|
|
63
|
+
Bucket=bucket_name, Key=key_name, Range="bytes=%d-%d" % (start, end)
|
|
64
|
+
)
|
|
65
|
+
return s3_object["Body"].read()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_int(bytes: bytes) -> int:
|
|
69
|
+
"""
|
|
70
|
+
Parses 2 or 4 little-endian bits into their corresponding integer value.
|
|
71
|
+
|
|
72
|
+
:param bytes: bytes to parse
|
|
73
|
+
:returns: parsed int
|
|
74
|
+
"""
|
|
75
|
+
val = (bytes[0]) + ((bytes[1]) << 8)
|
|
76
|
+
if len(bytes) > 3:
|
|
77
|
+
val += ((bytes[2]) << 16) + ((bytes[3]) << 24)
|
|
78
|
+
return val
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def list_files_in_s3_zipped_object(
|
|
82
|
+
bucket_name: str, key_name: str, client_s3: S3Client
|
|
83
|
+
) -> List[ZipInfo]:
|
|
84
|
+
"""
|
|
85
|
+
List files in s3 zipped object, without downloading it.
|
|
86
|
+
|
|
87
|
+
See https://stackoverflow.com/questions/41789176/how-to-count-files-inside-zip-in-aws-s3-without-downloading-it;
|
|
88
|
+
Based on https://stackoverflow.com/questions/51351000/read-zip-files-from-s3-without-downloading-the-entire-file
|
|
89
|
+
|
|
90
|
+
:param bucket_name: Bucket name of the object to fetch
|
|
91
|
+
:param key_name: Key name of the object to fetch
|
|
92
|
+
:param client_s3: s3 client used to fetch the object
|
|
93
|
+
:returns: List of files in zip
|
|
94
|
+
"""
|
|
95
|
+
response = client_s3.head_object(Bucket=bucket_name, Key=key_name)
|
|
96
|
+
size = response["ContentLength"]
|
|
97
|
+
|
|
98
|
+
# End Of Central Directory bytes
|
|
99
|
+
eocd = fetch(bucket_name, key_name, size - 22, 22, client_s3)
|
|
100
|
+
|
|
101
|
+
# start offset and size of the central directory
|
|
102
|
+
cd_start = parse_int(eocd[16:20])
|
|
103
|
+
cd_size = parse_int(eocd[12:16])
|
|
104
|
+
|
|
105
|
+
# fetch central directory, append EOCD, and open as zipfile
|
|
106
|
+
cd = fetch(bucket_name, key_name, cd_start, cd_size, client_s3)
|
|
107
|
+
zip = zipfile.ZipFile(io.BytesIO(cd + eocd))
|
|
108
|
+
|
|
109
|
+
logger.debug("Found %s files in %s" % (len(zip.filelist), key_name))
|
|
110
|
+
|
|
111
|
+
return zip.filelist
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def update_assets_from_s3(
|
|
115
|
+
product: EOProduct,
|
|
116
|
+
auth: AwsAuth,
|
|
117
|
+
s3_endpoint: Optional[str] = None,
|
|
118
|
+
content_url: Optional[str] = None,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Update ``EOProduct.assets`` using content listed in its ``remote_location`` or given
|
|
121
|
+
``content_url``.
|
|
122
|
+
|
|
123
|
+
If url points to a zipped archive, its content will also be be listed.
|
|
124
|
+
|
|
125
|
+
:param product: product to update
|
|
126
|
+
:param auth: Authentication plugin
|
|
127
|
+
:param s3_endpoint: s3 endpoint if not hosted on AWS
|
|
128
|
+
:param content_url: s3 URL pointing to the content that must be listed (defaults to
|
|
129
|
+
``product.remote_location`` if empty)
|
|
130
|
+
"""
|
|
131
|
+
required_creds = ["aws_access_key_id", "aws_secret_access_key"]
|
|
132
|
+
|
|
133
|
+
if content_url is None:
|
|
134
|
+
content_url = product.remote_location
|
|
135
|
+
|
|
136
|
+
bucket, prefix = get_bucket_name_and_prefix(content_url)
|
|
137
|
+
|
|
138
|
+
if bucket is None or prefix is None:
|
|
139
|
+
logger.debug(f"No s3 prefix could guessed from {content_url}")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
auth_dict = auth.authenticate()
|
|
144
|
+
|
|
145
|
+
if not all(x in auth_dict for x in required_creds):
|
|
146
|
+
raise MisconfiguredError(
|
|
147
|
+
f"Incomplete credentials for {product.provider}, missing "
|
|
148
|
+
f"{[x for x in required_creds if x not in auth_dict]}"
|
|
149
|
+
)
|
|
150
|
+
if not getattr(auth, "s3_client", None):
|
|
151
|
+
auth.s3_client = boto3.client(
|
|
152
|
+
service_name="s3",
|
|
153
|
+
endpoint_url=s3_endpoint,
|
|
154
|
+
aws_access_key_id=auth_dict.get("aws_access_key_id"),
|
|
155
|
+
aws_secret_access_key=auth_dict.get("aws_secret_access_key"),
|
|
156
|
+
aws_session_token=auth_dict.get("aws_session_token"),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
logger.debug("Listing assets in %s", prefix)
|
|
160
|
+
|
|
161
|
+
if prefix.endswith(".zip"):
|
|
162
|
+
# List prefix zip content
|
|
163
|
+
assets_urls = [
|
|
164
|
+
f"zip+s3://{bucket}/{prefix}!{f.filename}"
|
|
165
|
+
for f in list_files_in_s3_zipped_object(bucket, prefix, auth.s3_client)
|
|
166
|
+
]
|
|
167
|
+
else:
|
|
168
|
+
# List files in prefix
|
|
169
|
+
assets_urls = [
|
|
170
|
+
f"s3://{bucket}/{obj['Key']}"
|
|
171
|
+
for obj in auth.s3_client.list_objects(
|
|
172
|
+
Bucket=bucket, Prefix=prefix, MaxKeys=300
|
|
173
|
+
).get("Contents", [])
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
for asset_url in assets_urls:
|
|
177
|
+
out_of_zip_url = asset_url.split("!")[-1]
|
|
178
|
+
key, roles = product.driver.guess_asset_key_and_roles(
|
|
179
|
+
out_of_zip_url, product
|
|
180
|
+
)
|
|
181
|
+
parsed_url = urlparse(out_of_zip_url)
|
|
182
|
+
title = os.path.basename(parsed_url.path)
|
|
183
|
+
|
|
184
|
+
if key and key not in product.assets:
|
|
185
|
+
product.assets[key] = {
|
|
186
|
+
"title": title,
|
|
187
|
+
"roles": roles,
|
|
188
|
+
"href": asset_url,
|
|
189
|
+
}
|
|
190
|
+
if mime_type := guess_file_type(asset_url):
|
|
191
|
+
product.assets[key]["type"] = mime_type
|
|
192
|
+
|
|
193
|
+
# sort assets
|
|
194
|
+
product.assets.data = dict(sorted(product.assets.data.items()))
|
|
195
|
+
|
|
196
|
+
# update driver
|
|
197
|
+
product.driver = product.get_driver()
|
|
198
|
+
|
|
199
|
+
except botocore.exceptions.ClientError as e:
|
|
200
|
+
if hasattr(auth.config, "auth_error_code") and str(
|
|
201
|
+
auth.config.auth_error_code
|
|
202
|
+
) in str(e):
|
|
203
|
+
raise AuthenticationError(
|
|
204
|
+
f"Authentication failed on {s3_endpoint} s3"
|
|
205
|
+
) from e
|
|
206
|
+
raise NotAvailableError(
|
|
207
|
+
f"assets for product {prefix} could not be found"
|
|
208
|
+
) from e
|
eodag/utils/stac_reader.py
CHANGED
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import re
|
|
22
22
|
import socket
|
|
23
|
-
from typing import Any, Callable,
|
|
23
|
+
from typing import Any, Callable, Optional, Union
|
|
24
24
|
from urllib.error import URLError
|
|
25
25
|
from urllib.request import urlopen
|
|
26
26
|
|
|
@@ -108,7 +108,7 @@ def fetch_stac_items(
|
|
|
108
108
|
max_connections: int = 100,
|
|
109
109
|
timeout: int = HTTP_REQ_TIMEOUT,
|
|
110
110
|
ssl_verify: bool = True,
|
|
111
|
-
) ->
|
|
111
|
+
) -> list[dict[str, Any]]:
|
|
112
112
|
"""Fetch STAC item from a single item file or items from a catalog.
|
|
113
113
|
|
|
114
114
|
:param stac_path: A STAC object filepath
|
|
@@ -142,13 +142,13 @@ def _fetch_stac_items_from_catalog(
|
|
|
142
142
|
recursive: bool,
|
|
143
143
|
max_connections: int,
|
|
144
144
|
_text_opener: Callable[[str, bool], Any],
|
|
145
|
-
) ->
|
|
145
|
+
) -> list[Any]:
|
|
146
146
|
"""Fetch items from a STAC catalog"""
|
|
147
|
-
items:
|
|
147
|
+
items: list[dict[Any, Any]] = []
|
|
148
148
|
|
|
149
149
|
# pystac cannot yet return links from a single file catalog, see:
|
|
150
150
|
# https://github.com/stac-utils/pystac/issues/256
|
|
151
|
-
extensions: Optional[Union[
|
|
151
|
+
extensions: Optional[Union[list[str], str]] = getattr(cat, "stac_extensions", None)
|
|
152
152
|
if extensions:
|
|
153
153
|
extensions = extensions if isinstance(extensions, list) else [extensions]
|
|
154
154
|
if "single-file-stac" in extensions:
|
|
@@ -157,7 +157,7 @@ def _fetch_stac_items_from_catalog(
|
|
|
157
157
|
|
|
158
158
|
# Making the links absolutes allow for both relative and absolute links to be handled.
|
|
159
159
|
if not recursive:
|
|
160
|
-
hrefs:
|
|
160
|
+
hrefs: list[Optional[str]] = [
|
|
161
161
|
link.get_absolute_href() for link in cat.get_item_links()
|
|
162
162
|
]
|
|
163
163
|
else:
|
|
@@ -188,7 +188,7 @@ def fetch_stac_collections(
|
|
|
188
188
|
max_connections: int = 100,
|
|
189
189
|
timeout: int = HTTP_REQ_TIMEOUT,
|
|
190
190
|
ssl_verify: bool = True,
|
|
191
|
-
) ->
|
|
191
|
+
) -> list[dict[str, Any]]:
|
|
192
192
|
"""Fetch STAC collection(s) from a catalog.
|
|
193
193
|
|
|
194
194
|
:param stac_path: A STAC object filepath
|
|
@@ -217,12 +217,12 @@ def _fetch_stac_collections_from_catalog(
|
|
|
217
217
|
collection: Optional[str],
|
|
218
218
|
max_connections: int,
|
|
219
219
|
_text_opener: Callable[[str, bool], Any],
|
|
220
|
-
) ->
|
|
220
|
+
) -> list[Any]:
|
|
221
221
|
"""Fetch collections from a STAC catalog"""
|
|
222
|
-
collections:
|
|
222
|
+
collections: list[dict[Any, Any]] = []
|
|
223
223
|
|
|
224
224
|
# Making the links absolutes allow for both relative and absolute links to be handled.
|
|
225
|
-
hrefs:
|
|
225
|
+
hrefs: list[Optional[str]] = [
|
|
226
226
|
link.get_absolute_href()
|
|
227
227
|
for link in cat.get_child_links()
|
|
228
228
|
if collection is not None and link.title == collection
|