eodag 4.0.0a5__py3-none-any.whl → 4.0.0b1__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/collection.py +65 -1
- eodag/api/core.py +48 -16
- eodag/api/product/_assets.py +1 -1
- eodag/api/product/_product.py +108 -15
- eodag/api/product/drivers/__init__.py +3 -1
- eodag/api/product/drivers/base.py +3 -1
- eodag/api/product/drivers/generic.py +9 -5
- eodag/api/product/drivers/sentinel1.py +14 -9
- eodag/api/product/drivers/sentinel2.py +14 -7
- eodag/api/product/metadata_mapping.py +5 -2
- eodag/api/provider.py +1 -0
- eodag/api/search_result.py +4 -1
- eodag/cli.py +7 -7
- eodag/config.py +22 -4
- eodag/plugins/download/aws.py +3 -1
- eodag/plugins/download/http.py +4 -10
- eodag/plugins/search/base.py +8 -3
- eodag/plugins/search/build_search_result.py +108 -120
- eodag/plugins/search/cop_marine.py +3 -1
- eodag/plugins/search/qssearch.py +7 -6
- eodag/resources/collections.yml +255 -0
- eodag/resources/ext_collections.json +1 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/providers.yml +60 -25
- eodag/resources/user_conf_template.yml +6 -0
- eodag/types/__init__.py +22 -16
- eodag/types/download_args.py +3 -1
- eodag/types/queryables.py +125 -55
- eodag/types/stac_extensions.py +408 -0
- eodag/types/stac_metadata.py +312 -0
- eodag/utils/__init__.py +42 -4
- eodag/utils/dates.py +202 -2
- {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/METADATA +7 -13
- {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/RECORD +38 -36
- {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/WHEEL +1 -1
- {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/entry_points.txt +1 -1
- {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/top_level.txt +0 -0
eodag/cli.py
CHANGED
|
@@ -123,14 +123,14 @@ def _deprecated_cli(message: str, version: Optional[str] = None) -> Callable[...
|
|
|
123
123
|
help="Control the verbosity of the logs. For maximum verbosity, type -vvv",
|
|
124
124
|
)
|
|
125
125
|
@click.pass_context
|
|
126
|
-
def
|
|
126
|
+
def eodag_cli(ctx: Context, verbose: int) -> None:
|
|
127
127
|
"""Earth Observation Data Access Gateway: work on EO products from any provider"""
|
|
128
128
|
if ctx.obj is None:
|
|
129
129
|
ctx.obj = {}
|
|
130
130
|
ctx.obj["verbosity"] = verbose
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
@
|
|
133
|
+
@eodag_cli.command(name="version", help="Print eodag version and exit")
|
|
134
134
|
def version() -> None:
|
|
135
135
|
"""Print eodag version and exit"""
|
|
136
136
|
click.echo(
|
|
@@ -142,7 +142,7 @@ def version() -> None:
|
|
|
142
142
|
)
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
@
|
|
145
|
+
@eodag_cli.command(
|
|
146
146
|
name="search",
|
|
147
147
|
help="Search satellite images by their collections, instruments, constellation, "
|
|
148
148
|
"platform, processing level or sensor type. It is mandatory to provide "
|
|
@@ -407,7 +407,7 @@ def search_crunch(ctx: Context, **kwargs: Any) -> None:
|
|
|
407
407
|
ctx.obj["search_results"] = results
|
|
408
408
|
|
|
409
409
|
|
|
410
|
-
@
|
|
410
|
+
@eodag_cli.command(name="list", help="List supported collections")
|
|
411
411
|
@click.option("-p", "--provider", help="List collections supported by this provider")
|
|
412
412
|
@click.option(
|
|
413
413
|
"--instruments", help="List collections originating from these instruments"
|
|
@@ -489,7 +489,7 @@ def list_col(ctx: Context, **kwargs: Any) -> None:
|
|
|
489
489
|
sys.exit(1)
|
|
490
490
|
|
|
491
491
|
|
|
492
|
-
@
|
|
492
|
+
@eodag_cli.command(name="discover", help="Fetch providers to discover collections")
|
|
493
493
|
@click.option("-p", "--provider", help="Fetch only the given provider")
|
|
494
494
|
@click.option(
|
|
495
495
|
"--storage",
|
|
@@ -520,7 +520,7 @@ def discover_col(ctx: Context, **kwargs: Any) -> None:
|
|
|
520
520
|
click.echo("Results stored at '{}'".format(storage_filepath))
|
|
521
521
|
|
|
522
522
|
|
|
523
|
-
@
|
|
523
|
+
@eodag_cli.command(
|
|
524
524
|
help="""Download a list of products from a serialized search result or STAC items URLs/paths
|
|
525
525
|
|
|
526
526
|
Examples:
|
|
@@ -626,4 +626,4 @@ def download(ctx: Context, **kwargs: Any) -> None:
|
|
|
626
626
|
|
|
627
627
|
|
|
628
628
|
if __name__ == "__main__":
|
|
629
|
-
|
|
629
|
+
eodag_cli(obj={})
|
eodag/config.py
CHANGED
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
22
|
from importlib.resources import files as res_files
|
|
23
|
-
from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional,
|
|
23
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Union
|
|
24
24
|
|
|
25
25
|
import orjson
|
|
26
26
|
import requests
|
|
@@ -28,6 +28,7 @@ import yaml
|
|
|
28
28
|
import yaml.parser
|
|
29
29
|
from annotated_types import Gt
|
|
30
30
|
from jsonpath_ng import JSONPath
|
|
31
|
+
from typing_extensions import TypedDict
|
|
31
32
|
|
|
32
33
|
from eodag.utils import (
|
|
33
34
|
HTTP_REQ_TIMEOUT,
|
|
@@ -635,12 +636,29 @@ class PluginConfig(yaml.YAMLObject):
|
|
|
635
636
|
matching_conf = getattr(self, "matching_conf", {})
|
|
636
637
|
matching_url = getattr(self, "matching_url", None)
|
|
637
638
|
|
|
638
|
-
|
|
639
|
-
|
|
639
|
+
# both match
|
|
640
|
+
if (
|
|
641
|
+
target_matching_conf
|
|
642
|
+
and sort_dict(target_matching_conf) == sort_dict(matching_conf)
|
|
643
|
+
and target_matching_url
|
|
644
|
+
and target_matching_url == matching_url
|
|
640
645
|
):
|
|
641
646
|
return True
|
|
642
647
|
|
|
643
|
-
|
|
648
|
+
# conf matches and no matching_url expected
|
|
649
|
+
if (
|
|
650
|
+
target_matching_conf
|
|
651
|
+
and sort_dict(target_matching_conf) == sort_dict(matching_conf)
|
|
652
|
+
and not target_matching_url
|
|
653
|
+
):
|
|
654
|
+
return True
|
|
655
|
+
|
|
656
|
+
# url matches and no matching_conf expected
|
|
657
|
+
if (
|
|
658
|
+
target_matching_url
|
|
659
|
+
and target_matching_url == matching_url
|
|
660
|
+
and not target_matching_conf
|
|
661
|
+
):
|
|
644
662
|
return True
|
|
645
663
|
|
|
646
664
|
return False
|
eodag/plugins/download/aws.py
CHANGED
|
@@ -771,7 +771,9 @@ class AwsDownload(Download):
|
|
|
771
771
|
ignore_assets,
|
|
772
772
|
product,
|
|
773
773
|
)
|
|
774
|
-
|
|
774
|
+
|
|
775
|
+
# check if auth is a S3 resource by verifying it has the meta.client attribute.
|
|
776
|
+
if auth and hasattr(auth, "meta") and hasattr(auth.meta, "client"):
|
|
775
777
|
s3_resource = auth
|
|
776
778
|
else:
|
|
777
779
|
s3_resource = boto3.resource(
|
eodag/plugins/download/http.py
CHANGED
|
@@ -27,16 +27,7 @@ from email.message import Message
|
|
|
27
27
|
from itertools import chain
|
|
28
28
|
from json import JSONDecodeError
|
|
29
29
|
from pathlib import Path
|
|
30
|
-
from typing import
|
|
31
|
-
TYPE_CHECKING,
|
|
32
|
-
Any,
|
|
33
|
-
Iterator,
|
|
34
|
-
Literal,
|
|
35
|
-
Optional,
|
|
36
|
-
TypedDict,
|
|
37
|
-
Union,
|
|
38
|
-
cast,
|
|
39
|
-
)
|
|
30
|
+
from typing import TYPE_CHECKING, Any, Iterator, Literal, Optional, Union, cast
|
|
40
31
|
from urllib.parse import parse_qs, urlparse
|
|
41
32
|
|
|
42
33
|
import geojson
|
|
@@ -46,6 +37,7 @@ from lxml import etree
|
|
|
46
37
|
from requests import RequestException
|
|
47
38
|
from requests.auth import AuthBase
|
|
48
39
|
from requests.structures import CaseInsensitiveDict
|
|
40
|
+
from typing_extensions import TypedDict
|
|
49
41
|
from zipstream import ZipStream
|
|
50
42
|
|
|
51
43
|
from eodag.api.product.metadata_mapping import (
|
|
@@ -476,6 +468,8 @@ class HTTPDownload(Download):
|
|
|
476
468
|
if (
|
|
477
469
|
success_status and success_status != status_dict.get("eodag:order_status")
|
|
478
470
|
) or (success_code and success_code != response.status_code):
|
|
471
|
+
# Remove the download link if the order has not been completed or was not successful
|
|
472
|
+
product.properties.pop("eodag:download_link", None)
|
|
479
473
|
return None
|
|
480
474
|
|
|
481
475
|
product.properties["order:status"] = ONLINE_STATUS
|
eodag/plugins/search/base.py
CHANGED
|
@@ -35,6 +35,7 @@ from eodag.plugins.search import PreparedSearch
|
|
|
35
35
|
from eodag.types import model_fields_to_annotated
|
|
36
36
|
from eodag.types.queryables import Queryables, QueryablesDict
|
|
37
37
|
from eodag.types.search_args import SortByList
|
|
38
|
+
from eodag.types.stac_metadata import CommonStacMetadata, create_stac_metadata_model
|
|
38
39
|
from eodag.utils import (
|
|
39
40
|
GENERIC_COLLECTION,
|
|
40
41
|
copy_deepcopy,
|
|
@@ -358,7 +359,7 @@ class Search(PluginTopic):
|
|
|
358
359
|
queryables = self.discover_queryables(**{**default_values, **filters}) or {}
|
|
359
360
|
except NotImplementedError as e:
|
|
360
361
|
if str(e):
|
|
361
|
-
logger.debug(str(e))
|
|
362
|
+
logger.debug("%s, configured metadata-mapping used", str(e))
|
|
362
363
|
queryables = self.queryables_from_metadata_mapping(collection, alias)
|
|
363
364
|
|
|
364
365
|
return QueryablesDict(**queryables)
|
|
@@ -408,9 +409,10 @@ class Search(PluginTopic):
|
|
|
408
409
|
col_queryables = self._get_collection_queryables(col, None, filters)
|
|
409
410
|
all_queryables.update(col_queryables)
|
|
410
411
|
# reset defaults because they may vary between collections
|
|
412
|
+
queryables_fields = Queryables.from_stac_models().model_fields
|
|
411
413
|
for k, v in all_queryables.items():
|
|
412
414
|
v.__metadata__[0].default = getattr(
|
|
413
|
-
|
|
415
|
+
queryables_fields.get(k, Field(None)), "default", None
|
|
414
416
|
)
|
|
415
417
|
return QueryablesDict(
|
|
416
418
|
additional_properties=auto_discovery,
|
|
@@ -468,8 +470,11 @@ class Search(PluginTopic):
|
|
|
468
470
|
):
|
|
469
471
|
del metadata_mapping[param]
|
|
470
472
|
|
|
473
|
+
queryables_model = create_stac_metadata_model(
|
|
474
|
+
base_models=[Queryables, CommonStacMetadata]
|
|
475
|
+
)
|
|
471
476
|
eodag_queryables = copy_deepcopy(
|
|
472
|
-
model_fields_to_annotated(
|
|
477
|
+
model_fields_to_annotated(queryables_model.model_fields)
|
|
473
478
|
)
|
|
474
479
|
queryables["collection"] = eodag_queryables.pop("collection")
|
|
475
480
|
# add default value for collection
|
|
@@ -21,9 +21,9 @@ import hashlib
|
|
|
21
21
|
import logging
|
|
22
22
|
import re
|
|
23
23
|
from collections import OrderedDict
|
|
24
|
-
from datetime import
|
|
24
|
+
from datetime import datetime, timedelta
|
|
25
25
|
from types import MethodType
|
|
26
|
-
from typing import TYPE_CHECKING, Annotated, Any, Optional
|
|
26
|
+
from typing import TYPE_CHECKING, Annotated, Any, Optional
|
|
27
27
|
from urllib.parse import quote_plus, unquote_plus
|
|
28
28
|
|
|
29
29
|
import geojson
|
|
@@ -31,6 +31,7 @@ import orjson
|
|
|
31
31
|
from dateutil.parser import isoparse
|
|
32
32
|
from dateutil.tz import tzutc
|
|
33
33
|
from dateutil.utils import today
|
|
34
|
+
from pydantic import AliasChoices
|
|
34
35
|
from pydantic.fields import FieldInfo
|
|
35
36
|
from requests.auth import AuthBase
|
|
36
37
|
from typing_extensions import get_args # noqa: F401
|
|
@@ -58,10 +59,18 @@ from eodag.utils import (
|
|
|
58
59
|
format_string,
|
|
59
60
|
get_geometry_from_ecmwf_area,
|
|
60
61
|
get_geometry_from_ecmwf_feature,
|
|
62
|
+
get_geometry_from_ecmwf_location,
|
|
61
63
|
get_geometry_from_various,
|
|
62
64
|
)
|
|
63
65
|
from eodag.utils.cache import instance_cached_method
|
|
64
|
-
from eodag.utils.dates import
|
|
66
|
+
from eodag.utils.dates import (
|
|
67
|
+
COMPACT_DATE_RANGE_PATTERN,
|
|
68
|
+
DATE_RANGE_PATTERN,
|
|
69
|
+
format_date,
|
|
70
|
+
is_range_in_range,
|
|
71
|
+
parse_date,
|
|
72
|
+
parse_year_month_day,
|
|
73
|
+
)
|
|
65
74
|
from eodag.utils.exceptions import DownloadError, NotAvailableError, ValidationError
|
|
66
75
|
from eodag.utils.requests import fetch_json
|
|
67
76
|
|
|
@@ -184,6 +193,7 @@ COP_DS_KEYWORDS = {
|
|
|
184
193
|
"region",
|
|
185
194
|
"release_version",
|
|
186
195
|
"satellite",
|
|
196
|
+
"satellite_mission",
|
|
187
197
|
"sensor",
|
|
188
198
|
"sensor_and_algorithm",
|
|
189
199
|
"soil_level",
|
|
@@ -285,6 +295,27 @@ def _update_properties_from_element(
|
|
|
285
295
|
if element["type"] == "DateRangeWidget":
|
|
286
296
|
prop["description"] = "date formatted like yyyy-mm-dd/yyyy-mm-dd"
|
|
287
297
|
|
|
298
|
+
# a single geographic location
|
|
299
|
+
if element["type"] == "GeographicLocationWidget":
|
|
300
|
+
prop.update(
|
|
301
|
+
{
|
|
302
|
+
"type": "object",
|
|
303
|
+
"description": "Longitude and latitude of a single location",
|
|
304
|
+
"properties": {
|
|
305
|
+
"longitude": {
|
|
306
|
+
"type": "number",
|
|
307
|
+
"maximum": 180,
|
|
308
|
+
"minimum": -180,
|
|
309
|
+
},
|
|
310
|
+
"latitude": {
|
|
311
|
+
"type": "number",
|
|
312
|
+
"maximum": 90,
|
|
313
|
+
"minimum": -90,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
|
|
288
319
|
if description := element.get("help"):
|
|
289
320
|
prop["description"] = description
|
|
290
321
|
|
|
@@ -307,93 +338,6 @@ def ecmwf_format(v: str, alias: bool = True) -> str:
|
|
|
307
338
|
return f"{ECMWF_PREFIX[:-1]}{separator}{v}" if v in ALLOWED_KEYWORDS else v
|
|
308
339
|
|
|
309
340
|
|
|
310
|
-
def get_min_max(
|
|
311
|
-
value: Optional[Union[str, list[str]]] = None,
|
|
312
|
-
) -> tuple[Optional[str], Optional[str]]:
|
|
313
|
-
"""Returns the min and max from a list of strings or the same string if a single string is given."""
|
|
314
|
-
if isinstance(value, list):
|
|
315
|
-
sorted_values = sorted(value)
|
|
316
|
-
return sorted_values[0], sorted_values[-1]
|
|
317
|
-
return value, value
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def append_time(input_date: date, time: Optional[str]) -> datetime:
|
|
321
|
-
"""
|
|
322
|
-
Parses a time string in format HHMM and appends it to a date.
|
|
323
|
-
|
|
324
|
-
if the time string is in format HH:MM or HH_MM we convert it to HHMM
|
|
325
|
-
"""
|
|
326
|
-
if not time:
|
|
327
|
-
time = "0000"
|
|
328
|
-
time = re.sub(":|_", "", time)
|
|
329
|
-
if time == "2400":
|
|
330
|
-
time = "0000"
|
|
331
|
-
dt = datetime.combine(input_date, datetime.strptime(time, "%H%M").time())
|
|
332
|
-
dt.replace(tzinfo=timezone.utc)
|
|
333
|
-
return dt
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def parse_date(
|
|
337
|
-
date: str, time: Optional[Union[str, list[str]]]
|
|
338
|
-
) -> tuple[datetime, datetime]:
|
|
339
|
-
"""Parses a date string in formats YYYY-MM-DD, YYYMMDD, solo or in start/end or start/to/end intervals."""
|
|
340
|
-
if "to" in date:
|
|
341
|
-
start_date_str, end_date_str = date.split("/to/")
|
|
342
|
-
elif "/" in date:
|
|
343
|
-
dates = date.split("/")
|
|
344
|
-
start_date_str = dates[0]
|
|
345
|
-
end_date_str = dates[-1]
|
|
346
|
-
else:
|
|
347
|
-
start_date_str = end_date_str = date
|
|
348
|
-
|
|
349
|
-
# Update YYYYMMDD formatted dates
|
|
350
|
-
if re.match(r"^\d{8}$", start_date_str):
|
|
351
|
-
start_date_str = (
|
|
352
|
-
f"{start_date_str[:4]}-{start_date_str[4:6]}-{start_date_str[6:]}"
|
|
353
|
-
)
|
|
354
|
-
if re.match(r"^\d{8}$", end_date_str):
|
|
355
|
-
end_date_str = f"{end_date_str[:4]}-{end_date_str[4:6]}-{end_date_str[6:]}"
|
|
356
|
-
|
|
357
|
-
start_date = datetime.fromisoformat(start_date_str.rstrip("Z"))
|
|
358
|
-
end_date = datetime.fromisoformat(end_date_str.rstrip("Z"))
|
|
359
|
-
|
|
360
|
-
if time:
|
|
361
|
-
start_t, end_t = get_min_max(time)
|
|
362
|
-
start_date = append_time(start_date.date(), start_t)
|
|
363
|
-
end_date = append_time(end_date.date(), end_t)
|
|
364
|
-
|
|
365
|
-
return start_date, end_date
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def parse_year_month_day(
|
|
369
|
-
year: Union[str, list[str]],
|
|
370
|
-
month: Optional[Union[str, list[str]]] = None,
|
|
371
|
-
day: Optional[Union[str, list[str]]] = None,
|
|
372
|
-
time: Optional[Union[str, list[str]]] = None,
|
|
373
|
-
) -> tuple[datetime, datetime]:
|
|
374
|
-
"""Extracts and returns the year, month, day, and time from the parameters."""
|
|
375
|
-
|
|
376
|
-
def build_date(year, month=None, day=None, time=None) -> datetime:
|
|
377
|
-
"""Datetime from default_date with updated year, month, day and time."""
|
|
378
|
-
updated_date = datetime(int(year), 1, 1).replace(
|
|
379
|
-
month=int(month) if month is not None else 1,
|
|
380
|
-
day=int(day) if day is not None else 1,
|
|
381
|
-
)
|
|
382
|
-
if time is not None:
|
|
383
|
-
updated_date = append_time(updated_date.date(), time)
|
|
384
|
-
return updated_date
|
|
385
|
-
|
|
386
|
-
start_y, end_y = get_min_max(year)
|
|
387
|
-
start_m, end_m = get_min_max(month)
|
|
388
|
-
start_d, end_d = get_min_max(day)
|
|
389
|
-
start_t, end_t = get_min_max(time)
|
|
390
|
-
|
|
391
|
-
start_date = build_date(start_y, start_m, start_d, start_t)
|
|
392
|
-
end_date = build_date(end_y, end_m, end_d, end_t)
|
|
393
|
-
|
|
394
|
-
return start_date, end_date
|
|
395
|
-
|
|
396
|
-
|
|
397
341
|
def ecmwf_temporal_to_eodag(
|
|
398
342
|
params: dict[str, Any]
|
|
399
343
|
) -> tuple[Optional[str], Optional[str]]:
|
|
@@ -627,6 +571,12 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
627
571
|
if getattr(self.config, "dates_required", False):
|
|
628
572
|
self._check_date_params(params, collection)
|
|
629
573
|
|
|
574
|
+
# read 'start_datetime' and 'end_datetime' from 'date' range
|
|
575
|
+
if "date" in params:
|
|
576
|
+
start_date, end_date = parse_date(params["date"])
|
|
577
|
+
params[START] = format_date(start_date)
|
|
578
|
+
params[END] = format_date(end_date)
|
|
579
|
+
|
|
630
580
|
# adapt end date if it is midnight
|
|
631
581
|
if END in params:
|
|
632
582
|
end_date_excluded = getattr(self.config, "end_date_excluded", True)
|
|
@@ -666,6 +616,10 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
666
616
|
if "area" in params:
|
|
667
617
|
params["geometry"] = get_geometry_from_ecmwf_area(params["area"])
|
|
668
618
|
params.pop("area")
|
|
619
|
+
# single location
|
|
620
|
+
if "location" in params:
|
|
621
|
+
params["geometry"] = get_geometry_from_ecmwf_location(params["location"])
|
|
622
|
+
params.pop("location")
|
|
669
623
|
|
|
670
624
|
return params
|
|
671
625
|
|
|
@@ -870,9 +824,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
870
824
|
)
|
|
871
825
|
not in set(list(available_values.keys()) + [f["name"] for f in form])
|
|
872
826
|
):
|
|
873
|
-
raise ValidationError(
|
|
874
|
-
f"'{keyword}' is not a queryable parameter", {keyword}
|
|
875
|
-
)
|
|
827
|
+
raise ValidationError("'%s' is not a queryable parameter" % keyword)
|
|
876
828
|
|
|
877
829
|
# generate queryables
|
|
878
830
|
if form:
|
|
@@ -890,7 +842,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
890
842
|
# start and end filters are supported whenever combinations of "year", "month", "day" filters exist
|
|
891
843
|
queryable_prefix = f"{ECMWF_PREFIX[:-1]}_"
|
|
892
844
|
if (
|
|
893
|
-
|
|
845
|
+
f"{queryable_prefix}date" in queryables
|
|
894
846
|
or f"{queryable_prefix}year" in queryables
|
|
895
847
|
or f"{queryable_prefix}hyear" in queryables
|
|
896
848
|
):
|
|
@@ -959,13 +911,21 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
959
911
|
)
|
|
960
912
|
|
|
961
913
|
# We convert every single value to a list of string
|
|
962
|
-
filter_v = values if isinstance(values,
|
|
914
|
+
filter_v = list(values) if isinstance(values, tuple) else values
|
|
915
|
+
filter_v = filter_v if isinstance(filter_v, list) else [filter_v]
|
|
963
916
|
|
|
964
917
|
# We strip values of superfluous quotes (added by mapping converter to_geojson).
|
|
965
|
-
# ECMWF accept
|
|
966
|
-
# ECMWF accept
|
|
967
|
-
|
|
968
|
-
|
|
918
|
+
# ECMWF accept date ranges with /to/. We need to split it to an array
|
|
919
|
+
# ECMWF accept date ranges in format val1/val2. We need to split it to an array
|
|
920
|
+
date_regex = [
|
|
921
|
+
re.compile(p) for p in (DATE_RANGE_PATTERN, COMPACT_DATE_RANGE_PATTERN)
|
|
922
|
+
]
|
|
923
|
+
is_date = any(
|
|
924
|
+
any(r.match(v) is not None for r in date_regex) for v in filter_v
|
|
925
|
+
)
|
|
926
|
+
if is_date:
|
|
927
|
+
sep = re.compile(r"/to/|/")
|
|
928
|
+
filter_v = [i for v in filter_v for i in sep.split(str(v))]
|
|
969
929
|
|
|
970
930
|
# special handling for time 0000 converted to 0 by pre-formating with metadata_mapping
|
|
971
931
|
if keyword.split(":")[-1] == "time":
|
|
@@ -976,23 +936,35 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
976
936
|
|
|
977
937
|
# Filter constraints and check for missing values
|
|
978
938
|
filtered_constraints = []
|
|
939
|
+
# True if some constraint is defined for this keyword.
|
|
940
|
+
# In other words: if no constraint defines a list of values
|
|
941
|
+
# then any value is allowed for this keyword
|
|
942
|
+
keyword_constrained = False
|
|
979
943
|
for entry in constraints:
|
|
980
944
|
# Filter based on the presence of any value in filter_v
|
|
981
945
|
entry_values = entry.get(keyword, [])
|
|
946
|
+
if entry_values:
|
|
947
|
+
keyword_constrained = True
|
|
982
948
|
|
|
983
|
-
# date constraint may be intervals. We identify intervals with a "/" in the value
|
|
984
|
-
#
|
|
949
|
+
# date constraint may be intervals. We identify intervals with a "/" in the value.
|
|
950
|
+
# date constraint can be a mixed list of single values (e.g "2023-06-27")
|
|
951
|
+
# and intervals (e.g. "2024-11-12/2025-11-20").
|
|
952
|
+
# collections with mixed values: CAMS_GAC_FORECAST, CAMS_EU_AIR_QUALITY_FORECAST
|
|
985
953
|
present_values = []
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
954
|
+
for entry_value in entry_values:
|
|
955
|
+
if keyword == "date" and "/" in entry_value:
|
|
956
|
+
input_range = values
|
|
957
|
+
if isinstance(values, list):
|
|
958
|
+
input_range = values[0]
|
|
959
|
+
if "/" not in input_range:
|
|
960
|
+
input_range = f"{input_range}/{input_range}"
|
|
961
|
+
if is_range_in_range(entry_value, input_range):
|
|
962
|
+
present_values.extend(filter_v)
|
|
963
|
+
else:
|
|
964
|
+
new_values = [
|
|
965
|
+
value for value in filter_v if value == entry_value
|
|
966
|
+
]
|
|
967
|
+
present_values.extend(new_values)
|
|
996
968
|
|
|
997
969
|
# Remove present values from the missing_values set
|
|
998
970
|
missing_values -= set(present_values)
|
|
@@ -1002,7 +974,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1002
974
|
|
|
1003
975
|
# raise an error as no constraint entry matched the input keywords
|
|
1004
976
|
# raise an error if one value from input is not allowed
|
|
1005
|
-
if not filtered_constraints or missing_values:
|
|
977
|
+
if keyword_constrained and (not filtered_constraints or missing_values):
|
|
1006
978
|
allowed_values = list(
|
|
1007
979
|
{value for c in constraints for value in c.get(keyword, [])}
|
|
1008
980
|
)
|
|
@@ -1028,7 +1000,9 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1028
1000
|
)
|
|
1029
1001
|
|
|
1030
1002
|
parsed_keywords.append(keyword)
|
|
1031
|
-
|
|
1003
|
+
# if the keyword is not constrained then any value is allowed
|
|
1004
|
+
if keyword_constrained:
|
|
1005
|
+
constraints = filtered_constraints
|
|
1032
1006
|
|
|
1033
1007
|
available_values: dict[str, Any] = {k: set() for k in ordered_keywords}
|
|
1034
1008
|
|
|
@@ -1100,9 +1074,21 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1100
1074
|
if default and prop.get("type") == "string" and isinstance(default, list):
|
|
1101
1075
|
default = ",".join(default)
|
|
1102
1076
|
|
|
1103
|
-
is_required
|
|
1104
|
-
|
|
1105
|
-
|
|
1077
|
+
is_required: bool
|
|
1078
|
+
if available_values.get(name):
|
|
1079
|
+
# required by the filtered constraints (available_values[name] is a not empty list)
|
|
1080
|
+
is_required = True
|
|
1081
|
+
elif bool(element.get("required")):
|
|
1082
|
+
if name in available_values and not available_values[name]:
|
|
1083
|
+
# not required by the filtered constraints (available_values[name] is an empty list)
|
|
1084
|
+
is_required = False
|
|
1085
|
+
else:
|
|
1086
|
+
# required only by form
|
|
1087
|
+
is_required = True
|
|
1088
|
+
else:
|
|
1089
|
+
# not required by form
|
|
1090
|
+
is_required = False
|
|
1091
|
+
|
|
1106
1092
|
if is_required:
|
|
1107
1093
|
required_list.append(name)
|
|
1108
1094
|
|
|
@@ -1114,7 +1100,8 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1114
1100
|
prop,
|
|
1115
1101
|
default_value=default,
|
|
1116
1102
|
required=is_required,
|
|
1117
|
-
|
|
1103
|
+
validation_alias=AliasChoices(formatted_alias, name),
|
|
1104
|
+
serialization_alias=formatted_alias,
|
|
1118
1105
|
)
|
|
1119
1106
|
)
|
|
1120
1107
|
]
|
|
@@ -1153,7 +1140,8 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1153
1140
|
{"type": "string", "title": name, "enum": values},
|
|
1154
1141
|
default_value=defaults.get(name),
|
|
1155
1142
|
required=bool(formatted_alias in required),
|
|
1156
|
-
|
|
1143
|
+
validation_alias=AliasChoices(formatted_alias, name),
|
|
1144
|
+
serialization_alias=formatted_alias,
|
|
1157
1145
|
)
|
|
1158
1146
|
)
|
|
1159
1147
|
]
|
|
@@ -1279,7 +1267,7 @@ class ECMWFSearch(PostJsonSearch):
|
|
|
1279
1267
|
|
|
1280
1268
|
# collection alias (required by opentelemetry-instrumentation-eodag)
|
|
1281
1269
|
if alias := getattr(self.config, "collection_config", {}).get("alias"):
|
|
1282
|
-
|
|
1270
|
+
kwargs["collection"] = alias
|
|
1283
1271
|
|
|
1284
1272
|
qs = geojson.dumps(sorted_unpaginated_qp)
|
|
1285
1273
|
|
|
@@ -1521,7 +1509,7 @@ class MeteoblueSearch(ECMWFSearch):
|
|
|
1521
1509
|
properties = {ecmwf_format(k): v for k, v in parsed_properties.items()}
|
|
1522
1510
|
# collection alias (required by opentelemetry-instrumentation-eodag)
|
|
1523
1511
|
if alias := getattr(self.config, "collection_config", {}).get("alias"):
|
|
1524
|
-
|
|
1512
|
+
collection = alias
|
|
1525
1513
|
|
|
1526
1514
|
def slugify(date_str: str) -> str:
|
|
1527
1515
|
return date_str.split("T")[0].replace("-", "")
|
|
@@ -512,9 +512,11 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
512
512
|
else {}
|
|
513
513
|
)
|
|
514
514
|
|
|
515
|
+
number_matched = num_total if prep.count else None
|
|
516
|
+
|
|
515
517
|
formated_result = SearchResult(
|
|
516
518
|
products,
|
|
517
|
-
|
|
519
|
+
number_matched,
|
|
518
520
|
search_params=search_params,
|
|
519
521
|
next_page_token=str(start_index + 1),
|
|
520
522
|
)
|
eodag/plugins/search/qssearch.py
CHANGED
|
@@ -28,7 +28,6 @@ from typing import (
|
|
|
28
28
|
Callable,
|
|
29
29
|
Optional,
|
|
30
30
|
Sequence,
|
|
31
|
-
TypedDict,
|
|
32
31
|
cast,
|
|
33
32
|
get_args,
|
|
34
33
|
)
|
|
@@ -58,6 +57,7 @@ from pydantic.fields import FieldInfo
|
|
|
58
57
|
from requests import Response
|
|
59
58
|
from requests.adapters import HTTPAdapter
|
|
60
59
|
from requests.auth import AuthBase
|
|
60
|
+
from typing_extensions import TypedDict
|
|
61
61
|
from urllib3 import Retry
|
|
62
62
|
|
|
63
63
|
from eodag.api.product import EOProduct
|
|
@@ -1283,7 +1283,7 @@ class QueryStringSearch(Search):
|
|
|
1283
1283
|
)
|
|
1284
1284
|
# collection alias (required by opentelemetry-instrumentation-eodag)
|
|
1285
1285
|
if alias := getattr(self.config, "collection_config", {}).get("alias"):
|
|
1286
|
-
|
|
1286
|
+
kwargs["collection"] = alias
|
|
1287
1287
|
product = EOProduct(self.provider, properties, **kwargs)
|
|
1288
1288
|
|
|
1289
1289
|
additional_assets = self.get_assets_from_mapping(result)
|
|
@@ -2166,17 +2166,18 @@ class StacSearch(PostJsonSearch):
|
|
|
2166
2166
|
# convert json results to pydantic model fields
|
|
2167
2167
|
field_definitions: dict[str, Any] = dict()
|
|
2168
2168
|
eodag_queryables_and_defaults: list[tuple[str, Any]] = []
|
|
2169
|
+
StacQueryables = Queryables.from_stac_models()
|
|
2169
2170
|
for json_param, json_mtd in json_queryables.items():
|
|
2170
2171
|
param = get_queryable_from_provider(
|
|
2171
2172
|
json_param, self.get_metadata_mapping(collection)
|
|
2172
|
-
) or
|
|
2173
|
+
) or StacQueryables.get_queryable_from_alias(json_param)
|
|
2173
2174
|
# do not expose internal parameters, neither datetime
|
|
2174
2175
|
if param == "datetime" or param.startswith("_"):
|
|
2175
2176
|
continue
|
|
2176
2177
|
|
|
2177
2178
|
default = kwargs.get(param, json_mtd.get("default"))
|
|
2178
2179
|
|
|
2179
|
-
if param in
|
|
2180
|
+
if param in StacQueryables.model_fields:
|
|
2180
2181
|
# use eodag queryable as default
|
|
2181
2182
|
eodag_queryables_and_defaults += [(param, default)]
|
|
2182
2183
|
continue
|
|
@@ -2195,12 +2196,12 @@ class StacSearch(PostJsonSearch):
|
|
|
2195
2196
|
|
|
2196
2197
|
# append eodag queryables
|
|
2197
2198
|
for param, default in eodag_queryables_and_defaults:
|
|
2198
|
-
queryables_dict[param] =
|
|
2199
|
+
queryables_dict[param] = StacQueryables.get_with_default(param, default)
|
|
2199
2200
|
|
|
2200
2201
|
# append "datetime" as "start" & "end" if needed
|
|
2201
2202
|
if "datetime" in json_queryables:
|
|
2202
2203
|
eodag_queryables = copy_deepcopy(
|
|
2203
|
-
model_fields_to_annotated(
|
|
2204
|
+
model_fields_to_annotated(StacQueryables.model_fields)
|
|
2204
2205
|
)
|
|
2205
2206
|
queryables_dict.setdefault("start", eodag_queryables["start"])
|
|
2206
2207
|
queryables_dict.setdefault("end", eodag_queryables["end"])
|