eodag 3.1.0b2__py3-none-any.whl → 3.2.1__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 +348 -281
- 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 +956 -1173
- 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 +2 -2
- eodag/utils/s3.py +31 -8
- {eodag-3.1.0b2.dist-info → eodag-3.2.1.dist-info}/METADATA +10 -9
- {eodag-3.1.0b2.dist-info → eodag-3.2.1.dist-info}/RECORD +29 -29
- {eodag-3.1.0b2.dist-info → eodag-3.2.1.dist-info}/WHEEL +1 -1
- {eodag-3.1.0b2.dist-info → eodag-3.2.1.dist-info}/entry_points.txt +0 -0
- {eodag-3.1.0b2.dist-info → eodag-3.2.1.dist-info/licenses}/LICENSE +0 -0
- {eodag-3.1.0b2.dist-info → eodag-3.2.1.dist-info}/top_level.txt +0 -0
|
@@ -22,15 +22,15 @@ import hashlib
|
|
|
22
22
|
import logging
|
|
23
23
|
import re
|
|
24
24
|
from collections import OrderedDict
|
|
25
|
-
from datetime import datetime, timedelta
|
|
26
|
-
from typing import TYPE_CHECKING, Annotated, Any, Optional, Union
|
|
25
|
+
from datetime import date, datetime, timedelta, timezone
|
|
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"
|
|
194
199
|
|
|
200
|
+
START = "startTimeFromAscendingNode"
|
|
195
201
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
) -> dict[str, Any]:
|
|
202
|
+
|
|
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,126 @@ 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
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def get_min_max(
|
|
290
|
+
value: Optional[Union[str, list[str]]] = None,
|
|
291
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
292
|
+
"""Returns the min and max from a list of strings or the same string if a single string is given."""
|
|
293
|
+
if isinstance(value, list):
|
|
294
|
+
sorted_values = sorted(value)
|
|
295
|
+
return sorted_values[0], sorted_values[-1]
|
|
296
|
+
return value, value
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def append_time(input_date: date, time: Optional[str]) -> datetime:
|
|
300
|
+
"""
|
|
301
|
+
Parses a time string in format HHMM and appends it to a date.
|
|
302
|
+
|
|
303
|
+
if the time string is in format HH:MM we convert it to HHMM
|
|
304
|
+
"""
|
|
305
|
+
if not time:
|
|
306
|
+
time = "0000"
|
|
307
|
+
time = time.replace(":", "")
|
|
308
|
+
if time == "2400":
|
|
309
|
+
time = "0000"
|
|
310
|
+
dt = datetime.combine(input_date, datetime.strptime(time, "%H%M").time())
|
|
311
|
+
dt.replace(tzinfo=timezone.utc)
|
|
312
|
+
return dt
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def parse_date(
|
|
316
|
+
date_str: str, time: Optional[Union[str, list[str]]]
|
|
317
|
+
) -> tuple[datetime, datetime]:
|
|
318
|
+
"""Parses a date string in format YYYY-MM-DD or YYYY-MM-DD/YYYY-MM-DD or YYYY-MM-DD/to/YYYY-MM-DD."""
|
|
319
|
+
if "to" in date_str:
|
|
320
|
+
start_date_str, end_date_str = date_str.split("/to/")
|
|
321
|
+
elif "/" in date_str:
|
|
322
|
+
dates = date_str.split("/")
|
|
323
|
+
start_date_str = dates[0]
|
|
324
|
+
end_date_str = dates[-1]
|
|
325
|
+
else:
|
|
326
|
+
start_date_str = end_date_str = date_str
|
|
327
|
+
|
|
328
|
+
start_date = datetime.fromisoformat(start_date_str.rstrip("Z"))
|
|
329
|
+
end_date = datetime.fromisoformat(end_date_str.rstrip("Z"))
|
|
330
|
+
|
|
331
|
+
if time:
|
|
332
|
+
start_t, end_t = get_min_max(time)
|
|
333
|
+
start_date = append_time(start_date.date(), start_t)
|
|
334
|
+
end_date = append_time(end_date.date(), end_t)
|
|
335
|
+
|
|
336
|
+
return start_date, end_date
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def parse_year_month_day(
|
|
340
|
+
year: Union[str, list[str]],
|
|
341
|
+
month: Optional[Union[str, list[str]]] = None,
|
|
342
|
+
day: Optional[Union[str, list[str]]] = None,
|
|
343
|
+
time: Optional[Union[str, list[str]]] = None,
|
|
344
|
+
) -> tuple[datetime, datetime]:
|
|
345
|
+
"""Extracts and returns the year, month, day, and time from the parameters."""
|
|
346
|
+
|
|
347
|
+
def build_date(year, month=None, day=None, time=None) -> datetime:
|
|
348
|
+
"""Datetime from default_date with updated year, month, day and time."""
|
|
349
|
+
updated_date = datetime(int(year), 1, 1).replace(
|
|
350
|
+
month=int(month) if month is not None else 1,
|
|
351
|
+
day=int(day) if day is not None else 1,
|
|
352
|
+
)
|
|
353
|
+
if time is not None:
|
|
354
|
+
updated_date = append_time(updated_date.date(), time)
|
|
355
|
+
return updated_date
|
|
356
|
+
|
|
357
|
+
start_y, end_y = get_min_max(year)
|
|
358
|
+
start_m, end_m = get_min_max(month)
|
|
359
|
+
start_d, end_d = get_min_max(day)
|
|
360
|
+
start_t, end_t = get_min_max(time)
|
|
361
|
+
|
|
362
|
+
start_date = build_date(start_y, start_m, start_d, start_t)
|
|
363
|
+
end_date = build_date(end_y, end_m, end_d, end_t)
|
|
364
|
+
|
|
365
|
+
return start_date, end_date
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def ecmwf_temporal_to_eodag(
|
|
369
|
+
params: dict[str, Any]
|
|
370
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
371
|
+
"""
|
|
372
|
+
Converts ECMWF temporal parameters to EODAG temporal parameters.
|
|
373
|
+
|
|
374
|
+
ECMWF temporal parameters:
|
|
375
|
+
- **year** or **hyear**: Union[str, list[str]] — Year(s) as a string or list of strings.
|
|
376
|
+
- **month** or **hmonth**: Union[str, list[str]] — Month(s) as a string or list of strings.
|
|
377
|
+
- **day** or **hday**: Union[str, list[str]] — Day(s) as a string or list of strings.
|
|
378
|
+
- **time**: str — A string representing the time in the format `HHMM` (e.g., `0200`, `0800`, `1400`).
|
|
379
|
+
- **date**: str — A string in one of the formats:
|
|
380
|
+
- `YYYY-MM-DD`
|
|
381
|
+
- `YYYY-MM-DD/YYYY-MM-DD`
|
|
382
|
+
- `YYYY-MM-DD/to/YYYY-MM-DD`
|
|
383
|
+
|
|
384
|
+
:param params: Dictionary containing ECMWF temporal parameters.
|
|
385
|
+
:return: A tuple with:
|
|
386
|
+
- **start**: A string in the format `YYYY-MM-DDTHH:MM:SSZ`.
|
|
387
|
+
- **end**: A string in the format `YYYY-MM-DDTHH:MM:SSZ`.
|
|
388
|
+
"""
|
|
389
|
+
start = end = None
|
|
390
|
+
|
|
391
|
+
if date := params.get("date"):
|
|
392
|
+
start, end = parse_date(date, params.get("time"))
|
|
393
|
+
|
|
394
|
+
elif year := params.get("year") or params.get("hyear"):
|
|
395
|
+
year = params.get("year") or params.get("hyear")
|
|
396
|
+
month = params.get("month") or params.get("hmonth")
|
|
397
|
+
day = params.get("day") or params.get("hday")
|
|
398
|
+
time = params.get("time")
|
|
399
|
+
|
|
400
|
+
start, end = parse_year_month_day(year, month, day, time)
|
|
401
|
+
|
|
402
|
+
if start and end:
|
|
403
|
+
return start.strftime("%Y-%m-%dT%H:%M:%SZ"), end.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
404
|
+
else:
|
|
405
|
+
return None, None
|
|
313
406
|
|
|
314
407
|
|
|
315
408
|
class ECMWFSearch(PostJsonSearch):
|
|
@@ -342,54 +435,25 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
342
435
|
|
|
343
436
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
344
437
|
config.metadata_mapping = {
|
|
345
|
-
**
|
|
438
|
+
**ecmwf_mtd(),
|
|
439
|
+
**{
|
|
440
|
+
"id": "$.id",
|
|
441
|
+
"title": "$.id",
|
|
442
|
+
"storageStatus": OFFLINE_STATUS,
|
|
443
|
+
"downloadLink": "$.null",
|
|
444
|
+
"geometry": ["feature", "$.geometry"],
|
|
445
|
+
"defaultGeometry": "POLYGON((180 -90, 180 90, -180 90, -180 -90, 180 -90))",
|
|
446
|
+
},
|
|
346
447
|
**config.metadata_mapping,
|
|
347
448
|
}
|
|
348
449
|
|
|
349
450
|
super().__init__(provider, config)
|
|
350
451
|
|
|
452
|
+
# ECMWF providers do not feature any api_endpoint or next_page_query_obj.
|
|
453
|
+
# Searched is faked by EODAG.
|
|
351
454
|
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
455
|
self.config.pagination.setdefault("next_page_query_obj", "{{}}")
|
|
357
456
|
|
|
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
457
|
def do_search(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
|
|
394
458
|
"""Should perform the actual search request.
|
|
395
459
|
|
|
@@ -414,7 +478,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
414
478
|
product_type = prep.product_type
|
|
415
479
|
if not product_type:
|
|
416
480
|
product_type = kwargs.get("productType", None)
|
|
417
|
-
self._preprocess_search_params(kwargs, product_type)
|
|
481
|
+
kwargs = self._preprocess_search_params(kwargs, product_type)
|
|
418
482
|
result, num_items = super().query(prep, **kwargs)
|
|
419
483
|
if prep.count and not num_items:
|
|
420
484
|
num_items = 1
|
|
@@ -426,34 +490,31 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
426
490
|
super().clear()
|
|
427
491
|
|
|
428
492
|
def build_query_string(
|
|
429
|
-
self, product_type: str,
|
|
493
|
+
self, product_type: str, query_dict: dict[str, Any]
|
|
430
494
|
) -> tuple[dict[str, Any], str]:
|
|
431
495
|
"""Build The query string using the search parameters
|
|
432
496
|
|
|
433
497
|
:param product_type: product type id
|
|
434
|
-
:param
|
|
498
|
+
:param query_dict: keyword arguments to be used in the query string
|
|
435
499
|
:return: formatted query params and encode query string
|
|
436
500
|
"""
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if v not in [NOT_AVAILABLE, NOT_MAPPED]
|
|
447
|
-
}
|
|
501
|
+
query_dict["_date"] = f"{query_dict.get(START)}/{query_dict.get(END)}"
|
|
502
|
+
|
|
503
|
+
# Reorder kwargs to make sure year/month/day/time if set overwrite default datetime.
|
|
504
|
+
priority_keys = [
|
|
505
|
+
START,
|
|
506
|
+
END,
|
|
507
|
+
]
|
|
508
|
+
ordered_kwargs = {k: query_dict[k] for k in priority_keys if k in query_dict}
|
|
509
|
+
ordered_kwargs.update(query_dict)
|
|
448
510
|
|
|
449
|
-
# build and return the query
|
|
450
511
|
return super().build_query_string(
|
|
451
|
-
product_type=product_type,
|
|
512
|
+
product_type=product_type, query_dict=ordered_kwargs
|
|
452
513
|
)
|
|
453
514
|
|
|
454
515
|
def _preprocess_search_params(
|
|
455
516
|
self, params: dict[str, Any], product_type: Optional[str]
|
|
456
|
-
) ->
|
|
517
|
+
) -> dict[str, Any]:
|
|
457
518
|
"""Preprocess search parameters before making a request to the CDS API.
|
|
458
519
|
|
|
459
520
|
This method is responsible for checking and updating the provided search parameters
|
|
@@ -469,28 +530,20 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
469
530
|
# if available, update search params using datacube query-string
|
|
470
531
|
_dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
|
|
471
532
|
if "/to/" in _dc_qp.get("date", ""):
|
|
472
|
-
(
|
|
473
|
-
params["startTimeFromAscendingNode"],
|
|
474
|
-
params["completionTimeFromAscendingNode"],
|
|
475
|
-
) = _dc_qp["date"].split("/to/")
|
|
533
|
+
params[START], params[END] = _dc_qp["date"].split("/to/")
|
|
476
534
|
elif "/" in _dc_qp.get("date", ""):
|
|
477
|
-
(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
) = _dc_qp["date"].split("/")
|
|
535
|
+
(params[START], params[END],) = _dc_qp[
|
|
536
|
+
"date"
|
|
537
|
+
].split("/")
|
|
481
538
|
elif _dc_qp.get("date", None):
|
|
482
|
-
params[
|
|
483
|
-
"completionTimeFromAscendingNode"
|
|
484
|
-
] = _dc_qp["date"]
|
|
539
|
+
params[START] = params[END] = _dc_qp["date"]
|
|
485
540
|
|
|
486
541
|
if "/" in _dc_qp.get("area", ""):
|
|
487
542
|
params["geometry"] = _dc_qp["area"].split("/")
|
|
488
543
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
dataset = params.get("ecmwf:dataset", None)
|
|
493
|
-
params["productType"] = non_none_params.get("productType", dataset)
|
|
544
|
+
params = {
|
|
545
|
+
k.removeprefix(ECMWF_PREFIX): v for k, v in params.items() if v is not None
|
|
546
|
+
}
|
|
494
547
|
|
|
495
548
|
# dates
|
|
496
549
|
# check if default dates have to be added
|
|
@@ -498,25 +551,23 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
498
551
|
self._check_date_params(params, product_type)
|
|
499
552
|
|
|
500
553
|
# adapt end date if it is midnight
|
|
501
|
-
if
|
|
554
|
+
if END in params:
|
|
502
555
|
end_date_excluded = getattr(self.config, "end_date_excluded", True)
|
|
503
556
|
is_datetime = True
|
|
504
557
|
try:
|
|
505
|
-
end_date = datetime.strptime(
|
|
506
|
-
params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%SZ"
|
|
507
|
-
)
|
|
558
|
+
end_date = datetime.strptime(params[END], "%Y-%m-%dT%H:%M:%SZ")
|
|
508
559
|
end_date = end_date.replace(tzinfo=tzutc())
|
|
509
560
|
except ValueError:
|
|
510
561
|
try:
|
|
511
562
|
end_date = datetime.strptime(
|
|
512
|
-
params[
|
|
563
|
+
params[END],
|
|
513
564
|
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
514
565
|
)
|
|
515
566
|
end_date = end_date.replace(tzinfo=tzutc())
|
|
516
567
|
except ValueError:
|
|
517
|
-
end_date = isoparse(params[
|
|
568
|
+
end_date = isoparse(params[END])
|
|
518
569
|
is_datetime = False
|
|
519
|
-
start_date = isoparse(params[
|
|
570
|
+
start_date = isoparse(params[START])
|
|
520
571
|
if (
|
|
521
572
|
not end_date_excluded
|
|
522
573
|
and is_datetime
|
|
@@ -525,12 +576,72 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
525
576
|
== end_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
526
577
|
):
|
|
527
578
|
end_date += timedelta(days=-1)
|
|
528
|
-
params[
|
|
579
|
+
params[END] = end_date.isoformat()
|
|
529
580
|
|
|
530
581
|
# geometry
|
|
531
582
|
if "geometry" in params:
|
|
532
583
|
params["geometry"] = get_geometry_from_various(geometry=params["geometry"])
|
|
533
584
|
|
|
585
|
+
return params
|
|
586
|
+
|
|
587
|
+
def _check_date_params(
|
|
588
|
+
self, keywords: dict[str, Any], product_type: Optional[str]
|
|
589
|
+
) -> None:
|
|
590
|
+
"""checks if start and end date are present in the keywords and adds them if not"""
|
|
591
|
+
|
|
592
|
+
if START and END in keywords:
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
product_type_conf = getattr(self.config, "metadata_mapping", {})
|
|
596
|
+
if (
|
|
597
|
+
product_type
|
|
598
|
+
and product_type in self.config.products
|
|
599
|
+
and "metadata_mapping" in self.config.products[product_type]
|
|
600
|
+
):
|
|
601
|
+
product_type_conf = self.config.products[product_type]["metadata_mapping"]
|
|
602
|
+
|
|
603
|
+
# start time given, end time missing
|
|
604
|
+
if START in keywords:
|
|
605
|
+
keywords[END] = (
|
|
606
|
+
keywords[START]
|
|
607
|
+
if END in product_type_conf
|
|
608
|
+
else self.get_product_type_cfg_value(
|
|
609
|
+
"missionEndDate", today().isoformat()
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
if END in product_type_conf:
|
|
615
|
+
mapping = product_type_conf[START]
|
|
616
|
+
if not isinstance(mapping, list):
|
|
617
|
+
mapping = product_type_conf[END]
|
|
618
|
+
if isinstance(mapping, list):
|
|
619
|
+
# get time parameters (date, year, month, ...) from metadata mapping
|
|
620
|
+
input_mapping = mapping[0].replace("{{", "").replace("}}", "")
|
|
621
|
+
time_params = [
|
|
622
|
+
values.split(":")[0].strip() for values in input_mapping.split(",")
|
|
623
|
+
]
|
|
624
|
+
time_params = [
|
|
625
|
+
tp.replace('"', "").replace("'", "") for tp in time_params
|
|
626
|
+
]
|
|
627
|
+
# if startTime is not given but other time params (e.g. year/month/(day)) are given,
|
|
628
|
+
# no default date is required
|
|
629
|
+
start, end = ecmwf_temporal_to_eodag(keywords)
|
|
630
|
+
if start is None:
|
|
631
|
+
keywords[START] = self.get_product_type_cfg_value(
|
|
632
|
+
"missionStartDate", DEFAULT_MISSION_START_DATE
|
|
633
|
+
)
|
|
634
|
+
keywords[END] = (
|
|
635
|
+
keywords[START]
|
|
636
|
+
if END in product_type_conf
|
|
637
|
+
else self.get_product_type_cfg_value(
|
|
638
|
+
"missionEndDate", today().isoformat()
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
else:
|
|
642
|
+
keywords[START] = start
|
|
643
|
+
keywords[END] = end
|
|
644
|
+
|
|
534
645
|
def _get_product_type_queryables(
|
|
535
646
|
self, product_type: Optional[str], alias: Optional[str], filters: dict[str, Any]
|
|
536
647
|
) -> QueryablesDict:
|
|
@@ -555,61 +666,60 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
555
666
|
:returns: fetched queryable parameters dict
|
|
556
667
|
"""
|
|
557
668
|
product_type = kwargs.pop("productType")
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if "
|
|
566
|
-
|
|
669
|
+
|
|
670
|
+
pt_config = self.get_product_type_def_params(product_type)
|
|
671
|
+
|
|
672
|
+
default_values = deepcopy(pt_config)
|
|
673
|
+
default_values.pop("metadata_mapping", None)
|
|
674
|
+
filters = {**default_values, **kwargs}
|
|
675
|
+
|
|
676
|
+
if "start" in filters:
|
|
677
|
+
filters[START] = filters.pop("start")
|
|
678
|
+
if "end" in filters:
|
|
679
|
+
filters[END] = filters.pop("end")
|
|
567
680
|
|
|
568
681
|
# extract default datetime
|
|
569
|
-
|
|
570
|
-
|
|
682
|
+
processed_filters = self._preprocess_search_params(
|
|
683
|
+
deepcopy(filters), product_type
|
|
684
|
+
)
|
|
571
685
|
|
|
572
686
|
constraints_url = format_metadata(
|
|
573
687
|
getattr(self.config, "discover_queryables", {}).get("constraints_url", ""),
|
|
574
|
-
**
|
|
688
|
+
**filters,
|
|
575
689
|
)
|
|
576
690
|
constraints: list[dict[str, Any]] = self._fetch_data(constraints_url)
|
|
577
691
|
|
|
578
692
|
form_url = format_metadata(
|
|
579
693
|
getattr(self.config, "discover_queryables", {}).get("form_url", ""),
|
|
580
|
-
**
|
|
694
|
+
**filters,
|
|
581
695
|
)
|
|
582
696
|
form: list[dict[str, Any]] = self._fetch_data(form_url)
|
|
583
697
|
|
|
584
|
-
|
|
585
|
-
product_type,
|
|
698
|
+
formated_filters = self.format_as_provider_keyword(
|
|
699
|
+
product_type, processed_filters
|
|
586
700
|
)
|
|
587
701
|
# we re-apply kwargs input to consider override of year, month, day and time.
|
|
588
|
-
for
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
702
|
+
for k, v in {**default_values, **kwargs}.items():
|
|
703
|
+
key = k.removeprefix(ECMWF_PREFIX)
|
|
704
|
+
|
|
705
|
+
if key not in ALLOWED_KEYWORDS | {
|
|
706
|
+
START,
|
|
707
|
+
END,
|
|
594
708
|
"geom",
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
else:
|
|
709
|
+
"geometry",
|
|
710
|
+
}:
|
|
598
711
|
raise ValidationError(
|
|
599
712
|
f"{key} is not a queryable parameter for {self.provider}"
|
|
600
713
|
)
|
|
601
714
|
|
|
602
|
-
|
|
715
|
+
formated_filters[key] = v
|
|
716
|
+
|
|
717
|
+
# we use non empty filters as default to integrate user inputs
|
|
603
718
|
# it is needed because pydantic json schema does not represent "value"
|
|
604
719
|
# but only "default"
|
|
605
720
|
non_empty_formated: dict[str, Any] = {
|
|
606
721
|
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()
|
|
722
|
+
for k, v in formated_filters.items()
|
|
613
723
|
if v and (not isinstance(v, list) or all(v))
|
|
614
724
|
}
|
|
615
725
|
|
|
@@ -627,15 +737,16 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
627
737
|
# Pre-compute the required keywords (present in all constraint dicts)
|
|
628
738
|
# when form, required keywords are extracted directly from form
|
|
629
739
|
if not form:
|
|
630
|
-
required_keywords = set
|
|
631
|
-
|
|
632
|
-
|
|
740
|
+
required_keywords = set.intersection(
|
|
741
|
+
*(map(lambda d: set(d.keys()), constraints))
|
|
742
|
+
)
|
|
743
|
+
|
|
633
744
|
else:
|
|
634
745
|
values_url = getattr(self.config, "available_values_url", "")
|
|
635
746
|
if not values_url:
|
|
636
747
|
return self.queryables_from_metadata_mapping(product_type)
|
|
637
748
|
if "{" in values_url:
|
|
638
|
-
values_url = values_url.format(
|
|
749
|
+
values_url = values_url.format(**filters)
|
|
639
750
|
data = self._fetch_data(values_url)
|
|
640
751
|
available_values = data["constraints"]
|
|
641
752
|
required_keywords = data.get("required", [])
|
|
@@ -643,18 +754,18 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
643
754
|
# To check if all keywords are queryable parameters, we check if they are in the
|
|
644
755
|
# available values or the product type config (available values calculated from the
|
|
645
756
|
# constraints might not include all queryables)
|
|
646
|
-
for keyword in
|
|
757
|
+
for keyword in filters:
|
|
647
758
|
if (
|
|
648
759
|
keyword
|
|
649
760
|
not in available_values.keys()
|
|
650
|
-
|
|
|
761
|
+
| pt_config.keys()
|
|
651
762
|
| {
|
|
652
|
-
|
|
653
|
-
|
|
763
|
+
START,
|
|
764
|
+
END,
|
|
654
765
|
"geom",
|
|
655
766
|
}
|
|
656
767
|
and keyword not in [f["name"] for f in form]
|
|
657
|
-
and keyword.
|
|
768
|
+
and keyword.removeprefix(ECMWF_PREFIX)
|
|
658
769
|
not in set(list(available_values.keys()) + [f["name"] for f in form])
|
|
659
770
|
):
|
|
660
771
|
raise ValidationError(f"{keyword} is not a queryable parameter")
|
|
@@ -668,24 +779,24 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
668
779
|
)
|
|
669
780
|
else:
|
|
670
781
|
queryables = self.queryables_by_values(
|
|
671
|
-
available_values, list(required_keywords),
|
|
782
|
+
available_values, list(required_keywords), non_empty_formated
|
|
672
783
|
)
|
|
673
784
|
|
|
674
785
|
# ecmwf:date is replaced by start and end.
|
|
675
786
|
# start and end filters are supported whenever combinations of "year", "month", "day" filters exist
|
|
676
787
|
if (
|
|
677
|
-
queryables.pop("
|
|
678
|
-
or "
|
|
679
|
-
or "
|
|
788
|
+
queryables.pop(f"{ECMWF_PREFIX}date", None)
|
|
789
|
+
or f"{ECMWF_PREFIX}year" in queryables
|
|
790
|
+
or f"{ECMWF_PREFIX}hyear" in queryables
|
|
680
791
|
):
|
|
681
792
|
queryables.update(
|
|
682
793
|
{
|
|
683
794
|
"start": Queryables.get_with_default(
|
|
684
|
-
"start",
|
|
795
|
+
"start", processed_filters.get(START)
|
|
685
796
|
),
|
|
686
797
|
"end": Queryables.get_with_default(
|
|
687
798
|
"end",
|
|
688
|
-
|
|
799
|
+
processed_filters.get(END),
|
|
689
800
|
),
|
|
690
801
|
}
|
|
691
802
|
)
|
|
@@ -746,14 +857,15 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
746
857
|
raise ValidationError(
|
|
747
858
|
f"Parameter value as object is not supported: {keyword}={values}"
|
|
748
859
|
)
|
|
749
|
-
filter_v = values if isinstance(values, (list, tuple)) else [values]
|
|
750
860
|
|
|
751
861
|
# We convert every single value to a list of string
|
|
862
|
+
filter_v = values if isinstance(values, (list, tuple)) else [values]
|
|
863
|
+
|
|
752
864
|
# We strip values of superfluous quotes (added by mapping converter to_geojson).
|
|
753
865
|
# ECMWF accept values with /to/. We need to split it to an array
|
|
754
866
|
# ECMWF accept values in format val1/val2. We need to split it to an array
|
|
755
867
|
sep = re.compile(r"/to/|/")
|
|
756
|
-
filter_v = [i for v in filter_v for i in sep.split(
|
|
868
|
+
filter_v = [i for v in filter_v for i in sep.split(str(v))]
|
|
757
869
|
|
|
758
870
|
# special handling for time 0000 converted to 0 by pre-formating with metadata_mapping
|
|
759
871
|
if keyword.split(":")[-1] == "time":
|
|
@@ -772,7 +884,10 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
772
884
|
# we assume that if the first value is an interval, all values are intervals
|
|
773
885
|
present_values = []
|
|
774
886
|
if keyword == "date" and "/" in entry[keyword][0]:
|
|
775
|
-
|
|
887
|
+
input_range = values
|
|
888
|
+
if isinstance(values, list):
|
|
889
|
+
input_range = values[0]
|
|
890
|
+
if any(is_range_in_range(x, input_range) for x in entry[keyword]):
|
|
776
891
|
present_values = filter_v
|
|
777
892
|
else:
|
|
778
893
|
present_values = [
|
|
@@ -792,12 +907,12 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
792
907
|
{value for c in constraints for value in c.get(keyword, [])}
|
|
793
908
|
)
|
|
794
909
|
# restore ecmwf: prefix before raising error
|
|
795
|
-
keyword =
|
|
910
|
+
keyword = ECMWF_PREFIX + keyword
|
|
796
911
|
|
|
797
912
|
all_keywords_str = ""
|
|
798
913
|
if len(parsed_keywords) > 1:
|
|
799
914
|
keywords = [
|
|
800
|
-
f"
|
|
915
|
+
f"{ECMWF_PREFIX + k}={pk}"
|
|
801
916
|
for k in parsed_keywords
|
|
802
917
|
if (pk := input_keywords.get(k))
|
|
803
918
|
]
|
|
@@ -846,6 +961,8 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
846
961
|
if name in ("area_group", "global", "warning", "licences"):
|
|
847
962
|
continue
|
|
848
963
|
if "type" not in element or element["type"] == "FreeEditionWidget":
|
|
964
|
+
# FreeEditionWidget used to select the whole available region
|
|
965
|
+
# and to provide comments for the dataset
|
|
849
966
|
continue
|
|
850
967
|
|
|
851
968
|
# ordering done by id -> set id to high value if not present -> element will be last
|
|
@@ -876,12 +993,8 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
876
993
|
if name == "area" and isinstance(default, dict):
|
|
877
994
|
default = list(default.values())
|
|
878
995
|
|
|
879
|
-
if default:
|
|
880
|
-
# We strip values of superfluous quotes (addded by mapping converter to_geojson).
|
|
881
|
-
default = strip_quotes(default)
|
|
882
|
-
|
|
883
996
|
# sometimes form returns default as array instead of string
|
|
884
|
-
if default and prop
|
|
997
|
+
if default and prop.get("type") == "string" and isinstance(default, list):
|
|
885
998
|
default = ",".join(default)
|
|
886
999
|
|
|
887
1000
|
is_required = bool(element.get("required"))
|
|
@@ -925,13 +1038,11 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
925
1038
|
# Needed to map constraints like "xxxx" to eodag parameter "ecmwf:xxxx"
|
|
926
1039
|
key = ecmwf_format(name)
|
|
927
1040
|
|
|
928
|
-
default = defaults.get(key)
|
|
929
|
-
|
|
930
1041
|
queryables[key] = Annotated[
|
|
931
1042
|
get_args(
|
|
932
1043
|
json_field_definition_to_python(
|
|
933
1044
|
{"type": "string", "title": name, "enum": values},
|
|
934
|
-
default_value=
|
|
1045
|
+
default_value=defaults.get(name),
|
|
935
1046
|
required=bool(key in required),
|
|
936
1047
|
)
|
|
937
1048
|
)
|
|
@@ -948,16 +1059,26 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
948
1059
|
:param properties: dict of properties to be formatted
|
|
949
1060
|
:return: dict of formatted properties
|
|
950
1061
|
"""
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1062
|
+
properties["productType"] = product_type
|
|
1063
|
+
|
|
1064
|
+
# provider product type specific conf
|
|
1065
|
+
product_type_def_params = self.get_product_type_def_params(
|
|
1066
|
+
product_type, format_variables=properties
|
|
954
1067
|
)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1068
|
+
|
|
1069
|
+
# Add to the query, the queryable parameters set in the provider product type definition
|
|
1070
|
+
properties.update(
|
|
1071
|
+
{
|
|
1072
|
+
k: v
|
|
1073
|
+
for k, v in product_type_def_params.items()
|
|
1074
|
+
if k not in properties.keys()
|
|
1075
|
+
and k in self.config.metadata_mapping.keys()
|
|
1076
|
+
and isinstance(self.config.metadata_mapping[k], list)
|
|
1077
|
+
}
|
|
1078
|
+
)
|
|
1079
|
+
qp, _ = self.build_query_string(product_type, properties)
|
|
1080
|
+
|
|
1081
|
+
return qp
|
|
961
1082
|
|
|
962
1083
|
def _fetch_data(self, url: str) -> Any:
|
|
963
1084
|
"""
|
|
@@ -1045,67 +1166,37 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1045
1166
|
discovery_config=getattr(self.config, "discover_metadata", {}),
|
|
1046
1167
|
)
|
|
1047
1168
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1169
|
+
properties = {
|
|
1170
|
+
# use product_type_config as default properties
|
|
1171
|
+
**getattr(self.config, "product_type_config", {}),
|
|
1172
|
+
**{ecmwf_format(k): v for k, v in parsed_properties.items()},
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
def slugify(date_str: str) -> str:
|
|
1176
|
+
return date_str.split("T")[0].replace("-", "")
|
|
1050
1177
|
|
|
1051
1178
|
# 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
|
-
)
|
|
1179
|
+
product_id = (product_type or kwargs.get("dataset") or self.provider).upper()
|
|
1098
1180
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1181
|
+
start = properties.get(START, NOT_AVAILABLE)
|
|
1182
|
+
end = properties.get(END, NOT_AVAILABLE)
|
|
1183
|
+
|
|
1184
|
+
if start != NOT_AVAILABLE:
|
|
1185
|
+
product_id += f"_{slugify(start)}"
|
|
1186
|
+
if end != NOT_AVAILABLE:
|
|
1187
|
+
product_id += f"_{slugify(end)}"
|
|
1188
|
+
|
|
1189
|
+
product_id += f"_{query_hash}"
|
|
1190
|
+
|
|
1191
|
+
properties["id"] = properties["title"] = product_id
|
|
1192
|
+
|
|
1193
|
+
# used by server mode to generate downloadlink href
|
|
1194
|
+
properties["_dc_qs"] = quote_plus(qs)
|
|
1104
1195
|
|
|
1105
1196
|
product = EOProduct(
|
|
1106
1197
|
provider=self.provider,
|
|
1107
1198
|
productType=product_type,
|
|
1108
|
-
properties=
|
|
1199
|
+
properties=properties,
|
|
1109
1200
|
)
|
|
1110
1201
|
|
|
1111
1202
|
return [
|
|
@@ -1182,17 +1273,15 @@ class MeteoblueSearch(ECMWFSearch):
|
|
|
1182
1273
|
return [response.json()]
|
|
1183
1274
|
|
|
1184
1275
|
def build_query_string(
|
|
1185
|
-
self, product_type: str,
|
|
1276
|
+
self, product_type: str, query_dict: dict[str, Any]
|
|
1186
1277
|
) -> tuple[dict[str, Any], str]:
|
|
1187
1278
|
"""Build The query string using the search parameters
|
|
1188
1279
|
|
|
1189
1280
|
:param product_type: product type id
|
|
1190
|
-
:param
|
|
1281
|
+
:param query_dict: keyword arguments to be used in the query string
|
|
1191
1282
|
:return: formatted query params and encode query string
|
|
1192
1283
|
"""
|
|
1193
|
-
return QueryStringSearch.build_query_string(
|
|
1194
|
-
self, product_type=product_type, **kwargs
|
|
1195
|
-
)
|
|
1284
|
+
return QueryStringSearch.build_query_string(self, product_type, query_dict)
|
|
1196
1285
|
|
|
1197
1286
|
|
|
1198
1287
|
class WekeoECMWFSearch(ECMWFSearch):
|
|
@@ -1256,25 +1345,3 @@ class WekeoECMWFSearch(ECMWFSearch):
|
|
|
1256
1345
|
:return: list containing the results from the provider in json format
|
|
1257
1346
|
"""
|
|
1258
1347
|
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
|
-
)
|