eodag 2.12.0__py3-none-any.whl → 3.0.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 (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -165
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.0.dist-info/RECORD +0 -94
  92. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
@@ -18,24 +18,37 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import logging
21
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
21
+ from typing import TYPE_CHECKING
22
22
 
23
+ import orjson
23
24
  from pydantic.fields import Field, FieldInfo
24
25
 
25
26
  from eodag.api.product.metadata_mapping import (
26
27
  DEFAULT_METADATA_MAPPING,
28
+ NOT_MAPPED,
27
29
  mtd_cfg_as_conversion_and_querypath,
28
30
  )
29
31
  from eodag.plugins.base import PluginTopic
32
+ from eodag.plugins.search import PreparedSearch
33
+ from eodag.types import model_fields_to_annotated
34
+ from eodag.types.queryables import Queryables
35
+ from eodag.types.search_args import SortByList
30
36
  from eodag.utils import (
31
- DEFAULT_ITEMS_PER_PAGE,
32
- DEFAULT_PAGE,
33
37
  GENERIC_PRODUCT_TYPE,
34
38
  Annotated,
39
+ copy_deepcopy,
40
+ deepcopy,
35
41
  format_dict_items,
42
+ get_args,
43
+ update_nested_dict,
36
44
  )
45
+ from eodag.utils.exceptions import ValidationError
37
46
 
38
47
  if TYPE_CHECKING:
48
+ from typing import Any, Dict, List, Optional, Tuple, Union
49
+
50
+ from requests.auth import AuthBase
51
+
39
52
  from eodag.api.product import EOProduct
40
53
  from eodag.config import PluginConfig
41
54
 
@@ -46,16 +59,21 @@ class Search(PluginTopic):
46
59
  """Base Search Plugin.
47
60
 
48
61
  :param provider: An EODAG provider name
49
- :type provider: str
50
62
  :param config: An EODAG plugin configuration
51
- :type config: :class:`~eodag.config.PluginConfig`
52
63
  """
53
64
 
65
+ auth: Union[AuthBase, Dict[str, str]]
66
+ next_page_url: Optional[str]
67
+ next_page_query_obj: Optional[Dict[str, Any]]
68
+ total_items_nb: int
69
+ need_count: bool
70
+ _request: Any # needed by deprecated load_stac_items
71
+
54
72
  def __init__(self, provider: str, config: PluginConfig) -> None:
55
73
  super(Search, self).__init__(provider, config)
56
74
  # Prepare the metadata mapping
57
75
  # Do a shallow copy, the structure is flat enough for this to be sufficient
58
- metas = DEFAULT_METADATA_MAPPING.copy()
76
+ metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
59
77
  # Update the defaults with the mapping value. This will add any new key
60
78
  # added by the provider mapping that is not in the default metadata
61
79
  if self.config.metadata_mapping:
@@ -72,21 +90,18 @@ class Search(PluginTopic):
72
90
 
73
91
  def query(
74
92
  self,
75
- product_type: Optional[str] = None,
76
- items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
77
- page: int = DEFAULT_PAGE,
78
- count: bool = True,
93
+ prep: PreparedSearch = PreparedSearch(),
79
94
  **kwargs: Any,
80
95
  ) -> Tuple[List[EOProduct], Optional[int]]:
81
96
  """Implementation of how the products must be searched goes here.
82
97
 
83
98
  This method must return a tuple with (1) a list of EOProduct instances (see eodag.api.product module)
84
99
  which will be processed by a Download plugin (2) and the total number of products matching
85
- the search criteria. If ``count`` is False, the second element returned must be ``None``.
100
+ the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
86
101
  """
87
102
  raise NotImplementedError("A Search plugin must implement a method named query")
88
103
 
89
- def discover_product_types(self) -> Optional[Dict[str, Any]]:
104
+ def discover_product_types(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
90
105
  """Fetch product types list from provider using `discover_product_types` conf"""
91
106
  return None
92
107
 
@@ -97,25 +112,25 @@ class Search(PluginTopic):
97
112
 
98
113
  :param kwargs: additional filters for queryables (`productType` and other search
99
114
  arguments)
100
- :type kwargs: Any
101
115
  :returns: fetched queryable parameters dict
102
- :rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
103
116
  """
104
- return None
117
+ raise NotImplementedError(
118
+ f"discover_queryables is not implemeted for plugin {self.__class__.__name__}"
119
+ )
105
120
 
106
- def get_defaults_as_queryables(
121
+ def _get_defaults_as_queryables(
107
122
  self, product_type: str
108
123
  ) -> Dict[str, Annotated[Any, FieldInfo]]:
109
124
  """
110
- Return given product type defaut settings as queryables
125
+ Return given product type default settings as queryables
111
126
 
112
127
  :param product_type: given product type
113
- :type product_type: str
114
128
  :returns: queryable parameters dict
115
- :rtype: Dict[str, Annotated[Any, FieldInfo]]
116
129
  """
117
- defaults = self.config.products.get(product_type, {})
118
- queryables = {}
130
+ defaults = deepcopy(self.config.products.get(product_type, {}))
131
+ defaults.pop("metadata_mapping", None)
132
+
133
+ queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
119
134
  for parameter, value in defaults.items():
120
135
  queryables[parameter] = Annotated[type(value), Field(default=value)]
121
136
  return queryables
@@ -126,9 +141,7 @@ class Search(PluginTopic):
126
141
  """Get the provider product type from eodag product type
127
142
 
128
143
  :param product_type: eodag product type
129
- :type product_type: str
130
144
  :returns: provider product type
131
- :rtype: str
132
145
  """
133
146
  if product_type is None:
134
147
  return None
@@ -143,9 +156,7 @@ class Search(PluginTopic):
143
156
  """Get the provider product type definition parameters and specific settings
144
157
 
145
158
  :param product_type: the desired product type
146
- :type product_type: str
147
159
  :returns: The product type definition parameters
148
- :rtype: dict
149
160
  """
150
161
  if product_type in self.config.products.keys():
151
162
  logger.debug(
@@ -168,16 +179,214 @@ class Search(PluginTopic):
168
179
  else:
169
180
  return {}
170
181
 
182
+ def get_product_type_cfg_value(self, key: str, default: Any = None) -> Any:
183
+ """
184
+ Get the value of a configuration option specific to the current product type.
185
+
186
+ This method retrieves the value of a configuration option from the
187
+ `product_type_config` attribute. If the option is not found, the provided
188
+ default value is returned.
189
+
190
+ :param key: The configuration option key.
191
+ :type key: str
192
+ :param default: The default value to be returned if the option is not found (default is None).
193
+ :type default: Any
194
+
195
+ :return: The value of the specified configuration option or the default value.
196
+ :rtype: Any
197
+ """
198
+ product_type_cfg = getattr(self.config, "product_type_config", {})
199
+ non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
200
+
201
+ return non_none_cfg.get(key, default)
202
+
171
203
  def get_metadata_mapping(
172
204
  self, product_type: Optional[str] = None
173
- ) -> Dict[str, str]:
205
+ ) -> Dict[str, Union[str, List[str]]]:
174
206
  """Get the plugin metadata mapping configuration (product type specific if exists)
175
207
 
176
208
  :param product_type: the desired product type
177
- :type product_type: str
178
209
  :returns: The product type specific metadata-mapping
179
- :rtype: dict
180
210
  """
181
- return self.config.products.get(product_type, {}).get(
182
- "metadata_mapping", self.config.metadata_mapping
211
+ if product_type:
212
+ return self.config.products.get(product_type, {}).get(
213
+ "metadata_mapping", self.config.metadata_mapping
214
+ )
215
+ return self.config.metadata_mapping
216
+
217
+ def get_sort_by_arg(self, kwargs: Dict[str, Any]) -> Optional[SortByList]:
218
+ """Extract the "sort_by" argument from the kwargs or the provider default sort configuration
219
+
220
+ :param kwargs: Search arguments
221
+ :returns: The "sort_by" argument from the kwargs or the provider default sort configuration
222
+ """
223
+ # remove "sort_by" from search args if exists because it is not part of metadata mapping,
224
+ # it will complete the query string or body once metadata mapping will be done
225
+ sort_by_arg_tmp = kwargs.pop("sort_by", None)
226
+ sort_by_arg = sort_by_arg_tmp or getattr(self.config, "sort", {}).get(
227
+ "sort_by_default", None
228
+ )
229
+ if not sort_by_arg_tmp and sort_by_arg:
230
+ logger.info(
231
+ f"{self.provider} is configured with default sorting by '{sort_by_arg[0][0]}' "
232
+ f"in {'ascending' if sort_by_arg[0][1] == 'ASC' else 'descending'} order"
233
+ )
234
+ return sort_by_arg
235
+
236
+ def build_sort_by(
237
+ self, sort_by_arg: SortByList
238
+ ) -> Tuple[str, Dict[str, List[Dict[str, str]]]]:
239
+ """Build the sorting part of the query string or body by transforming
240
+ the "sort_by" argument into a provider-specific string or dictionary
241
+
242
+ :param sort_by_arg: the "sort_by" argument in EODAG format
243
+ :returns: The "sort_by" argument in provider-specific format
244
+ """
245
+ if not hasattr(self.config, "sort"):
246
+ raise ValidationError(f"{self.provider} does not support sorting feature")
247
+ # TODO: remove this code block when search args model validation is embeded
248
+ # remove duplicates
249
+ sort_by_arg = list(dict.fromkeys(sort_by_arg))
250
+
251
+ sort_by_qs: str = ""
252
+ sort_by_qp: Dict[str, Any] = {}
253
+
254
+ provider_sort_by_tuples_used: List[Tuple[str, str]] = []
255
+ for eodag_sort_by_tuple in sort_by_arg:
256
+ eodag_sort_param = eodag_sort_by_tuple[0]
257
+ provider_sort_param = self.config.sort["sort_param_mapping"].get(
258
+ eodag_sort_param, None
259
+ )
260
+ if not provider_sort_param:
261
+ joined_eodag_params_to_map = ", ".join(
262
+ k for k in self.config.sort["sort_param_mapping"].keys()
263
+ )
264
+ params = set(self.config.sort["sort_param_mapping"].keys())
265
+ params.add(eodag_sort_param)
266
+ raise ValidationError(
267
+ f"'{eodag_sort_param}' parameter is not sortable with {self.provider}. "
268
+ f"Here is the list of sortable parameter(s) with {self.provider}: {joined_eodag_params_to_map}",
269
+ params,
270
+ )
271
+ eodag_sort_order = eodag_sort_by_tuple[1]
272
+ # TODO: remove this code block when search args model validation is embeded
273
+ # Remove leading and trailing whitespace(s) if exist
274
+ eodag_sort_order = eodag_sort_order.strip().upper()
275
+ if eodag_sort_order[:3] != "ASC" and eodag_sort_order[:3] != "DES":
276
+ raise ValidationError(
277
+ "Sorting order is invalid: it must be set to 'ASC' (ASCENDING) or "
278
+ f"'DESC' (DESCENDING), got '{eodag_sort_order}' with '{eodag_sort_param}' instead"
279
+ )
280
+ eodag_sort_order = eodag_sort_order[:3]
281
+
282
+ provider_sort_order = (
283
+ self.config.sort["sort_order_mapping"]["ascending"]
284
+ if eodag_sort_order == "ASC"
285
+ else self.config.sort["sort_order_mapping"]["descending"]
286
+ )
287
+ provider_sort_by_tuple: Tuple[str, str] = (
288
+ provider_sort_param,
289
+ provider_sort_order,
290
+ )
291
+ # TODO: remove this code block when search args model validation is embeded
292
+ for provider_sort_by_tuple_used in provider_sort_by_tuples_used:
293
+ # since duplicated tuples or dictionnaries have been removed, if two sorting parameters are equal,
294
+ # then their sorting order is different and there is a contradiction that would raise an error
295
+ if provider_sort_by_tuple[0] == provider_sort_by_tuple_used[0]:
296
+ raise ValidationError(
297
+ f"'{eodag_sort_param}' parameter is called several times to sort results with different "
298
+ "sorting orders. Please set it to only one ('ASC' (ASCENDING) or 'DESC' (DESCENDING))",
299
+ set([eodag_sort_param]),
300
+ )
301
+ provider_sort_by_tuples_used.append(provider_sort_by_tuple)
302
+
303
+ # TODO: move this code block to the top of this method when search args model validation is embeded
304
+ # check if the limit number of sorting parameter(s) is respected with this sorting parameter
305
+ if (
306
+ self.config.sort.get("max_sort_params", None)
307
+ and len(provider_sort_by_tuples_used)
308
+ > self.config.sort["max_sort_params"]
309
+ ):
310
+ raise ValidationError(
311
+ f"Search results can be sorted by only {self.config.sort['max_sort_params']} "
312
+ f"parameter(s) with {self.provider}"
313
+ )
314
+
315
+ parsed_sort_by_tpl: str = self.config.sort["sort_by_tpl"].format(
316
+ sort_param=provider_sort_by_tuple[0],
317
+ sort_order=provider_sort_by_tuple[1],
318
+ )
319
+ try:
320
+ parsed_sort_by_tpl_dict: Dict[str, Any] = orjson.loads(
321
+ parsed_sort_by_tpl
322
+ )
323
+ sort_by_qp = update_nested_dict(
324
+ sort_by_qp, parsed_sort_by_tpl_dict, extend_list_values=True
325
+ )
326
+ except orjson.JSONDecodeError:
327
+ sort_by_qs += parsed_sort_by_tpl
328
+ return (sort_by_qs, sort_by_qp)
329
+
330
+ def list_queryables(
331
+ self,
332
+ filters: Dict[str, Any],
333
+ product_type: Optional[str] = None,
334
+ ) -> Dict[str, Annotated[Any, FieldInfo]]:
335
+ """
336
+ Get queryables
337
+
338
+ :param filters: Additional filters for queryables.
339
+ :param product_type: (optional) The product type.
340
+
341
+ :return: A dictionary containing the queryable properties, associating parameters to their
342
+ annotated type.
343
+ """
344
+ default_values: Dict[str, Any] = deepcopy(
345
+ getattr(self.config, "products", {}).get(product_type, {})
346
+ )
347
+ default_values.pop("metadata_mapping", None)
348
+
349
+ queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
350
+ try:
351
+ queryables = self.discover_queryables(**{**default_values, **filters}) or {}
352
+ except NotImplementedError:
353
+ pass
354
+
355
+ metadata_mapping: Dict[str, Any] = deepcopy(
356
+ self.get_metadata_mapping(product_type)
183
357
  )
358
+
359
+ for param in list(metadata_mapping.keys()):
360
+ if NOT_MAPPED in metadata_mapping[param] or not isinstance(
361
+ metadata_mapping[param], list
362
+ ):
363
+ del metadata_mapping[param]
364
+
365
+ eodag_queryables = copy_deepcopy(
366
+ model_fields_to_annotated(Queryables.model_fields)
367
+ )
368
+ for k, v in eodag_queryables.items():
369
+ eodag_queryable_field_info = (
370
+ get_args(v)[1] if len(get_args(v)) > 1 else None
371
+ )
372
+ if not isinstance(eodag_queryable_field_info, FieldInfo):
373
+ continue
374
+ # keep default field info of eodag queryables
375
+ if k in filters and k in queryables:
376
+ queryable_field_info = (
377
+ get_args(queryables[k])[1]
378
+ if len(get_args(queryables[k])) > 1
379
+ else None
380
+ )
381
+ if not isinstance(queryable_field_info, FieldInfo):
382
+ continue
383
+ queryable_field_info.default = filters[k]
384
+ continue
385
+ if k in queryables:
386
+ continue
387
+ if eodag_queryable_field_info.is_required() or (
388
+ (eodag_queryable_field_info.alias or k) in metadata_mapping
389
+ ):
390
+ queryables[k] = v
391
+
392
+ return queryables