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/repr.py
CHANGED
|
@@ -18,12 +18,21 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import collections.abc
|
|
21
|
+
import re
|
|
21
22
|
from typing import Any, Optional
|
|
22
23
|
from urllib.parse import urlparse
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def str_as_href(link: str) -> str:
|
|
26
|
-
"""URL to html link
|
|
27
|
+
"""URL to html link
|
|
28
|
+
|
|
29
|
+
:param link: URL to format
|
|
30
|
+
:returns: HMLT formatted link
|
|
31
|
+
|
|
32
|
+
>>> str_as_href("http://foo.bar")
|
|
33
|
+
"<a href='http://foo.bar' target='_blank'>http://foo.bar</a>"
|
|
34
|
+
|
|
35
|
+
"""
|
|
27
36
|
if urlparse(link).scheme in ("file", "http", "https", "s3"):
|
|
28
37
|
return f"<a href='{link}' target='_blank'>{link}</a>"
|
|
29
38
|
else:
|
|
@@ -31,7 +40,13 @@ def str_as_href(link: str) -> str:
|
|
|
31
40
|
|
|
32
41
|
|
|
33
42
|
def html_table(input: Any, depth: Optional[int] = None) -> str:
|
|
34
|
-
"""Transform input to HTML table
|
|
43
|
+
"""Transform input object to HTML table
|
|
44
|
+
|
|
45
|
+
:param input: input object to represent
|
|
46
|
+
:param depth: maximum depth level until which nested objects should be represented
|
|
47
|
+
in new tables (unlimited by default)
|
|
48
|
+
:returns: HTML table
|
|
49
|
+
"""
|
|
35
50
|
if isinstance(input, collections.abc.Mapping):
|
|
36
51
|
return dict_to_html_table(input, depth=depth)
|
|
37
52
|
elif isinstance(input, collections.abc.Sequence) and not isinstance(input, str):
|
|
@@ -47,7 +62,14 @@ def dict_to_html_table(
|
|
|
47
62
|
depth: Optional[int] = None,
|
|
48
63
|
brackets: bool = True,
|
|
49
64
|
) -> str:
|
|
50
|
-
"""Transform input dict to HTML table
|
|
65
|
+
"""Transform input dict to HTML table
|
|
66
|
+
|
|
67
|
+
:param input_dict: input dict to represent
|
|
68
|
+
:param depth: maximum depth level until which nested objects should be represented
|
|
69
|
+
in new tables (unlimited by default)
|
|
70
|
+
:param brackets: whether surrounding brackets should be displayed or not
|
|
71
|
+
:returns: HTML table
|
|
72
|
+
"""
|
|
51
73
|
opening_bracket = "<span style='color: grey;'>{</span>" if brackets else ""
|
|
52
74
|
closing_bracket = "<span style='color: grey;'>}</span>" if brackets else ""
|
|
53
75
|
indent = "10px" if brackets else "0"
|
|
@@ -90,7 +112,13 @@ def dict_to_html_table(
|
|
|
90
112
|
def list_to_html_table(
|
|
91
113
|
input_list: collections.abc.Sequence, depth: Optional[int] = None
|
|
92
114
|
) -> str:
|
|
93
|
-
"""Transform input list to HTML table
|
|
115
|
+
"""Transform input list to HTML table
|
|
116
|
+
|
|
117
|
+
:param input_list: input list to represent
|
|
118
|
+
:param depth: maximum depth level until which nested objects should be represented
|
|
119
|
+
in new tables (unlimited by default)
|
|
120
|
+
:returns: HTML table
|
|
121
|
+
"""
|
|
94
122
|
if depth is not None:
|
|
95
123
|
depth -= 1
|
|
96
124
|
separator = (
|
|
@@ -103,11 +131,42 @@ def list_to_html_table(
|
|
|
103
131
|
+ separator.join(
|
|
104
132
|
[
|
|
105
133
|
f"""<span style='text-align: left;'>{
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
html_table(v, depth=depth)
|
|
135
|
+
}</span>
|
|
108
136
|
"""
|
|
109
137
|
for v in input_list
|
|
110
138
|
]
|
|
111
139
|
)
|
|
112
140
|
+ "<span style='color: grey;'>]</span>"
|
|
113
141
|
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def remove_class_repr(type_repr: str) -> str:
|
|
145
|
+
"""Removes class tag from type representation
|
|
146
|
+
|
|
147
|
+
:param type_repr: input type representation
|
|
148
|
+
:returns: type without class tag
|
|
149
|
+
|
|
150
|
+
>>> remove_class_repr(str(type("foo")))
|
|
151
|
+
'str'
|
|
152
|
+
"""
|
|
153
|
+
return re.sub(r"<class '(\w+)'>", r"\1", type_repr)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def shorter_type_repr(long_type: str) -> str:
|
|
157
|
+
"""Shorten long type representation
|
|
158
|
+
|
|
159
|
+
:param long_type: long type representation
|
|
160
|
+
:returns: type reprensentation shortened
|
|
161
|
+
|
|
162
|
+
>>> import typing
|
|
163
|
+
>>> shorter_type_repr(str(typing.Literal["foo", "bar"]))
|
|
164
|
+
"Literal['foo', ...]"
|
|
165
|
+
"""
|
|
166
|
+
# shorten lists
|
|
167
|
+
shorter = re.sub(r",[^\[^\]]+\]", ", ...]", str(long_type))
|
|
168
|
+
# remove class prefix
|
|
169
|
+
shorter = remove_class_repr(shorter)
|
|
170
|
+
# remove parent objects
|
|
171
|
+
shorter = re.sub(r"\w+\.", "", shorter)
|
|
172
|
+
return shorter
|
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
|
|
|
@@ -30,7 +30,7 @@ logger = logging.getLogger("eodag.utils.requests")
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def fetch_json(
|
|
33
|
-
|
|
33
|
+
url: str,
|
|
34
34
|
req_session: Optional[requests.Session] = None,
|
|
35
35
|
auth: Optional[requests.auth.AuthBase] = None,
|
|
36
36
|
timeout: float = HTTP_REQ_TIMEOUT,
|
|
@@ -38,32 +38,32 @@ def fetch_json(
|
|
|
38
38
|
"""
|
|
39
39
|
Fetches http/distant or local json file
|
|
40
40
|
|
|
41
|
-
:param
|
|
41
|
+
:param url: url from which the file can be fetched
|
|
42
42
|
:param req_session: (optional) requests session
|
|
43
43
|
:param auth: (optional) authenticated object if request needs authentication
|
|
44
44
|
:param timeout: (optional) authenticated object
|
|
45
45
|
:returns: json file content
|
|
46
46
|
"""
|
|
47
47
|
if req_session is None:
|
|
48
|
-
req_session = requests.Session()
|
|
48
|
+
req_session = requests.sessions.Session()
|
|
49
49
|
try:
|
|
50
|
-
if not
|
|
51
|
-
|
|
50
|
+
if not url.lower().startswith("http"):
|
|
51
|
+
url = path_to_uri(os.path.abspath(url))
|
|
52
52
|
req_session.mount("file://", LocalFileAdapter())
|
|
53
53
|
|
|
54
54
|
headers = USER_AGENT
|
|
55
|
-
logger.debug(f"fetching {
|
|
55
|
+
logger.debug(f"fetching {url}")
|
|
56
56
|
res = req_session.get(
|
|
57
|
-
|
|
57
|
+
url,
|
|
58
58
|
headers=headers,
|
|
59
59
|
auth=auth,
|
|
60
60
|
timeout=timeout,
|
|
61
61
|
)
|
|
62
62
|
res.raise_for_status()
|
|
63
63
|
except requests.exceptions.Timeout as exc:
|
|
64
|
-
raise TimeOutError(exc, timeout=
|
|
64
|
+
raise TimeOutError(exc, timeout=timeout) from exc
|
|
65
65
|
except requests.exceptions.RequestException as exc:
|
|
66
|
-
raise RequestError.from_error(exc, f"Unable to fetch {
|
|
66
|
+
raise RequestError.from_error(exc, f"Unable to fetch {url}") from exc
|
|
67
67
|
else:
|
|
68
68
|
return res.json()
|
|
69
69
|
|
|
@@ -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
|
|
@@ -100,8 +100,8 @@ class LocalFileAdapter(requests.adapters.BaseAdapter):
|
|
|
100
100
|
) -> requests.Response:
|
|
101
101
|
"""Wraps a file, described in request, in a Response object.
|
|
102
102
|
|
|
103
|
-
:param
|
|
104
|
-
:param kwargs: (not used)
|
|
103
|
+
:param request: The PreparedRequest being "sent".
|
|
104
|
+
:param kwargs: (not used) additional arguments of the request
|
|
105
105
|
:returns: a Response object containing the file
|
|
106
106
|
"""
|
|
107
107
|
response = requests.Response()
|
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,231 @@
|
|
|
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 ZipFile, 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 open_s3_zipped_object(
|
|
82
|
+
bucket_name: str, key_name: str, client_s3: S3Client, partial: bool = True
|
|
83
|
+
) -> ZipFile:
|
|
84
|
+
"""
|
|
85
|
+
Open 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
|
+
:param partial: fetch partial data if only content info is needed
|
|
94
|
+
:returns: List of files in zip
|
|
95
|
+
"""
|
|
96
|
+
response = client_s3.head_object(Bucket=bucket_name, Key=key_name)
|
|
97
|
+
size = response["ContentLength"]
|
|
98
|
+
|
|
99
|
+
# End Of Central Directory bytes
|
|
100
|
+
eocd = fetch(bucket_name, key_name, size - 22, 22, client_s3)
|
|
101
|
+
|
|
102
|
+
# start offset and size of the central directory
|
|
103
|
+
cd_start = parse_int(eocd[16:20])
|
|
104
|
+
cd_size = parse_int(eocd[12:16])
|
|
105
|
+
|
|
106
|
+
# fetch central directory, append EOCD, and open as zipfile
|
|
107
|
+
cd = fetch(bucket_name, key_name, cd_start, cd_size, client_s3)
|
|
108
|
+
|
|
109
|
+
zip_data = (
|
|
110
|
+
cd + eocd if partial else fetch(bucket_name, key_name, 0, size, client_s3)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
zip = zipfile.ZipFile(io.BytesIO(zip_data))
|
|
114
|
+
|
|
115
|
+
return zip
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def list_files_in_s3_zipped_object(
|
|
119
|
+
bucket_name: str, key_name: str, client_s3: S3Client
|
|
120
|
+
) -> List[ZipInfo]:
|
|
121
|
+
"""
|
|
122
|
+
List files in s3 zipped object, without downloading it.
|
|
123
|
+
|
|
124
|
+
See https://stackoverflow.com/questions/41789176/how-to-count-files-inside-zip-in-aws-s3-without-downloading-it;
|
|
125
|
+
Based on https://stackoverflow.com/questions/51351000/read-zip-files-from-s3-without-downloading-the-entire-file
|
|
126
|
+
|
|
127
|
+
:param bucket_name: Bucket name of the object to fetch
|
|
128
|
+
:param key_name: Key name of the object to fetch
|
|
129
|
+
:param client_s3: s3 client used to fetch the object
|
|
130
|
+
:returns: List of files in zip
|
|
131
|
+
"""
|
|
132
|
+
with open_s3_zipped_object(bucket_name, key_name, client_s3) as zip_file:
|
|
133
|
+
logger.debug("Found %s files in %s" % (len(zip_file.filelist), key_name))
|
|
134
|
+
return zip_file.filelist
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def update_assets_from_s3(
|
|
138
|
+
product: EOProduct,
|
|
139
|
+
auth: AwsAuth,
|
|
140
|
+
s3_endpoint: Optional[str] = None,
|
|
141
|
+
content_url: Optional[str] = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Update ``EOProduct.assets`` using content listed in its ``remote_location`` or given
|
|
144
|
+
``content_url``.
|
|
145
|
+
|
|
146
|
+
If url points to a zipped archive, its content will also be be listed.
|
|
147
|
+
|
|
148
|
+
:param product: product to update
|
|
149
|
+
:param auth: Authentication plugin
|
|
150
|
+
:param s3_endpoint: s3 endpoint if not hosted on AWS
|
|
151
|
+
:param content_url: s3 URL pointing to the content that must be listed (defaults to
|
|
152
|
+
``product.remote_location`` if empty)
|
|
153
|
+
"""
|
|
154
|
+
required_creds = ["aws_access_key_id", "aws_secret_access_key"]
|
|
155
|
+
|
|
156
|
+
if content_url is None:
|
|
157
|
+
content_url = product.remote_location
|
|
158
|
+
|
|
159
|
+
bucket, prefix = get_bucket_name_and_prefix(content_url)
|
|
160
|
+
|
|
161
|
+
if bucket is None or prefix is None:
|
|
162
|
+
logger.debug(f"No s3 prefix could guessed from {content_url}")
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
auth_dict = auth.authenticate()
|
|
167
|
+
|
|
168
|
+
if not all(x in auth_dict for x in required_creds):
|
|
169
|
+
raise MisconfiguredError(
|
|
170
|
+
f"Incomplete credentials for {product.provider}, missing "
|
|
171
|
+
f"{[x for x in required_creds if x not in auth_dict]}"
|
|
172
|
+
)
|
|
173
|
+
if not getattr(auth, "s3_client", None):
|
|
174
|
+
auth.s3_client = boto3.client(
|
|
175
|
+
service_name="s3",
|
|
176
|
+
endpoint_url=s3_endpoint,
|
|
177
|
+
aws_access_key_id=auth_dict.get("aws_access_key_id"),
|
|
178
|
+
aws_secret_access_key=auth_dict.get("aws_secret_access_key"),
|
|
179
|
+
aws_session_token=auth_dict.get("aws_session_token"),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
logger.debug("Listing assets in %s", prefix)
|
|
183
|
+
|
|
184
|
+
if prefix.endswith(".zip"):
|
|
185
|
+
# List prefix zip content
|
|
186
|
+
assets_urls = [
|
|
187
|
+
f"zip+s3://{bucket}/{prefix}!{f.filename}"
|
|
188
|
+
for f in list_files_in_s3_zipped_object(bucket, prefix, auth.s3_client)
|
|
189
|
+
]
|
|
190
|
+
else:
|
|
191
|
+
# List files in prefix
|
|
192
|
+
assets_urls = [
|
|
193
|
+
f"s3://{bucket}/{obj['Key']}"
|
|
194
|
+
for obj in auth.s3_client.list_objects(
|
|
195
|
+
Bucket=bucket, Prefix=prefix, MaxKeys=300
|
|
196
|
+
).get("Contents", [])
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
for asset_url in assets_urls:
|
|
200
|
+
out_of_zip_url = asset_url.split("!")[-1]
|
|
201
|
+
key, roles = product.driver.guess_asset_key_and_roles(
|
|
202
|
+
out_of_zip_url, product
|
|
203
|
+
)
|
|
204
|
+
parsed_url = urlparse(out_of_zip_url)
|
|
205
|
+
title = os.path.basename(parsed_url.path)
|
|
206
|
+
|
|
207
|
+
if key and key not in product.assets:
|
|
208
|
+
product.assets[key] = {
|
|
209
|
+
"title": title,
|
|
210
|
+
"roles": roles,
|
|
211
|
+
"href": asset_url,
|
|
212
|
+
}
|
|
213
|
+
if mime_type := guess_file_type(asset_url):
|
|
214
|
+
product.assets[key]["type"] = mime_type
|
|
215
|
+
|
|
216
|
+
# sort assets
|
|
217
|
+
product.assets.data = dict(sorted(product.assets.data.items()))
|
|
218
|
+
|
|
219
|
+
# update driver
|
|
220
|
+
product.driver = product.get_driver()
|
|
221
|
+
|
|
222
|
+
except botocore.exceptions.ClientError as e:
|
|
223
|
+
if hasattr(auth.config, "auth_error_code") and str(
|
|
224
|
+
auth.config.auth_error_code
|
|
225
|
+
) in str(e):
|
|
226
|
+
raise AuthenticationError(
|
|
227
|
+
f"Authentication failed on {s3_endpoint} s3"
|
|
228
|
+
) from e
|
|
229
|
+
raise NotAvailableError(
|
|
230
|
+
f"assets for product {prefix} could not be found"
|
|
231
|
+
) 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
|