eodag 3.6.0__py3-none-any.whl → 3.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. eodag/api/core.py +110 -189
  2. eodag/api/product/metadata_mapping.py +42 -3
  3. eodag/cli.py +6 -3
  4. eodag/config.py +7 -1
  5. eodag/plugins/authentication/openid_connect.py +1 -2
  6. eodag/plugins/download/aws.py +145 -178
  7. eodag/plugins/download/base.py +3 -2
  8. eodag/plugins/download/creodias_s3.py +10 -5
  9. eodag/plugins/download/http.py +14 -6
  10. eodag/plugins/download/s3rest.py +7 -3
  11. eodag/plugins/manager.py +1 -1
  12. eodag/plugins/search/base.py +34 -4
  13. eodag/plugins/search/build_search_result.py +3 -0
  14. eodag/plugins/search/cop_marine.py +2 -0
  15. eodag/plugins/search/data_request_search.py +6 -1
  16. eodag/plugins/search/qssearch.py +64 -25
  17. eodag/resources/ext_product_types.json +1 -1
  18. eodag/resources/product_types.yml +30 -171
  19. eodag/resources/providers.yml +87 -328
  20. eodag/resources/stac.yml +1 -2
  21. eodag/resources/stac_provider.yml +1 -1
  22. eodag/resources/user_conf_template.yml +0 -11
  23. eodag/rest/core.py +5 -16
  24. eodag/rest/stac.py +0 -4
  25. eodag/utils/__init__.py +41 -27
  26. eodag/utils/exceptions.py +4 -0
  27. eodag/utils/free_text_search.py +229 -0
  28. eodag/utils/s3.py +605 -65
  29. {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/METADATA +7 -9
  30. {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/RECORD +34 -34
  31. eodag/types/whoosh.py +0 -203
  32. {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/WHEEL +0 -0
  33. {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/entry_points.txt +0 -0
  34. {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/licenses/LICENSE +0 -0
  35. {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/top_level.txt +0 -0
eodag/api/core.py CHANGED
@@ -30,10 +30,6 @@ from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
30
30
 
31
31
  import geojson
32
32
  import yaml.parser
33
- from whoosh import analysis, fields
34
- from whoosh.fields import Schema
35
- from whoosh.index import exists_in, open_dir
36
- from whoosh.qparser import QueryParser
37
33
 
38
34
  from eodag.api.product.metadata_mapping import (
39
35
  NOT_AVAILABLE,
@@ -61,7 +57,6 @@ from eodag.plugins.search.build_search_result import MeteoblueSearch
61
57
  from eodag.plugins.search.qssearch import PostJsonSearch
62
58
  from eodag.types import model_fields_to_annotated
63
59
  from eodag.types.queryables import CommonQueryables, QueryablesDict
64
- from eodag.types.whoosh import EODAGQueryParser, create_in
65
60
  from eodag.utils import (
66
61
  DEFAULT_DOWNLOAD_TIMEOUT,
67
62
  DEFAULT_DOWNLOAD_WAIT,
@@ -75,7 +70,6 @@ from eodag.utils import (
75
70
  _deprecated,
76
71
  get_geometry_from_various,
77
72
  makedirs,
78
- obj_md5sum,
79
73
  sort_dict,
80
74
  string_to_jsonpath,
81
75
  uri_to_path,
@@ -83,19 +77,18 @@ from eodag.utils import (
83
77
  from eodag.utils.env import is_env_var_true
84
78
  from eodag.utils.exceptions import (
85
79
  AuthenticationError,
86
- EodagError,
87
80
  NoMatchingProductType,
88
81
  PluginImplementationError,
89
82
  RequestError,
90
83
  UnsupportedProductType,
91
84
  UnsupportedProvider,
92
85
  )
86
+ from eodag.utils.free_text_search import compile_free_text_query
93
87
  from eodag.utils.rest import rfc3339_str_to_datetime
94
88
  from eodag.utils.stac_reader import fetch_stac_items
95
89
 
96
90
  if TYPE_CHECKING:
97
91
  from shapely.geometry.base import BaseGeometry
98
- from whoosh.index import Index
99
92
 
100
93
  from eodag.api.product import EOProduct
101
94
  from eodag.plugins.apis.base import Api
@@ -125,7 +118,6 @@ class EODataAccessGateway:
125
118
  res_files("eodag") / "resources" / "product_types.yml"
126
119
  )
127
120
  self.product_types_config = SimpleYamlProxyConfig(product_types_config_path)
128
- self.product_types_config_md5 = obj_md5sum(self.product_types_config.source)
129
121
  self.providers_config = load_default_config()
130
122
 
131
123
  env_var_cfg_dir = "EODAG_CFG_DIR"
@@ -189,6 +181,8 @@ class EODataAccessGateway:
189
181
  self._sync_provider_product_types(
190
182
  provider, available_product_types, strict_mode
191
183
  )
184
+ # init product types configuration
185
+ self._product_types_config_init()
192
186
 
193
187
  # re-build _plugins_manager using up-to-date providers_config
194
188
  self._plugins_manager.rebuild(self.providers_config)
@@ -201,10 +195,6 @@ class EODataAccessGateway:
201
195
  # Sort providers taking into account of possible new priority orders
202
196
  self._plugins_manager.sort_providers()
203
197
 
204
- # Build a search index for product types
205
- self._product_types_index: Optional[Index] = None
206
- self.build_index()
207
-
208
198
  # set locations configuration
209
199
  if locations_conf_path is None:
210
200
  locations_conf_path = os.getenv("EODAG_LOCS_CFG_FILE")
@@ -235,6 +225,11 @@ class EODataAccessGateway:
235
225
  )
236
226
  self.set_locations_conf(locations_conf_path)
237
227
 
228
+ def _product_types_config_init(self) -> None:
229
+ """Initialize product types configuration."""
230
+ for pt_id, pd_dict in self.product_types_config.source.items():
231
+ self.product_types_config.source[pt_id].setdefault("_id", pt_id)
232
+
238
233
  def _sync_provider_product_types(
239
234
  self,
240
235
  provider: str,
@@ -294,95 +289,6 @@ class EODataAccessGateway:
294
289
  """Get eodag package version"""
295
290
  return version("eodag")
296
291
 
297
- def build_index(self) -> None:
298
- """Build a `Whoosh <https://whoosh.readthedocs.io/en/latest/index.html>`_
299
- index for product types searches.
300
- """
301
- index_dir = os.path.join(self.conf_dir, ".index")
302
-
303
- try:
304
- create_index = not exists_in(index_dir)
305
- except ValueError as ve:
306
- # Whoosh uses pickle internally. New versions of Python sometimes introduce
307
- # a new pickle protocol (e.g. 3.4 -> 4, 3.8 -> 5), the new version not
308
- # being supported by previous versions of Python (e.g. Python 3.7 doesn't
309
- # support Protocol 5). In that case, we need to recreate the .index.
310
- if "unsupported pickle protocol" in str(ve):
311
- logger.debug("Need to recreate whoosh .index: '%s'", ve)
312
- create_index = True
313
- # Unexpected error
314
- else:
315
- logger.error(
316
- "Error while opening .index using whoosh, "
317
- "please report this issue and try to delete '%s' manually",
318
- index_dir,
319
- )
320
- raise
321
- # check index version
322
- if not create_index:
323
- if self._product_types_index is None:
324
- logger.debug("Opening product types index in %s", index_dir)
325
- self._product_types_index = open_dir(index_dir)
326
-
327
- with self._product_types_index.searcher() as searcher:
328
- p = QueryParser("md5", self._product_types_index.schema, plugins=[])
329
- query = p.parse(self.product_types_config_md5)
330
- results = searcher.search(query, limit=1)
331
-
332
- if not results:
333
- create_index = True
334
- logger.debug(
335
- "Out-of-date product types index removed from %s", index_dir
336
- )
337
-
338
- if create_index:
339
- logger.debug("Creating product types index in %s", index_dir)
340
- makedirs(index_dir)
341
-
342
- kw_analyzer = (
343
- analysis.CommaSeparatedTokenizer()
344
- | analysis.LowercaseFilter()
345
- | analysis.SubstitutionFilter("-", "")
346
- | analysis.SubstitutionFilter("_", "")
347
- )
348
-
349
- product_types_schema = Schema(
350
- ID=fields.ID(stored=True),
351
- abstract=fields.TEXT,
352
- instrument=fields.IDLIST,
353
- platform=fields.ID,
354
- platformSerialIdentifier=fields.IDLIST,
355
- processingLevel=fields.ID,
356
- sensorType=fields.ID,
357
- md5=fields.ID,
358
- license=fields.ID,
359
- title=fields.TEXT,
360
- missionStartDate=fields.STORED,
361
- missionEndDate=fields.STORED,
362
- keywords=fields.KEYWORD(analyzer=kw_analyzer),
363
- stacCollection=fields.STORED,
364
- )
365
- self._product_types_index = create_in(index_dir, product_types_schema)
366
- ix_writer = self._product_types_index.writer()
367
- for product_type in self.list_product_types(fetch_providers=False):
368
- versioned_product_type = dict(
369
- product_type, **{"md5": self.product_types_config_md5}
370
- )
371
- # add to index
372
- try:
373
- ix_writer.add_document(
374
- **{
375
- k: v
376
- for k, v in versioned_product_type.items()
377
- if k in product_types_schema.names()
378
- }
379
- )
380
- except TypeError as e:
381
- logger.error(
382
- f"Cannot write product type {product_type['ID']} into index. e={e} product_type={product_type}"
383
- )
384
- ix_writer.commit()
385
-
386
292
  def set_preferred_provider(self, provider: str) -> None:
387
293
  """Set max priority for the given provider.
388
294
 
@@ -674,8 +580,6 @@ class EODataAccessGateway:
674
580
  continue
675
581
 
676
582
  config = self.product_types_config[product_type_id]
677
- config["_id"] = product_type_id
678
-
679
583
  if "alias" in config:
680
584
  product_type_id = config["alias"]
681
585
  product_type = {"ID": product_type_id, **config}
@@ -977,14 +881,12 @@ class EODataAccessGateway:
977
881
  # to self.product_types_config
978
882
  self.product_types_config.source.update(
979
883
  {
980
- new_product_type: new_product_types_conf[
981
- "product_types_config"
982
- ][new_product_type]
884
+ new_product_type: {"_id": new_product_type}
885
+ | new_product_types_conf["product_types_config"][
886
+ new_product_type
887
+ ]
983
888
  }
984
889
  )
985
- self.product_types_config_md5 = obj_md5sum(
986
- self.product_types_config.source
987
- )
988
890
  ext_product_types_conf[provider] = new_product_types_conf
989
891
  new_product_types.append(new_product_type)
990
892
  if new_product_types:
@@ -1000,9 +902,6 @@ class EODataAccessGateway:
1000
902
  # re-create _plugins_manager using up-to-date providers_config
1001
903
  self._plugins_manager.build_product_type_to_provider_config_map()
1002
904
 
1003
- # rebuild index after product types list update
1004
- self.build_index()
1005
-
1006
905
  def available_providers(
1007
906
  self, product_type: Optional[str] = None, by_group: bool = False
1008
907
  ) -> list[str]:
@@ -1103,11 +1002,11 @@ class EODataAccessGateway:
1103
1002
  """
1104
1003
  Find EODAG product type IDs that best match a set of search parameters.
1105
1004
 
1106
- See https://whoosh.readthedocs.io/en/latest/querylang.html#the-default-query-language
1107
- for syntax.
1005
+ When using several filters, product types that match most of them will be returned at first.
1108
1006
 
1109
- :param free_text: Whoosh-compatible free text search filter used to search
1110
- accross all the following parameters
1007
+ :param free_text: Free text search filter used to search accross all the following parameters. Handles logical
1008
+ operators with parenthesis (``AND``/``OR``/``NOT``), quoted phrases (``"exact phrase"``),
1009
+ ``*`` and ``?`` wildcards.
1111
1010
  :param intersect: Join results for each parameter using INTERSECT instead of UNION.
1112
1011
  :param instrument: Instrument parameter.
1113
1012
  :param platform: Platform parameter.
@@ -1125,69 +1024,105 @@ class EODataAccessGateway:
1125
1024
  if productType := kwargs.get("productType"):
1126
1025
  return [productType]
1127
1026
 
1128
- if not self._product_types_index:
1129
- raise EodagError("Missing product types index")
1130
-
1131
- filters = {
1132
- "instrument": instrument,
1133
- "platform": platform,
1134
- "platformSerialIdentifier": platformSerialIdentifier,
1135
- "processingLevel": processingLevel,
1136
- "sensorType": sensorType,
1137
- "keywords": keywords,
1138
- "abstract": abstract,
1139
- "title": title,
1027
+ filters: dict[str, str] = {
1028
+ k: v
1029
+ for k, v in {
1030
+ "instrument": instrument,
1031
+ "platform": platform,
1032
+ "platformSerialIdentifier": platformSerialIdentifier,
1033
+ "processingLevel": processingLevel,
1034
+ "sensorType": sensorType,
1035
+ "keywords": keywords,
1036
+ "abstract": abstract,
1037
+ "title": title,
1038
+ }.items()
1039
+ if v is not None
1140
1040
  }
1141
- joint = " AND " if intersect else " OR "
1142
- filters_text = joint.join(
1143
- [f"{k}:({v})" for k, v in filters.items() if v is not None]
1041
+
1042
+ only_dates = (
1043
+ True
1044
+ if (not free_text and not filters and (missionStartDate or missionEndDate))
1045
+ else False
1144
1046
  )
1145
1047
 
1146
- text = f"({free_text})" if free_text else ""
1147
- if free_text and filters_text:
1148
- text += joint
1149
- if filters_text:
1150
- text += f"({filters_text})"
1151
-
1152
- if not text and (missionStartDate or missionEndDate):
1153
- text = "*"
1154
-
1155
- with self._product_types_index.searcher() as searcher:
1156
- p = EODAGQueryParser(list(filters.keys()), self._product_types_index.schema)
1157
- query = p.parse(text)
1158
- results = searcher.search(query, limit=None)
1159
-
1160
- guesses: list[dict[str, str]] = [dict(r) for r in results or []]
1161
-
1162
- # datetime filtering
1163
- if missionStartDate or missionEndDate:
1164
- min_aware = datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
1165
- max_aware = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
1166
- guesses = [
1167
- g
1168
- for g in guesses
1169
- if (
1170
- max(
1171
- rfc3339_str_to_datetime(missionStartDate)
1172
- if missionStartDate
1173
- else min_aware,
1174
- rfc3339_str_to_datetime(g["missionStartDate"])
1175
- if g.get("missionStartDate")
1176
- else min_aware,
1177
- )
1178
- <= min(
1179
- rfc3339_str_to_datetime(missionEndDate)
1180
- if missionEndDate
1181
- else max_aware,
1182
- rfc3339_str_to_datetime(g["missionEndDate"])
1183
- if g.get("missionEndDate")
1184
- else max_aware,
1185
- )
1048
+ free_text_evaluator = (
1049
+ compile_free_text_query(free_text) if free_text else lambda _: True
1050
+ )
1051
+
1052
+ guesses_with_score: list[tuple[str, int]] = []
1053
+
1054
+ for pt_id, pt_dict in self.product_types_config.source.items():
1055
+ if (
1056
+ pt_id == GENERIC_PRODUCT_TYPE
1057
+ or pt_id
1058
+ not in self._plugins_manager.product_type_to_provider_config_map
1059
+ ):
1060
+ continue
1061
+
1062
+ score = 0 # how many filters matched
1063
+
1064
+ # free text search
1065
+ if free_text:
1066
+ match = free_text_evaluator(pt_dict)
1067
+ if match:
1068
+ score += 1
1069
+ elif intersect:
1070
+ continue # must match all filters
1071
+
1072
+ # individual filters
1073
+ if filters:
1074
+ filters_matching_method = all if intersect else any
1075
+ filters_evaluators = {
1076
+ filter_name: compile_free_text_query(value)
1077
+ for filter_name, value in filters.items()
1078
+ if value is not None
1079
+ }
1080
+
1081
+ filter_matches = [
1082
+ filters_evaluators[filter_name]({filter_name: pt_dict[filter_name]})
1083
+ for filter_name, value in filters.items()
1084
+ if filter_name in pt_dict
1085
+ ]
1086
+
1087
+ if filters_matching_method(filter_matches):
1088
+ # add number of True matches to score
1089
+ score += sum(filter_matches)
1090
+ elif intersect:
1091
+ continue # must match all filters
1092
+
1093
+ if score == 0 and not only_dates:
1094
+ continue
1095
+
1096
+ # datetime filtering
1097
+ if missionStartDate or missionEndDate:
1098
+ min_aware = datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
1099
+ max_aware = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
1100
+
1101
+ max_start = max(
1102
+ rfc3339_str_to_datetime(missionStartDate)
1103
+ if missionStartDate
1104
+ else min_aware,
1105
+ rfc3339_str_to_datetime(pt_dict["missionStartDate"])
1106
+ if pt_dict.get("missionStartDate")
1107
+ else min_aware,
1186
1108
  )
1187
- ]
1109
+ min_end = min(
1110
+ rfc3339_str_to_datetime(missionEndDate)
1111
+ if missionEndDate
1112
+ else max_aware,
1113
+ rfc3339_str_to_datetime(pt_dict["missionEndDate"])
1114
+ if pt_dict.get("missionEndDate")
1115
+ else max_aware,
1116
+ )
1117
+ if not (max_start <= min_end):
1118
+ continue
1188
1119
 
1189
- if guesses:
1190
- return [g["ID"] for g in guesses or []]
1120
+ guesses_with_score.append((pt_id, score))
1121
+
1122
+ if guesses_with_score:
1123
+ # sort by score descending, then pt_id for stability
1124
+ guesses_with_score.sort(key=lambda x: (-x[1], x[0]))
1125
+ return [pt_id for pt_id, _ in guesses_with_score]
1191
1126
 
1192
1127
  raise NoMatchingProductType()
1193
1128
 
@@ -2007,20 +1942,6 @@ class EODataAccessGateway:
2007
1942
  nb_res,
2008
1943
  search_plugin.provider,
2009
1944
  )
2010
- # Hitting for instance
2011
- # https://theia.cnes.fr/atdistrib/resto2/api/collections/SENTINEL2/
2012
- # search.json?startDate=2019-03-01&completionDate=2019-06-15
2013
- # &processingLevel=LEVEL2A&maxRecords=1&page=1
2014
- # returns a number (properties.totalResults) that is the number of
2015
- # products in the collection (here SENTINEL2) instead of the estimated
2016
- # total number of products matching the search criteria (start/end date).
2017
- # Remove this warning when this is fixed upstream by THEIA.
2018
- if search_plugin.provider == "theia":
2019
- logger.warning(
2020
- "Results found on provider 'theia' is the total number of products "
2021
- "available in the searched collection (e.g. SENTINEL2) instead of "
2022
- "the total number of products matching the search criteria"
2023
- )
2024
1945
  except Exception as e:
2025
1946
  if raise_errors:
2026
1947
  # Raise the error, letting the application wrapping eodag know that
@@ -42,6 +42,7 @@ from shapely.ops import transform
42
42
  from eodag.types.queryables import Queryables
43
43
  from eodag.utils import (
44
44
  DEFAULT_PROJ,
45
+ _deprecated,
45
46
  deepcopy,
46
47
  dict_items_recursive_apply,
47
48
  format_string,
@@ -180,6 +181,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
180
181
  - ``to_datetime_dict``: convert a datetime string to a dictionary where values are either a string or a list
181
182
  - ``get_ecmwf_time``: get the time of a datetime string in the ECMWF format
182
183
  - ``sanitize``: sanitize string
184
+ - ``ceda_collection_name``: generate a CEDA collection name from a string
183
185
 
184
186
  :param search_param: The string to be formatted
185
187
  :param args: (optional) Additional arguments to use in the formatting process
@@ -219,7 +221,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
219
221
  elif value is not None:
220
222
  converted = self.custom_converter(value)
221
223
  else:
222
- converted = ""
224
+ converted = None
223
225
  # Clear this state variable in case the same converter is used to
224
226
  # resolve other named arguments
225
227
  self.custom_converter = None
@@ -374,6 +376,18 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
374
376
  def convert_to_geojson(value: Any) -> str:
375
377
  return geojson.dumps(value)
376
378
 
379
+ @staticmethod
380
+ def convert_to_geojson_polytope(
381
+ value: BaseGeometry,
382
+ ) -> Union[dict[Any, Any], str]:
383
+ # ECMWF Polytope uses non-geojson structure for features
384
+ if isinstance(value, Polygon):
385
+ return {
386
+ "type": "polygon",
387
+ "shape": [[y, x] for x, y in value.exterior.coords],
388
+ }
389
+ raise ValidationError("to_geojson_polytope only accepts shapely Polygon")
390
+
377
391
  @staticmethod
378
392
  def convert_from_ewkt(ewkt_string: str) -> Union[BaseGeometry, str]:
379
393
  """Convert EWKT (Extended Well-Known text) to shapely geometry"""
@@ -488,10 +502,14 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
488
502
 
489
503
  @staticmethod
490
504
  def convert_get_group_name(string: str, pattern: str) -> str:
505
+ sanitized_pattern = pattern.replace(" ", "_SPACE_")
491
506
  try:
492
- match = re.search(pattern, str(string))
507
+ match = re.search(sanitized_pattern, str(string))
493
508
  if match:
494
- return match.lastgroup or NOT_AVAILABLE
509
+ if result := match.lastgroup:
510
+ return result.replace("_SPACE_", " ")
511
+ else:
512
+ return NOT_AVAILABLE
495
513
  except AttributeError:
496
514
  pass
497
515
  logger.warning(
@@ -511,6 +529,14 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
511
529
  old, new = ast.literal_eval(args)
512
530
  return re.sub(old, new, value)
513
531
 
532
+ @staticmethod
533
+ def convert_ceda_collection_name(value: str) -> str:
534
+ data_regex = re.compile(r"/data/(?P<name>.+?)/?$")
535
+ match = data_regex.search(value)
536
+ if match:
537
+ return match.group("name").replace("/", "_").upper()
538
+ return "NOT_AVAILABLE"
539
+
514
540
  @staticmethod
515
541
  def convert_recursive_sub_str(
516
542
  input_obj: Union[dict[Any, Any], list[Any]], args: str
@@ -615,6 +641,10 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
615
641
  return NOT_AVAILABLE
616
642
 
617
643
  @staticmethod
644
+ @_deprecated(
645
+ reason="Method that was used in previous wekeo provider configuration, but not used anymore",
646
+ version="3.7.1",
647
+ )
618
648
  def convert_split_id_into_s1_params(product_id: str) -> dict[str, str]:
619
649
  parts: list[str] = re.split(r"_(?!_)", product_id)
620
650
  if len(parts) < 9:
@@ -667,6 +697,10 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
667
697
  return params
668
698
 
669
699
  @staticmethod
700
+ @_deprecated(
701
+ reason="Method that was used in previous wekeo provider configuration, but not used anymore",
702
+ version="3.7.1",
703
+ )
670
704
  def convert_split_id_into_s5p_params(product_id: str) -> dict[str, str]:
671
705
  parts: list[str] = re.split(r"_(?!_)", product_id)
672
706
  params = {
@@ -685,6 +719,10 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
685
719
  return params
686
720
 
687
721
  @staticmethod
722
+ @_deprecated(
723
+ reason="Method that was used in previous wekeo provider configuration, but not used anymore",
724
+ version="3.7.1",
725
+ )
688
726
  def convert_split_cop_dem_id(product_id: str) -> list[int]:
689
727
  parts = product_id.split("_")
690
728
  lattitude = parts[3]
@@ -1342,6 +1380,7 @@ def format_query_params(
1342
1380
  formatted_query_param = remove_str_array_quotes(
1343
1381
  formatted_query_param
1344
1382
  )
1383
+
1345
1384
  # json query string (for POST request)
1346
1385
  update_nested_dict(
1347
1386
  query_params,
eodag/cli.py CHANGED
@@ -49,11 +49,12 @@ import sys
49
49
  import textwrap
50
50
  from importlib.metadata import metadata
51
51
  from typing import TYPE_CHECKING, Any, Mapping
52
+ from urllib.parse import parse_qs
52
53
 
53
54
  import click
54
55
 
55
56
  from eodag.api.core import EODataAccessGateway, SearchResult
56
- from eodag.utils import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE, parse_qs
57
+ from eodag.utils import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE
57
58
  from eodag.utils.exceptions import NoMatchingProductType, UnsupportedProvider
58
59
  from eodag.utils.logging import setup_logging
59
60
 
@@ -109,8 +110,9 @@ class MutuallyExclusiveOption(click.Option):
109
110
  """Raise error or use parent handle_parse_result()"""
110
111
  if self.mutually_exclusive.intersection(opts) and self.name in opts:
111
112
  raise click.UsageError(
112
- "Illegal usage: `{}` is mutually exclusive with "
113
- "arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive))
113
+ "Illegal usage: `{}` is mutually exclusive with arguments `{}`.".format(
114
+ self.name, ", ".join(self.mutually_exclusive)
115
+ )
114
116
  )
115
117
 
116
118
  return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args)
@@ -687,6 +689,7 @@ def serve_rest(
687
689
  setup_logging(verbose=ctx.obj["verbosity"])
688
690
  try:
689
691
  import uvicorn
692
+ import uvicorn.config
690
693
  except ImportError:
691
694
  raise ImportError(
692
695
  "Feature not available, please install eodag[server] or eodag[all]"
eodag/config.py CHANGED
@@ -307,7 +307,8 @@ class PluginConfig(yaml.YAMLObject):
307
307
  single_collection_fetch_url: str
308
308
  #: Query string to be added to the fetch_url to filter for a collection
309
309
  single_collection_fetch_qs: str
310
- #: Mapping for product type metadata returned by the endpoint given in single_collection_fetch_url
310
+ #: Mapping for product type metadata returned by the endpoint given in single_collection_fetch_url. If ``ID``
311
+ #: is redefined in this mapping, it will replace ``generic_product_type_id`` value
311
312
  single_product_type_parsable_metadata: dict[str, str]
312
313
 
313
314
  class DiscoverQueryables(TypedDict, total=False):
@@ -452,6 +453,11 @@ class PluginConfig(yaml.YAMLObject):
452
453
  discover_queryables: PluginConfig.DiscoverQueryables
453
454
  #: :class:`~eodag.plugins.search.base.Search` The mapping between eodag metadata and the plugin specific metadata
454
455
  metadata_mapping: dict[str, Union[str, list[str]]]
456
+ #: :class:`~eodag.plugins.search.base.Search` :attr:`~eodag.config.PluginConfig.metadata_mapping` got from the given
457
+ #: product type
458
+ metadata_mapping_from_product: str
459
+ #: :class:`~eodag.plugins.search.base.Search` A mapping for the metadata of individual assets
460
+ assets_mapping: dict[str, dict[str, Any]]
455
461
  #: :class:`~eodag.plugins.search.base.Search` Parameters to remove from queryables
456
462
  remove_from_queryables: list[str]
457
463
  #: :class:`~eodag.plugins.search.base.Search` Parameters to be passed as is in the search url query string
@@ -23,6 +23,7 @@ import string
23
23
  from datetime import datetime, timedelta, timezone
24
24
  from random import SystemRandom
25
25
  from typing import TYPE_CHECKING, Any, Optional
26
+ from urllib.parse import parse_qs, urlparse
26
27
 
27
28
  import jwt
28
29
  import requests
@@ -34,9 +35,7 @@ from eodag.utils import (
34
35
  DEFAULT_TOKEN_EXPIRATION_MARGIN,
35
36
  HTTP_REQ_TIMEOUT,
36
37
  USER_AGENT,
37
- parse_qs,
38
38
  repeatfunc,
39
- urlparse,
40
39
  )
41
40
  from eodag.utils.exceptions import (
42
41
  AuthenticationError,