eodag 3.6.0__py3-none-any.whl → 3.7.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 +0 -14
- eodag/api/product/metadata_mapping.py +20 -3
- eodag/cli.py +6 -3
- eodag/config.py +5 -0
- eodag/plugins/authentication/openid_connect.py +1 -2
- eodag/plugins/download/aws.py +145 -178
- eodag/plugins/download/base.py +3 -2
- eodag/plugins/download/creodias_s3.py +10 -5
- eodag/plugins/download/http.py +14 -6
- eodag/plugins/download/s3rest.py +1 -2
- eodag/plugins/manager.py +1 -1
- eodag/plugins/search/base.py +34 -4
- eodag/plugins/search/build_search_result.py +3 -0
- eodag/plugins/search/cop_marine.py +2 -0
- eodag/plugins/search/qssearch.py +44 -25
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +30 -153
- eodag/resources/providers.yml +48 -325
- eodag/resources/stac.yml +1 -2
- eodag/resources/user_conf_template.yml +0 -11
- eodag/rest/core.py +5 -9
- eodag/utils/__init__.py +41 -27
- eodag/utils/exceptions.py +4 -0
- eodag/utils/s3.py +605 -65
- {eodag-3.6.0.dist-info → eodag-3.7.0.dist-info}/METADATA +7 -8
- {eodag-3.6.0.dist-info → eodag-3.7.0.dist-info}/RECORD +30 -30
- {eodag-3.6.0.dist-info → eodag-3.7.0.dist-info}/WHEEL +0 -0
- {eodag-3.6.0.dist-info → eodag-3.7.0.dist-info}/entry_points.txt +0 -0
- {eodag-3.6.0.dist-info → eodag-3.7.0.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.6.0.dist-info → eodag-3.7.0.dist-info}/top_level.txt +0 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
16
|
# See the License for the specific language governing permissions and
|
|
17
17
|
# limitations under the License.
|
|
18
|
-
from typing import Optional
|
|
18
|
+
from typing import TYPE_CHECKING, Optional
|
|
19
19
|
|
|
20
20
|
import boto3
|
|
21
21
|
from botocore.exceptions import ClientError
|
|
@@ -24,6 +24,9 @@ from eodag import EOProduct
|
|
|
24
24
|
from eodag.plugins.download.aws import AwsDownload
|
|
25
25
|
from eodag.utils.exceptions import MisconfiguredError
|
|
26
26
|
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from mypy_boto3_s3.service_resource import S3ServiceResource
|
|
29
|
+
|
|
27
30
|
|
|
28
31
|
class CreodiasS3Download(AwsDownload):
|
|
29
32
|
"""
|
|
@@ -60,19 +63,21 @@ class CreodiasS3Download(AwsDownload):
|
|
|
60
63
|
)
|
|
61
64
|
|
|
62
65
|
s3_session = boto3.session.Session(**auth_dict)
|
|
63
|
-
s3_resource = s3_session.resource(
|
|
66
|
+
s3_resource: S3ServiceResource = s3_session.resource(
|
|
64
67
|
"s3", endpoint_url=getattr(self.config, "s3_endpoint", None)
|
|
65
68
|
)
|
|
66
69
|
objects = s3_resource.Bucket(bucket_name).objects.filter()
|
|
67
70
|
list(objects.filter(Prefix=prefix).limit(1))
|
|
68
71
|
self.s3_session = s3_session
|
|
72
|
+
self.s3_resource = s3_resource
|
|
69
73
|
return objects
|
|
70
74
|
|
|
71
75
|
def _get_bucket_names_and_prefixes(
|
|
72
76
|
self,
|
|
73
77
|
product: EOProduct,
|
|
74
|
-
asset_filter: Optional[str]
|
|
75
|
-
ignore_assets:
|
|
78
|
+
asset_filter: Optional[str],
|
|
79
|
+
ignore_assets: bool,
|
|
80
|
+
complementary_url_keys: list[str],
|
|
76
81
|
) -> list[tuple[str, Optional[str]]]:
|
|
77
82
|
"""
|
|
78
83
|
Retrieves the bucket names and path prefixes for the assets
|
|
@@ -85,7 +90,7 @@ class CreodiasS3Download(AwsDownload):
|
|
|
85
90
|
# if assets are defined, use them instead of scanning product.location
|
|
86
91
|
if len(product.assets) > 0 and not ignore_assets:
|
|
87
92
|
bucket_names_and_prefixes = super()._get_bucket_names_and_prefixes(
|
|
88
|
-
product, asset_filter, ignore_assets
|
|
93
|
+
product, asset_filter, ignore_assets, complementary_url_keys
|
|
89
94
|
)
|
|
90
95
|
else:
|
|
91
96
|
# if no assets are given, use productIdentifier to get S3 path for download
|
eodag/plugins/download/http.py
CHANGED
|
@@ -28,7 +28,16 @@ from email.message import Message
|
|
|
28
28
|
from itertools import chain
|
|
29
29
|
from json import JSONDecodeError
|
|
30
30
|
from pathlib import Path
|
|
31
|
-
from typing import
|
|
31
|
+
from typing import (
|
|
32
|
+
TYPE_CHECKING,
|
|
33
|
+
Any,
|
|
34
|
+
Iterator,
|
|
35
|
+
Literal,
|
|
36
|
+
Optional,
|
|
37
|
+
TypedDict,
|
|
38
|
+
Union,
|
|
39
|
+
cast,
|
|
40
|
+
)
|
|
32
41
|
from urllib.parse import parse_qs, urlparse
|
|
33
42
|
|
|
34
43
|
import geojson
|
|
@@ -745,7 +754,8 @@ class HTTPDownload(Download):
|
|
|
745
754
|
self,
|
|
746
755
|
product: EOProduct,
|
|
747
756
|
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
748
|
-
|
|
757
|
+
byte_range: tuple[Optional[int], Optional[int]] = (None, None),
|
|
758
|
+
compress: Literal["zip", "raw", "auto"] = "auto",
|
|
749
759
|
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
750
760
|
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
751
761
|
**kwargs: Unpack[DownloadConf],
|
|
@@ -779,7 +789,7 @@ class HTTPDownload(Download):
|
|
|
779
789
|
chunks_tuples = self._stream_download_assets(
|
|
780
790
|
product,
|
|
781
791
|
auth,
|
|
782
|
-
|
|
792
|
+
None,
|
|
783
793
|
assets_values=assets_values,
|
|
784
794
|
**kwargs,
|
|
785
795
|
)
|
|
@@ -825,9 +835,7 @@ class HTTPDownload(Download):
|
|
|
825
835
|
else:
|
|
826
836
|
pass
|
|
827
837
|
|
|
828
|
-
chunk_iterator = self._stream_download(
|
|
829
|
-
product, auth, progress_callback, **kwargs
|
|
830
|
-
)
|
|
838
|
+
chunk_iterator = self._stream_download(product, auth, None, **kwargs)
|
|
831
839
|
|
|
832
840
|
# start reading chunks to set product.headers
|
|
833
841
|
try:
|
eodag/plugins/download/s3rest.py
CHANGED
|
@@ -21,6 +21,7 @@ import logging
|
|
|
21
21
|
import os
|
|
22
22
|
import os.path
|
|
23
23
|
from typing import TYPE_CHECKING, Optional, Union
|
|
24
|
+
from urllib.parse import unquote, urljoin
|
|
24
25
|
from xml.dom import minidom
|
|
25
26
|
from xml.parsers.expat import ExpatError
|
|
26
27
|
|
|
@@ -40,8 +41,6 @@ from eodag.utils import (
|
|
|
40
41
|
ProgressCallback,
|
|
41
42
|
get_bucket_name_and_prefix,
|
|
42
43
|
path_to_uri,
|
|
43
|
-
unquote,
|
|
44
|
-
urljoin,
|
|
45
44
|
)
|
|
46
45
|
from eodag.utils.exceptions import (
|
|
47
46
|
AuthenticationError,
|
eodag/plugins/manager.py
CHANGED
|
@@ -109,7 +109,7 @@ class PluginManager:
|
|
|
109
109
|
logger.warning("Reason:\n%s", tb.format_exc())
|
|
110
110
|
logger.warning(
|
|
111
111
|
"Check that the plugin module (%s) is importable",
|
|
112
|
-
entry_point.
|
|
112
|
+
entry_point.name,
|
|
113
113
|
)
|
|
114
114
|
if entry_point.dist and entry_point.dist.name != "eodag":
|
|
115
115
|
# use plugin providers if any
|
eodag/plugins/search/base.py
CHANGED
|
@@ -25,6 +25,7 @@ from pydantic.fields import Field, FieldInfo
|
|
|
25
25
|
|
|
26
26
|
from eodag.api.product.metadata_mapping import (
|
|
27
27
|
DEFAULT_METADATA_MAPPING,
|
|
28
|
+
NOT_AVAILABLE,
|
|
28
29
|
NOT_MAPPED,
|
|
29
30
|
mtd_cfg_as_conversion_and_querypath,
|
|
30
31
|
)
|
|
@@ -38,6 +39,7 @@ from eodag.utils import (
|
|
|
38
39
|
copy_deepcopy,
|
|
39
40
|
deepcopy,
|
|
40
41
|
format_dict_items,
|
|
42
|
+
string_to_jsonpath,
|
|
41
43
|
update_nested_dict,
|
|
42
44
|
)
|
|
43
45
|
from eodag.utils.exceptions import ValidationError
|
|
@@ -158,10 +160,6 @@ class Search(PluginTopic):
|
|
|
158
160
|
:returns: The product type definition parameters
|
|
159
161
|
"""
|
|
160
162
|
if product_type in self.config.products.keys():
|
|
161
|
-
logger.debug(
|
|
162
|
-
"Getting provider product type definition parameters for %s",
|
|
163
|
-
product_type,
|
|
164
|
-
)
|
|
165
163
|
return self.config.products[product_type]
|
|
166
164
|
elif GENERIC_PRODUCT_TYPE in self.config.products.keys():
|
|
167
165
|
logger.debug(
|
|
@@ -439,3 +437,35 @@ class Search(PluginTopic):
|
|
|
439
437
|
):
|
|
440
438
|
queryables[k] = v
|
|
441
439
|
return queryables
|
|
440
|
+
|
|
441
|
+
def get_assets_from_mapping(self, provider_item: dict[str, Any]) -> dict[str, Any]:
|
|
442
|
+
"""
|
|
443
|
+
Create assets based on the assets_mapping in the provider's config
|
|
444
|
+
and an item returned by the provider
|
|
445
|
+
|
|
446
|
+
:param provider_item: dict of item properties returned by the provider
|
|
447
|
+
:returns: dict containing the asset metadata
|
|
448
|
+
"""
|
|
449
|
+
assets_mapping = getattr(self.config, "assets_mapping", None)
|
|
450
|
+
if not assets_mapping:
|
|
451
|
+
return {}
|
|
452
|
+
assets = {}
|
|
453
|
+
for key, values in assets_mapping.items():
|
|
454
|
+
asset_href = values.get("href")
|
|
455
|
+
if not asset_href:
|
|
456
|
+
logger.warning(
|
|
457
|
+
"asset mapping %s skipped because no href is available", key
|
|
458
|
+
)
|
|
459
|
+
continue
|
|
460
|
+
json_url_path = string_to_jsonpath(asset_href)
|
|
461
|
+
if isinstance(json_url_path, str):
|
|
462
|
+
url_path = json_url_path
|
|
463
|
+
else:
|
|
464
|
+
url_match = json_url_path.find(provider_item)
|
|
465
|
+
if len(url_match) == 1:
|
|
466
|
+
url_path = url_match[0].value
|
|
467
|
+
else:
|
|
468
|
+
url_path = NOT_AVAILABLE
|
|
469
|
+
assets[key] = deepcopy(values)
|
|
470
|
+
assets[key]["href"] = url_path
|
|
471
|
+
return assets
|
|
@@ -94,6 +94,7 @@ ECMWF_KEYWORDS = {
|
|
|
94
94
|
"fcperiod",
|
|
95
95
|
"fieldset",
|
|
96
96
|
"filter",
|
|
97
|
+
"feature",
|
|
97
98
|
"format",
|
|
98
99
|
"frame",
|
|
99
100
|
"frequency",
|
|
@@ -548,7 +549,9 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
548
549
|
:param params: Search parameters to be preprocessed.
|
|
549
550
|
:param product_type: (optional) product type id
|
|
550
551
|
"""
|
|
552
|
+
|
|
551
553
|
_dc_qs = params.get("_dc_qs")
|
|
554
|
+
|
|
552
555
|
if _dc_qs is not None:
|
|
553
556
|
# if available, update search params using datacube query-string
|
|
554
557
|
_dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
|
|
@@ -284,6 +284,8 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
284
284
|
"type": "application/x-netcdf",
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
additional_assets = self.get_assets_from_mapping(dataset_item)
|
|
288
|
+
assets.update(additional_assets)
|
|
287
289
|
product = EOProduct(self.provider, properties, productType=product_type)
|
|
288
290
|
# use product_type_config as default properties
|
|
289
291
|
product_type_config = getattr(self.config, "product_type_config", {})
|
eodag/plugins/search/qssearch.py
CHANGED
|
@@ -35,9 +35,11 @@ from typing import (
|
|
|
35
35
|
from urllib.error import URLError
|
|
36
36
|
from urllib.parse import (
|
|
37
37
|
parse_qsl,
|
|
38
|
+
quote,
|
|
38
39
|
quote_plus,
|
|
39
40
|
unquote,
|
|
40
41
|
unquote_plus,
|
|
42
|
+
urlencode,
|
|
41
43
|
urlparse,
|
|
42
44
|
urlunparse,
|
|
43
45
|
)
|
|
@@ -86,10 +88,8 @@ from eodag.utils import (
|
|
|
86
88
|
dict_items_recursive_apply,
|
|
87
89
|
format_dict_items,
|
|
88
90
|
get_ssl_context,
|
|
89
|
-
quote,
|
|
90
91
|
string_to_jsonpath,
|
|
91
92
|
update_nested_dict,
|
|
92
|
-
urlencode,
|
|
93
93
|
)
|
|
94
94
|
from eodag.utils.exceptions import (
|
|
95
95
|
AuthenticationError,
|
|
@@ -386,36 +386,48 @@ class QueryStringSearch(Search):
|
|
|
386
386
|
|
|
387
387
|
# parse jsonpath on init: product type specific metadata-mapping
|
|
388
388
|
for product_type in self.config.products.keys():
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
)
|
|
389
|
+
|
|
390
|
+
product_type_metadata_mapping = {}
|
|
391
|
+
# product-type specific metadata-mapping
|
|
392
|
+
if any(
|
|
393
|
+
mm in self.config.products[product_type].keys()
|
|
394
|
+
for mm in ("metadata_mapping", "metadata_mapping_from_product")
|
|
395
|
+
):
|
|
395
396
|
# Complete and ready to use product type specific metadata-mapping
|
|
396
397
|
product_type_metadata_mapping = deepcopy(self.config.metadata_mapping)
|
|
397
398
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
),
|
|
399
|
+
# metadata_mapping from another product
|
|
400
|
+
if other_product_for_mapping := self.config.products[product_type].get(
|
|
401
|
+
"metadata_mapping_from_product"
|
|
402
|
+
):
|
|
403
|
+
other_product_type_def_params = self.get_product_type_def_params(
|
|
404
|
+
other_product_for_mapping,
|
|
405
405
|
)
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
406
|
+
# parse mapping to apply
|
|
407
|
+
if other_product_type_mtd_mapping := other_product_type_def_params.get(
|
|
408
|
+
"metadata_mapping", {}
|
|
409
|
+
):
|
|
410
410
|
other_product_type_mtd_mapping = (
|
|
411
411
|
mtd_cfg_as_conversion_and_querypath(
|
|
412
412
|
other_product_type_def_params.get("metadata_mapping", {})
|
|
413
413
|
)
|
|
414
414
|
)
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
else:
|
|
416
|
+
msg = f"Cannot reuse empty metadata_mapping from {other_product_for_mapping} for {product_type}"
|
|
417
|
+
raise MisconfiguredError(msg)
|
|
418
|
+
# update mapping
|
|
419
|
+
for metadata, mapping in other_product_type_mtd_mapping.items():
|
|
420
|
+
product_type_metadata_mapping.pop(metadata, None)
|
|
421
|
+
product_type_metadata_mapping[metadata] = mapping
|
|
422
|
+
|
|
423
|
+
# metadata_mapping from current product
|
|
424
|
+
if "metadata_mapping" in self.config.products[product_type].keys():
|
|
425
|
+
# parse mapping to apply
|
|
426
|
+
self.config.products[product_type][
|
|
427
|
+
"metadata_mapping"
|
|
428
|
+
] = mtd_cfg_as_conversion_and_querypath(
|
|
429
|
+
self.config.products[product_type]["metadata_mapping"]
|
|
430
|
+
)
|
|
419
431
|
|
|
420
432
|
# from current product, updated mapping at the end
|
|
421
433
|
for metadata, mapping in self.config.products[product_type][
|
|
@@ -424,6 +436,7 @@ class QueryStringSearch(Search):
|
|
|
424
436
|
product_type_metadata_mapping.pop(metadata, None)
|
|
425
437
|
product_type_metadata_mapping[metadata] = mapping
|
|
426
438
|
|
|
439
|
+
if product_type_metadata_mapping:
|
|
427
440
|
self.config.products[product_type][
|
|
428
441
|
"metadata_mapping"
|
|
429
442
|
] = product_type_metadata_mapping
|
|
@@ -1090,6 +1103,8 @@ class QueryStringSearch(Search):
|
|
|
1090
1103
|
product.properties = dict(
|
|
1091
1104
|
getattr(self.config, "product_type_config", {}), **product.properties
|
|
1092
1105
|
)
|
|
1106
|
+
additional_assets = self.get_assets_from_mapping(result)
|
|
1107
|
+
product.assets.update(additional_assets)
|
|
1093
1108
|
# move assets from properties to product's attr, normalize keys & roles
|
|
1094
1109
|
for key, asset in product.properties.pop("assets", {}).items():
|
|
1095
1110
|
norm_key, asset["roles"] = product.driver.guess_asset_key_and_roles(
|
|
@@ -1481,13 +1496,17 @@ class PostJsonSearch(QueryStringSearch):
|
|
|
1481
1496
|
)
|
|
1482
1497
|
|
|
1483
1498
|
# Add to the query, the queryable parameters set in the provider product type definition
|
|
1499
|
+
product_type_metadata_mapping = {
|
|
1500
|
+
**getattr(self.config, "metadata_mapping", {}),
|
|
1501
|
+
**prep.product_type_def_params.get("metadata_mapping", {}),
|
|
1502
|
+
}
|
|
1484
1503
|
keywords.update(
|
|
1485
1504
|
{
|
|
1486
1505
|
k: v
|
|
1487
1506
|
for k, v in prep.product_type_def_params.items()
|
|
1488
1507
|
if k not in keywords.keys()
|
|
1489
|
-
and k in
|
|
1490
|
-
and isinstance(
|
|
1508
|
+
and k in product_type_metadata_mapping.keys()
|
|
1509
|
+
and isinstance(product_type_metadata_mapping[k], list)
|
|
1491
1510
|
}
|
|
1492
1511
|
)
|
|
1493
1512
|
|