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