eodag 3.10.1__py3-none-any.whl → 4.0.0a2__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 (75) hide show
  1. eodag/__init__.py +6 -1
  2. eodag/api/collection.py +353 -0
  3. eodag/api/core.py +606 -641
  4. eodag/api/product/__init__.py +3 -3
  5. eodag/api/product/_product.py +74 -56
  6. eodag/api/product/drivers/__init__.py +4 -46
  7. eodag/api/product/drivers/base.py +0 -28
  8. eodag/api/product/metadata_mapping.py +178 -216
  9. eodag/api/search_result.py +156 -15
  10. eodag/cli.py +83 -403
  11. eodag/config.py +81 -51
  12. eodag/plugins/apis/base.py +2 -2
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +55 -40
  15. eodag/plugins/authentication/base.py +1 -3
  16. eodag/plugins/crunch/filter_date.py +3 -3
  17. eodag/plugins/crunch/filter_latest_intersect.py +2 -2
  18. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  19. eodag/plugins/download/aws.py +46 -42
  20. eodag/plugins/download/base.py +13 -14
  21. eodag/plugins/download/http.py +65 -65
  22. eodag/plugins/manager.py +28 -29
  23. eodag/plugins/search/__init__.py +6 -4
  24. eodag/plugins/search/base.py +131 -80
  25. eodag/plugins/search/build_search_result.py +245 -173
  26. eodag/plugins/search/cop_marine.py +87 -56
  27. eodag/plugins/search/csw.py +47 -37
  28. eodag/plugins/search/qssearch.py +653 -429
  29. eodag/plugins/search/stac_list_assets.py +1 -1
  30. eodag/plugins/search/static_stac_search.py +43 -44
  31. eodag/resources/{product_types.yml → collections.yml} +2594 -2453
  32. eodag/resources/ext_collections.json +1 -1
  33. eodag/resources/ext_product_types.json +1 -1
  34. eodag/resources/providers.yml +2706 -2733
  35. eodag/resources/stac_provider.yml +50 -92
  36. eodag/resources/user_conf_template.yml +9 -0
  37. eodag/types/__init__.py +2 -0
  38. eodag/types/queryables.py +70 -91
  39. eodag/types/search_args.py +1 -1
  40. eodag/utils/__init__.py +97 -21
  41. eodag/utils/dates.py +0 -12
  42. eodag/utils/exceptions.py +6 -6
  43. eodag/utils/free_text_search.py +3 -3
  44. eodag/utils/repr.py +2 -0
  45. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/METADATA +13 -99
  46. eodag-4.0.0a2.dist-info/RECORD +93 -0
  47. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/entry_points.txt +0 -4
  48. eodag/plugins/authentication/oauth.py +0 -60
  49. eodag/plugins/download/creodias_s3.py +0 -71
  50. eodag/plugins/download/s3rest.py +0 -351
  51. eodag/plugins/search/data_request_search.py +0 -565
  52. eodag/resources/stac.yml +0 -294
  53. eodag/resources/stac_api.yml +0 -2105
  54. eodag/rest/__init__.py +0 -24
  55. eodag/rest/cache.py +0 -70
  56. eodag/rest/config.py +0 -67
  57. eodag/rest/constants.py +0 -26
  58. eodag/rest/core.py +0 -764
  59. eodag/rest/errors.py +0 -210
  60. eodag/rest/server.py +0 -604
  61. eodag/rest/server.wsgi +0 -6
  62. eodag/rest/stac.py +0 -1032
  63. eodag/rest/templates/README +0 -1
  64. eodag/rest/types/__init__.py +0 -18
  65. eodag/rest/types/collections_search.py +0 -44
  66. eodag/rest/types/eodag_search.py +0 -386
  67. eodag/rest/types/queryables.py +0 -174
  68. eodag/rest/types/stac_search.py +0 -272
  69. eodag/rest/utils/__init__.py +0 -207
  70. eodag/rest/utils/cql_evaluate.py +0 -119
  71. eodag/rest/utils/rfc3339.py +0 -64
  72. eodag-3.10.1.dist-info/RECORD +0 -116
  73. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/WHEEL +0 -0
  74. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
  75. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/top_level.txt +0 -0
@@ -43,7 +43,6 @@ from eodag.types.queryables import Queryables
43
43
  from eodag.utils import (
44
44
  DEFAULT_PROJ,
45
45
  DEFAULT_SHAPELY_GEOMETRY,
46
- _deprecated,
47
46
  deepcopy,
48
47
  dict_items_recursive_apply,
49
48
  format_string,
@@ -59,6 +58,8 @@ from eodag.utils.dates import get_timestamp
59
58
  from eodag.utils.exceptions import ValidationError
60
59
 
61
60
  if TYPE_CHECKING:
61
+ from collections.abc import Mapping, Sequence
62
+
62
63
  from shapely.geometry.base import BaseGeometry
63
64
 
64
65
  from eodag.config import PluginConfig
@@ -71,9 +72,9 @@ INGEST_CONVERSION_REGEX = re.compile(
71
72
  )
72
73
  NOT_AVAILABLE = "Not Available"
73
74
  NOT_MAPPED = "Not Mapped"
74
- ONLINE_STATUS = "ONLINE"
75
- STAGING_STATUS = "STAGING"
76
- OFFLINE_STATUS = "OFFLINE"
75
+ ONLINE_STATUS = "succeeded"
76
+ STAGING_STATUS = "ordered"
77
+ OFFLINE_STATUS = "orderable"
77
78
  COORDS_ROUNDING_PRECISION = 4
78
79
  WKT_MAX_LEN = 1600
79
80
  COMPLEX_QS_REGEX = re.compile(r"^(.+=)?([^=]*)({.+})+([^=&]*)$")
@@ -97,23 +98,23 @@ def get_metadata_path(
97
98
  search:
98
99
  ...
99
100
  metadata_mapping:
100
- productType:
101
- - productType
102
- - $.properties.productType
101
+ platform:
102
+ - platform
103
+ - $.properties.platform
103
104
  id: $.properties.id
104
105
  ...
105
106
  ...
106
107
  ...
107
108
 
108
- Then the metadata `id` is not queryable for this provider meanwhile `productType`
109
- is queryable. The first value of the `metadata_mapping.productType` is how the
110
- eodag search parameter `productType` is interpreted in the
109
+ Then the metadata `id` is not queryable for this provider meanwhile `platform`
110
+ is queryable. The first value of the `metadata_mapping.platform` is how the
111
+ eodag search parameter `platform` is interpreted in the
111
112
  :class:`~eodag.plugins.search.base.Search` plugin implemented by `provider`, and is
112
113
  used when eodag delegates search process to the corresponding plugin.
113
114
 
114
115
  :param map_value: The value originating from the definition of `metadata_mapping`
115
116
  in the provider search config. For example, it is the list
116
- `['productType', '$.properties.productType']` with the sample
117
+ `['platform', '$.properties.platform']` with the sample
117
118
  above. Or the string `$.properties.id`.
118
119
  :returns: Either, None and the path to the metadata value, or a list of converter
119
120
  and its args, and the path to the metadata value.
@@ -151,6 +152,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
151
152
 
152
153
  The currently understood converters are:
153
154
  - ``ceda_collection_name``: generate a CEDA collection name from a string
155
+ - ``wekeo_to_cop_collection``: converts the name of a collection from the WEkEO format to the Copernicus format
154
156
  - ``csv_list``: convert to a comma separated list
155
157
  - ``datetime_to_timestamp_milliseconds``: converts a utc date string to a timestamp in milliseconds
156
158
  - ``dict_filter_and_sub``: filter dict items using jsonpath and then apply recursive_sub_str
@@ -160,15 +162,20 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
160
162
  - ``from_georss``: convert GeoRSS to shapely geometry / WKT in DEFAULT_PROJ
161
163
  - ``get_ecmwf_time``: get the time of a datetime string in the ECMWF format
162
164
  - ``get_group_name``: get the matching regex group name
165
+ - ``literalize_unicode``: convert a string to its raw Unicode literal form
166
+ - ``not_available``: replace value with "Not Available"
163
167
  - ``recursive_sub_str``: recursively substitue in the structure (e.g. dict) values matching a regex
164
168
  - ``remove_extension``: on a string that contains dots, only take the first part of the list obtained by
165
169
  splitting the string on dots
166
170
  - ``replace_str``: execute "string".replace(old, new)
171
+ - ``replace_str_tuple``: apply multiple replacements on a string (parts or complete)
172
+ - ``replace_tuple``: apply multiple replacements matching whole value
167
173
  - ``s2msil2a_title_to_aws_productinfo``: used to generate SAFE format metadata for data from AWS
168
174
  - ``sanitize``: sanitize string
169
175
  - ``slice_str``: slice a string (equivalent to s[start, end, step])
176
+ - ``split``: split a string using given separator
170
177
  - ``split_cop_dem_id``: get the bbox by splitting the product id
171
- - ``split_corine_id``: get the product type by splitting the product id
178
+ - ``split_corine_id``: get the collection by splitting the product id
172
179
  - ``to_bounds_lists``: convert to list(s) of bounds
173
180
  - ``to_datetime_dict``: convert a datetime string to a dictionary where values are either a string or a list
174
181
  - ``to_ewkt``: convert to EWKT (Extended Well-Known text)
@@ -200,6 +207,44 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
200
207
  self.custom_converter: Optional[Callable] = None
201
208
  self.custom_args: Optional[str] = None
202
209
 
210
+ def parse(self, format_string: str):
211
+ """
212
+ Rewrite field names in the template before the base parser sees them.
213
+ Replaces `{foo:bar}` with `{foo__bar}`.
214
+ """
215
+ pattern = re.compile(r"{([^{}]+)}")
216
+
217
+ def rewrite_field(field: str) -> str:
218
+ # If there's a format spec (e.g., {foo:bar:.2f}), preserve it
219
+ if ":" in field and not field.lstrip().startswith(("!", ".", ":")):
220
+ before_colon, *after = field.split(":")
221
+ # Don't confuse format spec with field name colons
222
+ if len(after) == 1 and "." in after[0]:
223
+ # It's a format specifier, leave it
224
+ return field
225
+ return field.replace(":", "__", 1)
226
+ return field
227
+
228
+ # Replace in string (but not in format_spec itself)
229
+ safe_template = pattern.sub(
230
+ lambda m: "{" + rewrite_field(m.group(1)) + "}", format_string
231
+ )
232
+
233
+ # Yield from base class
234
+ yield from super().parse(safe_template)
235
+
236
+ def get_value(
237
+ self, key: Any, args: "Sequence[Any]", kwargs: "Mapping[str, Any]"
238
+ ) -> Any:
239
+ """
240
+ Look up rewritten field name in kwargs by converting __ back to :
241
+ """
242
+ if isinstance(key, str):
243
+ original_key = key.replace("__", ":")
244
+ key_with_COLON = key.replace("__", "_COLON_")
245
+ return kwargs.get(original_key) or kwargs.get(key_with_COLON)
246
+ return super().get_value(key, args, kwargs)
247
+
203
248
  def get_field(self, field_name: str, args: Any, kwargs: Any) -> Any:
204
249
  conversion_func_spec = self.CONVERSION_REGEX.match(field_name)
205
250
  # Register a custom converter if any for later use (see convert_field)
@@ -209,6 +254,9 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
209
254
  field_name = conversion_func_spec.groupdict()["field_name"]
210
255
  converter = conversion_func_spec.groupdict()["converter"]
211
256
  self.custom_args = conversion_func_spec.groupdict()["args"]
257
+ # converts back "_COLON_" to ":"
258
+ if self.custom_args is not None and "_COLON_" in self.custom_args:
259
+ self.custom_args = self.custom_args.replace("_COLON_", ":")
212
260
  self.custom_converter = getattr(self, "convert_{}".format(converter))
213
261
 
214
262
  return super(MetadataFormatter, self).get_field(field_name, args, kwargs)
@@ -532,11 +580,15 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
532
580
  return re.sub(old, new, value)
533
581
 
534
582
  @staticmethod
535
- def convert_replace_str_tuple(value: Any, args: str) -> str:
583
+ def convert_replace_str_tuple(
584
+ value: Union[str, dict[Any, Any]], args: str
585
+ ) -> str:
536
586
  """
537
- Apply multiple replacements on a string.
538
- args should be a string representing a list/tuple of (old, new) pairs.
539
- Example: '(("old1", "new1"), ("old2", "new2"))'
587
+ Apply multiple replacements on a string (parts or complete).
588
+
589
+ :param value: input string or dict.
590
+ :param args: string representing a list/tuple of (old, new) pairs, like
591
+ ``'(("old1", "new1"), ("old2", "new2"))'``
540
592
  """
541
593
  if isinstance(value, dict):
542
594
  value = MetadataFormatter.convert_to_geojson(value)
@@ -560,13 +612,70 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
560
612
 
561
613
  return value
562
614
 
615
+ @staticmethod
616
+ def convert_replace_tuple(value: Any, args: str) -> Any:
617
+ """
618
+ Apply multiple replacements matching whole value.
619
+
620
+ :param value: input to replace
621
+ :param args: string representing a list/tuple of (old, new) pairs, like
622
+ ``'((["old1"], "new1"), ("old2", ["new2"]))'``
623
+ """
624
+ # args sera une chaîne représentant une liste/tuple de tuples
625
+ replacements = ast.literal_eval(args)
626
+
627
+ if not isinstance(replacements, (list, tuple)):
628
+ raise TypeError(
629
+ f"convert_replace_str_tuple expects a list/tuple of (old,new) pairs. "
630
+ f"Got {type(replacements)}: {replacements}"
631
+ )
632
+
633
+ for old, new in replacements:
634
+ if old == value:
635
+ return new
636
+
637
+ return value
638
+
639
+ @staticmethod
640
+ def convert_not_available(value: Any) -> str:
641
+ """Convert any value to "Not Available".
642
+
643
+ This is more useful than "$.null" to keep original jsonpath while parsing in metadata_mapping.
644
+ """
645
+ return NOT_AVAILABLE
646
+
647
+ @staticmethod
648
+ def convert_split(value: str, separator: str) -> list[str]:
649
+ """Split a string using given separator"""
650
+ if value == NOT_AVAILABLE:
651
+ return [NOT_AVAILABLE]
652
+ if not isinstance(value, str):
653
+ logger.warning(
654
+ "Could not split non-string value %s (type %s)", value, type(value)
655
+ )
656
+ return [NOT_AVAILABLE]
657
+ if not isinstance(separator, str):
658
+ logger.warning(
659
+ "Could not split string using non-string separator %s (type %s)",
660
+ separator,
661
+ type(separator),
662
+ )
663
+ return [NOT_AVAILABLE]
664
+ return value.split(separator)
665
+
563
666
  @staticmethod
564
667
  def convert_ceda_collection_name(value: str) -> str:
565
668
  data_regex = re.compile(r"/data/(?P<name>.+?)/?$")
566
669
  match = data_regex.search(value)
567
670
  if match:
568
671
  return match.group("name").replace("/", "_").upper()
569
- return "NOT_AVAILABLE"
672
+ return NOT_AVAILABLE
673
+
674
+ @staticmethod
675
+ def convert_literalize_unicode(value: str) -> str:
676
+ if value == NOT_AVAILABLE:
677
+ return value
678
+ return value.encode("raw_unicode_escape").decode("utf-8")
570
679
 
571
680
  @staticmethod
572
681
  def convert_recursive_sub_str(
@@ -656,7 +765,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
656
765
  int(x.strip()) if x.strip().lstrip("-").isdigit() else None
657
766
  for x in args.split(",")
658
767
  ]
659
- return string[cmin:cmax:cstep]
768
+ return string[cmin:cmax:cstep] or NOT_AVAILABLE
660
769
 
661
770
  @staticmethod
662
771
  def convert_to_lower(string: str) -> str:
@@ -705,7 +814,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
705
814
  if id_match:
706
815
  id_dict = id_match.groupdict()
707
816
  return (
708
- "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/%s/%s/%s/%s/%s/%s/0/{collection}.json"
817
+ "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/%s/%s/%s/%s/%s/%s/0/{_collection}.json"
709
818
  % (
710
819
  id_dict["tile1"],
711
820
  id_dict["tile2"],
@@ -719,49 +828,10 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
719
828
  logger.error("Could not extract title infos from %s" % string)
720
829
  return NOT_AVAILABLE
721
830
 
722
- @staticmethod
723
- @_deprecated(
724
- reason="Method that was used in previous wekeo provider configuration, but not used anymore",
725
- version="3.7.1",
726
- )
727
- def convert_split_id_into_s1_params(product_id: str) -> dict[str, str]:
728
- parts: list[str] = re.split(r"_(?!_)", product_id)
729
- if len(parts) < 9:
730
- logger.error(
731
- "id %s does not match expected Sentinel-1 id format", product_id
732
- )
733
- raise ValueError
734
- params = {"sensorMode": parts[1]}
735
- level = "LEVEL" + parts[3][0]
736
- params["processingLevel"] = level
737
- start_date = datetime.strptime(parts[4], "%Y%m%dT%H%M%S") - timedelta(
738
- seconds=1
739
- )
740
- params["startDate"] = start_date.strftime("%Y-%m-%dT%H:%M:%SZ")
741
- end_date = datetime.strptime(parts[5], "%Y%m%dT%H%M%S") + timedelta(
742
- seconds=1
743
- )
744
- params["endDate"] = end_date.strftime("%Y-%m-%dT%H:%M:%SZ")
745
- product_type = parts[2][:3]
746
- if product_type == "GRD" and parts[-1] == "COG":
747
- product_type = "GRD-COG"
748
- elif product_type == "GRD" and parts[-2] == "CARD" and parts[-1] == "BS":
749
- product_type = "CARD-BS"
750
- params["productType"] = product_type
751
- polarisation_mapping = {
752
- "SV": "VV",
753
- "SH": "HH",
754
- "DH": "HH+HV",
755
- "DV": "VV+VH",
756
- }
757
- polarisation = polarisation_mapping[parts[3][2:]]
758
- params["polarisation"] = polarisation
759
- return params
760
-
761
831
  @staticmethod
762
832
  def convert_split_id_into_s3_params(product_id: str) -> dict[str, str]:
763
833
  parts: list[str] = re.split(r"_(?!_)", product_id)
764
- params = {"productType": product_id[4:15]}
834
+ params = {"collection": product_id[4:15]}
765
835
  dates = re.findall("[0-9]{8}T[0-9]{6}", product_id)
766
836
  start_date = datetime.strptime(dates[0], "%Y%m%dT%H%M%S") - timedelta(
767
837
  seconds=1
@@ -775,48 +845,6 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
775
845
  params["sat"] = "Sentinel-" + parts[0][1:]
776
846
  return params
777
847
 
778
- @staticmethod
779
- @_deprecated(
780
- reason="Method that was used in previous wekeo provider configuration, but not used anymore",
781
- version="3.7.1",
782
- )
783
- def convert_split_id_into_s5p_params(product_id: str) -> dict[str, str]:
784
- parts: list[str] = re.split(r"_(?!_)", product_id)
785
- params = {
786
- "productType": product_id[9:19],
787
- "processingMode": parts[1],
788
- "processingLevel": parts[2].replace("_", ""),
789
- }
790
- start_date = datetime.strptime(parts[-6], "%Y%m%dT%H%M%S") - timedelta(
791
- seconds=10
792
- )
793
- params["startDate"] = start_date.strftime("%Y-%m-%dT%H:%M:%SZ")
794
- end_date = datetime.strptime(parts[-5], "%Y%m%dT%H%M%S") + timedelta(
795
- seconds=10
796
- )
797
- params["endDate"] = end_date.strftime("%Y-%m-%dT%H:%M:%SZ")
798
- return params
799
-
800
- @staticmethod
801
- @_deprecated(
802
- reason="Method that was used in previous wekeo provider configuration, but not used anymore",
803
- version="3.7.1",
804
- )
805
- def convert_split_cop_dem_id(product_id: str) -> list[int]:
806
- parts = product_id.split("_")
807
- lattitude = parts[3]
808
- longitude = parts[5]
809
- if lattitude[0] == "N":
810
- lat_num = int(lattitude[1:])
811
- else:
812
- lat_num = -1 * int(lattitude[1:])
813
- if longitude[0] == "E":
814
- long_num = int(longitude[1:])
815
- else:
816
- long_num = -1 * int(longitude[1:])
817
- bbox = [long_num - 1, lat_num - 1, long_num + 1, lat_num + 1]
818
- return bbox
819
-
820
848
  @staticmethod
821
849
  def convert_dates_from_cmems_id(product_id: str):
822
850
  date_format_1 = "[0-9]{10}"
@@ -1037,10 +1065,29 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
1037
1065
  assets_dict[asset_basename] = assets_dict.pop(asset_name)
1038
1066
  return assets_dict
1039
1067
 
1068
+ @staticmethod
1069
+ def convert_wekeo_to_cop_collection(val: str, prefix: str) -> str:
1070
+ """Converts the name of a collection from the WEkEO format to the Copernicus format."""
1071
+ return val.removeprefix(prefix).lower().replace("_", "-")
1072
+
1040
1073
  # if stac extension colon separator `:` is in search params, parse it to prevent issues with vformat
1041
- if re.search(r"{[\w-]*:[\w#-]*}", search_param):
1042
- search_param = re.sub(r"{([\w-]*):([\w#-]*)}", r"{\1_COLON_\2}", search_param)
1074
+ if re.search(r"{[\w-]*:[\w#-]*\(?.*}", search_param):
1075
+ search_param = re.sub(
1076
+ r"{([\w-]*):([\w#-]*\(?.*)}",
1077
+ r"{\1_COLON_\2}",
1078
+ search_param,
1079
+ )
1043
1080
  kwargs = {k.replace(":", "_COLON_"): v for k, v in kwargs.items()}
1081
+ # convert colons `:` in the parameters passed to the converter (e.g. 'foo#boo(fun:with:colons)')
1082
+ if re.search(r"{[\w-]*#[\w-]*\([^)]*:.*}", search_param):
1083
+ search_param = re.sub(
1084
+ r"({[\w-]*#[\w-]*)\(([^)]*)(.*})",
1085
+ lambda m: m.group(1)
1086
+ + "("
1087
+ + m.group(2).replace(":", "_COLON_")
1088
+ + m.group(3),
1089
+ search_param,
1090
+ )
1044
1091
 
1045
1092
  return MetadataFormatter().vformat(search_param, args, kwargs)
1046
1093
 
@@ -1062,6 +1109,7 @@ def properties_from_json(
1062
1109
  `discovery_path` (String representation of jsonpath)
1063
1110
  :returns: The metadata of the :class:`~eodag.api.product._product.EOProduct`
1064
1111
  """
1112
+ extracted_value: Any
1065
1113
  properties: dict[str, Any] = {}
1066
1114
  templates = {}
1067
1115
  used_jsonpaths = []
@@ -1072,7 +1120,7 @@ def properties_from_json(
1072
1120
  else:
1073
1121
  conversion_or_none, path_or_text = value
1074
1122
  if isinstance(path_or_text, str):
1075
- if re.search(r"({[^{}:]+})+", path_or_text):
1123
+ if re.search(r"{[^{}]+}", path_or_text):
1076
1124
  templates[metadata] = path_or_text
1077
1125
  else:
1078
1126
  properties[metadata] = path_or_text
@@ -1081,11 +1129,13 @@ def properties_from_json(
1081
1129
  match = path_or_text.find(json)
1082
1130
  except KeyError:
1083
1131
  match = []
1084
- if len(match) == 1:
1132
+ if len(match) == 0:
1133
+ extracted_value = NOT_AVAILABLE
1134
+ elif len(match) == 1:
1085
1135
  extracted_value = match[0].value
1086
1136
  used_jsonpaths.append(match[0].full_path)
1087
1137
  else:
1088
- extracted_value = NOT_AVAILABLE
1138
+ extracted_value = [m.value for m in match]
1089
1139
  if extracted_value is None:
1090
1140
  properties[metadata] = None
1091
1141
  else:
@@ -1157,6 +1207,7 @@ def properties_from_json(
1157
1207
  if isinstance(discovery_jsonpath, JSONPath)
1158
1208
  else []
1159
1209
  )
1210
+ mtd_prefix = discovery_config.get("metadata_prefix", "provider")
1160
1211
  for found_jsonpath in discovered_properties:
1161
1212
  if "metadata_path_id" in discovery_config.keys():
1162
1213
  found_key_paths = string_to_jsonpath(
@@ -1178,8 +1229,13 @@ def properties_from_json(
1178
1229
  if (
1179
1230
  re.compile(discovery_pattern).match(found_key)
1180
1231
  and found_key not in properties.keys()
1232
+ and f"{mtd_prefix}:{found_key}" not in properties.keys()
1181
1233
  and used_jsonpath not in used_jsonpaths
1182
1234
  ):
1235
+ # prepend with default STAC prefix if none is already used
1236
+ if ":" not in found_key:
1237
+ found_key = f"{mtd_prefix}:{found_key}"
1238
+
1183
1239
  if "metadata_path_value" in discovery_config.keys():
1184
1240
  found_value_path = string_to_jsonpath(
1185
1241
  discovery_config["metadata_path_value"], force=True
@@ -1404,7 +1460,7 @@ def mtd_cfg_as_conversion_and_querypath(
1404
1460
 
1405
1461
 
1406
1462
  def format_query_params(
1407
- product_type: str,
1463
+ collection: str,
1408
1464
  config: PluginConfig,
1409
1465
  query_dict: dict[str, Any],
1410
1466
  error_context: str = "",
@@ -1415,14 +1471,14 @@ def format_query_params(
1415
1471
  # . not allowed in eodag_search_key, replaced with %2E
1416
1472
  query_dict = {k.replace(".", "%2E"): v for k, v in query_dict.items()}
1417
1473
 
1418
- product_type_metadata_mapping = dict(
1474
+ collection_metadata_mapping = dict(
1419
1475
  config.metadata_mapping,
1420
- **config.products.get(product_type, {}).get("metadata_mapping", {}),
1476
+ **config.products.get(collection, {}).get("metadata_mapping", {}),
1421
1477
  )
1422
1478
 
1423
1479
  # Raise error if non-queryables parameters are used and raise_mtd_discovery_error configured
1424
1480
  if (
1425
- raise_mtd_discovery_error := config.products.get(product_type, {})
1481
+ raise_mtd_discovery_error := config.products.get(collection, {})
1426
1482
  .get("discover_metadata", {})
1427
1483
  .get("raise_mtd_discovery_error")
1428
1484
  ) is None:
@@ -1436,7 +1492,7 @@ def format_query_params(
1436
1492
  queryables = _get_queryables(
1437
1493
  query_dict,
1438
1494
  config,
1439
- product_type_metadata_mapping,
1495
+ collection_metadata_mapping,
1440
1496
  raise_mtd_discovery_error,
1441
1497
  error_context,
1442
1498
  )
@@ -1461,7 +1517,7 @@ def format_query_params(
1461
1517
  parts = provider_search_param.split("=")
1462
1518
  if len(parts) == 1:
1463
1519
  formatted_query_param = format_metadata(
1464
- provider_search_param, product_type, **query_dict
1520
+ provider_search_param, collection, **query_dict
1465
1521
  )
1466
1522
  formatted_query_param = formatted_query_param.replace("'", '"')
1467
1523
  if "{{" in provider_search_param:
@@ -1472,6 +1528,11 @@ def format_query_params(
1472
1528
  formatted_query_param = remove_str_array_quotes(
1473
1529
  formatted_query_param
1474
1530
  )
1531
+ if NOT_AVAILABLE in formatted_query_param:
1532
+ raise ValidationError(
1533
+ "Could not parse %s query parameter, got %s"
1534
+ % (eodag_search_key, formatted_query_param)
1535
+ )
1475
1536
 
1476
1537
  # json query string (for POST request)
1477
1538
  update_nested_dict(
@@ -1485,7 +1546,7 @@ def format_query_params(
1485
1546
  else:
1486
1547
  provider_search_key, provider_value = parts
1487
1548
  query_params[provider_search_key] = format_metadata(
1488
- provider_value, product_type, **query_dict
1549
+ provider_value, collection, **query_dict
1489
1550
  )
1490
1551
  else:
1491
1552
  query_params[provider_search_param] = user_input
@@ -1499,12 +1560,12 @@ def format_query_params(
1499
1560
  # Now add formatted free text search parameters (this is for cases where a
1500
1561
  # complex query through a free text search parameter is available for the
1501
1562
  # provider and needed for the consumer)
1502
- product_type_metadata_mapping = dict(
1563
+ collection_metadata_mapping = dict(
1503
1564
  config.metadata_mapping,
1504
- **config.products.get(product_type, {}).get("metadata_mapping", {}),
1565
+ **config.products.get(collection, {}).get("metadata_mapping", {}),
1505
1566
  )
1506
1567
  literal_search_params.update(
1507
- _format_free_text_search(config, product_type_metadata_mapping, **query_dict)
1568
+ _format_free_text_search(config, collection_metadata_mapping, **query_dict)
1508
1569
  )
1509
1570
  for provider_search_key, provider_value in literal_search_params.items():
1510
1571
  if isinstance(provider_value, list):
@@ -1601,7 +1662,7 @@ def _get_queryables(
1601
1662
  # raise an error when a query param not allowed by the provider is found
1602
1663
  if not isinstance(md_mapping, list) and raise_mtd_discovery_error:
1603
1664
  raise ValidationError(
1604
- "Search parameters which are not queryable are disallowed for this product type on this provider: "
1665
+ "Search parameters which are not queryable are disallowed for this collection on this provider: "
1605
1666
  f"please remove '{eodag_search_key}' from your search parameters. {error_context}",
1606
1667
  {eodag_search_key},
1607
1668
  )
@@ -1723,102 +1784,3 @@ def get_provider_queryable_key(
1723
1784
  return ""
1724
1785
  else:
1725
1786
  return eodag_key
1726
-
1727
-
1728
- # Keys taken from OpenSearch extension for Earth Observation http://docs.opengeospatial.org/is/13-026r9/13-026r9.html
1729
- # For a metadata to be queryable, The way to query it must be specified in the
1730
- # provider metadata_mapping configuration parameter. It will be automatically
1731
- # detected as queryable by eodag when this is done
1732
- OSEO_METADATA_MAPPING = {
1733
- # Opensearch resource identifier within the search engine context (in our case
1734
- # within the context of the data provider)
1735
- "uid": "$.uid",
1736
- # OpenSearch Parameters for Collection Search (Table 3)
1737
- "productType": "$.properties.productType",
1738
- "doi": "$.properties.doi",
1739
- "platform": "$.properties.platform",
1740
- "platformSerialIdentifier": "$.properties.platformSerialIdentifier",
1741
- "instrument": "$.properties.instrument",
1742
- "sensorType": "$.properties.sensorType",
1743
- "compositeType": "$.properties.compositeType",
1744
- "processingLevel": "$.properties.processingLevel",
1745
- "orbitType": "$.properties.orbitType",
1746
- "spectralRange": "$.properties.spectralRange",
1747
- "wavelengths": "$.properties.wavelengths",
1748
- "hasSecurityConstraints": "$.properties.hasSecurityConstraints",
1749
- "dissemination": "$.properties.dissemination",
1750
- # INSPIRE obligated OpenSearch Parameters for Collection Search (Table 4)
1751
- "title": "$.properties.title",
1752
- "topicCategory": "$.properties.topicCategory",
1753
- "keyword": "$.properties.keyword",
1754
- "abstract": "$.properties.abstract",
1755
- "resolution": "$.properties.resolution",
1756
- "organisationName": "$.properties.organisationName",
1757
- "organisationRole": "$.properties.organisationRole",
1758
- "publicationDate": "$.properties.publicationDate",
1759
- "lineage": "$.properties.lineage",
1760
- "useLimitation": "$.properties.useLimitation",
1761
- "accessConstraint": "$.properties.accessConstraint",
1762
- "otherConstraint": "$.properties.otherConstraint",
1763
- "classification": "$.properties.classification",
1764
- "language": "$.properties.language",
1765
- "specification": "$.properties.specification",
1766
- # OpenSearch Parameters for Product Search (Table 5)
1767
- "parentIdentifier": "$.properties.parentIdentifier",
1768
- "productionStatus": "$.properties.productionStatus",
1769
- "acquisitionType": "$.properties.acquisitionType",
1770
- "orbitNumber": "$.properties.orbitNumber",
1771
- "orbitDirection": "$.properties.orbitDirection",
1772
- "track": "$.properties.track",
1773
- "frame": "$.properties.frame",
1774
- "swathIdentifier": "$.properties.swathIdentifier",
1775
- "cloudCover": "$.properties.cloudCover",
1776
- "snowCover": "$.properties.snowCover",
1777
- "lowestLocation": "$.properties.lowestLocation",
1778
- "highestLocation": "$.properties.highestLocation",
1779
- "productVersion": "$.properties.productVersion",
1780
- "productQualityStatus": "$.properties.productQualityStatus",
1781
- "productQualityDegradationTag": "$.properties.productQualityDegradationTag",
1782
- "processorName": "$.properties.processorName",
1783
- "processingCenter": "$.properties.processingCenter",
1784
- "creationDate": "$.properties.creationDate",
1785
- "modificationDate": "$.properties.modificationDate",
1786
- "processingDate": "$.properties.processingDate",
1787
- "sensorMode": "$.properties.sensorMode",
1788
- "archivingCenter": "$.properties.archivingCenter",
1789
- "processingMode": "$.properties.processingMode",
1790
- # OpenSearch Parameters for Acquistion Parameters Search (Table 6)
1791
- "availabilityTime": "$.properties.availabilityTime",
1792
- "acquisitionStation": "$.properties.acquisitionStation",
1793
- "acquisitionSubType": "$.properties.acquisitionSubType",
1794
- "startTimeFromAscendingNode": "$.properties.startTimeFromAscendingNode",
1795
- "completionTimeFromAscendingNode": "$.properties.completionTimeFromAscendingNode",
1796
- "illuminationAzimuthAngle": "$.properties.illuminationAzimuthAngle",
1797
- "illuminationZenithAngle": "$.properties.illuminationZenithAngle",
1798
- "illuminationElevationAngle": "$.properties.illuminationElevationAngle",
1799
- "polarizationMode": "$.properties.polarizationMode",
1800
- "polarizationChannels": "$.properties.polarizationChannels",
1801
- "antennaLookDirection": "$.properties.antennaLookDirection",
1802
- "minimumIncidenceAngle": "$.properties.minimumIncidenceAngle",
1803
- "maximumIncidenceAngle": "$.properties.maximumIncidenceAngle",
1804
- "dopplerFrequency": "$.properties.dopplerFrequency",
1805
- "incidenceAngleVariation": "$.properties.incidenceAngleVariation",
1806
- }
1807
- DEFAULT_METADATA_MAPPING = dict(
1808
- OSEO_METADATA_MAPPING,
1809
- **{
1810
- # Custom parameters (not defined in the base document referenced above)
1811
- # id differs from uid. The id is an identifier by which a product which is
1812
- # distributed by many providers can be retrieved (a property that it has in common
1813
- # in the catalogues of all the providers on which it is referenced)
1814
- "id": "$.id",
1815
- # The geographic extent of the product
1816
- "geometry": "$.geometry",
1817
- # The url of the quicklook
1818
- "quicklook": "$.properties.quicklook",
1819
- # The url to download the product "as is" (literal or as a template to be completed
1820
- # either after the search result is obtained from the provider or during the eodag
1821
- # download phase)
1822
- "downloadLink": "$.properties.downloadLink",
1823
- },
1824
- )