eodag 2.12.0__py3-none-any.whl → 3.0.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/__init__.py +6 -8
- eodag/api/core.py +654 -538
- eodag/api/product/__init__.py +12 -2
- eodag/api/product/_assets.py +59 -16
- eodag/api/product/_product.py +100 -93
- eodag/api/product/drivers/__init__.py +7 -2
- eodag/api/product/drivers/base.py +0 -3
- eodag/api/product/metadata_mapping.py +192 -96
- eodag/api/search_result.py +69 -10
- eodag/cli.py +55 -25
- eodag/config.py +391 -116
- eodag/plugins/apis/base.py +11 -165
- eodag/plugins/apis/ecmwf.py +36 -25
- eodag/plugins/apis/usgs.py +80 -35
- eodag/plugins/authentication/aws_auth.py +13 -4
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +17 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +268 -49
- eodag/plugins/authentication/qsauth.py +4 -1
- eodag/plugins/authentication/sas_auth.py +9 -2
- eodag/plugins/authentication/token.py +98 -47
- eodag/plugins/authentication/token_exchange.py +122 -0
- eodag/plugins/crunch/base.py +3 -1
- eodag/plugins/crunch/filter_date.py +3 -9
- eodag/plugins/crunch/filter_latest_intersect.py +0 -3
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
- eodag/plugins/crunch/filter_overlap.py +4 -8
- eodag/plugins/crunch/filter_property.py +5 -11
- eodag/plugins/download/aws.py +149 -185
- eodag/plugins/download/base.py +88 -97
- eodag/plugins/download/creodias_s3.py +1 -1
- eodag/plugins/download/http.py +638 -310
- eodag/plugins/download/s3rest.py +47 -45
- eodag/plugins/manager.py +228 -88
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +239 -30
- eodag/plugins/search/build_search_result.py +382 -37
- eodag/plugins/search/cop_marine.py +441 -0
- eodag/plugins/search/creodias_s3.py +25 -20
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +61 -30
- eodag/plugins/search/qssearch.py +713 -255
- eodag/plugins/search/static_stac_search.py +106 -40
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1921 -34
- eodag/resources/providers.yml +4091 -3655
- eodag/resources/stac.yml +50 -216
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +89 -32
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +26 -0
- eodag/rest/core.py +735 -0
- eodag/rest/errors.py +178 -0
- eodag/rest/server.py +264 -431
- eodag/rest/stac.py +442 -836
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +238 -47
- eodag/rest/types/queryables.py +164 -0
- eodag/rest/types/stac_search.py +273 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +64 -0
- eodag/types/__init__.py +106 -10
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +40 -0
- eodag/types/search_args.py +57 -7
- eodag/types/whoosh.py +79 -0
- eodag/utils/__init__.py +110 -91
- eodag/utils/constraints.py +37 -45
- eodag/utils/exceptions.py +39 -22
- eodag/utils/import_system.py +0 -4
- eodag/utils/logging.py +37 -80
- eodag/utils/notebook.py +4 -4
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +128 -0
- eodag/utils/rest.py +100 -0
- eodag/utils/stac_reader.py +93 -21
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
- eodag-3.0.0.dist-info/RECORD +109 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/types/stac_queryables.py +0 -134
- eodag/rest/utils.py +0 -1133
- eodag-2.12.0.dist-info/RECORD +0 -94
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
eodag/plugins/apis/base.py
CHANGED
|
@@ -17,193 +17,39 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
import
|
|
21
|
-
from
|
|
20
|
+
from eodag.plugins.download.base import Download
|
|
21
|
+
from eodag.plugins.search.base import Search
|
|
22
22
|
|
|
23
|
-
from pydantic.fields import Field, FieldInfo
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
from eodag.api.product import EOProduct
|
|
27
|
-
from eodag.api.search_result import SearchResult
|
|
28
|
-
from eodag.config import PluginConfig
|
|
29
|
-
from eodag.utils import DownloadedCallback, ProgressCallback
|
|
30
|
-
|
|
31
|
-
from eodag.plugins.base import PluginTopic
|
|
32
|
-
from eodag.utils import (
|
|
33
|
-
DEFAULT_DOWNLOAD_TIMEOUT,
|
|
34
|
-
DEFAULT_DOWNLOAD_WAIT,
|
|
35
|
-
DEFAULT_ITEMS_PER_PAGE,
|
|
36
|
-
DEFAULT_PAGE,
|
|
37
|
-
Annotated,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
logger = logging.getLogger("eodag.apis.base")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class Api(PluginTopic):
|
|
24
|
+
class Api(Search, Download):
|
|
44
25
|
"""Plugins API Base plugin
|
|
45
26
|
|
|
46
|
-
An Api plugin
|
|
27
|
+
An Api plugin inherit the methods from Search and Download plugins.
|
|
47
28
|
|
|
29
|
+
There are three methods that it must implement:
|
|
48
30
|
- ``query``: search for products
|
|
49
31
|
- ``download``: download a single :class:`~eodag.api.product._product.EOProduct`
|
|
50
32
|
- ``download_all``: download multiple products from a :class:`~eodag.api.search_result.SearchResult`
|
|
51
33
|
|
|
52
34
|
The download methods must:
|
|
53
35
|
|
|
54
|
-
- download data in the ``
|
|
36
|
+
- download data in the ``output_dir`` folder defined in the plugin's
|
|
55
37
|
configuration or passed through kwargs
|
|
56
38
|
- extract products from their archive (if relevant) if ``extract`` is set to True
|
|
57
39
|
(True by default)
|
|
58
|
-
- save a product in an archive/directory (in ``
|
|
40
|
+
- save a product in an archive/directory (in ``output_dir``) whose name must be
|
|
59
41
|
the product's ``title`` property
|
|
60
42
|
- update the product's ``location`` attribute once its data is downloaded (and
|
|
61
43
|
eventually after it's extracted) to the product's location given as a file URI
|
|
62
44
|
(e.g. 'file:///tmp/product_folder' on Linux or
|
|
63
45
|
'file:///C:/Users/username/AppData/LOcal/Temp' on Windows)
|
|
64
|
-
- save a *record* file in the directory ``
|
|
65
|
-
is built on the MD5 hash of the product's ``
|
|
66
|
-
(``hashlib.md5(
|
|
67
|
-
the product's ``remote_location`` attribute itself.
|
|
46
|
+
- save a *record* file in the directory ``output_dir/.downloaded`` whose name
|
|
47
|
+
is built on the MD5 hash of the product's ``product_type`` and ``properties['id']``
|
|
48
|
+
attributes (``hashlib.md5((product.product_type+"-"+product.properties['id']).encode("utf-8")).hexdigest()``)
|
|
49
|
+
and whose content is the product's ``remote_location`` attribute itself.
|
|
68
50
|
- not try to download a product whose ``location`` attribute already points to an
|
|
69
51
|
existing file/directory
|
|
70
52
|
- not try to download a product if its *record* file exists as long as the expected
|
|
71
53
|
product's file/directory. If the *record* file only is found, it must be deleted
|
|
72
54
|
(it certainly indicates that the download didn't complete)
|
|
73
55
|
"""
|
|
74
|
-
|
|
75
|
-
def clear(self) -> None:
|
|
76
|
-
"""Method used to clear a search context between two searches."""
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
|
-
def query(
|
|
80
|
-
self,
|
|
81
|
-
product_type: Optional[str] = None,
|
|
82
|
-
items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
|
|
83
|
-
page: int = DEFAULT_PAGE,
|
|
84
|
-
count: bool = True,
|
|
85
|
-
**kwargs: Any,
|
|
86
|
-
) -> Tuple[List[EOProduct], Optional[int]]:
|
|
87
|
-
"""Implementation of how the products must be searched goes here.
|
|
88
|
-
|
|
89
|
-
This method must return a tuple with (1) a list of EOProduct instances (see eodag.api.product module)
|
|
90
|
-
which will be processed by a Download plugin (2) and the total number of products matching
|
|
91
|
-
the search criteria. If ``count`` is False, the second element returned must be ``None``.
|
|
92
|
-
"""
|
|
93
|
-
raise NotImplementedError("A Api plugin must implement a method named query")
|
|
94
|
-
|
|
95
|
-
def discover_product_types(self) -> Optional[Dict[str, Any]]:
|
|
96
|
-
"""Fetch product types list from provider using `discover_product_types` conf"""
|
|
97
|
-
return None
|
|
98
|
-
|
|
99
|
-
def discover_queryables(
|
|
100
|
-
self, **kwargs: Any
|
|
101
|
-
) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
|
|
102
|
-
"""Fetch queryables list from provider using `discover_queryables` conf
|
|
103
|
-
|
|
104
|
-
:param kwargs: additional filters for queryables (`productType` and other search
|
|
105
|
-
arguments)
|
|
106
|
-
:type kwargs: Any
|
|
107
|
-
:returns: fetched queryable parameters dict
|
|
108
|
-
:rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
|
|
109
|
-
"""
|
|
110
|
-
return None
|
|
111
|
-
|
|
112
|
-
def get_defaults_as_queryables(
|
|
113
|
-
self, product_type: str
|
|
114
|
-
) -> Dict[str, Annotated[Any, FieldInfo]]:
|
|
115
|
-
"""
|
|
116
|
-
Return given product type defaut settings as queryables
|
|
117
|
-
|
|
118
|
-
:param product_type: given product type
|
|
119
|
-
:type product_type: str
|
|
120
|
-
:returns: queryable parameters dict
|
|
121
|
-
:rtype: Dict[str, Annotated[Any, FieldInfo]]
|
|
122
|
-
"""
|
|
123
|
-
defaults = self.config.products.get(product_type, {})
|
|
124
|
-
queryables = {}
|
|
125
|
-
for parameter, value in defaults.items():
|
|
126
|
-
queryables[parameter] = Annotated[type(value), Field(default=value)]
|
|
127
|
-
return queryables
|
|
128
|
-
|
|
129
|
-
def download(
|
|
130
|
-
self,
|
|
131
|
-
product: EOProduct,
|
|
132
|
-
auth: Optional[PluginConfig] = None,
|
|
133
|
-
progress_callback: Optional[ProgressCallback] = None,
|
|
134
|
-
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
135
|
-
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
136
|
-
**kwargs: Any,
|
|
137
|
-
) -> Optional[str]:
|
|
138
|
-
"""
|
|
139
|
-
Base download method. Not available, it must be defined for each plugin.
|
|
140
|
-
|
|
141
|
-
:param product: The EO product to download
|
|
142
|
-
:type product: :class:`~eodag.api.product._product.EOProduct`
|
|
143
|
-
:param auth: (optional) The configuration of a plugin of type Authentication
|
|
144
|
-
:type auth: :class:`~eodag.config.PluginConfig`
|
|
145
|
-
:param progress_callback: (optional) A progress callback
|
|
146
|
-
:type progress_callback: :class:`~eodag.utils.ProgressCallback`
|
|
147
|
-
:param wait: (optional) If download fails, wait time in minutes between two download tries
|
|
148
|
-
:type wait: int
|
|
149
|
-
:param timeout: (optional) If download fails, maximum time in minutes before stop retrying
|
|
150
|
-
to download
|
|
151
|
-
:type timeout: int
|
|
152
|
-
:param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool)
|
|
153
|
-
and `dl_url_params` (dict) can be provided as additional kwargs
|
|
154
|
-
and will override any other values defined in a configuration
|
|
155
|
-
file or with environment variables.
|
|
156
|
-
:type kwargs: Union[str, bool, dict]
|
|
157
|
-
:returns: The absolute path to the downloaded product in the local filesystem
|
|
158
|
-
(e.g. '/tmp/product.zip' on Linux or
|
|
159
|
-
'C:\\Users\\username\\AppData\\Local\\Temp\\product.zip' on Windows)
|
|
160
|
-
:rtype: str
|
|
161
|
-
"""
|
|
162
|
-
raise NotImplementedError(
|
|
163
|
-
"An Api plugin must implement a method named download"
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
def download_all(
|
|
167
|
-
self,
|
|
168
|
-
products: SearchResult,
|
|
169
|
-
auth: Optional[PluginConfig] = None,
|
|
170
|
-
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
171
|
-
progress_callback: Optional[ProgressCallback] = None,
|
|
172
|
-
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
173
|
-
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
174
|
-
**kwargs: Any,
|
|
175
|
-
) -> List[str]:
|
|
176
|
-
"""
|
|
177
|
-
Base download_all method.
|
|
178
|
-
|
|
179
|
-
:param products: Products to download
|
|
180
|
-
:type products: :class:`~eodag.api.search_result.SearchResult`
|
|
181
|
-
:param auth: (optional) The configuration of a plugin of type Authentication
|
|
182
|
-
:type auth: :class:`~eodag.config.PluginConfig`
|
|
183
|
-
:param downloaded_callback: (optional) A method or a callable object which takes
|
|
184
|
-
as parameter the ``product``. You can use the base class
|
|
185
|
-
:class:`~eodag.api.product.DownloadedCallback` and override
|
|
186
|
-
its ``__call__`` method. Will be called each time a product
|
|
187
|
-
finishes downloading
|
|
188
|
-
:type downloaded_callback: Callable[[:class:`~eodag.api.product._product.EOProduct`], None]
|
|
189
|
-
or None
|
|
190
|
-
:param progress_callback: (optional) A progress callback
|
|
191
|
-
:type progress_callback: :class:`~eodag.utils.ProgressCallback`
|
|
192
|
-
:param wait: (optional) If download fails, wait time in minutes between two download tries
|
|
193
|
-
:type wait: int
|
|
194
|
-
:param timeout: (optional) If download fails, maximum time in minutes before stop retrying
|
|
195
|
-
to download
|
|
196
|
-
:type timeout: int
|
|
197
|
-
:param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool)
|
|
198
|
-
and `dl_url_params` (dict) can be provided as additional kwargs
|
|
199
|
-
and will override any other values defined in a configuration
|
|
200
|
-
file or with environment variables.
|
|
201
|
-
:type kwargs: Union[str, bool, dict]
|
|
202
|
-
:returns: List of absolute paths to the downloaded products in the local
|
|
203
|
-
filesystem (e.g. ``['/tmp/product.zip']`` on Linux or
|
|
204
|
-
``['C:\\Users\\username\\AppData\\Local\\Temp\\product.zip']`` on Windows)
|
|
205
|
-
:rtype: list
|
|
206
|
-
"""
|
|
207
|
-
raise NotImplementedError(
|
|
208
|
-
"A Api plugin must implement a method named download_all"
|
|
209
|
-
)
|
eodag/plugins/apis/ecmwf.py
CHANGED
|
@@ -18,42 +18,47 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
-
|
|
22
|
-
from
|
|
21
|
+
import os
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
23
24
|
|
|
24
25
|
import geojson
|
|
25
26
|
from ecmwfapi import ECMWFDataServer, ECMWFService
|
|
26
27
|
from ecmwfapi.api import APIException, Connection, get_apikey_values
|
|
27
28
|
|
|
28
29
|
from eodag.plugins.apis.base import Api
|
|
29
|
-
from eodag.plugins.
|
|
30
|
+
from eodag.plugins.search import PreparedSearch
|
|
30
31
|
from eodag.plugins.search.base import Search
|
|
31
32
|
from eodag.plugins.search.build_search_result import BuildPostSearchResult
|
|
32
|
-
from eodag.rest.stac import DEFAULT_MISSION_START_DATE
|
|
33
33
|
from eodag.utils import (
|
|
34
34
|
DEFAULT_DOWNLOAD_TIMEOUT,
|
|
35
35
|
DEFAULT_DOWNLOAD_WAIT,
|
|
36
|
-
|
|
37
|
-
DEFAULT_PAGE,
|
|
36
|
+
DEFAULT_MISSION_START_DATE,
|
|
38
37
|
get_geometry_from_various,
|
|
39
38
|
path_to_uri,
|
|
39
|
+
sanitize,
|
|
40
40
|
urlsplit,
|
|
41
41
|
)
|
|
42
42
|
from eodag.utils.exceptions import AuthenticationError, DownloadError
|
|
43
43
|
from eodag.utils.logging import get_logging_verbose
|
|
44
44
|
|
|
45
45
|
if TYPE_CHECKING:
|
|
46
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
47
|
+
|
|
48
|
+
from requests.auth import AuthBase
|
|
49
|
+
|
|
46
50
|
from eodag.api.product import EOProduct
|
|
47
51
|
from eodag.api.search_result import SearchResult
|
|
48
52
|
from eodag.config import PluginConfig
|
|
49
|
-
from eodag.
|
|
53
|
+
from eodag.types.download_args import DownloadConf
|
|
54
|
+
from eodag.utils import DownloadedCallback, ProgressCallback, Unpack
|
|
50
55
|
|
|
51
56
|
logger = logging.getLogger("eodag.apis.ecmwf")
|
|
52
57
|
|
|
53
58
|
ECMWF_MARS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
|
|
54
59
|
|
|
55
60
|
|
|
56
|
-
class EcmwfApi(
|
|
61
|
+
class EcmwfApi(Api, BuildPostSearchResult):
|
|
57
62
|
"""A plugin that enables to build download-request and download data on ECMWF MARS.
|
|
58
63
|
|
|
59
64
|
Builds a single ready-to-download :class:`~eodag.api.product._product.EOProduct`
|
|
@@ -84,10 +89,7 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
|
|
|
84
89
|
|
|
85
90
|
def query(
|
|
86
91
|
self,
|
|
87
|
-
|
|
88
|
-
items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
|
|
89
|
-
page: int = DEFAULT_PAGE,
|
|
90
|
-
count: bool = True,
|
|
92
|
+
prep: PreparedSearch = PreparedSearch(),
|
|
91
93
|
**kwargs: Any,
|
|
92
94
|
) -> Tuple[List[EOProduct], Optional[int]]:
|
|
93
95
|
"""Build ready-to-download SearchResult"""
|
|
@@ -112,7 +114,7 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
|
|
|
112
114
|
if "completionTimeFromAscendingNode" not in kwargs:
|
|
113
115
|
kwargs["completionTimeFromAscendingNode"] = getattr(
|
|
114
116
|
self.config, "product_type_config", {}
|
|
115
|
-
).get("missionEndDate", None) or datetime.
|
|
117
|
+
).get("missionEndDate", None) or datetime.now(timezone.utc).isoformat(
|
|
116
118
|
timespec="seconds"
|
|
117
119
|
)
|
|
118
120
|
|
|
@@ -120,15 +122,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
|
|
|
120
122
|
if "geometry" in kwargs:
|
|
121
123
|
kwargs["geometry"] = get_geometry_from_various(geometry=kwargs["geometry"])
|
|
122
124
|
|
|
123
|
-
return BuildPostSearchResult.query(
|
|
124
|
-
self, items_per_page=items_per_page, page=page, count=count, **kwargs
|
|
125
|
-
)
|
|
125
|
+
return BuildPostSearchResult.query(self, prep, **kwargs)
|
|
126
126
|
|
|
127
127
|
def authenticate(self) -> Dict[str, Optional[str]]:
|
|
128
128
|
"""Check credentials and returns information needed for auth
|
|
129
129
|
|
|
130
130
|
:returns: {key, url, email} dictionary
|
|
131
|
-
:rtype: dict
|
|
132
131
|
:raises: :class:`~eodag.utils.exceptions.AuthenticationError`
|
|
133
132
|
"""
|
|
134
133
|
# Get credentials from eodag or using ecmwf conf
|
|
@@ -156,21 +155,23 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
|
|
|
156
155
|
def download(
|
|
157
156
|
self,
|
|
158
157
|
product: EOProduct,
|
|
159
|
-
auth: Optional[
|
|
158
|
+
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
160
159
|
progress_callback: Optional[ProgressCallback] = None,
|
|
161
160
|
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
162
161
|
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
163
|
-
**kwargs:
|
|
162
|
+
**kwargs: Unpack[DownloadConf],
|
|
164
163
|
) -> Optional[str]:
|
|
165
164
|
"""Download data from ECMWF MARS"""
|
|
166
165
|
product_format = product.properties.get("format", "grib")
|
|
167
166
|
product_extension = ECMWF_MARS_KNOWN_FORMATS.get(product_format, product_format)
|
|
167
|
+
kwargs["output_extension"] = kwargs.get(
|
|
168
|
+
"output_extension", f".{product_extension}"
|
|
169
|
+
)
|
|
168
170
|
|
|
169
171
|
# Prepare download
|
|
170
172
|
fs_path, record_filename = self._prepare_download(
|
|
171
173
|
product,
|
|
172
174
|
progress_callback=progress_callback,
|
|
173
|
-
outputs_extension=f".{product_extension}",
|
|
174
175
|
**kwargs,
|
|
175
176
|
)
|
|
176
177
|
|
|
@@ -179,6 +180,13 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
|
|
|
179
180
|
product.location = path_to_uri(fs_path)
|
|
180
181
|
return fs_path
|
|
181
182
|
|
|
183
|
+
new_fs_path = os.path.join(
|
|
184
|
+
os.path.dirname(fs_path), sanitize(product.properties["title"])
|
|
185
|
+
)
|
|
186
|
+
if not os.path.isdir(new_fs_path):
|
|
187
|
+
os.makedirs(new_fs_path)
|
|
188
|
+
fs_path = os.path.join(new_fs_path, os.path.basename(fs_path))
|
|
189
|
+
|
|
182
190
|
# get download request dict from product.location/downloadLink url query string
|
|
183
191
|
# separate url & parameters
|
|
184
192
|
download_request = geojson.loads(urlsplit(product.location).query)
|
|
@@ -222,13 +230,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
|
|
|
222
230
|
fh.write(product.properties["downloadLink"])
|
|
223
231
|
logger.debug("Download recorded in %s", record_filename)
|
|
224
232
|
|
|
225
|
-
# do not try to extract
|
|
233
|
+
# do not try to extract a directory
|
|
226
234
|
kwargs["extract"] = False
|
|
227
235
|
|
|
228
236
|
product_path = self._finalize(
|
|
229
|
-
|
|
237
|
+
new_fs_path,
|
|
230
238
|
progress_callback=progress_callback,
|
|
231
|
-
outputs_extension=f".{product_extension}",
|
|
232
239
|
**kwargs,
|
|
233
240
|
)
|
|
234
241
|
product.location = path_to_uri(product_path)
|
|
@@ -237,12 +244,12 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
|
|
|
237
244
|
def download_all(
|
|
238
245
|
self,
|
|
239
246
|
products: SearchResult,
|
|
240
|
-
auth: Optional[
|
|
247
|
+
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
241
248
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
242
249
|
progress_callback: Optional[ProgressCallback] = None,
|
|
243
250
|
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
244
251
|
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
245
|
-
**kwargs:
|
|
252
|
+
**kwargs: Unpack[DownloadConf],
|
|
246
253
|
) -> List[str]:
|
|
247
254
|
"""
|
|
248
255
|
Download all using parent (base plugin) method
|
|
@@ -256,3 +263,7 @@ class EcmwfApi(Download, Api, BuildPostSearchResult):
|
|
|
256
263
|
timeout=timeout,
|
|
257
264
|
**kwargs,
|
|
258
265
|
)
|
|
266
|
+
|
|
267
|
+
def clear(self) -> None:
|
|
268
|
+
"""Clear search context"""
|
|
269
|
+
pass
|
eodag/plugins/apis/usgs.py
CHANGED
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
+
import os
|
|
21
22
|
import shutil
|
|
22
23
|
import tarfile
|
|
23
24
|
import zipfile
|
|
24
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
|
|
25
26
|
|
|
26
27
|
import requests
|
|
27
28
|
from jsonpath_ng.ext import parse
|
|
@@ -35,7 +36,7 @@ from eodag.api.product.metadata_mapping import (
|
|
|
35
36
|
properties_from_json,
|
|
36
37
|
)
|
|
37
38
|
from eodag.plugins.apis.base import Api
|
|
38
|
-
from eodag.plugins.
|
|
39
|
+
from eodag.plugins.search import PreparedSearch
|
|
39
40
|
from eodag.utils import (
|
|
40
41
|
DEFAULT_DOWNLOAD_TIMEOUT,
|
|
41
42
|
DEFAULT_DOWNLOAD_WAIT,
|
|
@@ -52,17 +53,21 @@ from eodag.utils.exceptions import (
|
|
|
52
53
|
NoMatchingProductType,
|
|
53
54
|
NotAvailableError,
|
|
54
55
|
RequestError,
|
|
56
|
+
ValidationError,
|
|
55
57
|
)
|
|
56
58
|
|
|
57
59
|
if TYPE_CHECKING:
|
|
60
|
+
from requests.auth import AuthBase
|
|
61
|
+
|
|
58
62
|
from eodag.api.search_result import SearchResult
|
|
59
63
|
from eodag.config import PluginConfig
|
|
60
|
-
from eodag.
|
|
64
|
+
from eodag.types.download_args import DownloadConf
|
|
65
|
+
from eodag.utils import DownloadedCallback, Unpack
|
|
61
66
|
|
|
62
67
|
logger = logging.getLogger("eodag.apis.usgs")
|
|
63
68
|
|
|
64
69
|
|
|
65
|
-
class UsgsApi(
|
|
70
|
+
class UsgsApi(Api):
|
|
66
71
|
"""A plugin that enables to query and download data on the USGS catalogues"""
|
|
67
72
|
|
|
68
73
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
@@ -71,7 +76,7 @@ class UsgsApi(Download, Api):
|
|
|
71
76
|
# Same method as in base.py, Search.__init__()
|
|
72
77
|
# Prepare the metadata mapping
|
|
73
78
|
# Do a shallow copy, the structure is flat enough for this to be sufficient
|
|
74
|
-
metas = DEFAULT_METADATA_MAPPING.copy()
|
|
79
|
+
metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
|
|
75
80
|
# Update the defaults with the mapping value. This will add any new key
|
|
76
81
|
# added by the provider mapping that is not in the default metadata.
|
|
77
82
|
metas.update(self.config.metadata_mapping)
|
|
@@ -98,30 +103,39 @@ class UsgsApi(Download, Api):
|
|
|
98
103
|
except USGSAuthExpiredError:
|
|
99
104
|
api.logout()
|
|
100
105
|
continue
|
|
101
|
-
except USGSError:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
except USGSError as e:
|
|
107
|
+
if i == 0:
|
|
108
|
+
# `.usgs` API file key might be obsolete
|
|
109
|
+
# Remove it and try again
|
|
110
|
+
os.remove(api.TMPFILE)
|
|
111
|
+
continue
|
|
112
|
+
raise AuthenticationError("Please check your USGS credentials.") from e
|
|
105
113
|
|
|
106
114
|
def query(
|
|
107
115
|
self,
|
|
108
|
-
|
|
109
|
-
items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
|
|
110
|
-
page: int = DEFAULT_PAGE,
|
|
111
|
-
count: bool = True,
|
|
116
|
+
prep: PreparedSearch = PreparedSearch(),
|
|
112
117
|
**kwargs: Any,
|
|
113
118
|
) -> Tuple[List[EOProduct], Optional[int]]:
|
|
114
119
|
"""Search for data on USGS catalogues"""
|
|
120
|
+
page = prep.page if prep.page is not None else DEFAULT_PAGE
|
|
121
|
+
items_per_page = (
|
|
122
|
+
prep.items_per_page
|
|
123
|
+
if prep.items_per_page is not None
|
|
124
|
+
else DEFAULT_ITEMS_PER_PAGE
|
|
125
|
+
)
|
|
115
126
|
product_type = kwargs.get("productType")
|
|
116
127
|
if product_type is None:
|
|
117
128
|
raise NoMatchingProductType(
|
|
118
129
|
"Cannot search on USGS without productType specified"
|
|
119
130
|
)
|
|
131
|
+
if kwargs.get("sort_by"):
|
|
132
|
+
raise ValidationError("USGS does not support sorting feature")
|
|
120
133
|
|
|
121
134
|
self.authenticate()
|
|
122
135
|
|
|
123
136
|
product_type_def_params = self.config.products.get( # type: ignore
|
|
124
|
-
product_type,
|
|
137
|
+
product_type,
|
|
138
|
+
self.config.products[GENERIC_PRODUCT_TYPE], # type: ignore
|
|
125
139
|
)
|
|
126
140
|
usgs_dataset = format_dict_items(product_type_def_params, **kwargs)["dataset"]
|
|
127
141
|
start_date = kwargs.pop("startTimeFromAscendingNode", None)
|
|
@@ -159,11 +173,39 @@ class UsgsApi(Download, Api):
|
|
|
159
173
|
max_results=items_per_page,
|
|
160
174
|
starting_number=(1 + (page - 1) * items_per_page),
|
|
161
175
|
)
|
|
162
|
-
logger.info(
|
|
163
|
-
f"Sending search request for {usgs_dataset} with {api_search_kwargs}"
|
|
164
|
-
)
|
|
165
176
|
|
|
166
|
-
|
|
177
|
+
# search by id
|
|
178
|
+
if searched_id := kwargs.get("id"):
|
|
179
|
+
dataset_filters = api.dataset_filters(usgs_dataset)
|
|
180
|
+
# ip pattern set as parameter queryable (first element of param conf list)
|
|
181
|
+
id_pattern = self.config.metadata_mapping["id"][0]
|
|
182
|
+
# loop on matching dataset_filters until one returns expected results
|
|
183
|
+
for dataset_filter in dataset_filters["data"]:
|
|
184
|
+
if id_pattern in dataset_filter["searchSql"]:
|
|
185
|
+
logger.debug(
|
|
186
|
+
f"Try using {dataset_filter['searchSql']} dataset filter to search by id on {usgs_dataset}"
|
|
187
|
+
)
|
|
188
|
+
full_api_search_kwargs = {
|
|
189
|
+
"where": {
|
|
190
|
+
"filter_id": dataset_filter["id"],
|
|
191
|
+
"value": searched_id,
|
|
192
|
+
},
|
|
193
|
+
**api_search_kwargs,
|
|
194
|
+
}
|
|
195
|
+
logger.info(
|
|
196
|
+
f"Sending search request for {usgs_dataset} with {full_api_search_kwargs}"
|
|
197
|
+
)
|
|
198
|
+
results = api.scene_search(
|
|
199
|
+
usgs_dataset, **full_api_search_kwargs
|
|
200
|
+
)
|
|
201
|
+
if len(results["data"]["results"]) == 1:
|
|
202
|
+
# search by id using this dataset_filter succeeded
|
|
203
|
+
break
|
|
204
|
+
else:
|
|
205
|
+
logger.info(
|
|
206
|
+
f"Sending search request for {usgs_dataset} with {api_search_kwargs}"
|
|
207
|
+
)
|
|
208
|
+
results = api.scene_search(usgs_dataset, **api_search_kwargs)
|
|
167
209
|
|
|
168
210
|
# update results with storage info from download_options()
|
|
169
211
|
results_by_entity_id = {
|
|
@@ -214,7 +256,7 @@ class UsgsApi(Download, Api):
|
|
|
214
256
|
f"Product type {usgs_dataset} may not exist on USGS EE catalog"
|
|
215
257
|
)
|
|
216
258
|
api.logout()
|
|
217
|
-
raise RequestError(e)
|
|
259
|
+
raise RequestError.from_error(e) from e
|
|
218
260
|
|
|
219
261
|
api.logout()
|
|
220
262
|
|
|
@@ -230,11 +272,11 @@ class UsgsApi(Download, Api):
|
|
|
230
272
|
def download(
|
|
231
273
|
self,
|
|
232
274
|
product: EOProduct,
|
|
233
|
-
auth: Optional[
|
|
275
|
+
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
234
276
|
progress_callback: Optional[ProgressCallback] = None,
|
|
235
277
|
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
236
278
|
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
237
|
-
**kwargs:
|
|
279
|
+
**kwargs: Unpack[DownloadConf],
|
|
238
280
|
) -> Optional[str]:
|
|
239
281
|
"""Download data from USGS catalogues"""
|
|
240
282
|
|
|
@@ -244,17 +286,17 @@ class UsgsApi(Download, Api):
|
|
|
244
286
|
)
|
|
245
287
|
progress_callback = ProgressCallback(disable=True)
|
|
246
288
|
|
|
247
|
-
|
|
289
|
+
output_extension = cast(
|
|
248
290
|
str,
|
|
249
291
|
self.config.products.get( # type: ignore
|
|
250
292
|
product.product_type, self.config.products[GENERIC_PRODUCT_TYPE] # type: ignore
|
|
251
|
-
).get("
|
|
293
|
+
).get("output_extension", ".tar.gz"),
|
|
252
294
|
)
|
|
295
|
+
kwargs["output_extension"] = kwargs.get("output_extension", output_extension)
|
|
253
296
|
|
|
254
297
|
fs_path, record_filename = self._prepare_download(
|
|
255
298
|
product,
|
|
256
299
|
progress_callback=progress_callback,
|
|
257
|
-
outputs_extension=outputs_extension,
|
|
258
300
|
**kwargs,
|
|
259
301
|
)
|
|
260
302
|
if not fs_path or not record_filename:
|
|
@@ -308,13 +350,14 @@ class UsgsApi(Download, Api):
|
|
|
308
350
|
req_url = req_urls[0]
|
|
309
351
|
progress_callback.reset()
|
|
310
352
|
logger.debug(f"Downloading {req_url}")
|
|
353
|
+
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
311
354
|
|
|
312
355
|
@self._download_retry(product, wait, timeout)
|
|
313
356
|
def download_request(
|
|
314
357
|
product: EOProduct,
|
|
315
358
|
fs_path: str,
|
|
316
359
|
progress_callback: ProgressCallback,
|
|
317
|
-
**kwargs:
|
|
360
|
+
**kwargs: Unpack[DownloadConf],
|
|
318
361
|
) -> None:
|
|
319
362
|
try:
|
|
320
363
|
with requests.get(
|
|
@@ -322,6 +365,7 @@ class UsgsApi(Download, Api):
|
|
|
322
365
|
stream=True,
|
|
323
366
|
headers=USER_AGENT,
|
|
324
367
|
timeout=wait * 60,
|
|
368
|
+
verify=ssl_verify,
|
|
325
369
|
) as stream:
|
|
326
370
|
try:
|
|
327
371
|
stream.raise_for_status()
|
|
@@ -334,7 +378,9 @@ class UsgsApi(Download, Api):
|
|
|
334
378
|
error_message = str(e)
|
|
335
379
|
raise NotAvailableError(error_message)
|
|
336
380
|
else:
|
|
337
|
-
stream_size =
|
|
381
|
+
stream_size = (
|
|
382
|
+
int(stream.headers.get("content-length", 0)) or None
|
|
383
|
+
)
|
|
338
384
|
progress_callback.reset(total=stream_size)
|
|
339
385
|
with open(fs_path, "wb") as fhandle:
|
|
340
386
|
for chunk in stream.iter_content(chunk_size=64 * 1024):
|
|
@@ -357,13 +403,12 @@ class UsgsApi(Download, Api):
|
|
|
357
403
|
api.logout()
|
|
358
404
|
|
|
359
405
|
# Check downloaded file format
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
):
|
|
406
|
+
if (
|
|
407
|
+
kwargs["output_extension"] == ".tar.gz" and tarfile.is_tarfile(fs_path)
|
|
408
|
+
) or (kwargs["output_extension"] == ".zip" and zipfile.is_zipfile(fs_path)):
|
|
363
409
|
product_path = self._finalize(
|
|
364
410
|
fs_path,
|
|
365
411
|
progress_callback=progress_callback,
|
|
366
|
-
outputs_extension=outputs_extension,
|
|
367
412
|
**kwargs,
|
|
368
413
|
)
|
|
369
414
|
product.location = path_to_uri(product_path)
|
|
@@ -372,7 +417,7 @@ class UsgsApi(Download, Api):
|
|
|
372
417
|
logger.info(
|
|
373
418
|
"Downloaded product detected as a tar File, but was was expected to be a zip file"
|
|
374
419
|
)
|
|
375
|
-
new_fs_path = fs_path[: fs_path.index(
|
|
420
|
+
new_fs_path = fs_path[: fs_path.index(output_extension)] + ".tar.gz"
|
|
376
421
|
shutil.move(fs_path, new_fs_path)
|
|
377
422
|
product.location = path_to_uri(new_fs_path)
|
|
378
423
|
return new_fs_path
|
|
@@ -380,7 +425,7 @@ class UsgsApi(Download, Api):
|
|
|
380
425
|
logger.info(
|
|
381
426
|
"Downloaded product detected as a zip File, but was was expected to be a tar file"
|
|
382
427
|
)
|
|
383
|
-
new_fs_path = fs_path[: fs_path.index(
|
|
428
|
+
new_fs_path = fs_path[: fs_path.index(output_extension)] + ".zip"
|
|
384
429
|
shutil.move(fs_path, new_fs_path)
|
|
385
430
|
product.location = path_to_uri(new_fs_path)
|
|
386
431
|
return new_fs_path
|
|
@@ -388,7 +433,7 @@ class UsgsApi(Download, Api):
|
|
|
388
433
|
logger.warning(
|
|
389
434
|
"Downloaded product is not a tar or a zip File. Please check its file type before using it"
|
|
390
435
|
)
|
|
391
|
-
new_fs_path = fs_path[: fs_path.index(
|
|
436
|
+
new_fs_path = fs_path[: fs_path.index(output_extension)]
|
|
392
437
|
shutil.move(fs_path, new_fs_path)
|
|
393
438
|
product.location = path_to_uri(new_fs_path)
|
|
394
439
|
return new_fs_path
|
|
@@ -396,12 +441,12 @@ class UsgsApi(Download, Api):
|
|
|
396
441
|
def download_all(
|
|
397
442
|
self,
|
|
398
443
|
products: SearchResult,
|
|
399
|
-
auth: Optional[
|
|
444
|
+
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
400
445
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
401
446
|
progress_callback: Optional[ProgressCallback] = None,
|
|
402
447
|
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
403
448
|
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
404
|
-
**kwargs:
|
|
449
|
+
**kwargs: Unpack[DownloadConf],
|
|
405
450
|
) -> List[str]:
|
|
406
451
|
"""
|
|
407
452
|
Download all using parent (base plugin) method
|