eodag 2.12.1__py3-none-any.whl → 3.0.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 +440 -321
- eodag/api/product/__init__.py +5 -1
- eodag/api/product/_assets.py +57 -2
- eodag/api/product/_product.py +89 -68
- eodag/api/product/metadata_mapping.py +181 -66
- eodag/api/search_result.py +48 -1
- eodag/cli.py +20 -6
- eodag/config.py +95 -6
- eodag/plugins/apis/base.py +8 -165
- eodag/plugins/apis/ecmwf.py +36 -24
- eodag/plugins/apis/usgs.py +40 -24
- eodag/plugins/authentication/aws_auth.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +13 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +256 -46
- eodag/plugins/authentication/qsauth.py +3 -0
- eodag/plugins/authentication/sas_auth.py +8 -1
- eodag/plugins/authentication/token.py +92 -46
- eodag/plugins/authentication/token_exchange.py +120 -0
- eodag/plugins/download/aws.py +86 -91
- eodag/plugins/download/base.py +72 -40
- eodag/plugins/download/http.py +607 -264
- eodag/plugins/download/s3rest.py +28 -15
- eodag/plugins/manager.py +74 -57
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +225 -18
- eodag/plugins/search/build_search_result.py +389 -32
- eodag/plugins/search/cop_marine.py +378 -0
- eodag/plugins/search/creodias_s3.py +15 -14
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +44 -20
- eodag/plugins/search/qssearch.py +508 -203
- eodag/plugins/search/static_stac_search.py +99 -36
- eodag/resources/constraints/climate-dt.json +13 -0
- eodag/resources/constraints/extremes-dt.json +8 -0
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1897 -34
- eodag/resources/providers.yml +3539 -3277
- eodag/resources/stac.yml +48 -54
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +51 -3
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +27 -0
- eodag/rest/core.py +757 -0
- eodag/rest/server.py +397 -258
- eodag/rest/stac.py +438 -307
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +232 -43
- eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
- eodag/rest/types/stac_search.py +277 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +65 -0
- eodag/types/__init__.py +99 -9
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +31 -0
- eodag/types/search_args.py +58 -7
- eodag/types/whoosh.py +81 -0
- eodag/utils/__init__.py +72 -9
- eodag/utils/constraints.py +37 -37
- eodag/utils/exceptions.py +23 -17
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +138 -0
- eodag/utils/rest.py +104 -0
- eodag/utils/stac_reader.py +100 -16
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/METADATA +65 -44
- eodag-3.0.0b2.dist-info/RECORD +110 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/WHEEL +1 -1
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/entry_points.txt +6 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/utils.py +0 -1133
- eodag-2.12.1.dist-info/RECORD +0 -94
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/LICENSE +0 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/top_level.txt +0 -0
eodag/plugins/apis/cds.py
DELETED
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
# Copyright 2022, 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 logging
|
|
21
|
-
from datetime import datetime, timedelta
|
|
22
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, cast
|
|
23
|
-
from urllib.parse import unquote_plus
|
|
24
|
-
|
|
25
|
-
import cdsapi
|
|
26
|
-
import geojson
|
|
27
|
-
import requests
|
|
28
|
-
from dateutil.parser import isoparse
|
|
29
|
-
from pydantic import create_model
|
|
30
|
-
from pydantic.fields import FieldInfo
|
|
31
|
-
from typing_extensions import get_args
|
|
32
|
-
|
|
33
|
-
from eodag.api.product._assets import Asset
|
|
34
|
-
from eodag.api.product.metadata_mapping import (
|
|
35
|
-
get_queryable_from_provider,
|
|
36
|
-
mtd_cfg_as_conversion_and_querypath,
|
|
37
|
-
)
|
|
38
|
-
from eodag.plugins.apis.base import Api
|
|
39
|
-
from eodag.plugins.download.http import HTTPDownload
|
|
40
|
-
from eodag.plugins.search.base import Search
|
|
41
|
-
from eodag.plugins.search.build_search_result import BuildPostSearchResult
|
|
42
|
-
from eodag.rest.stac import DEFAULT_MISSION_START_DATE
|
|
43
|
-
from eodag.types import json_field_definition_to_python, model_fields_to_annotated
|
|
44
|
-
from eodag.types.queryables import CommonQueryables
|
|
45
|
-
from eodag.utils import (
|
|
46
|
-
DEFAULT_DOWNLOAD_TIMEOUT,
|
|
47
|
-
DEFAULT_DOWNLOAD_WAIT,
|
|
48
|
-
DEFAULT_ITEMS_PER_PAGE,
|
|
49
|
-
DEFAULT_PAGE,
|
|
50
|
-
Annotated,
|
|
51
|
-
datetime_range,
|
|
52
|
-
deepcopy,
|
|
53
|
-
get_geometry_from_various,
|
|
54
|
-
path_to_uri,
|
|
55
|
-
urlencode,
|
|
56
|
-
urlsplit,
|
|
57
|
-
)
|
|
58
|
-
from eodag.utils.constraints import (
|
|
59
|
-
fetch_constraints,
|
|
60
|
-
get_constraint_queryables_with_additional_params,
|
|
61
|
-
)
|
|
62
|
-
from eodag.utils.exceptions import (
|
|
63
|
-
AuthenticationError,
|
|
64
|
-
DownloadError,
|
|
65
|
-
RequestError,
|
|
66
|
-
ValidationError,
|
|
67
|
-
)
|
|
68
|
-
from eodag.utils.logging import get_logging_verbose
|
|
69
|
-
|
|
70
|
-
if TYPE_CHECKING:
|
|
71
|
-
from eodag.api.product import EOProduct
|
|
72
|
-
from eodag.api.search_result import SearchResult
|
|
73
|
-
from eodag.config import PluginConfig
|
|
74
|
-
from eodag.utils import DownloadedCallback, ProgressCallback
|
|
75
|
-
|
|
76
|
-
logger = logging.getLogger("eodag.apis.cds")
|
|
77
|
-
|
|
78
|
-
CDS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class CdsApi(HTTPDownload, Api, BuildPostSearchResult):
|
|
82
|
-
"""A plugin that enables to build download-request and download data on CDS API.
|
|
83
|
-
|
|
84
|
-
Builds a single ready-to-download :class:`~eodag.api.product._product.EOProduct`
|
|
85
|
-
during the search stage.
|
|
86
|
-
|
|
87
|
-
This class inherits from :class:`~eodag.plugins.apis.base.Api` for compatibility,
|
|
88
|
-
:class:`~eodag.plugins.download.base.Download` for download methods, and
|
|
89
|
-
:class:`~eodag.plugins.search.qssearch.QueryStringSearch` for metadata-mapping and
|
|
90
|
-
query build methods.
|
|
91
|
-
"""
|
|
92
|
-
|
|
93
|
-
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
94
|
-
# init self.config.metadata_mapping using Search Base plugin
|
|
95
|
-
Search.__init__(self, provider, config)
|
|
96
|
-
|
|
97
|
-
# needed by QueryStringSearch.build_query_string / format_free_text_search
|
|
98
|
-
self.config.__dict__.setdefault("free_text_search_operations", {})
|
|
99
|
-
# needed for compatibility
|
|
100
|
-
self.config.__dict__.setdefault("pagination", {"next_page_query_obj": "{{}}"})
|
|
101
|
-
|
|
102
|
-
# parse jsonpath on init: product type specific metadata-mapping
|
|
103
|
-
for product_type in self.config.products.keys():
|
|
104
|
-
if "metadata_mapping" in self.config.products[product_type].keys():
|
|
105
|
-
self.config.products[product_type][
|
|
106
|
-
"metadata_mapping"
|
|
107
|
-
] = mtd_cfg_as_conversion_and_querypath(
|
|
108
|
-
self.config.products[product_type]["metadata_mapping"]
|
|
109
|
-
)
|
|
110
|
-
# Complete and ready to use product type specific metadata-mapping
|
|
111
|
-
product_type_metadata_mapping = deepcopy(self.config.metadata_mapping)
|
|
112
|
-
|
|
113
|
-
# update config using provider product type definition metadata_mapping
|
|
114
|
-
# from another product
|
|
115
|
-
other_product_for_mapping = cast(
|
|
116
|
-
str,
|
|
117
|
-
self.config.products[product_type].get(
|
|
118
|
-
"metadata_mapping_from_product", ""
|
|
119
|
-
),
|
|
120
|
-
)
|
|
121
|
-
if other_product_for_mapping:
|
|
122
|
-
other_product_type_def_params = self.get_product_type_def_params(
|
|
123
|
-
other_product_for_mapping, # **kwargs
|
|
124
|
-
)
|
|
125
|
-
product_type_metadata_mapping.update(
|
|
126
|
-
other_product_type_def_params.get("metadata_mapping", {})
|
|
127
|
-
)
|
|
128
|
-
# from current product
|
|
129
|
-
product_type_metadata_mapping.update(
|
|
130
|
-
self.config.products[product_type]["metadata_mapping"]
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
self.config.products[product_type][
|
|
134
|
-
"metadata_mapping"
|
|
135
|
-
] = product_type_metadata_mapping
|
|
136
|
-
|
|
137
|
-
def get_product_type_cfg(self, key: str, default: Any = None) -> Any:
|
|
138
|
-
"""
|
|
139
|
-
Get the value of a configuration option specific to the current product type.
|
|
140
|
-
|
|
141
|
-
This method retrieves the value of a configuration option from the
|
|
142
|
-
`_product_type_config` attribute. If the option is not found, the provided
|
|
143
|
-
default value is returned.
|
|
144
|
-
|
|
145
|
-
:param key: The configuration option key.
|
|
146
|
-
:type key: str
|
|
147
|
-
:param default: The default value to be returned if the option is not found (default is None).
|
|
148
|
-
:type default: Any
|
|
149
|
-
|
|
150
|
-
:return: The value of the specified configuration option or the default value.
|
|
151
|
-
:rtype: Any
|
|
152
|
-
"""
|
|
153
|
-
product_type_cfg = getattr(self.config, "product_type_config", {})
|
|
154
|
-
non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
|
|
155
|
-
|
|
156
|
-
return non_none_cfg.get(key, default)
|
|
157
|
-
|
|
158
|
-
def _preprocess_search_params(self, params: Dict[Any]) -> None:
|
|
159
|
-
"""Preprocess search parameters before making a request to the CDS API.
|
|
160
|
-
|
|
161
|
-
This method is responsible for checking and updating the provided search parameters
|
|
162
|
-
to ensure that required parameters like 'productType', 'startTimeFromAscendingNode',
|
|
163
|
-
'completionTimeFromAscendingNode', and 'geometry' are properly set. If not specified
|
|
164
|
-
in the input parameters, default values or values from the configuration are used.
|
|
165
|
-
|
|
166
|
-
:param params: Search parameters to be preprocessed.
|
|
167
|
-
:type params: dict
|
|
168
|
-
"""
|
|
169
|
-
_dc_qs = params.get("_dc_qs", None)
|
|
170
|
-
if _dc_qs is not None:
|
|
171
|
-
# if available, update search params using datacube query-string
|
|
172
|
-
_dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
|
|
173
|
-
if "/" in _dc_qp.get("date", ""):
|
|
174
|
-
(
|
|
175
|
-
params["startTimeFromAscendingNode"],
|
|
176
|
-
params["completionTimeFromAscendingNode"],
|
|
177
|
-
) = _dc_qp["date"].split("/")
|
|
178
|
-
elif _dc_qp.get("date", None):
|
|
179
|
-
params["startTimeFromAscendingNode"] = params[
|
|
180
|
-
"completionTimeFromAscendingNode"
|
|
181
|
-
] = _dc_qp["date"]
|
|
182
|
-
|
|
183
|
-
if "/" in _dc_qp.get("area", ""):
|
|
184
|
-
params["geometry"] = _dc_qp["area"].split("/")
|
|
185
|
-
|
|
186
|
-
non_none_params = {k: v for k, v in params.items() if v}
|
|
187
|
-
|
|
188
|
-
# productType
|
|
189
|
-
dataset = params.get("dataset", None)
|
|
190
|
-
params["productType"] = non_none_params.get("productType", dataset)
|
|
191
|
-
|
|
192
|
-
# dates
|
|
193
|
-
mission_start_dt = datetime.fromisoformat(
|
|
194
|
-
self.get_product_type_cfg(
|
|
195
|
-
"missionStartDate", DEFAULT_MISSION_START_DATE
|
|
196
|
-
).replace(
|
|
197
|
-
"Z", "+00:00"
|
|
198
|
-
) # before 3.11
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
default_end_from_cfg = self.config.products.get(params["productType"], {}).get(
|
|
202
|
-
"_default_end_date", None
|
|
203
|
-
)
|
|
204
|
-
default_end_str = (
|
|
205
|
-
default_end_from_cfg
|
|
206
|
-
or (
|
|
207
|
-
datetime.utcnow()
|
|
208
|
-
if params.get("startTimeFromAscendingNode")
|
|
209
|
-
else mission_start_dt + timedelta(days=1)
|
|
210
|
-
).isoformat()
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
params["startTimeFromAscendingNode"] = non_none_params.get(
|
|
214
|
-
"startTimeFromAscendingNode", mission_start_dt.isoformat()
|
|
215
|
-
)
|
|
216
|
-
params["completionTimeFromAscendingNode"] = non_none_params.get(
|
|
217
|
-
"completionTimeFromAscendingNode", default_end_str
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
# temporary _date parameter mixing start & end
|
|
221
|
-
end_date = isoparse(params["completionTimeFromAscendingNode"]) + timedelta(
|
|
222
|
-
days=-1
|
|
223
|
-
)
|
|
224
|
-
params[
|
|
225
|
-
"_date"
|
|
226
|
-
] = f"{params['startTimeFromAscendingNode']}/{end_date.isoformat()}"
|
|
227
|
-
|
|
228
|
-
# geometry
|
|
229
|
-
if "geometry" in params:
|
|
230
|
-
params["geometry"] = get_geometry_from_various(geometry=params["geometry"])
|
|
231
|
-
|
|
232
|
-
def build_query_string(
|
|
233
|
-
self, product_type: str, **kwargs: Any
|
|
234
|
-
) -> Tuple[Dict[str, Any], str]:
|
|
235
|
-
"""Build The query string using the search parameters"""
|
|
236
|
-
qp, _ = BuildPostSearchResult.build_query_string(
|
|
237
|
-
self, product_type=product_type, **kwargs
|
|
238
|
-
)
|
|
239
|
-
if "_date" in qp:
|
|
240
|
-
qp.update(qp.pop("_date", {}))
|
|
241
|
-
|
|
242
|
-
return qp, urlencode(qp, doseq=True, quote_via=lambda x, *_args, **_kwargs: x)
|
|
243
|
-
|
|
244
|
-
def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
245
|
-
"""Should perform the actual search request."""
|
|
246
|
-
return [{}]
|
|
247
|
-
|
|
248
|
-
def query(
|
|
249
|
-
self,
|
|
250
|
-
product_type: Optional[str] = None,
|
|
251
|
-
items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
|
|
252
|
-
page: int = DEFAULT_PAGE,
|
|
253
|
-
count: bool = True,
|
|
254
|
-
**kwargs: Any,
|
|
255
|
-
) -> Tuple[List[EOProduct], Optional[int]]:
|
|
256
|
-
"""Build ready-to-download SearchResult"""
|
|
257
|
-
|
|
258
|
-
self._preprocess_search_params(kwargs)
|
|
259
|
-
|
|
260
|
-
return BuildPostSearchResult.query(
|
|
261
|
-
self, items_per_page=items_per_page, page=page, count=count, **kwargs
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
def _get_cds_client(self, **auth_dict: Any) -> cdsapi.Client:
|
|
265
|
-
"""Returns cdsapi client."""
|
|
266
|
-
# eodag logging info
|
|
267
|
-
eodag_verbosity = get_logging_verbose()
|
|
268
|
-
eodag_logger = logging.getLogger("eodag")
|
|
269
|
-
|
|
270
|
-
client = cdsapi.Client(
|
|
271
|
-
# disable cdsapi default logging and handle it on eodag side
|
|
272
|
-
# until https://github.com/ecmwf/cdsapi/pull/47 is merged
|
|
273
|
-
quiet=True,
|
|
274
|
-
verify=True,
|
|
275
|
-
**auth_dict,
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
if eodag_verbosity is None or eodag_verbosity == 1:
|
|
279
|
-
client.logger.setLevel(logging.WARNING)
|
|
280
|
-
elif eodag_verbosity == 2:
|
|
281
|
-
client.logger.setLevel(logging.INFO)
|
|
282
|
-
elif eodag_verbosity == 3:
|
|
283
|
-
client.logger.setLevel(logging.DEBUG)
|
|
284
|
-
else:
|
|
285
|
-
client.logger.setLevel(logging.WARNING)
|
|
286
|
-
|
|
287
|
-
if len(eodag_logger.handlers) > 0:
|
|
288
|
-
client.logger.addHandler(eodag_logger.handlers[0])
|
|
289
|
-
|
|
290
|
-
return client
|
|
291
|
-
|
|
292
|
-
def authenticate(self) -> Dict[str, str]:
|
|
293
|
-
"""Returns information needed for auth
|
|
294
|
-
|
|
295
|
-
:returns: {key, url} dictionary
|
|
296
|
-
:rtype: dict
|
|
297
|
-
:raises: :class:`~eodag.utils.exceptions.AuthenticationError`
|
|
298
|
-
:raises: :class:`~eodag.utils.exceptions.RequestError`
|
|
299
|
-
"""
|
|
300
|
-
# Get credentials from eodag or using cds conf
|
|
301
|
-
uid = getattr(self.config, "credentials", {}).get("username", None)
|
|
302
|
-
api_key = getattr(self.config, "credentials", {}).get("password", None)
|
|
303
|
-
url = getattr(self.config, "api_endpoint", None)
|
|
304
|
-
if not all([uid, api_key, url]):
|
|
305
|
-
raise AuthenticationError("Missing authentication information")
|
|
306
|
-
|
|
307
|
-
auth_dict: Dict[str, str] = {"key": f"{uid}:{api_key}", "url": url}
|
|
308
|
-
|
|
309
|
-
client = self._get_cds_client(**auth_dict)
|
|
310
|
-
try:
|
|
311
|
-
client.status()
|
|
312
|
-
logger.debug("Connection checked on CDS API")
|
|
313
|
-
except requests.exceptions.ConnectionError as e:
|
|
314
|
-
logger.error(e)
|
|
315
|
-
raise RequestError(f"Could not connect to the CDS API '{url}'")
|
|
316
|
-
except requests.exceptions.HTTPError as e:
|
|
317
|
-
logger.error(e)
|
|
318
|
-
raise RequestError("The CDS API has returned an unexpected error")
|
|
319
|
-
|
|
320
|
-
return auth_dict
|
|
321
|
-
|
|
322
|
-
def _prepare_download_link(self, product):
|
|
323
|
-
"""Update product download link with http url obtained from cds api"""
|
|
324
|
-
# get download request dict from product.location/downloadLink url query string
|
|
325
|
-
# separate url & parameters
|
|
326
|
-
query_str = "".join(urlsplit(product.location).fragment.split("?", 1)[1:])
|
|
327
|
-
download_request = geojson.loads(query_str)
|
|
328
|
-
|
|
329
|
-
date_range = download_request.pop("date_range", False)
|
|
330
|
-
if date_range:
|
|
331
|
-
date = download_request.pop("date")
|
|
332
|
-
start, end, *_ = date.split("/")
|
|
333
|
-
_start = datetime.fromisoformat(start)
|
|
334
|
-
_end = datetime.fromisoformat(end)
|
|
335
|
-
d_range = [d for d in datetime_range(_start, _end)]
|
|
336
|
-
download_request["year"] = [*{str(d.year) for d in d_range}]
|
|
337
|
-
download_request["month"] = [*{str(d.month) for d in d_range}]
|
|
338
|
-
download_request["day"] = [*{str(d.day) for d in d_range}]
|
|
339
|
-
|
|
340
|
-
auth_dict = self.authenticate()
|
|
341
|
-
dataset_name = download_request.pop("dataset")
|
|
342
|
-
|
|
343
|
-
# Send download request to CDS web API
|
|
344
|
-
logger.info(
|
|
345
|
-
"Request download on CDS API: dataset=%s, request=%s",
|
|
346
|
-
dataset_name,
|
|
347
|
-
download_request,
|
|
348
|
-
)
|
|
349
|
-
try:
|
|
350
|
-
client = self._get_cds_client(**auth_dict)
|
|
351
|
-
result = client._api(
|
|
352
|
-
"%s/resources/%s" % (client.url, dataset_name), download_request, "POST"
|
|
353
|
-
)
|
|
354
|
-
# update product download link through a new asset
|
|
355
|
-
product.assets["data"] = Asset(product, "data", {"href": result.location})
|
|
356
|
-
except Exception as e:
|
|
357
|
-
logger.error(e)
|
|
358
|
-
raise DownloadError(e)
|
|
359
|
-
|
|
360
|
-
def download(
|
|
361
|
-
self,
|
|
362
|
-
product: EOProduct,
|
|
363
|
-
auth: Optional[PluginConfig] = None,
|
|
364
|
-
progress_callback: Optional[ProgressCallback] = None,
|
|
365
|
-
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
366
|
-
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
367
|
-
**kwargs: Any,
|
|
368
|
-
) -> Optional[str]:
|
|
369
|
-
"""Download data from providers using CDS API"""
|
|
370
|
-
product_format = product.properties.get("format", "grib")
|
|
371
|
-
product_extension = CDS_KNOWN_FORMATS.get(product_format, product_format)
|
|
372
|
-
|
|
373
|
-
# Prepare download
|
|
374
|
-
fs_path, record_filename = self._prepare_download(
|
|
375
|
-
product,
|
|
376
|
-
progress_callback=progress_callback,
|
|
377
|
-
outputs_extension=f".{product_extension}",
|
|
378
|
-
**kwargs,
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
if not fs_path or not record_filename:
|
|
382
|
-
if fs_path:
|
|
383
|
-
product.location = path_to_uri(fs_path)
|
|
384
|
-
return fs_path
|
|
385
|
-
|
|
386
|
-
self._prepare_download_link(product)
|
|
387
|
-
|
|
388
|
-
try:
|
|
389
|
-
return super(CdsApi, self).download(
|
|
390
|
-
product,
|
|
391
|
-
progress_callback=progress_callback,
|
|
392
|
-
**kwargs,
|
|
393
|
-
)
|
|
394
|
-
except Exception as e:
|
|
395
|
-
logger.error(e)
|
|
396
|
-
raise DownloadError(e)
|
|
397
|
-
|
|
398
|
-
def _stream_download_dict(
|
|
399
|
-
self,
|
|
400
|
-
product: EOProduct,
|
|
401
|
-
auth: Optional[PluginConfig] = None,
|
|
402
|
-
progress_callback: Optional[ProgressCallback] = None,
|
|
403
|
-
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
404
|
-
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
405
|
-
**kwargs: Union[str, bool, Dict[str, Any]],
|
|
406
|
-
) -> Dict[str, Any]:
|
|
407
|
-
"""Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
|
|
408
|
-
It contains a generator to streamed download chunks and the response headers."""
|
|
409
|
-
|
|
410
|
-
self._prepare_download_link(product)
|
|
411
|
-
return super(CdsApi, self)._stream_download_dict(
|
|
412
|
-
product,
|
|
413
|
-
auth=auth,
|
|
414
|
-
progress_callback=progress_callback,
|
|
415
|
-
wait=wait,
|
|
416
|
-
timeout=timeout,
|
|
417
|
-
**kwargs,
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
def download_all(
|
|
421
|
-
self,
|
|
422
|
-
products: SearchResult,
|
|
423
|
-
auth: Optional[PluginConfig] = None,
|
|
424
|
-
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
425
|
-
progress_callback: Optional[ProgressCallback] = None,
|
|
426
|
-
wait: int = DEFAULT_DOWNLOAD_WAIT,
|
|
427
|
-
timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
428
|
-
**kwargs: Any,
|
|
429
|
-
):
|
|
430
|
-
"""
|
|
431
|
-
Download all using parent (base plugin) method
|
|
432
|
-
"""
|
|
433
|
-
return super(CdsApi, self).download_all(
|
|
434
|
-
products,
|
|
435
|
-
auth=auth,
|
|
436
|
-
downloaded_callback=downloaded_callback,
|
|
437
|
-
progress_callback=progress_callback,
|
|
438
|
-
wait=wait,
|
|
439
|
-
timeout=timeout,
|
|
440
|
-
**kwargs,
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
def discover_queryables(
|
|
444
|
-
self, **kwargs: Any
|
|
445
|
-
) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
|
|
446
|
-
"""Fetch queryables list from provider using `discover_queryables` conf
|
|
447
|
-
|
|
448
|
-
:param kwargs: additional filters for queryables (`productType` and other search
|
|
449
|
-
arguments)
|
|
450
|
-
:type kwargs: Any
|
|
451
|
-
:returns: fetched queryable parameters dict
|
|
452
|
-
:rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
|
|
453
|
-
"""
|
|
454
|
-
constraints_file_url = getattr(self.config, "constraints_file_url", "")
|
|
455
|
-
if not constraints_file_url:
|
|
456
|
-
return {}
|
|
457
|
-
product_type = kwargs.pop("productType", None)
|
|
458
|
-
if not product_type:
|
|
459
|
-
return {}
|
|
460
|
-
|
|
461
|
-
provider_product_type = self.config.products.get(product_type, {}).get(
|
|
462
|
-
"dataset", None
|
|
463
|
-
)
|
|
464
|
-
user_provider_product_type = kwargs.pop("dataset", None)
|
|
465
|
-
if (
|
|
466
|
-
user_provider_product_type
|
|
467
|
-
and user_provider_product_type != provider_product_type
|
|
468
|
-
):
|
|
469
|
-
raise ValidationError(
|
|
470
|
-
f"Cannot change dataset from {provider_product_type} to {user_provider_product_type}"
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
non_empty_kwargs = {k: v for k, v in kwargs.items() if v}
|
|
474
|
-
|
|
475
|
-
if "{" in constraints_file_url:
|
|
476
|
-
constraints_file_url = constraints_file_url.format(
|
|
477
|
-
dataset=provider_product_type
|
|
478
|
-
)
|
|
479
|
-
constraints = fetch_constraints(constraints_file_url, self)
|
|
480
|
-
if not constraints:
|
|
481
|
-
return {}
|
|
482
|
-
|
|
483
|
-
# defaults
|
|
484
|
-
default_queryables = self.get_defaults_as_queryables(product_type)
|
|
485
|
-
# remove dataset from queryables
|
|
486
|
-
default_queryables.pop("dataset", None)
|
|
487
|
-
|
|
488
|
-
constraint_params: Dict[str, Dict[str, Set[Any]]] = {}
|
|
489
|
-
if len(kwargs) == 0:
|
|
490
|
-
# get values from constraints without additional filters
|
|
491
|
-
for constraint in constraints:
|
|
492
|
-
for key in constraint.keys():
|
|
493
|
-
if key in constraint_params:
|
|
494
|
-
constraint_params[key]["enum"].update(constraint[key])
|
|
495
|
-
else:
|
|
496
|
-
constraint_params[key] = {}
|
|
497
|
-
constraint_params[key]["enum"] = set(constraint[key])
|
|
498
|
-
else:
|
|
499
|
-
# get values from constraints with additional filters
|
|
500
|
-
constraints_input_params = {k: v for k, v in non_empty_kwargs.items()}
|
|
501
|
-
constraint_params = get_constraint_queryables_with_additional_params(
|
|
502
|
-
constraints, constraints_input_params, self, product_type
|
|
503
|
-
)
|
|
504
|
-
# query params that are not in constraints but might be default queryables
|
|
505
|
-
if len(constraint_params) == 1 and "not_available" in constraint_params:
|
|
506
|
-
not_queryables = set()
|
|
507
|
-
for constraint_param in constraint_params["not_available"]["enum"]:
|
|
508
|
-
param = CommonQueryables.get_queryable_from_alias(constraint_param)
|
|
509
|
-
if param in dict(
|
|
510
|
-
CommonQueryables.model_fields, **default_queryables
|
|
511
|
-
):
|
|
512
|
-
non_empty_kwargs.pop(constraint_param)
|
|
513
|
-
else:
|
|
514
|
-
not_queryables.add(constraint_param)
|
|
515
|
-
if not_queryables:
|
|
516
|
-
raise ValidationError(
|
|
517
|
-
f"parameter(s) {str(not_queryables)} not queryable"
|
|
518
|
-
)
|
|
519
|
-
else:
|
|
520
|
-
# get constraints again without common queryables
|
|
521
|
-
constraint_params = (
|
|
522
|
-
get_constraint_queryables_with_additional_params(
|
|
523
|
-
constraints, non_empty_kwargs, self, product_type
|
|
524
|
-
)
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
field_definitions = dict()
|
|
528
|
-
for json_param, json_mtd in constraint_params.items():
|
|
529
|
-
param = (
|
|
530
|
-
get_queryable_from_provider(json_param, self.config.metadata_mapping)
|
|
531
|
-
or json_param
|
|
532
|
-
)
|
|
533
|
-
default = kwargs.get(param, None)
|
|
534
|
-
annotated_def = json_field_definition_to_python(
|
|
535
|
-
json_mtd, default_value=default, required=True
|
|
536
|
-
)
|
|
537
|
-
field_definitions[param] = get_args(annotated_def)
|
|
538
|
-
|
|
539
|
-
python_queryables = create_model("m", **field_definitions).model_fields
|
|
540
|
-
return dict(default_queryables, **model_fields_to_annotated(python_queryables))
|