eodag 3.1.0b2__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 +10 -11
- eodag/api/product/_assets.py +45 -9
- eodag/api/product/_product.py +14 -18
- eodag/api/product/metadata_mapping.py +23 -5
- eodag/config.py +11 -11
- eodag/plugins/apis/ecmwf.py +2 -6
- eodag/plugins/apis/usgs.py +1 -1
- eodag/plugins/authentication/openid_connect.py +6 -0
- eodag/plugins/download/aws.py +90 -11
- eodag/plugins/search/base.py +3 -2
- eodag/plugins/search/build_search_result.py +229 -280
- eodag/plugins/search/data_request_search.py +3 -3
- eodag/plugins/search/qssearch.py +32 -103
- eodag/plugins/search/static_stac_search.py +1 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +564 -114
- eodag/resources/providers.yml +947 -1164
- eodag/resources/user_conf_template.yml +1 -11
- eodag/rest/stac.py +1 -0
- eodag/rest/types/queryables.py +28 -16
- eodag/types/__init__.py +73 -11
- eodag/utils/__init__.py +1 -1
- eodag/utils/s3.py +31 -8
- {eodag-3.1.0b2.dist-info → eodag-3.2.0.dist-info}/METADATA +10 -9
- {eodag-3.1.0b2.dist-info → eodag-3.2.0.dist-info}/RECORD +29 -29
- {eodag-3.1.0b2.dist-info → eodag-3.2.0.dist-info}/WHEEL +1 -1
- {eodag-3.1.0b2.dist-info → eodag-3.2.0.dist-info}/entry_points.txt +0 -0
- {eodag-3.1.0b2.dist-info → eodag-3.2.0.dist-info/licenses}/LICENSE +0 -0
- {eodag-3.1.0b2.dist-info → eodag-3.2.0.dist-info}/top_level.txt +0 -0
|
@@ -23,14 +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 TYPE_CHECKING, Annotated, Any, Optional, Union
|
|
26
|
+
from typing import TYPE_CHECKING, Annotated, Any, Optional, Union
|
|
27
27
|
from urllib.parse import quote_plus, unquote_plus
|
|
28
28
|
|
|
29
29
|
import geojson
|
|
30
30
|
import orjson
|
|
31
31
|
from dateutil.parser import isoparse
|
|
32
32
|
from dateutil.tz import tzutc
|
|
33
|
-
from
|
|
33
|
+
from dateutil.utils import today
|
|
34
34
|
from pydantic import Field
|
|
35
35
|
from pydantic.fields import FieldInfo
|
|
36
36
|
from requests.auth import AuthBase
|
|
@@ -40,10 +40,8 @@ from typing_extensions import get_args
|
|
|
40
40
|
from eodag.api.product import EOProduct
|
|
41
41
|
from eodag.api.product.metadata_mapping import (
|
|
42
42
|
NOT_AVAILABLE,
|
|
43
|
-
|
|
43
|
+
OFFLINE_STATUS,
|
|
44
44
|
format_metadata,
|
|
45
|
-
format_query_params,
|
|
46
|
-
mtd_cfg_as_conversion_and_querypath,
|
|
47
45
|
properties_from_json,
|
|
48
46
|
)
|
|
49
47
|
from eodag.api.search_result import RawSearchResult
|
|
@@ -52,6 +50,7 @@ from eodag.plugins.search.qssearch import PostJsonSearch, QueryStringSearch
|
|
|
52
50
|
from eodag.types import json_field_definition_to_python
|
|
53
51
|
from eodag.types.queryables import Queryables, QueryablesDict
|
|
54
52
|
from eodag.utils import (
|
|
53
|
+
DEFAULT_MISSION_START_DATE,
|
|
55
54
|
DEFAULT_SEARCH_TIMEOUT,
|
|
56
55
|
deepcopy,
|
|
57
56
|
dict_items_recursive_sort,
|
|
@@ -66,9 +65,11 @@ if TYPE_CHECKING:
|
|
|
66
65
|
|
|
67
66
|
logger = logging.getLogger("eodag.search.build_search_result")
|
|
68
67
|
|
|
68
|
+
ECMWF_PREFIX = "ecmwf:"
|
|
69
|
+
|
|
69
70
|
# keywords from ECMWF keyword database + "dataset" (not part of database but exists)
|
|
70
71
|
# database: https://confluence.ecmwf.int/display/UDOC/Keywords+in+MARS+and+Dissemination+requests
|
|
71
|
-
ECMWF_KEYWORDS =
|
|
72
|
+
ECMWF_KEYWORDS = {
|
|
72
73
|
"dataset",
|
|
73
74
|
"accuracy",
|
|
74
75
|
"activity",
|
|
@@ -132,10 +133,10 @@ ECMWF_KEYWORDS = [
|
|
|
132
133
|
"truncation",
|
|
133
134
|
"type",
|
|
134
135
|
"use",
|
|
135
|
-
|
|
136
|
+
}
|
|
136
137
|
|
|
137
138
|
# additional keywords from copernicus services
|
|
138
|
-
COP_DS_KEYWORDS =
|
|
139
|
+
COP_DS_KEYWORDS = {
|
|
139
140
|
"aerosol_type",
|
|
140
141
|
"altitude",
|
|
141
142
|
"product_type",
|
|
@@ -190,55 +191,28 @@ COP_DS_KEYWORDS = [
|
|
|
190
191
|
"variable_type",
|
|
191
192
|
"version",
|
|
192
193
|
"year",
|
|
193
|
-
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
ALLOWED_KEYWORDS = ECMWF_KEYWORDS | COP_DS_KEYWORDS
|
|
197
|
+
|
|
198
|
+
END = "completionTimeFromAscendingNode"
|
|
199
|
+
|
|
200
|
+
START = "startTimeFromAscendingNode"
|
|
194
201
|
|
|
195
202
|
|
|
196
|
-
def
|
|
197
|
-
keywords: list[str], prefix: Optional[str] = None
|
|
198
|
-
) -> dict[str, Any]:
|
|
203
|
+
def ecmwf_mtd() -> dict[str, Any]:
|
|
199
204
|
"""
|
|
200
|
-
Make metadata mapping dict from a list of
|
|
205
|
+
Make metadata mapping dict from a list of defined ECMWF Keywords
|
|
201
206
|
|
|
202
|
-
|
|
203
|
-
- keyword
|
|
204
|
-
- $."prefix:keyword"
|
|
207
|
+
We automatically add the #to_geojson convert to prevent modification of entries by eval() in the metadata mapping.
|
|
205
208
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{'ecmwf:month': ['month', '$."ecmwf:month"'], 'ecmwf:year': ['year', '$."ecmwf:year"']}
|
|
209
|
+
keyword:
|
|
210
|
+
- keyword
|
|
211
|
+
- $."keyword"#to_geojson
|
|
210
212
|
|
|
211
|
-
:param keywords: List of keywords to be converted
|
|
212
|
-
:param prefix: prefix to be added to the parameter in the mapping
|
|
213
213
|
:return: metadata mapping dict
|
|
214
214
|
"""
|
|
215
|
-
|
|
216
|
-
for keyword in keywords:
|
|
217
|
-
key = f"{prefix}:{keyword}" if prefix else keyword
|
|
218
|
-
mdt[key] = [keyword, f'$."{key}"']
|
|
219
|
-
return mdt
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def strip_quotes(value: Any) -> Any:
|
|
223
|
-
"""Strip superfluous quotes from elements (added by mapping converter to_geojson).
|
|
224
|
-
|
|
225
|
-
>>> strip_quotes("'abc'")
|
|
226
|
-
'abc'
|
|
227
|
-
>>> strip_quotes(["'abc'", '"def'])
|
|
228
|
-
['abc', 'def']
|
|
229
|
-
>>> strip_quotes({"'abc'": 'def"'})
|
|
230
|
-
{'abc': 'def'}
|
|
231
|
-
|
|
232
|
-
:param value: value from which quotes should be removed (should be either str or list)
|
|
233
|
-
:return: value without quotes
|
|
234
|
-
:raises: NotImplementedError
|
|
235
|
-
"""
|
|
236
|
-
if isinstance(value, (list, tuple)):
|
|
237
|
-
return [strip_quotes(v) for v in value]
|
|
238
|
-
elif isinstance(value, dict):
|
|
239
|
-
return {strip_quotes(k): strip_quotes(v) for k, v in value.items()}
|
|
240
|
-
else:
|
|
241
|
-
return str(value).strip("'\"")
|
|
215
|
+
return {k: [k, f'{{$."{k}"#to_geojson}}'] for k in ALLOWED_KEYWORDS}
|
|
242
216
|
|
|
243
217
|
|
|
244
218
|
def _update_properties_from_element(
|
|
@@ -309,7 +283,7 @@ def _update_properties_from_element(
|
|
|
309
283
|
|
|
310
284
|
def ecmwf_format(v: str) -> str:
|
|
311
285
|
"""Add ECMWF prefix to value v if v is a ECMWF keyword."""
|
|
312
|
-
return
|
|
286
|
+
return ECMWF_PREFIX + v if v in ALLOWED_KEYWORDS else v
|
|
313
287
|
|
|
314
288
|
|
|
315
289
|
class ECMWFSearch(PostJsonSearch):
|
|
@@ -342,54 +316,25 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
342
316
|
|
|
343
317
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
344
318
|
config.metadata_mapping = {
|
|
345
|
-
**
|
|
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
|
+
},
|
|
346
328
|
**config.metadata_mapping,
|
|
347
329
|
}
|
|
348
330
|
|
|
349
331
|
super().__init__(provider, config)
|
|
350
332
|
|
|
333
|
+
# ECMWF providers do not feature any api_endpoint or next_page_query_obj.
|
|
334
|
+
# Searched is faked by EODAG.
|
|
351
335
|
self.config.__dict__.setdefault("api_endpoint", "")
|
|
352
|
-
|
|
353
|
-
# needed by QueryStringSearch.build_query_string / format_free_text_search
|
|
354
|
-
self.config.__dict__.setdefault("free_text_search_operations", {})
|
|
355
|
-
# needed for compatibility
|
|
356
336
|
self.config.pagination.setdefault("next_page_query_obj", "{{}}")
|
|
357
337
|
|
|
358
|
-
# parse jsonpath on init: product type specific metadata-mapping
|
|
359
|
-
for product_type in self.config.products.keys():
|
|
360
|
-
if "metadata_mapping" in self.config.products[product_type].keys():
|
|
361
|
-
self.config.products[product_type][
|
|
362
|
-
"metadata_mapping"
|
|
363
|
-
] = mtd_cfg_as_conversion_and_querypath(
|
|
364
|
-
self.config.products[product_type]["metadata_mapping"]
|
|
365
|
-
)
|
|
366
|
-
# Complete and ready to use product type specific metadata-mapping
|
|
367
|
-
product_type_metadata_mapping = deepcopy(self.config.metadata_mapping)
|
|
368
|
-
|
|
369
|
-
# update config using provider product type definition metadata_mapping
|
|
370
|
-
# from another product
|
|
371
|
-
other_product_for_mapping = cast(
|
|
372
|
-
str,
|
|
373
|
-
self.config.products[product_type].get(
|
|
374
|
-
"metadata_mapping_from_product", ""
|
|
375
|
-
),
|
|
376
|
-
)
|
|
377
|
-
if other_product_for_mapping:
|
|
378
|
-
other_product_type_def_params = self.get_product_type_def_params(
|
|
379
|
-
other_product_for_mapping,
|
|
380
|
-
)
|
|
381
|
-
product_type_metadata_mapping.update(
|
|
382
|
-
other_product_type_def_params.get("metadata_mapping", {})
|
|
383
|
-
)
|
|
384
|
-
# from current product
|
|
385
|
-
product_type_metadata_mapping.update(
|
|
386
|
-
self.config.products[product_type]["metadata_mapping"]
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
self.config.products[product_type][
|
|
390
|
-
"metadata_mapping"
|
|
391
|
-
] = product_type_metadata_mapping
|
|
392
|
-
|
|
393
338
|
def do_search(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
|
|
394
339
|
"""Should perform the actual search request.
|
|
395
340
|
|
|
@@ -414,7 +359,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
414
359
|
product_type = prep.product_type
|
|
415
360
|
if not product_type:
|
|
416
361
|
product_type = kwargs.get("productType", None)
|
|
417
|
-
self._preprocess_search_params(kwargs, product_type)
|
|
362
|
+
kwargs = self._preprocess_search_params(kwargs, product_type)
|
|
418
363
|
result, num_items = super().query(prep, **kwargs)
|
|
419
364
|
if prep.count and not num_items:
|
|
420
365
|
num_items = 1
|
|
@@ -426,34 +371,31 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
426
371
|
super().clear()
|
|
427
372
|
|
|
428
373
|
def build_query_string(
|
|
429
|
-
self, product_type: str,
|
|
374
|
+
self, product_type: str, query_dict: dict[str, Any]
|
|
430
375
|
) -> tuple[dict[str, Any], str]:
|
|
431
376
|
"""Build The query string using the search parameters
|
|
432
377
|
|
|
433
378
|
:param product_type: product type id
|
|
434
|
-
:param
|
|
379
|
+
:param query_dict: keyword arguments to be used in the query string
|
|
435
380
|
:return: formatted query params and encode query string
|
|
436
381
|
"""
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if v not in [NOT_AVAILABLE, NOT_MAPPED]
|
|
447
|
-
}
|
|
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)
|
|
448
391
|
|
|
449
|
-
# build and return the query
|
|
450
392
|
return super().build_query_string(
|
|
451
|
-
product_type=product_type,
|
|
393
|
+
product_type=product_type, query_dict=ordered_kwargs
|
|
452
394
|
)
|
|
453
395
|
|
|
454
396
|
def _preprocess_search_params(
|
|
455
397
|
self, params: dict[str, Any], product_type: Optional[str]
|
|
456
|
-
) ->
|
|
398
|
+
) -> dict[str, Any]:
|
|
457
399
|
"""Preprocess search parameters before making a request to the CDS API.
|
|
458
400
|
|
|
459
401
|
This method is responsible for checking and updating the provided search parameters
|
|
@@ -469,28 +411,20 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
469
411
|
# if available, update search params using datacube query-string
|
|
470
412
|
_dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
|
|
471
413
|
if "/to/" in _dc_qp.get("date", ""):
|
|
472
|
-
(
|
|
473
|
-
params["startTimeFromAscendingNode"],
|
|
474
|
-
params["completionTimeFromAscendingNode"],
|
|
475
|
-
) = _dc_qp["date"].split("/to/")
|
|
414
|
+
params[START], params[END] = _dc_qp["date"].split("/to/")
|
|
476
415
|
elif "/" in _dc_qp.get("date", ""):
|
|
477
|
-
(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
) = _dc_qp["date"].split("/")
|
|
416
|
+
(params[START], params[END],) = _dc_qp[
|
|
417
|
+
"date"
|
|
418
|
+
].split("/")
|
|
481
419
|
elif _dc_qp.get("date", None):
|
|
482
|
-
params[
|
|
483
|
-
"completionTimeFromAscendingNode"
|
|
484
|
-
] = _dc_qp["date"]
|
|
420
|
+
params[START] = params[END] = _dc_qp["date"]
|
|
485
421
|
|
|
486
422
|
if "/" in _dc_qp.get("area", ""):
|
|
487
423
|
params["geometry"] = _dc_qp["area"].split("/")
|
|
488
424
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
dataset = params.get("ecmwf:dataset", None)
|
|
493
|
-
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
|
+
}
|
|
494
428
|
|
|
495
429
|
# dates
|
|
496
430
|
# check if default dates have to be added
|
|
@@ -498,25 +432,23 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
498
432
|
self._check_date_params(params, product_type)
|
|
499
433
|
|
|
500
434
|
# adapt end date if it is midnight
|
|
501
|
-
if
|
|
435
|
+
if END in params:
|
|
502
436
|
end_date_excluded = getattr(self.config, "end_date_excluded", True)
|
|
503
437
|
is_datetime = True
|
|
504
438
|
try:
|
|
505
|
-
end_date = datetime.strptime(
|
|
506
|
-
params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%SZ"
|
|
507
|
-
)
|
|
439
|
+
end_date = datetime.strptime(params[END], "%Y-%m-%dT%H:%M:%SZ")
|
|
508
440
|
end_date = end_date.replace(tzinfo=tzutc())
|
|
509
441
|
except ValueError:
|
|
510
442
|
try:
|
|
511
443
|
end_date = datetime.strptime(
|
|
512
|
-
params[
|
|
444
|
+
params[END],
|
|
513
445
|
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
514
446
|
)
|
|
515
447
|
end_date = end_date.replace(tzinfo=tzutc())
|
|
516
448
|
except ValueError:
|
|
517
|
-
end_date = isoparse(params[
|
|
449
|
+
end_date = isoparse(params[END])
|
|
518
450
|
is_datetime = False
|
|
519
|
-
start_date = isoparse(params[
|
|
451
|
+
start_date = isoparse(params[START])
|
|
520
452
|
if (
|
|
521
453
|
not end_date_excluded
|
|
522
454
|
and is_datetime
|
|
@@ -525,12 +457,73 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
525
457
|
== end_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
526
458
|
):
|
|
527
459
|
end_date += timedelta(days=-1)
|
|
528
|
-
params[
|
|
460
|
+
params[END] = end_date.isoformat()
|
|
529
461
|
|
|
530
462
|
# geometry
|
|
531
463
|
if "geometry" in params:
|
|
532
464
|
params["geometry"] = get_geometry_from_various(geometry=params["geometry"])
|
|
533
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
|
+
|
|
534
527
|
def _get_product_type_queryables(
|
|
535
528
|
self, product_type: Optional[str], alias: Optional[str], filters: dict[str, Any]
|
|
536
529
|
) -> QueryablesDict:
|
|
@@ -555,61 +548,60 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
555
548
|
:returns: fetched queryable parameters dict
|
|
556
549
|
"""
|
|
557
550
|
product_type = kwargs.pop("productType")
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if "
|
|
566
|
-
|
|
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")
|
|
567
562
|
|
|
568
563
|
# extract default datetime
|
|
569
|
-
|
|
570
|
-
|
|
564
|
+
processed_filters = self._preprocess_search_params(
|
|
565
|
+
deepcopy(filters), product_type
|
|
566
|
+
)
|
|
571
567
|
|
|
572
568
|
constraints_url = format_metadata(
|
|
573
569
|
getattr(self.config, "discover_queryables", {}).get("constraints_url", ""),
|
|
574
|
-
**
|
|
570
|
+
**filters,
|
|
575
571
|
)
|
|
576
572
|
constraints: list[dict[str, Any]] = self._fetch_data(constraints_url)
|
|
577
573
|
|
|
578
574
|
form_url = format_metadata(
|
|
579
575
|
getattr(self.config, "discover_queryables", {}).get("form_url", ""),
|
|
580
|
-
**
|
|
576
|
+
**filters,
|
|
581
577
|
)
|
|
582
578
|
form: list[dict[str, Any]] = self._fetch_data(form_url)
|
|
583
579
|
|
|
584
|
-
|
|
585
|
-
product_type,
|
|
580
|
+
formated_filters = self.format_as_provider_keyword(
|
|
581
|
+
product_type, processed_filters
|
|
586
582
|
)
|
|
587
583
|
# we re-apply kwargs input to consider override of year, month, day and time.
|
|
588
|
-
for
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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,
|
|
594
590
|
"geom",
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
else:
|
|
591
|
+
"geometry",
|
|
592
|
+
}:
|
|
598
593
|
raise ValidationError(
|
|
599
594
|
f"{key} is not a queryable parameter for {self.provider}"
|
|
600
595
|
)
|
|
601
596
|
|
|
602
|
-
|
|
597
|
+
formated_filters[key] = v
|
|
598
|
+
|
|
599
|
+
# we use non empty filters as default to integrate user inputs
|
|
603
600
|
# it is needed because pydantic json schema does not represent "value"
|
|
604
601
|
# but only "default"
|
|
605
602
|
non_empty_formated: dict[str, Any] = {
|
|
606
603
|
k: v
|
|
607
|
-
for k, v in
|
|
608
|
-
if v and (not isinstance(v, list) or all(v))
|
|
609
|
-
}
|
|
610
|
-
non_empty_kwargs: dict[str, Any] = {
|
|
611
|
-
k: v
|
|
612
|
-
for k, v in processed_kwargs.items()
|
|
604
|
+
for k, v in formated_filters.items()
|
|
613
605
|
if v and (not isinstance(v, list) or all(v))
|
|
614
606
|
}
|
|
615
607
|
|
|
@@ -627,15 +619,16 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
627
619
|
# Pre-compute the required keywords (present in all constraint dicts)
|
|
628
620
|
# when form, required keywords are extracted directly from form
|
|
629
621
|
if not form:
|
|
630
|
-
required_keywords = set
|
|
631
|
-
|
|
632
|
-
|
|
622
|
+
required_keywords = set.intersection(
|
|
623
|
+
*(map(lambda d: set(d.keys()), constraints))
|
|
624
|
+
)
|
|
625
|
+
|
|
633
626
|
else:
|
|
634
627
|
values_url = getattr(self.config, "available_values_url", "")
|
|
635
628
|
if not values_url:
|
|
636
629
|
return self.queryables_from_metadata_mapping(product_type)
|
|
637
630
|
if "{" in values_url:
|
|
638
|
-
values_url = values_url.format(
|
|
631
|
+
values_url = values_url.format(**filters)
|
|
639
632
|
data = self._fetch_data(values_url)
|
|
640
633
|
available_values = data["constraints"]
|
|
641
634
|
required_keywords = data.get("required", [])
|
|
@@ -643,18 +636,18 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
643
636
|
# To check if all keywords are queryable parameters, we check if they are in the
|
|
644
637
|
# available values or the product type config (available values calculated from the
|
|
645
638
|
# constraints might not include all queryables)
|
|
646
|
-
for keyword in
|
|
639
|
+
for keyword in filters:
|
|
647
640
|
if (
|
|
648
641
|
keyword
|
|
649
642
|
not in available_values.keys()
|
|
650
|
-
|
|
|
643
|
+
| pt_config.keys()
|
|
651
644
|
| {
|
|
652
|
-
|
|
653
|
-
|
|
645
|
+
START,
|
|
646
|
+
END,
|
|
654
647
|
"geom",
|
|
655
648
|
}
|
|
656
649
|
and keyword not in [f["name"] for f in form]
|
|
657
|
-
and keyword.
|
|
650
|
+
and keyword.removeprefix(ECMWF_PREFIX)
|
|
658
651
|
not in set(list(available_values.keys()) + [f["name"] for f in form])
|
|
659
652
|
):
|
|
660
653
|
raise ValidationError(f"{keyword} is not a queryable parameter")
|
|
@@ -668,24 +661,24 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
668
661
|
)
|
|
669
662
|
else:
|
|
670
663
|
queryables = self.queryables_by_values(
|
|
671
|
-
available_values, list(required_keywords),
|
|
664
|
+
available_values, list(required_keywords), non_empty_formated
|
|
672
665
|
)
|
|
673
666
|
|
|
674
667
|
# ecmwf:date is replaced by start and end.
|
|
675
668
|
# start and end filters are supported whenever combinations of "year", "month", "day" filters exist
|
|
676
669
|
if (
|
|
677
|
-
queryables.pop("
|
|
678
|
-
or "
|
|
679
|
-
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
|
|
680
673
|
):
|
|
681
674
|
queryables.update(
|
|
682
675
|
{
|
|
683
676
|
"start": Queryables.get_with_default(
|
|
684
|
-
"start",
|
|
677
|
+
"start", processed_filters.get(START)
|
|
685
678
|
),
|
|
686
679
|
"end": Queryables.get_with_default(
|
|
687
680
|
"end",
|
|
688
|
-
|
|
681
|
+
processed_filters.get(END),
|
|
689
682
|
),
|
|
690
683
|
}
|
|
691
684
|
)
|
|
@@ -746,14 +739,15 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
746
739
|
raise ValidationError(
|
|
747
740
|
f"Parameter value as object is not supported: {keyword}={values}"
|
|
748
741
|
)
|
|
749
|
-
filter_v = values if isinstance(values, (list, tuple)) else [values]
|
|
750
742
|
|
|
751
743
|
# We convert every single value to a list of string
|
|
744
|
+
filter_v = values if isinstance(values, (list, tuple)) else [values]
|
|
745
|
+
|
|
752
746
|
# We strip values of superfluous quotes (added by mapping converter to_geojson).
|
|
753
747
|
# ECMWF accept values with /to/. We need to split it to an array
|
|
754
748
|
# ECMWF accept values in format val1/val2. We need to split it to an array
|
|
755
749
|
sep = re.compile(r"/to/|/")
|
|
756
|
-
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))]
|
|
757
751
|
|
|
758
752
|
# special handling for time 0000 converted to 0 by pre-formating with metadata_mapping
|
|
759
753
|
if keyword.split(":")[-1] == "time":
|
|
@@ -772,7 +766,10 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
772
766
|
# we assume that if the first value is an interval, all values are intervals
|
|
773
767
|
present_values = []
|
|
774
768
|
if keyword == "date" and "/" in entry[keyword][0]:
|
|
775
|
-
|
|
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]):
|
|
776
773
|
present_values = filter_v
|
|
777
774
|
else:
|
|
778
775
|
present_values = [
|
|
@@ -792,12 +789,12 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
792
789
|
{value for c in constraints for value in c.get(keyword, [])}
|
|
793
790
|
)
|
|
794
791
|
# restore ecmwf: prefix before raising error
|
|
795
|
-
keyword =
|
|
792
|
+
keyword = ECMWF_PREFIX + keyword
|
|
796
793
|
|
|
797
794
|
all_keywords_str = ""
|
|
798
795
|
if len(parsed_keywords) > 1:
|
|
799
796
|
keywords = [
|
|
800
|
-
f"
|
|
797
|
+
f"{ECMWF_PREFIX + k}={pk}"
|
|
801
798
|
for k in parsed_keywords
|
|
802
799
|
if (pk := input_keywords.get(k))
|
|
803
800
|
]
|
|
@@ -846,6 +843,8 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
846
843
|
if name in ("area_group", "global", "warning", "licences"):
|
|
847
844
|
continue
|
|
848
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
|
|
849
848
|
continue
|
|
850
849
|
|
|
851
850
|
# ordering done by id -> set id to high value if not present -> element will be last
|
|
@@ -876,12 +875,8 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
876
875
|
if name == "area" and isinstance(default, dict):
|
|
877
876
|
default = list(default.values())
|
|
878
877
|
|
|
879
|
-
if default:
|
|
880
|
-
# We strip values of superfluous quotes (addded by mapping converter to_geojson).
|
|
881
|
-
default = strip_quotes(default)
|
|
882
|
-
|
|
883
878
|
# sometimes form returns default as array instead of string
|
|
884
|
-
if default and prop
|
|
879
|
+
if default and prop.get("type") == "string" and isinstance(default, list):
|
|
885
880
|
default = ",".join(default)
|
|
886
881
|
|
|
887
882
|
is_required = bool(element.get("required"))
|
|
@@ -925,13 +920,11 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
925
920
|
# Needed to map constraints like "xxxx" to eodag parameter "ecmwf:xxxx"
|
|
926
921
|
key = ecmwf_format(name)
|
|
927
922
|
|
|
928
|
-
default = defaults.get(key)
|
|
929
|
-
|
|
930
923
|
queryables[key] = Annotated[
|
|
931
924
|
get_args(
|
|
932
925
|
json_field_definition_to_python(
|
|
933
926
|
{"type": "string", "title": name, "enum": values},
|
|
934
|
-
default_value=
|
|
927
|
+
default_value=defaults.get(name),
|
|
935
928
|
required=bool(key in required),
|
|
936
929
|
)
|
|
937
930
|
)
|
|
@@ -948,16 +941,26 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
948
941
|
:param properties: dict of properties to be formatted
|
|
949
942
|
:return: dict of formatted properties
|
|
950
943
|
"""
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
|
954
949
|
)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
|
961
964
|
|
|
962
965
|
def _fetch_data(self, url: str) -> Any:
|
|
963
966
|
"""
|
|
@@ -1045,67 +1048,37 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1045
1048
|
discovery_config=getattr(self.config, "discover_metadata", {}),
|
|
1046
1049
|
)
|
|
1047
1050
|
|
|
1048
|
-
|
|
1049
|
-
|
|
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("-", "")
|
|
1050
1059
|
|
|
1051
1060
|
# build product id
|
|
1052
|
-
|
|
1053
|
-
if (
|
|
1054
|
-
"startTimeFromAscendingNode" in parsed_properties
|
|
1055
|
-
and parsed_properties["startTimeFromAscendingNode"] != "Not Available"
|
|
1056
|
-
and "completionTimeFromAscendingNode" in parsed_properties
|
|
1057
|
-
and parsed_properties["completionTimeFromAscendingNode"] != "Not Available"
|
|
1058
|
-
):
|
|
1059
|
-
product_id = "%s_%s_%s_%s" % (
|
|
1060
|
-
id_prefix,
|
|
1061
|
-
parsed_properties["startTimeFromAscendingNode"]
|
|
1062
|
-
.split("T")[0]
|
|
1063
|
-
.replace("-", ""),
|
|
1064
|
-
parsed_properties["completionTimeFromAscendingNode"]
|
|
1065
|
-
.split("T")[0]
|
|
1066
|
-
.replace("-", ""),
|
|
1067
|
-
query_hash,
|
|
1068
|
-
)
|
|
1069
|
-
elif (
|
|
1070
|
-
"startTimeFromAscendingNode" in parsed_properties
|
|
1071
|
-
and parsed_properties["startTimeFromAscendingNode"] != "Not Available"
|
|
1072
|
-
):
|
|
1073
|
-
product_id = "%s_%s_%s" % (
|
|
1074
|
-
id_prefix,
|
|
1075
|
-
parsed_properties["startTimeFromAscendingNode"]
|
|
1076
|
-
.split("T")[0]
|
|
1077
|
-
.replace("-", ""),
|
|
1078
|
-
query_hash,
|
|
1079
|
-
)
|
|
1080
|
-
else:
|
|
1081
|
-
product_id = f"{id_prefix}_{query_hash}"
|
|
1082
|
-
|
|
1083
|
-
parsed_properties["id"] = parsed_properties["title"] = product_id
|
|
1084
|
-
|
|
1085
|
-
# update downloadLink and orderLink
|
|
1086
|
-
parsed_properties["_dc_qs"] = quote_plus(qs)
|
|
1087
|
-
if parsed_properties["downloadLink"] != "Not Available":
|
|
1088
|
-
parsed_properties["downloadLink"] += f"?{qs}"
|
|
1089
|
-
|
|
1090
|
-
# parse metadata needing downloadLink
|
|
1091
|
-
dl_path = Fields("downloadLink")
|
|
1092
|
-
dl_path_from_root = Child(Root(), dl_path)
|
|
1093
|
-
for param, mapping in self.config.metadata_mapping.items():
|
|
1094
|
-
if dl_path in mapping or dl_path_from_root in mapping:
|
|
1095
|
-
parsed_properties.update(
|
|
1096
|
-
properties_from_json(parsed_properties, {param: mapping})
|
|
1097
|
-
)
|
|
1061
|
+
product_id = (product_type or kwargs.get("dataset") or self.provider).upper()
|
|
1098
1062
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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)
|
|
1104
1077
|
|
|
1105
1078
|
product = EOProduct(
|
|
1106
1079
|
provider=self.provider,
|
|
1107
1080
|
productType=product_type,
|
|
1108
|
-
properties=
|
|
1081
|
+
properties=properties,
|
|
1109
1082
|
)
|
|
1110
1083
|
|
|
1111
1084
|
return [
|
|
@@ -1182,17 +1155,15 @@ class MeteoblueSearch(ECMWFSearch):
|
|
|
1182
1155
|
return [response.json()]
|
|
1183
1156
|
|
|
1184
1157
|
def build_query_string(
|
|
1185
|
-
self, product_type: str,
|
|
1158
|
+
self, product_type: str, query_dict: dict[str, Any]
|
|
1186
1159
|
) -> tuple[dict[str, Any], str]:
|
|
1187
1160
|
"""Build The query string using the search parameters
|
|
1188
1161
|
|
|
1189
1162
|
:param product_type: product type id
|
|
1190
|
-
:param
|
|
1163
|
+
:param query_dict: keyword arguments to be used in the query string
|
|
1191
1164
|
:return: formatted query params and encode query string
|
|
1192
1165
|
"""
|
|
1193
|
-
return QueryStringSearch.build_query_string(
|
|
1194
|
-
self, product_type=product_type, **kwargs
|
|
1195
|
-
)
|
|
1166
|
+
return QueryStringSearch.build_query_string(self, product_type, query_dict)
|
|
1196
1167
|
|
|
1197
1168
|
|
|
1198
1169
|
class WekeoECMWFSearch(ECMWFSearch):
|
|
@@ -1256,25 +1227,3 @@ class WekeoECMWFSearch(ECMWFSearch):
|
|
|
1256
1227
|
:return: list containing the results from the provider in json format
|
|
1257
1228
|
"""
|
|
1258
1229
|
return QueryStringSearch.do_search(self, *args, **kwargs)
|
|
1259
|
-
|
|
1260
|
-
def build_query_string(
|
|
1261
|
-
self, product_type: str, **kwargs: Any
|
|
1262
|
-
) -> tuple[dict[str, Any], str]:
|
|
1263
|
-
"""Build The query string using the search parameters
|
|
1264
|
-
|
|
1265
|
-
:param product_type: product type id
|
|
1266
|
-
:param kwargs: keyword arguments to be used in the query string
|
|
1267
|
-
:return: formatted query params and encode query string
|
|
1268
|
-
"""
|
|
1269
|
-
# Reorder kwargs to make sure year/month/day/time if set overwrite default datetime.
|
|
1270
|
-
# strip_quotes to remove duplicated quotes like "'1_1'" produced by convertors like to_geojson.
|
|
1271
|
-
priority_keys = [
|
|
1272
|
-
"startTimeFromAscendingNode",
|
|
1273
|
-
"completionTimeFromAscendingNode",
|
|
1274
|
-
]
|
|
1275
|
-
ordered_kwargs = {k: kwargs[k] for k in priority_keys if k in kwargs}
|
|
1276
|
-
ordered_kwargs.update({k: strip_quotes(v) for k, v in kwargs.items()})
|
|
1277
|
-
|
|
1278
|
-
return QueryStringSearch.build_query_string(
|
|
1279
|
-
self, product_type=product_type, **ordered_kwargs
|
|
1280
|
-
)
|