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.
Files changed (38) hide show
  1. eodag/api/collection.py +65 -1
  2. eodag/api/core.py +48 -16
  3. eodag/api/product/_assets.py +1 -1
  4. eodag/api/product/_product.py +108 -15
  5. eodag/api/product/drivers/__init__.py +3 -1
  6. eodag/api/product/drivers/base.py +3 -1
  7. eodag/api/product/drivers/generic.py +9 -5
  8. eodag/api/product/drivers/sentinel1.py +14 -9
  9. eodag/api/product/drivers/sentinel2.py +14 -7
  10. eodag/api/product/metadata_mapping.py +5 -2
  11. eodag/api/provider.py +1 -0
  12. eodag/api/search_result.py +4 -1
  13. eodag/cli.py +7 -7
  14. eodag/config.py +22 -4
  15. eodag/plugins/download/aws.py +3 -1
  16. eodag/plugins/download/http.py +4 -10
  17. eodag/plugins/search/base.py +8 -3
  18. eodag/plugins/search/build_search_result.py +108 -120
  19. eodag/plugins/search/cop_marine.py +3 -1
  20. eodag/plugins/search/qssearch.py +7 -6
  21. eodag/resources/collections.yml +255 -0
  22. eodag/resources/ext_collections.json +1 -1
  23. eodag/resources/ext_product_types.json +1 -1
  24. eodag/resources/providers.yml +60 -25
  25. eodag/resources/user_conf_template.yml +6 -0
  26. eodag/types/__init__.py +22 -16
  27. eodag/types/download_args.py +3 -1
  28. eodag/types/queryables.py +125 -55
  29. eodag/types/stac_extensions.py +408 -0
  30. eodag/types/stac_metadata.py +312 -0
  31. eodag/utils/__init__.py +42 -4
  32. eodag/utils/dates.py +202 -2
  33. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/METADATA +7 -13
  34. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/RECORD +38 -36
  35. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/WHEEL +1 -1
  36. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/entry_points.txt +1 -1
  37. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
  38. {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 eodag(ctx: Context, verbose: int) -> None:
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
- @eodag.command(name="version", help="Print eodag version and exit")
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
- @eodag.command(
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
- @eodag.command(name="list", help="List supported collections")
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
- @eodag.command(name="discover", help="Fetch providers to discover collections")
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
- @eodag.command(
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
- eodag(obj={})
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, TypedDict, Union
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
- if target_matching_conf and sort_dict(target_matching_conf) == sort_dict(
639
- matching_conf
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
- if target_matching_url and target_matching_url == matching_url:
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
@@ -771,7 +771,9 @@ class AwsDownload(Download):
771
771
  ignore_assets,
772
772
  product,
773
773
  )
774
- if auth and isinstance(auth, boto3.resource("s3").__class__):
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(
@@ -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
@@ -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
- Queryables.model_fields.get(k, Field(None)), "default", None
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(Queryables.model_fields)
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 date, datetime, timedelta, timezone
24
+ from datetime import datetime, timedelta
25
25
  from types import MethodType
26
- from typing import TYPE_CHECKING, Annotated, Any, Optional, Union
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 is_range_in_range
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
- queryables.pop(f"{queryable_prefix}date", None)
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, (list, tuple)) else [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 values with /to/. We need to split it to an array
966
- # ECMWF accept values in format val1/val2. We need to split it to an array
967
- sep = re.compile(r"/to/|/")
968
- filter_v = [i for v in filter_v for i in sep.split(str(v))]
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
- # we assume that if the first value is an interval, all values are intervals
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
- if keyword == "date" and "/" in entry[keyword][0]:
987
- input_range = values
988
- if isinstance(values, list):
989
- input_range = values[0]
990
- if any(is_range_in_range(x, input_range) for x in entry[keyword]):
991
- present_values = filter_v
992
- else:
993
- present_values = [
994
- value for value in filter_v if value in entry_values
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
- constraints = filtered_constraints
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 = bool(element.get("required")) and bool(
1104
- available_values.get(name)
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
- alias=formatted_alias,
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
- alias=formatted_alias,
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
- properties["eodag:alias"] = alias
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
- properties["eodag:alias"] = alias
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
- num_total,
519
+ number_matched,
518
520
  search_params=search_params,
519
521
  next_page_token=str(start_index + 1),
520
522
  )
@@ -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
- properties["eodag:alias"] = alias
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 Queryables.get_queryable_from_alias(json_param)
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 Queryables.model_fields:
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] = Queryables.get_with_default(param, default)
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(Queryables.model_fields)
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"])