eodag 3.1.0b1__py3-none-any.whl → 3.2.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 +69 -63
- eodag/api/product/_assets.py +49 -13
- eodag/api/product/_product.py +41 -30
- 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 +85 -79
- eodag/api/search_result.py +13 -23
- eodag/cli.py +4 -4
- eodag/config.py +77 -80
- eodag/plugins/apis/base.py +1 -1
- eodag/plugins/apis/ecmwf.py +12 -15
- eodag/plugins/apis/usgs.py +12 -11
- 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 +20 -14
- eodag/plugins/authentication/sas_auth.py +4 -4
- eodag/plugins/authentication/token.py +7 -7
- eodag/plugins/authentication/token_exchange.py +1 -1
- eodag/plugins/base.py +4 -4
- eodag/plugins/crunch/base.py +4 -4
- eodag/plugins/crunch/filter_date.py +4 -4
- eodag/plugins/crunch/filter_latest_intersect.py +6 -6
- eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
- eodag/plugins/crunch/filter_overlap.py +4 -4
- eodag/plugins/crunch/filter_property.py +4 -4
- eodag/plugins/download/aws.py +137 -77
- eodag/plugins/download/base.py +8 -17
- eodag/plugins/download/creodias_s3.py +2 -2
- eodag/plugins/download/http.py +30 -32
- eodag/plugins/download/s3rest.py +5 -4
- eodag/plugins/manager.py +10 -20
- eodag/plugins/search/__init__.py +6 -5
- eodag/plugins/search/base.py +38 -42
- eodag/plugins/search/build_search_result.py +286 -336
- eodag/plugins/search/cop_marine.py +22 -12
- eodag/plugins/search/creodias_s3.py +8 -78
- eodag/plugins/search/csw.py +11 -11
- eodag/plugins/search/data_request_search.py +19 -18
- eodag/plugins/search/qssearch.py +84 -151
- 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 +848 -398
- eodag/resources/providers.yml +1038 -1115
- 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 +24 -24
- eodag/rest/errors.py +5 -5
- eodag/rest/server.py +3 -11
- eodag/rest/stac.py +41 -38
- eodag/rest/types/collections_search.py +3 -3
- eodag/rest/types/eodag_search.py +23 -23
- eodag/rest/types/queryables.py +40 -28
- eodag/rest/types/stac_search.py +15 -25
- eodag/rest/utils/__init__.py +11 -21
- eodag/rest/utils/cql_evaluate.py +6 -6
- eodag/rest/utils/rfc3339.py +2 -2
- eodag/types/__init__.py +97 -29
- eodag/types/bbox.py +2 -2
- eodag/types/download_args.py +2 -2
- eodag/types/queryables.py +5 -2
- eodag/types/search_args.py +4 -4
- eodag/types/whoosh.py +1 -3
- eodag/utils/__init__.py +82 -41
- eodag/utils/exceptions.py +2 -2
- eodag/utils/import_system.py +2 -2
- eodag/utils/requests.py +2 -2
- eodag/utils/rest.py +2 -2
- eodag/utils/s3.py +231 -0
- eodag/utils/stac_reader.py +10 -10
- {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info}/METADATA +12 -10
- eodag-3.2.0.dist-info/RECORD +113 -0
- {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info}/WHEEL +1 -1
- {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info}/entry_points.txt +1 -0
- eodag-3.1.0b1.dist-info/RECORD +0 -108
- {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info/licenses}/LICENSE +0 -0
- {eodag-3.1.0b1.dist-info → eodag-3.2.0.dist-info}/top_level.txt +0 -0
|
@@ -23,25 +23,14 @@ import logging
|
|
|
23
23
|
import re
|
|
24
24
|
from collections import OrderedDict
|
|
25
25
|
from datetime import datetime, timedelta
|
|
26
|
-
from typing import
|
|
27
|
-
TYPE_CHECKING,
|
|
28
|
-
Annotated,
|
|
29
|
-
Any,
|
|
30
|
-
Dict,
|
|
31
|
-
List,
|
|
32
|
-
Optional,
|
|
33
|
-
Set,
|
|
34
|
-
Tuple,
|
|
35
|
-
Union,
|
|
36
|
-
cast,
|
|
37
|
-
)
|
|
26
|
+
from typing import TYPE_CHECKING, Annotated, Any, Optional, Union
|
|
38
27
|
from urllib.parse import quote_plus, unquote_plus
|
|
39
28
|
|
|
40
29
|
import geojson
|
|
41
30
|
import orjson
|
|
42
31
|
from dateutil.parser import isoparse
|
|
43
32
|
from dateutil.tz import tzutc
|
|
44
|
-
from
|
|
33
|
+
from dateutil.utils import today
|
|
45
34
|
from pydantic import Field
|
|
46
35
|
from pydantic.fields import FieldInfo
|
|
47
36
|
from requests.auth import AuthBase
|
|
@@ -51,19 +40,18 @@ from typing_extensions import get_args
|
|
|
51
40
|
from eodag.api.product import EOProduct
|
|
52
41
|
from eodag.api.product.metadata_mapping import (
|
|
53
42
|
NOT_AVAILABLE,
|
|
54
|
-
|
|
43
|
+
OFFLINE_STATUS,
|
|
55
44
|
format_metadata,
|
|
56
|
-
format_query_params,
|
|
57
|
-
mtd_cfg_as_conversion_and_querypath,
|
|
58
45
|
properties_from_json,
|
|
59
46
|
)
|
|
60
47
|
from eodag.api.search_result import RawSearchResult
|
|
61
48
|
from eodag.plugins.search import PreparedSearch
|
|
62
49
|
from eodag.plugins.search.qssearch import PostJsonSearch, QueryStringSearch
|
|
63
50
|
from eodag.types import json_field_definition_to_python
|
|
64
|
-
from eodag.types.queryables import Queryables
|
|
51
|
+
from eodag.types.queryables import Queryables, QueryablesDict
|
|
65
52
|
from eodag.utils import (
|
|
66
|
-
|
|
53
|
+
DEFAULT_MISSION_START_DATE,
|
|
54
|
+
DEFAULT_SEARCH_TIMEOUT,
|
|
67
55
|
deepcopy,
|
|
68
56
|
dict_items_recursive_sort,
|
|
69
57
|
get_geometry_from_various,
|
|
@@ -77,9 +65,11 @@ if TYPE_CHECKING:
|
|
|
77
65
|
|
|
78
66
|
logger = logging.getLogger("eodag.search.build_search_result")
|
|
79
67
|
|
|
68
|
+
ECMWF_PREFIX = "ecmwf:"
|
|
69
|
+
|
|
80
70
|
# keywords from ECMWF keyword database + "dataset" (not part of database but exists)
|
|
81
71
|
# database: https://confluence.ecmwf.int/display/UDOC/Keywords+in+MARS+and+Dissemination+requests
|
|
82
|
-
ECMWF_KEYWORDS =
|
|
72
|
+
ECMWF_KEYWORDS = {
|
|
83
73
|
"dataset",
|
|
84
74
|
"accuracy",
|
|
85
75
|
"activity",
|
|
@@ -143,10 +133,10 @@ ECMWF_KEYWORDS = [
|
|
|
143
133
|
"truncation",
|
|
144
134
|
"type",
|
|
145
135
|
"use",
|
|
146
|
-
|
|
136
|
+
}
|
|
147
137
|
|
|
148
138
|
# additional keywords from copernicus services
|
|
149
|
-
COP_DS_KEYWORDS =
|
|
139
|
+
COP_DS_KEYWORDS = {
|
|
150
140
|
"aerosol_type",
|
|
151
141
|
"altitude",
|
|
152
142
|
"product_type",
|
|
@@ -201,57 +191,32 @@ COP_DS_KEYWORDS = [
|
|
|
201
191
|
"variable_type",
|
|
202
192
|
"version",
|
|
203
193
|
"year",
|
|
204
|
-
|
|
194
|
+
}
|
|
205
195
|
|
|
196
|
+
ALLOWED_KEYWORDS = ECMWF_KEYWORDS | COP_DS_KEYWORDS
|
|
206
197
|
|
|
207
|
-
|
|
208
|
-
keywords: List[str], prefix: Optional[str] = None
|
|
209
|
-
) -> Dict[str, Any]:
|
|
210
|
-
"""
|
|
211
|
-
Make metadata mapping dict from a list of keywords
|
|
198
|
+
END = "completionTimeFromAscendingNode"
|
|
212
199
|
|
|
213
|
-
|
|
214
|
-
- keyword
|
|
215
|
-
- $."prefix:keyword"
|
|
200
|
+
START = "startTimeFromAscendingNode"
|
|
216
201
|
|
|
217
|
-
>>> keywords_to_mdt(["month", "year"])
|
|
218
|
-
{'month': ['month', '$."month"'], 'year': ['year', '$."year"']}
|
|
219
|
-
>>> keywords_to_mdt(["month", "year"], "ecmwf")
|
|
220
|
-
{'ecmwf:month': ['month', '$."ecmwf:month"'], 'ecmwf:year': ['year', '$."ecmwf:year"']}
|
|
221
202
|
|
|
222
|
-
|
|
223
|
-
:param prefix: prefix to be added to the parameter in the mapping
|
|
224
|
-
:return: metadata mapping dict
|
|
203
|
+
def ecmwf_mtd() -> dict[str, Any]:
|
|
225
204
|
"""
|
|
226
|
-
|
|
227
|
-
for keyword in keywords:
|
|
228
|
-
key = f"{prefix}:{keyword}" if prefix else keyword
|
|
229
|
-
mdt[key] = [keyword, f'$."{key}"']
|
|
230
|
-
return mdt
|
|
231
|
-
|
|
205
|
+
Make metadata mapping dict from a list of defined ECMWF Keywords
|
|
232
206
|
|
|
233
|
-
|
|
234
|
-
"""Strip superfluous quotes from elements (added by mapping converter to_geojson).
|
|
207
|
+
We automatically add the #to_geojson convert to prevent modification of entries by eval() in the metadata mapping.
|
|
235
208
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
['abc', 'def']
|
|
209
|
+
keyword:
|
|
210
|
+
- keyword
|
|
211
|
+
- $."keyword"#to_geojson
|
|
240
212
|
|
|
241
|
-
:
|
|
242
|
-
:return: value without quotes
|
|
243
|
-
:raises: NotImplementedError
|
|
213
|
+
:return: metadata mapping dict
|
|
244
214
|
"""
|
|
245
|
-
|
|
246
|
-
return [strip_quotes(v) for v in value]
|
|
247
|
-
elif isinstance(value, dict):
|
|
248
|
-
raise NotImplementedError("Dict value is not supported.")
|
|
249
|
-
else:
|
|
250
|
-
return str(value).strip("'\"")
|
|
215
|
+
return {k: [k, f'{{$."{k}"#to_geojson}}'] for k in ALLOWED_KEYWORDS}
|
|
251
216
|
|
|
252
217
|
|
|
253
218
|
def _update_properties_from_element(
|
|
254
|
-
prop:
|
|
219
|
+
prop: dict[str, Any], element: dict[str, Any], values: list[str]
|
|
255
220
|
) -> None:
|
|
256
221
|
"""updates a property dict with the given values based on the information from the element dict
|
|
257
222
|
e.g. the type is set based on the type of the element
|
|
@@ -318,7 +283,7 @@ def _update_properties_from_element(
|
|
|
318
283
|
|
|
319
284
|
def ecmwf_format(v: str) -> str:
|
|
320
285
|
"""Add ECMWF prefix to value v if v is a ECMWF keyword."""
|
|
321
|
-
return
|
|
286
|
+
return ECMWF_PREFIX + v if v in ALLOWED_KEYWORDS else v
|
|
322
287
|
|
|
323
288
|
|
|
324
289
|
class ECMWFSearch(PostJsonSearch):
|
|
@@ -333,7 +298,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
333
298
|
:param provider: An eodag providers configuration dictionary
|
|
334
299
|
:param config: Search plugin configuration:
|
|
335
300
|
|
|
336
|
-
* :attr:`~eodag.config.PluginConfig.remove_from_query` (``
|
|
301
|
+
* :attr:`~eodag.config.PluginConfig.remove_from_query` (``list[str]``): List of parameters
|
|
337
302
|
used to parse metadata but that must not be included to the query
|
|
338
303
|
* :attr:`~eodag.config.PluginConfig.end_date_excluded` (``bool``): Set to `False` if
|
|
339
304
|
provider does not include end date to search
|
|
@@ -350,59 +315,27 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
350
315
|
"""
|
|
351
316
|
|
|
352
317
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
353
|
-
# cache fetching method
|
|
354
|
-
self.fetch_data = functools.lru_cache()(self._fetch_data)
|
|
355
|
-
|
|
356
318
|
config.metadata_mapping = {
|
|
357
|
-
**
|
|
319
|
+
**ecmwf_mtd(),
|
|
320
|
+
**{
|
|
321
|
+
"id": "$.id",
|
|
322
|
+
"title": "$.id",
|
|
323
|
+
"storageStatus": OFFLINE_STATUS,
|
|
324
|
+
"downloadLink": "$.null",
|
|
325
|
+
"geometry": ["feature", "$.geometry"],
|
|
326
|
+
"defaultGeometry": "POLYGON((180 -90, 180 90, -180 90, -180 -90, 180 -90))",
|
|
327
|
+
},
|
|
358
328
|
**config.metadata_mapping,
|
|
359
329
|
}
|
|
360
330
|
|
|
361
331
|
super().__init__(provider, config)
|
|
362
332
|
|
|
333
|
+
# ECMWF providers do not feature any api_endpoint or next_page_query_obj.
|
|
334
|
+
# Searched is faked by EODAG.
|
|
363
335
|
self.config.__dict__.setdefault("api_endpoint", "")
|
|
364
|
-
|
|
365
|
-
# needed by QueryStringSearch.build_query_string / format_free_text_search
|
|
366
|
-
self.config.__dict__.setdefault("free_text_search_operations", {})
|
|
367
|
-
# needed for compatibility
|
|
368
336
|
self.config.pagination.setdefault("next_page_query_obj", "{{}}")
|
|
369
337
|
|
|
370
|
-
|
|
371
|
-
for product_type in self.config.products.keys():
|
|
372
|
-
if "metadata_mapping" in self.config.products[product_type].keys():
|
|
373
|
-
self.config.products[product_type][
|
|
374
|
-
"metadata_mapping"
|
|
375
|
-
] = mtd_cfg_as_conversion_and_querypath(
|
|
376
|
-
self.config.products[product_type]["metadata_mapping"]
|
|
377
|
-
)
|
|
378
|
-
# Complete and ready to use product type specific metadata-mapping
|
|
379
|
-
product_type_metadata_mapping = deepcopy(self.config.metadata_mapping)
|
|
380
|
-
|
|
381
|
-
# update config using provider product type definition metadata_mapping
|
|
382
|
-
# from another product
|
|
383
|
-
other_product_for_mapping = cast(
|
|
384
|
-
str,
|
|
385
|
-
self.config.products[product_type].get(
|
|
386
|
-
"metadata_mapping_from_product", ""
|
|
387
|
-
),
|
|
388
|
-
)
|
|
389
|
-
if other_product_for_mapping:
|
|
390
|
-
other_product_type_def_params = self.get_product_type_def_params(
|
|
391
|
-
other_product_for_mapping,
|
|
392
|
-
)
|
|
393
|
-
product_type_metadata_mapping.update(
|
|
394
|
-
other_product_type_def_params.get("metadata_mapping", {})
|
|
395
|
-
)
|
|
396
|
-
# from current product
|
|
397
|
-
product_type_metadata_mapping.update(
|
|
398
|
-
self.config.products[product_type]["metadata_mapping"]
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
self.config.products[product_type][
|
|
402
|
-
"metadata_mapping"
|
|
403
|
-
] = product_type_metadata_mapping
|
|
404
|
-
|
|
405
|
-
def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
338
|
+
def do_search(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
|
|
406
339
|
"""Should perform the actual search request.
|
|
407
340
|
|
|
408
341
|
:param args: arguments to be used in the search
|
|
@@ -416,7 +349,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
416
349
|
self,
|
|
417
350
|
prep: PreparedSearch = PreparedSearch(),
|
|
418
351
|
**kwargs: Any,
|
|
419
|
-
) ->
|
|
352
|
+
) -> tuple[list[EOProduct], Optional[int]]:
|
|
420
353
|
"""Build ready-to-download SearchResult
|
|
421
354
|
|
|
422
355
|
:param prep: :class:`~eodag.plugins.search.PreparedSearch` object containing information needed for the search
|
|
@@ -426,7 +359,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
426
359
|
product_type = prep.product_type
|
|
427
360
|
if not product_type:
|
|
428
361
|
product_type = kwargs.get("productType", None)
|
|
429
|
-
self._preprocess_search_params(kwargs, product_type)
|
|
362
|
+
kwargs = self._preprocess_search_params(kwargs, product_type)
|
|
430
363
|
result, num_items = super().query(prep, **kwargs)
|
|
431
364
|
if prep.count and not num_items:
|
|
432
365
|
num_items = 1
|
|
@@ -438,34 +371,31 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
438
371
|
super().clear()
|
|
439
372
|
|
|
440
373
|
def build_query_string(
|
|
441
|
-
self, product_type: str,
|
|
442
|
-
) ->
|
|
374
|
+
self, product_type: str, query_dict: dict[str, Any]
|
|
375
|
+
) -> tuple[dict[str, Any], str]:
|
|
443
376
|
"""Build The query string using the search parameters
|
|
444
377
|
|
|
445
378
|
:param product_type: product type id
|
|
446
|
-
:param
|
|
379
|
+
:param query_dict: keyword arguments to be used in the query string
|
|
447
380
|
:return: formatted query params and encode query string
|
|
448
381
|
"""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if v not in [NOT_AVAILABLE, NOT_MAPPED]
|
|
459
|
-
}
|
|
382
|
+
query_dict["_date"] = f"{query_dict.get(START)}/{query_dict.get(END)}"
|
|
383
|
+
|
|
384
|
+
# Reorder kwargs to make sure year/month/day/time if set overwrite default datetime.
|
|
385
|
+
priority_keys = [
|
|
386
|
+
START,
|
|
387
|
+
END,
|
|
388
|
+
]
|
|
389
|
+
ordered_kwargs = {k: query_dict[k] for k in priority_keys if k in query_dict}
|
|
390
|
+
ordered_kwargs.update(query_dict)
|
|
460
391
|
|
|
461
|
-
# build and return the query
|
|
462
392
|
return super().build_query_string(
|
|
463
|
-
product_type=product_type,
|
|
393
|
+
product_type=product_type, query_dict=ordered_kwargs
|
|
464
394
|
)
|
|
465
395
|
|
|
466
396
|
def _preprocess_search_params(
|
|
467
|
-
self, params:
|
|
468
|
-
) ->
|
|
397
|
+
self, params: dict[str, Any], product_type: Optional[str]
|
|
398
|
+
) -> dict[str, Any]:
|
|
469
399
|
"""Preprocess search parameters before making a request to the CDS API.
|
|
470
400
|
|
|
471
401
|
This method is responsible for checking and updating the provided search parameters
|
|
@@ -481,28 +411,20 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
481
411
|
# if available, update search params using datacube query-string
|
|
482
412
|
_dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
|
|
483
413
|
if "/to/" in _dc_qp.get("date", ""):
|
|
484
|
-
(
|
|
485
|
-
params["startTimeFromAscendingNode"],
|
|
486
|
-
params["completionTimeFromAscendingNode"],
|
|
487
|
-
) = _dc_qp["date"].split("/to/")
|
|
414
|
+
params[START], params[END] = _dc_qp["date"].split("/to/")
|
|
488
415
|
elif "/" in _dc_qp.get("date", ""):
|
|
489
|
-
(
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
) = _dc_qp["date"].split("/")
|
|
416
|
+
(params[START], params[END],) = _dc_qp[
|
|
417
|
+
"date"
|
|
418
|
+
].split("/")
|
|
493
419
|
elif _dc_qp.get("date", None):
|
|
494
|
-
params[
|
|
495
|
-
"completionTimeFromAscendingNode"
|
|
496
|
-
] = _dc_qp["date"]
|
|
420
|
+
params[START] = params[END] = _dc_qp["date"]
|
|
497
421
|
|
|
498
422
|
if "/" in _dc_qp.get("area", ""):
|
|
499
423
|
params["geometry"] = _dc_qp["area"].split("/")
|
|
500
424
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
dataset = params.get("ecmwf:dataset", None)
|
|
505
|
-
params["productType"] = non_none_params.get("productType", dataset)
|
|
425
|
+
params = {
|
|
426
|
+
k.removeprefix(ECMWF_PREFIX): v for k, v in params.items() if v is not None
|
|
427
|
+
}
|
|
506
428
|
|
|
507
429
|
# dates
|
|
508
430
|
# check if default dates have to be added
|
|
@@ -510,25 +432,23 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
510
432
|
self._check_date_params(params, product_type)
|
|
511
433
|
|
|
512
434
|
# adapt end date if it is midnight
|
|
513
|
-
if
|
|
435
|
+
if END in params:
|
|
514
436
|
end_date_excluded = getattr(self.config, "end_date_excluded", True)
|
|
515
437
|
is_datetime = True
|
|
516
438
|
try:
|
|
517
|
-
end_date = datetime.strptime(
|
|
518
|
-
params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%SZ"
|
|
519
|
-
)
|
|
439
|
+
end_date = datetime.strptime(params[END], "%Y-%m-%dT%H:%M:%SZ")
|
|
520
440
|
end_date = end_date.replace(tzinfo=tzutc())
|
|
521
441
|
except ValueError:
|
|
522
442
|
try:
|
|
523
443
|
end_date = datetime.strptime(
|
|
524
|
-
params[
|
|
444
|
+
params[END],
|
|
525
445
|
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
526
446
|
)
|
|
527
447
|
end_date = end_date.replace(tzinfo=tzutc())
|
|
528
448
|
except ValueError:
|
|
529
|
-
end_date = isoparse(params[
|
|
449
|
+
end_date = isoparse(params[END])
|
|
530
450
|
is_datetime = False
|
|
531
|
-
start_date = isoparse(params[
|
|
451
|
+
start_date = isoparse(params[START])
|
|
532
452
|
if (
|
|
533
453
|
not end_date_excluded
|
|
534
454
|
and is_datetime
|
|
@@ -537,15 +457,90 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
537
457
|
== end_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
538
458
|
):
|
|
539
459
|
end_date += timedelta(days=-1)
|
|
540
|
-
params[
|
|
460
|
+
params[END] = end_date.isoformat()
|
|
541
461
|
|
|
542
462
|
# geometry
|
|
543
463
|
if "geometry" in params:
|
|
544
464
|
params["geometry"] = get_geometry_from_various(geometry=params["geometry"])
|
|
545
465
|
|
|
466
|
+
return params
|
|
467
|
+
|
|
468
|
+
def _check_date_params(
|
|
469
|
+
self, keywords: dict[str, Any], product_type: Optional[str]
|
|
470
|
+
) -> None:
|
|
471
|
+
"""checks if start and end date are present in the keywords and adds them if not"""
|
|
472
|
+
|
|
473
|
+
if START and END in keywords:
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
product_type_conf = getattr(self.config, "metadata_mapping", {})
|
|
477
|
+
if (
|
|
478
|
+
product_type
|
|
479
|
+
and product_type in self.config.products
|
|
480
|
+
and "metadata_mapping" in self.config.products[product_type]
|
|
481
|
+
):
|
|
482
|
+
product_type_conf = self.config.products[product_type]["metadata_mapping"]
|
|
483
|
+
|
|
484
|
+
# start time given, end time missing
|
|
485
|
+
if START in keywords:
|
|
486
|
+
keywords[END] = (
|
|
487
|
+
keywords[START]
|
|
488
|
+
if END in product_type_conf
|
|
489
|
+
else self.get_product_type_cfg_value(
|
|
490
|
+
"missionEndDate", today().isoformat()
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
if END in product_type_conf:
|
|
496
|
+
mapping = product_type_conf[START]
|
|
497
|
+
if not isinstance(mapping, list):
|
|
498
|
+
mapping = product_type_conf[END]
|
|
499
|
+
if isinstance(mapping, list):
|
|
500
|
+
# get time parameters (date, year, month, ...) from metadata mapping
|
|
501
|
+
input_mapping = mapping[0].replace("{{", "").replace("}}", "")
|
|
502
|
+
time_params = [
|
|
503
|
+
values.split(":")[0].strip() for values in input_mapping.split(",")
|
|
504
|
+
]
|
|
505
|
+
time_params = [
|
|
506
|
+
tp.replace('"', "").replace("'", "") for tp in time_params
|
|
507
|
+
]
|
|
508
|
+
# if startTime is not given but other time params (e.g. year/month/(day)) are given,
|
|
509
|
+
# no default date is required
|
|
510
|
+
in_keywords = True
|
|
511
|
+
for tp in time_params:
|
|
512
|
+
if tp not in keywords:
|
|
513
|
+
in_keywords = False
|
|
514
|
+
break
|
|
515
|
+
if not in_keywords:
|
|
516
|
+
keywords[START] = self.get_product_type_cfg_value(
|
|
517
|
+
"missionStartDate", DEFAULT_MISSION_START_DATE
|
|
518
|
+
)
|
|
519
|
+
keywords[END] = (
|
|
520
|
+
keywords[START]
|
|
521
|
+
if END in product_type_conf
|
|
522
|
+
else self.get_product_type_cfg_value(
|
|
523
|
+
"missionEndDate", today().isoformat()
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def _get_product_type_queryables(
|
|
528
|
+
self, product_type: Optional[str], alias: Optional[str], filters: dict[str, Any]
|
|
529
|
+
) -> QueryablesDict:
|
|
530
|
+
"""Override to set additional_properties to false."""
|
|
531
|
+
default_values: dict[str, Any] = deepcopy(
|
|
532
|
+
getattr(self.config, "products", {}).get(product_type, {})
|
|
533
|
+
)
|
|
534
|
+
default_values.pop("metadata_mapping", None)
|
|
535
|
+
|
|
536
|
+
filters["productType"] = product_type
|
|
537
|
+
queryables = self.discover_queryables(**{**default_values, **filters}) or {}
|
|
538
|
+
|
|
539
|
+
return QueryablesDict(additional_properties=False, **queryables)
|
|
540
|
+
|
|
546
541
|
def discover_queryables(
|
|
547
542
|
self, **kwargs: Any
|
|
548
|
-
) -> Optional[
|
|
543
|
+
) -> Optional[dict[str, Annotated[Any, FieldInfo]]]:
|
|
549
544
|
"""Fetch queryables list from provider using its constraints file
|
|
550
545
|
|
|
551
546
|
:param kwargs: additional filters for queryables (`productType` and other search
|
|
@@ -553,65 +548,64 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
553
548
|
:returns: fetched queryable parameters dict
|
|
554
549
|
"""
|
|
555
550
|
product_type = kwargs.pop("productType")
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if "
|
|
564
|
-
|
|
551
|
+
|
|
552
|
+
pt_config = self.get_product_type_def_params(product_type)
|
|
553
|
+
|
|
554
|
+
default_values = deepcopy(pt_config)
|
|
555
|
+
default_values.pop("metadata_mapping", None)
|
|
556
|
+
filters = {**default_values, **kwargs}
|
|
557
|
+
|
|
558
|
+
if "start" in filters:
|
|
559
|
+
filters[START] = filters.pop("start")
|
|
560
|
+
if "end" in filters:
|
|
561
|
+
filters[END] = filters.pop("end")
|
|
565
562
|
|
|
566
563
|
# extract default datetime
|
|
567
|
-
|
|
568
|
-
|
|
564
|
+
processed_filters = self._preprocess_search_params(
|
|
565
|
+
deepcopy(filters), product_type
|
|
566
|
+
)
|
|
569
567
|
|
|
570
568
|
constraints_url = format_metadata(
|
|
571
569
|
getattr(self.config, "discover_queryables", {}).get("constraints_url", ""),
|
|
572
|
-
**
|
|
570
|
+
**filters,
|
|
573
571
|
)
|
|
574
|
-
constraints:
|
|
572
|
+
constraints: list[dict[str, Any]] = self._fetch_data(constraints_url)
|
|
575
573
|
|
|
576
574
|
form_url = format_metadata(
|
|
577
575
|
getattr(self.config, "discover_queryables", {}).get("form_url", ""),
|
|
578
|
-
**
|
|
576
|
+
**filters,
|
|
579
577
|
)
|
|
580
|
-
form = self.
|
|
578
|
+
form: list[dict[str, Any]] = self._fetch_data(form_url)
|
|
581
579
|
|
|
582
|
-
|
|
583
|
-
product_type,
|
|
580
|
+
formated_filters = self.format_as_provider_keyword(
|
|
581
|
+
product_type, processed_filters
|
|
584
582
|
)
|
|
585
583
|
# we re-apply kwargs input to consider override of year, month, day and time.
|
|
586
|
-
for
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
584
|
+
for k, v in {**default_values, **kwargs}.items():
|
|
585
|
+
key = k.removeprefix(ECMWF_PREFIX)
|
|
586
|
+
|
|
587
|
+
if key not in ALLOWED_KEYWORDS | {
|
|
588
|
+
START,
|
|
589
|
+
END,
|
|
592
590
|
"geom",
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
else:
|
|
591
|
+
"geometry",
|
|
592
|
+
}:
|
|
596
593
|
raise ValidationError(
|
|
597
594
|
f"{key} is not a queryable parameter for {self.provider}"
|
|
598
595
|
)
|
|
599
596
|
|
|
600
|
-
|
|
597
|
+
formated_filters[key] = v
|
|
598
|
+
|
|
599
|
+
# we use non empty filters as default to integrate user inputs
|
|
601
600
|
# it is needed because pydantic json schema does not represent "value"
|
|
602
601
|
# but only "default"
|
|
603
|
-
non_empty_formated:
|
|
602
|
+
non_empty_formated: dict[str, Any] = {
|
|
604
603
|
k: v
|
|
605
|
-
for k, v in
|
|
606
|
-
if v and (not isinstance(v, list) or all(v))
|
|
607
|
-
}
|
|
608
|
-
non_empty_kwargs: Dict[str, Any] = {
|
|
609
|
-
k: v
|
|
610
|
-
for k, v in processed_kwargs.items()
|
|
604
|
+
for k, v in formated_filters.items()
|
|
611
605
|
if v and (not isinstance(v, list) or all(v))
|
|
612
606
|
}
|
|
613
607
|
|
|
614
|
-
required_keywords:
|
|
608
|
+
required_keywords: set[str] = set()
|
|
615
609
|
|
|
616
610
|
# calculate available values
|
|
617
611
|
if constraints:
|
|
@@ -625,33 +619,36 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
625
619
|
# Pre-compute the required keywords (present in all constraint dicts)
|
|
626
620
|
# when form, required keywords are extracted directly from form
|
|
627
621
|
if not form:
|
|
628
|
-
required_keywords = set
|
|
629
|
-
|
|
630
|
-
|
|
622
|
+
required_keywords = set.intersection(
|
|
623
|
+
*(map(lambda d: set(d.keys()), constraints))
|
|
624
|
+
)
|
|
625
|
+
|
|
631
626
|
else:
|
|
632
627
|
values_url = getattr(self.config, "available_values_url", "")
|
|
633
628
|
if not values_url:
|
|
634
629
|
return self.queryables_from_metadata_mapping(product_type)
|
|
635
630
|
if "{" in values_url:
|
|
636
|
-
values_url = values_url.format(
|
|
637
|
-
data = self.
|
|
631
|
+
values_url = values_url.format(**filters)
|
|
632
|
+
data = self._fetch_data(values_url)
|
|
638
633
|
available_values = data["constraints"]
|
|
639
634
|
required_keywords = data.get("required", [])
|
|
640
635
|
|
|
641
636
|
# To check if all keywords are queryable parameters, we check if they are in the
|
|
642
637
|
# available values or the product type config (available values calculated from the
|
|
643
638
|
# constraints might not include all queryables)
|
|
644
|
-
for keyword in
|
|
639
|
+
for keyword in filters:
|
|
645
640
|
if (
|
|
646
641
|
keyword
|
|
647
642
|
not in available_values.keys()
|
|
648
|
-
|
|
|
643
|
+
| pt_config.keys()
|
|
649
644
|
| {
|
|
650
|
-
|
|
651
|
-
|
|
645
|
+
START,
|
|
646
|
+
END,
|
|
652
647
|
"geom",
|
|
653
648
|
}
|
|
654
|
-
and keyword
|
|
649
|
+
and keyword not in [f["name"] for f in form]
|
|
650
|
+
and keyword.removeprefix(ECMWF_PREFIX)
|
|
651
|
+
not in set(list(available_values.keys()) + [f["name"] for f in form])
|
|
655
652
|
):
|
|
656
653
|
raise ValidationError(f"{keyword} is not a queryable parameter")
|
|
657
654
|
|
|
@@ -664,24 +661,24 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
664
661
|
)
|
|
665
662
|
else:
|
|
666
663
|
queryables = self.queryables_by_values(
|
|
667
|
-
available_values, list(required_keywords),
|
|
664
|
+
available_values, list(required_keywords), non_empty_formated
|
|
668
665
|
)
|
|
669
666
|
|
|
670
667
|
# ecmwf:date is replaced by start and end.
|
|
671
668
|
# start and end filters are supported whenever combinations of "year", "month", "day" filters exist
|
|
672
669
|
if (
|
|
673
|
-
queryables.pop("
|
|
674
|
-
or "
|
|
675
|
-
or "
|
|
670
|
+
queryables.pop(f"{ECMWF_PREFIX}date", None)
|
|
671
|
+
or f"{ECMWF_PREFIX}year" in queryables
|
|
672
|
+
or f"{ECMWF_PREFIX}hyear" in queryables
|
|
676
673
|
):
|
|
677
674
|
queryables.update(
|
|
678
675
|
{
|
|
679
676
|
"start": Queryables.get_with_default(
|
|
680
|
-
"start",
|
|
677
|
+
"start", processed_filters.get(START)
|
|
681
678
|
),
|
|
682
679
|
"end": Queryables.get_with_default(
|
|
683
680
|
"end",
|
|
684
|
-
|
|
681
|
+
processed_filters.get(END),
|
|
685
682
|
),
|
|
686
683
|
}
|
|
687
684
|
)
|
|
@@ -689,7 +686,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
689
686
|
# area is geom in EODAG.
|
|
690
687
|
if queryables.pop("area", None):
|
|
691
688
|
queryables["geom"] = Annotated[
|
|
692
|
-
Union[str,
|
|
689
|
+
Union[str, dict[str, float], BaseGeometry],
|
|
693
690
|
Field(
|
|
694
691
|
None,
|
|
695
692
|
description="Read EODAG documentation for all supported geometry format.",
|
|
@@ -700,10 +697,10 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
700
697
|
|
|
701
698
|
def available_values_from_constraints(
|
|
702
699
|
self,
|
|
703
|
-
constraints: list[
|
|
704
|
-
input_keywords:
|
|
705
|
-
form_keywords:
|
|
706
|
-
) ->
|
|
700
|
+
constraints: list[dict[str, Any]],
|
|
701
|
+
input_keywords: dict[str, Any],
|
|
702
|
+
form_keywords: list[str],
|
|
703
|
+
) -> dict[str, list[str]]:
|
|
707
704
|
"""
|
|
708
705
|
Filter constraints using input_keywords. Return list of available queryables.
|
|
709
706
|
All constraint entries must have the same parameters.
|
|
@@ -727,9 +724,9 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
727
724
|
)
|
|
728
725
|
|
|
729
726
|
# filter constraint entries matching input keyword values
|
|
730
|
-
filtered_constraints:
|
|
727
|
+
filtered_constraints: list[dict[str, Any]]
|
|
731
728
|
|
|
732
|
-
parsed_keywords:
|
|
729
|
+
parsed_keywords: list[str] = []
|
|
733
730
|
for keyword in ordered_keywords:
|
|
734
731
|
values = input_keywords.get(keyword)
|
|
735
732
|
|
|
@@ -742,14 +739,15 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
742
739
|
raise ValidationError(
|
|
743
740
|
f"Parameter value as object is not supported: {keyword}={values}"
|
|
744
741
|
)
|
|
745
|
-
filter_v = values if isinstance(values, (list, tuple)) else [values]
|
|
746
742
|
|
|
747
743
|
# We convert every single value to a list of string
|
|
744
|
+
filter_v = values if isinstance(values, (list, tuple)) else [values]
|
|
745
|
+
|
|
748
746
|
# We strip values of superfluous quotes (added by mapping converter to_geojson).
|
|
749
747
|
# ECMWF accept values with /to/. We need to split it to an array
|
|
750
748
|
# ECMWF accept values in format val1/val2. We need to split it to an array
|
|
751
749
|
sep = re.compile(r"/to/|/")
|
|
752
|
-
filter_v = [i for v in filter_v for i in sep.split(
|
|
750
|
+
filter_v = [i for v in filter_v for i in sep.split(str(v))]
|
|
753
751
|
|
|
754
752
|
# special handling for time 0000 converted to 0 by pre-formating with metadata_mapping
|
|
755
753
|
if keyword.split(":")[-1] == "time":
|
|
@@ -768,7 +766,10 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
768
766
|
# we assume that if the first value is an interval, all values are intervals
|
|
769
767
|
present_values = []
|
|
770
768
|
if keyword == "date" and "/" in entry[keyword][0]:
|
|
771
|
-
|
|
769
|
+
input_range = values
|
|
770
|
+
if isinstance(values, list):
|
|
771
|
+
input_range = values[0]
|
|
772
|
+
if any(is_range_in_range(x, input_range) for x in entry[keyword]):
|
|
772
773
|
present_values = filter_v
|
|
773
774
|
else:
|
|
774
775
|
present_values = [
|
|
@@ -788,12 +789,12 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
788
789
|
{value for c in constraints for value in c.get(keyword, [])}
|
|
789
790
|
)
|
|
790
791
|
# restore ecmwf: prefix before raising error
|
|
791
|
-
keyword =
|
|
792
|
+
keyword = ECMWF_PREFIX + keyword
|
|
792
793
|
|
|
793
794
|
all_keywords_str = ""
|
|
794
795
|
if len(parsed_keywords) > 1:
|
|
795
796
|
keywords = [
|
|
796
|
-
f"
|
|
797
|
+
f"{ECMWF_PREFIX + k}={pk}"
|
|
797
798
|
for k in parsed_keywords
|
|
798
799
|
if (pk := input_keywords.get(k))
|
|
799
800
|
]
|
|
@@ -808,7 +809,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
808
809
|
parsed_keywords.append(keyword)
|
|
809
810
|
constraints = filtered_constraints
|
|
810
811
|
|
|
811
|
-
available_values:
|
|
812
|
+
available_values: dict[str, Any] = {k: set() for k in ordered_keywords}
|
|
812
813
|
|
|
813
814
|
# we aggregate the constraint entries left
|
|
814
815
|
for entry in constraints:
|
|
@@ -819,10 +820,10 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
819
820
|
|
|
820
821
|
def queryables_by_form(
|
|
821
822
|
self,
|
|
822
|
-
form:
|
|
823
|
-
available_values:
|
|
824
|
-
defaults:
|
|
825
|
-
) ->
|
|
823
|
+
form: list[dict[str, Any]],
|
|
824
|
+
available_values: dict[str, list[str]],
|
|
825
|
+
defaults: dict[str, Any],
|
|
826
|
+
) -> dict[str, Annotated[Any, FieldInfo]]:
|
|
826
827
|
"""
|
|
827
828
|
Generate Annotated field definitions from form entries and available values
|
|
828
829
|
Used by Copernicus services like cop_cds, cop_ads, cop_ewds.
|
|
@@ -832,9 +833,9 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
832
833
|
:param defaults: default values for the parameters
|
|
833
834
|
:return: dict of annotated queryables
|
|
834
835
|
"""
|
|
835
|
-
queryables:
|
|
836
|
+
queryables: dict[str, Annotated[Any, FieldInfo]] = {}
|
|
836
837
|
|
|
837
|
-
required_list:
|
|
838
|
+
required_list: list[str] = []
|
|
838
839
|
for element in form:
|
|
839
840
|
name: str = element["name"]
|
|
840
841
|
|
|
@@ -842,6 +843,8 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
842
843
|
if name in ("area_group", "global", "warning", "licences"):
|
|
843
844
|
continue
|
|
844
845
|
if "type" not in element or element["type"] == "FreeEditionWidget":
|
|
846
|
+
# FreeEditionWidget used to select the whole available region
|
|
847
|
+
# and to provide comments for the dataset
|
|
845
848
|
continue
|
|
846
849
|
|
|
847
850
|
# ordering done by id -> set id to high value if not present -> element will be last
|
|
@@ -869,18 +872,11 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
869
872
|
if fields and (comment := fields[0].get("comment")):
|
|
870
873
|
prop["description"] = comment
|
|
871
874
|
|
|
872
|
-
if d := details.get("default"):
|
|
873
|
-
default = default or (d[0] if fields else d)
|
|
874
|
-
|
|
875
875
|
if name == "area" and isinstance(default, dict):
|
|
876
876
|
default = list(default.values())
|
|
877
877
|
|
|
878
|
-
if default:
|
|
879
|
-
# We strip values of superfluous quotes (addded by mapping converter to_geojson).
|
|
880
|
-
default = strip_quotes(default)
|
|
881
|
-
|
|
882
878
|
# sometimes form returns default as array instead of string
|
|
883
|
-
if default and prop
|
|
879
|
+
if default and prop.get("type") == "string" and isinstance(default, list):
|
|
884
880
|
default = ",".join(default)
|
|
885
881
|
|
|
886
882
|
is_required = bool(element.get("required"))
|
|
@@ -901,10 +897,10 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
901
897
|
|
|
902
898
|
def queryables_by_values(
|
|
903
899
|
self,
|
|
904
|
-
available_values:
|
|
905
|
-
required_keywords:
|
|
906
|
-
defaults:
|
|
907
|
-
) ->
|
|
900
|
+
available_values: dict[str, list[str]],
|
|
901
|
+
required_keywords: list[str],
|
|
902
|
+
defaults: dict[str, Any],
|
|
903
|
+
) -> dict[str, Annotated[Any, FieldInfo]]:
|
|
908
904
|
"""
|
|
909
905
|
Generate Annotated field definitions from available values.
|
|
910
906
|
Used by ECMWF data providers like dedt_lumi.
|
|
@@ -918,19 +914,17 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
918
914
|
# Needed to map constraints like "xxxx" to eodag parameter "ecmwf:xxxx"
|
|
919
915
|
required = [ecmwf_format(k) for k in required_keywords]
|
|
920
916
|
|
|
921
|
-
queryables:
|
|
917
|
+
queryables: dict[str, Annotated[Any, FieldInfo]] = {}
|
|
922
918
|
for name, values in available_values.items():
|
|
923
919
|
# Rename keywords from form with metadata mapping.
|
|
924
920
|
# Needed to map constraints like "xxxx" to eodag parameter "ecmwf:xxxx"
|
|
925
921
|
key = ecmwf_format(name)
|
|
926
922
|
|
|
927
|
-
default = defaults.get(key)
|
|
928
|
-
|
|
929
923
|
queryables[key] = Annotated[
|
|
930
924
|
get_args(
|
|
931
925
|
json_field_definition_to_python(
|
|
932
926
|
{"type": "string", "title": name, "enum": values},
|
|
933
|
-
default_value=
|
|
927
|
+
default_value=defaults.get(name),
|
|
934
928
|
required=bool(key in required),
|
|
935
929
|
)
|
|
936
930
|
)
|
|
@@ -939,24 +933,34 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
939
933
|
return queryables
|
|
940
934
|
|
|
941
935
|
def format_as_provider_keyword(
|
|
942
|
-
self, product_type: str, properties:
|
|
943
|
-
) ->
|
|
936
|
+
self, product_type: str, properties: dict[str, Any]
|
|
937
|
+
) -> dict[str, Any]:
|
|
944
938
|
"""Return provider equivalent keyword names from EODAG keywords.
|
|
945
939
|
|
|
946
940
|
:param product_type: product type id
|
|
947
941
|
:param properties: dict of properties to be formatted
|
|
948
942
|
:return: dict of formatted properties
|
|
949
943
|
"""
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
944
|
+
properties["productType"] = product_type
|
|
945
|
+
|
|
946
|
+
# provider product type specific conf
|
|
947
|
+
product_type_def_params = self.get_product_type_def_params(
|
|
948
|
+
product_type, format_variables=properties
|
|
953
949
|
)
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
950
|
+
|
|
951
|
+
# Add to the query, the queryable parameters set in the provider product type definition
|
|
952
|
+
properties.update(
|
|
953
|
+
{
|
|
954
|
+
k: v
|
|
955
|
+
for k, v in product_type_def_params.items()
|
|
956
|
+
if k not in properties.keys()
|
|
957
|
+
and k in self.config.metadata_mapping.keys()
|
|
958
|
+
and isinstance(self.config.metadata_mapping[k], list)
|
|
959
|
+
}
|
|
960
|
+
)
|
|
961
|
+
qp, _ = self.build_query_string(product_type, properties)
|
|
962
|
+
|
|
963
|
+
return qp
|
|
960
964
|
|
|
961
965
|
def _fetch_data(self, url: str) -> Any:
|
|
962
966
|
"""
|
|
@@ -973,12 +977,12 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
973
977
|
if hasattr(self, "auth") and isinstance(self.auth, AuthBase)
|
|
974
978
|
else None
|
|
975
979
|
)
|
|
976
|
-
timeout = getattr(self.config, "timeout",
|
|
977
|
-
return fetch_json(url, auth=auth, timeout=timeout)
|
|
980
|
+
timeout = getattr(self.config, "timeout", DEFAULT_SEARCH_TIMEOUT)
|
|
981
|
+
return functools.lru_cache()(fetch_json)(url, auth=auth, timeout=timeout)
|
|
978
982
|
|
|
979
983
|
def normalize_results(
|
|
980
984
|
self, results: RawSearchResult, **kwargs: Any
|
|
981
|
-
) ->
|
|
985
|
+
) -> list[EOProduct]:
|
|
982
986
|
"""Build :class:`~eodag.api.product._product.EOProduct` from provider result
|
|
983
987
|
|
|
984
988
|
:param results: Raw provider result as single dict in list
|
|
@@ -1044,67 +1048,37 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1044
1048
|
discovery_config=getattr(self.config, "discover_metadata", {}),
|
|
1045
1049
|
)
|
|
1046
1050
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1051
|
+
properties = {
|
|
1052
|
+
# use product_type_config as default properties
|
|
1053
|
+
**getattr(self.config, "product_type_config", {}),
|
|
1054
|
+
**{ecmwf_format(k): v for k, v in parsed_properties.items()},
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
def slugify(date_str: str) -> str:
|
|
1058
|
+
return date_str.split("T")[0].replace("-", "")
|
|
1049
1059
|
|
|
1050
1060
|
# build product id
|
|
1051
|
-
|
|
1052
|
-
if (
|
|
1053
|
-
"startTimeFromAscendingNode" in parsed_properties
|
|
1054
|
-
and parsed_properties["startTimeFromAscendingNode"] != "Not Available"
|
|
1055
|
-
and "completionTimeFromAscendingNode" in parsed_properties
|
|
1056
|
-
and parsed_properties["completionTimeFromAscendingNode"] != "Not Available"
|
|
1057
|
-
):
|
|
1058
|
-
product_id = "%s_%s_%s_%s" % (
|
|
1059
|
-
id_prefix,
|
|
1060
|
-
parsed_properties["startTimeFromAscendingNode"]
|
|
1061
|
-
.split("T")[0]
|
|
1062
|
-
.replace("-", ""),
|
|
1063
|
-
parsed_properties["completionTimeFromAscendingNode"]
|
|
1064
|
-
.split("T")[0]
|
|
1065
|
-
.replace("-", ""),
|
|
1066
|
-
query_hash,
|
|
1067
|
-
)
|
|
1068
|
-
elif (
|
|
1069
|
-
"startTimeFromAscendingNode" in parsed_properties
|
|
1070
|
-
and parsed_properties["startTimeFromAscendingNode"] != "Not Available"
|
|
1071
|
-
):
|
|
1072
|
-
product_id = "%s_%s_%s" % (
|
|
1073
|
-
id_prefix,
|
|
1074
|
-
parsed_properties["startTimeFromAscendingNode"]
|
|
1075
|
-
.split("T")[0]
|
|
1076
|
-
.replace("-", ""),
|
|
1077
|
-
query_hash,
|
|
1078
|
-
)
|
|
1079
|
-
else:
|
|
1080
|
-
product_id = f"{id_prefix}_{query_hash}"
|
|
1081
|
-
|
|
1082
|
-
parsed_properties["id"] = parsed_properties["title"] = product_id
|
|
1083
|
-
|
|
1084
|
-
# update downloadLink and orderLink
|
|
1085
|
-
parsed_properties["_dc_qs"] = quote_plus(qs)
|
|
1086
|
-
if parsed_properties["downloadLink"] != "Not Available":
|
|
1087
|
-
parsed_properties["downloadLink"] += f"?{qs}"
|
|
1088
|
-
|
|
1089
|
-
# parse metadata needing downloadLink
|
|
1090
|
-
dl_path = Fields("downloadLink")
|
|
1091
|
-
dl_path_from_root = Child(Root(), dl_path)
|
|
1092
|
-
for param, mapping in self.config.metadata_mapping.items():
|
|
1093
|
-
if dl_path in mapping or dl_path_from_root in mapping:
|
|
1094
|
-
parsed_properties.update(
|
|
1095
|
-
properties_from_json(parsed_properties, {param: mapping})
|
|
1096
|
-
)
|
|
1061
|
+
product_id = (product_type or kwargs.get("dataset") or self.provider).upper()
|
|
1097
1062
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1063
|
+
start = properties.get(START, NOT_AVAILABLE)
|
|
1064
|
+
end = properties.get(END, NOT_AVAILABLE)
|
|
1065
|
+
|
|
1066
|
+
if start != NOT_AVAILABLE:
|
|
1067
|
+
product_id += f"_{slugify(start)}"
|
|
1068
|
+
if end != NOT_AVAILABLE:
|
|
1069
|
+
product_id += f"_{slugify(end)}"
|
|
1070
|
+
|
|
1071
|
+
product_id += f"_{query_hash}"
|
|
1072
|
+
|
|
1073
|
+
properties["id"] = properties["title"] = product_id
|
|
1074
|
+
|
|
1075
|
+
# used by server mode to generate downloadlink href
|
|
1076
|
+
properties["_dc_qs"] = quote_plus(qs)
|
|
1103
1077
|
|
|
1104
1078
|
product = EOProduct(
|
|
1105
1079
|
provider=self.provider,
|
|
1106
1080
|
productType=product_type,
|
|
1107
|
-
properties=
|
|
1081
|
+
properties=properties,
|
|
1108
1082
|
)
|
|
1109
1083
|
|
|
1110
1084
|
return [
|
|
@@ -1150,7 +1124,7 @@ class MeteoblueSearch(ECMWFSearch):
|
|
|
1150
1124
|
self,
|
|
1151
1125
|
prep: PreparedSearch = PreparedSearch(),
|
|
1152
1126
|
**kwargs: Any,
|
|
1153
|
-
) ->
|
|
1127
|
+
) -> tuple[list[str], int]:
|
|
1154
1128
|
"""Wraps PostJsonSearch.collect_search_urls to force product count to 1
|
|
1155
1129
|
|
|
1156
1130
|
:param prep: :class:`~eodag.plugins.search.PreparedSearch` object containing information for the search
|
|
@@ -1162,7 +1136,7 @@ class MeteoblueSearch(ECMWFSearch):
|
|
|
1162
1136
|
|
|
1163
1137
|
def do_search(
|
|
1164
1138
|
self, prep: PreparedSearch = PreparedSearch(items_per_page=None), **kwargs: Any
|
|
1165
|
-
) ->
|
|
1139
|
+
) -> list[dict[str, Any]]:
|
|
1166
1140
|
"""Perform the actual search request, and return result in a single element.
|
|
1167
1141
|
|
|
1168
1142
|
:param prep: :class:`~eodag.plugins.search.PreparedSearch` object containing information for the search
|
|
@@ -1181,17 +1155,15 @@ class MeteoblueSearch(ECMWFSearch):
|
|
|
1181
1155
|
return [response.json()]
|
|
1182
1156
|
|
|
1183
1157
|
def build_query_string(
|
|
1184
|
-
self, product_type: str,
|
|
1185
|
-
) ->
|
|
1158
|
+
self, product_type: str, query_dict: dict[str, Any]
|
|
1159
|
+
) -> tuple[dict[str, Any], str]:
|
|
1186
1160
|
"""Build The query string using the search parameters
|
|
1187
1161
|
|
|
1188
1162
|
:param product_type: product type id
|
|
1189
|
-
:param
|
|
1163
|
+
:param query_dict: keyword arguments to be used in the query string
|
|
1190
1164
|
:return: formatted query params and encode query string
|
|
1191
1165
|
"""
|
|
1192
|
-
return QueryStringSearch.build_query_string(
|
|
1193
|
-
self, product_type=product_type, **kwargs
|
|
1194
|
-
)
|
|
1166
|
+
return QueryStringSearch.build_query_string(self, product_type, query_dict)
|
|
1195
1167
|
|
|
1196
1168
|
|
|
1197
1169
|
class WekeoECMWFSearch(ECMWFSearch):
|
|
@@ -1221,7 +1193,7 @@ class WekeoECMWFSearch(ECMWFSearch):
|
|
|
1221
1193
|
|
|
1222
1194
|
def normalize_results(
|
|
1223
1195
|
self, results: RawSearchResult, **kwargs: Any
|
|
1224
|
-
) ->
|
|
1196
|
+
) -> list[EOProduct]:
|
|
1225
1197
|
"""Build :class:`~eodag.api.product._product.EOProduct` from provider result
|
|
1226
1198
|
|
|
1227
1199
|
:param results: Raw provider result as single dict in list
|
|
@@ -1247,7 +1219,7 @@ class WekeoECMWFSearch(ECMWFSearch):
|
|
|
1247
1219
|
|
|
1248
1220
|
return normalized
|
|
1249
1221
|
|
|
1250
|
-
def do_search(self, *args: Any, **kwargs: Any) ->
|
|
1222
|
+
def do_search(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
|
|
1251
1223
|
"""Should perform the actual search request.
|
|
1252
1224
|
|
|
1253
1225
|
:param args: arguments to be used in the search
|
|
@@ -1255,25 +1227,3 @@ class WekeoECMWFSearch(ECMWFSearch):
|
|
|
1255
1227
|
:return: list containing the results from the provider in json format
|
|
1256
1228
|
"""
|
|
1257
1229
|
return QueryStringSearch.do_search(self, *args, **kwargs)
|
|
1258
|
-
|
|
1259
|
-
def build_query_string(
|
|
1260
|
-
self, product_type: str, **kwargs: Any
|
|
1261
|
-
) -> Tuple[Dict[str, Any], str]:
|
|
1262
|
-
"""Build The query string using the search parameters
|
|
1263
|
-
|
|
1264
|
-
:param product_type: product type id
|
|
1265
|
-
:param kwargs: keyword arguments to be used in the query string
|
|
1266
|
-
:return: formatted query params and encode query string
|
|
1267
|
-
"""
|
|
1268
|
-
# Reorder kwargs to make sure year/month/day/time if set overwrite default datetime.
|
|
1269
|
-
# strip_quotes to remove duplicated quotes like "'1_1'" produced by convertors like to_geojson.
|
|
1270
|
-
priority_keys = [
|
|
1271
|
-
"startTimeFromAscendingNode",
|
|
1272
|
-
"completionTimeFromAscendingNode",
|
|
1273
|
-
]
|
|
1274
|
-
ordered_kwargs = {k: kwargs[k] for k in priority_keys if k in kwargs}
|
|
1275
|
-
ordered_kwargs.update({k: strip_quotes(v) for k, v in kwargs.items()})
|
|
1276
|
-
|
|
1277
|
-
return QueryStringSearch.build_query_string(
|
|
1278
|
-
self, product_type=product_type, **ordered_kwargs
|
|
1279
|
-
)
|