eodag 3.0.0b3__py3-none-any.whl → 3.1.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 (94) hide show
  1. eodag/api/core.py +347 -247
  2. eodag/api/product/_assets.py +44 -15
  3. eodag/api/product/_product.py +58 -47
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +129 -93
  10. eodag/api/search_result.py +28 -12
  11. eodag/cli.py +61 -24
  12. eodag/config.py +457 -167
  13. eodag/plugins/apis/base.py +10 -4
  14. eodag/plugins/apis/ecmwf.py +53 -23
  15. eodag/plugins/apis/usgs.py +41 -17
  16. eodag/plugins/authentication/aws_auth.py +30 -18
  17. eodag/plugins/authentication/base.py +14 -3
  18. eodag/plugins/authentication/generic.py +14 -3
  19. eodag/plugins/authentication/header.py +14 -6
  20. eodag/plugins/authentication/keycloak.py +44 -25
  21. eodag/plugins/authentication/oauth.py +18 -4
  22. eodag/plugins/authentication/openid_connect.py +192 -171
  23. eodag/plugins/authentication/qsauth.py +12 -4
  24. eodag/plugins/authentication/sas_auth.py +22 -5
  25. eodag/plugins/authentication/token.py +95 -17
  26. eodag/plugins/authentication/token_exchange.py +19 -19
  27. eodag/plugins/base.py +4 -4
  28. eodag/plugins/crunch/base.py +8 -5
  29. eodag/plugins/crunch/filter_date.py +9 -6
  30. eodag/plugins/crunch/filter_latest_intersect.py +9 -8
  31. eodag/plugins/crunch/filter_latest_tpl_name.py +8 -8
  32. eodag/plugins/crunch/filter_overlap.py +9 -11
  33. eodag/plugins/crunch/filter_property.py +10 -10
  34. eodag/plugins/download/aws.py +181 -105
  35. eodag/plugins/download/base.py +49 -67
  36. eodag/plugins/download/creodias_s3.py +40 -2
  37. eodag/plugins/download/http.py +247 -223
  38. eodag/plugins/download/s3rest.py +29 -28
  39. eodag/plugins/manager.py +176 -41
  40. eodag/plugins/search/__init__.py +6 -5
  41. eodag/plugins/search/base.py +123 -60
  42. eodag/plugins/search/build_search_result.py +1046 -355
  43. eodag/plugins/search/cop_marine.py +132 -39
  44. eodag/plugins/search/creodias_s3.py +19 -68
  45. eodag/plugins/search/csw.py +48 -8
  46. eodag/plugins/search/data_request_search.py +124 -23
  47. eodag/plugins/search/qssearch.py +531 -310
  48. eodag/plugins/search/stac_list_assets.py +85 -0
  49. eodag/plugins/search/static_stac_search.py +23 -24
  50. eodag/resources/ext_product_types.json +1 -1
  51. eodag/resources/product_types.yml +1295 -355
  52. eodag/resources/providers.yml +1819 -3010
  53. eodag/resources/stac.yml +3 -163
  54. eodag/resources/stac_api.yml +2 -2
  55. eodag/resources/user_conf_template.yml +115 -99
  56. eodag/rest/cache.py +2 -2
  57. eodag/rest/config.py +3 -4
  58. eodag/rest/constants.py +0 -1
  59. eodag/rest/core.py +157 -117
  60. eodag/rest/errors.py +181 -0
  61. eodag/rest/server.py +57 -339
  62. eodag/rest/stac.py +133 -581
  63. eodag/rest/types/collections_search.py +3 -3
  64. eodag/rest/types/eodag_search.py +41 -30
  65. eodag/rest/types/queryables.py +42 -32
  66. eodag/rest/types/stac_search.py +15 -16
  67. eodag/rest/utils/__init__.py +14 -21
  68. eodag/rest/utils/cql_evaluate.py +6 -6
  69. eodag/rest/utils/rfc3339.py +2 -2
  70. eodag/types/__init__.py +153 -32
  71. eodag/types/bbox.py +2 -2
  72. eodag/types/download_args.py +4 -4
  73. eodag/types/queryables.py +183 -73
  74. eodag/types/search_args.py +6 -6
  75. eodag/types/whoosh.py +127 -3
  76. eodag/utils/__init__.py +228 -106
  77. eodag/utils/exceptions.py +47 -26
  78. eodag/utils/import_system.py +2 -2
  79. eodag/utils/logging.py +37 -77
  80. eodag/utils/repr.py +65 -6
  81. eodag/utils/requests.py +13 -15
  82. eodag/utils/rest.py +2 -2
  83. eodag/utils/s3.py +231 -0
  84. eodag/utils/stac_reader.py +11 -11
  85. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/METADATA +81 -81
  86. eodag-3.1.0.dist-info/RECORD +113 -0
  87. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  88. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +5 -2
  89. eodag/resources/constraints/climate-dt.json +0 -13
  90. eodag/resources/constraints/extremes-dt.json +0 -8
  91. eodag/utils/constraints.py +0 -244
  92. eodag-3.0.0b3.dist-info/RECORD +0 -110
  93. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  94. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
@@ -18,7 +18,7 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import logging
21
- from typing import TYPE_CHECKING
21
+ from typing import TYPE_CHECKING, Annotated, get_args
22
22
 
23
23
  import orjson
24
24
  from pydantic.fields import Field, FieldInfo
@@ -31,26 +31,25 @@ from eodag.api.product.metadata_mapping import (
31
31
  from eodag.plugins.base import PluginTopic
32
32
  from eodag.plugins.search import PreparedSearch
33
33
  from eodag.types import model_fields_to_annotated
34
- from eodag.types.queryables import Queryables
34
+ from eodag.types.queryables import Queryables, QueryablesDict
35
35
  from eodag.types.search_args import SortByList
36
36
  from eodag.utils import (
37
37
  GENERIC_PRODUCT_TYPE,
38
- Annotated,
39
38
  copy_deepcopy,
40
39
  deepcopy,
41
40
  format_dict_items,
42
- get_args,
43
41
  update_nested_dict,
44
42
  )
45
43
  from eodag.utils.exceptions import ValidationError
46
44
 
47
45
  if TYPE_CHECKING:
48
- from typing import Any, Dict, List, Optional, Tuple, Union
46
+ from typing import Any, Optional, Union
49
47
 
50
48
  from requests.auth import AuthBase
51
49
 
52
50
  from eodag.api.product import EOProduct
53
51
  from eodag.config import PluginConfig
52
+ from eodag.types import S3SessionKwargs
54
53
 
55
54
  logger = logging.getLogger("eodag.search.base")
56
55
 
@@ -62,9 +61,9 @@ class Search(PluginTopic):
62
61
  :param config: An EODAG plugin configuration
63
62
  """
64
63
 
65
- auth: Union[AuthBase, Dict[str, str]]
64
+ auth: Union[AuthBase, S3SessionKwargs]
66
65
  next_page_url: Optional[str]
67
- next_page_query_obj: Optional[Dict[str, Any]]
66
+ next_page_query_obj: Optional[dict[str, Any]]
68
67
  total_items_nb: int
69
68
  need_count: bool
70
69
  _request: Any # needed by deprecated load_stac_items
@@ -73,7 +72,7 @@ class Search(PluginTopic):
73
72
  super(Search, self).__init__(provider, config)
74
73
  # Prepare the metadata mapping
75
74
  # Do a shallow copy, the structure is flat enough for this to be sufficient
76
- metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
75
+ metas: dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
77
76
  # Update the defaults with the mapping value. This will add any new key
78
77
  # added by the provider mapping that is not in the default metadata
79
78
  if self.config.metadata_mapping:
@@ -92,25 +91,25 @@ class Search(PluginTopic):
92
91
  self,
93
92
  prep: PreparedSearch = PreparedSearch(),
94
93
  **kwargs: Any,
95
- ) -> Tuple[List[EOProduct], Optional[int]]:
94
+ ) -> tuple[list[EOProduct], Optional[int]]:
96
95
  """Implementation of how the products must be searched goes here.
97
96
 
98
- This method must return a tuple with (1) a list of EOProduct instances (see eodag.api.product module)
99
- which will be processed by a Download plugin (2) and the total number of products matching
100
- the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
97
+ This method must return a tuple with (1) a list of :class:`~eodag.api.product._product.EOProduct` instances
98
+ which will be processed by a :class:`~eodag.plugins.download.base.Download` plugin (2) and the total number of
99
+ products matching the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
101
100
  """
102
101
  raise NotImplementedError("A Search plugin must implement a method named query")
103
102
 
104
- def discover_product_types(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
103
+ def discover_product_types(self, **kwargs: Any) -> Optional[dict[str, Any]]:
105
104
  """Fetch product types list from provider using `discover_product_types` conf"""
106
105
  return None
107
106
 
108
107
  def discover_queryables(
109
108
  self, **kwargs: Any
110
- ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
111
- """Fetch queryables list from provider using `discover_queryables` conf
109
+ ) -> Optional[dict[str, Annotated[Any, FieldInfo]]]:
110
+ """Fetch queryables list from provider using :attr:`~eodag.config.PluginConfig.discover_queryables` conf
112
111
 
113
- :param kwargs: additional filters for queryables (`productType` and other search
112
+ :param kwargs: additional filters for queryables (``productType`` and other search
114
113
  arguments)
115
114
  :returns: fetched queryable parameters dict
116
115
  """
@@ -120,7 +119,7 @@ class Search(PluginTopic):
120
119
 
121
120
  def _get_defaults_as_queryables(
122
121
  self, product_type: str
123
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
122
+ ) -> dict[str, Annotated[Any, FieldInfo]]:
124
123
  """
125
124
  Return given product type default settings as queryables
126
125
 
@@ -130,7 +129,7 @@ class Search(PluginTopic):
130
129
  defaults = deepcopy(self.config.products.get(product_type, {}))
131
130
  defaults.pop("metadata_mapping", None)
132
131
 
133
- queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
132
+ queryables: dict[str, Annotated[Any, FieldInfo]] = {}
134
133
  for parameter, value in defaults.items():
135
134
  queryables[parameter] = Annotated[type(value), Field(default=value)]
136
135
  return queryables
@@ -151,8 +150,8 @@ class Search(PluginTopic):
151
150
  )
152
151
 
153
152
  def get_product_type_def_params(
154
- self, product_type: str, **kwargs: Any
155
- ) -> Dict[str, Any]:
153
+ self, product_type: str, format_variables: Optional[dict[str, Any]] = None
154
+ ) -> dict[str, Any]:
156
155
  """Get the provider product type definition parameters and specific settings
157
156
 
158
157
  :param product_type: the desired product type
@@ -172,16 +171,38 @@ class Search(PluginTopic):
172
171
  return {
173
172
  k: v
174
173
  for k, v in format_dict_items(
175
- self.config.products[GENERIC_PRODUCT_TYPE], **kwargs
174
+ self.config.products[GENERIC_PRODUCT_TYPE],
175
+ **(format_variables or {}),
176
176
  ).items()
177
177
  if v
178
178
  }
179
179
  else:
180
180
  return {}
181
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
+
182
203
  def get_metadata_mapping(
183
204
  self, product_type: Optional[str] = None
184
- ) -> Dict[str, Union[str, List[str]]]:
205
+ ) -> dict[str, Union[str, list[str]]]:
185
206
  """Get the plugin metadata mapping configuration (product type specific if exists)
186
207
 
187
208
  :param product_type: the desired product type
@@ -193,11 +214,11 @@ class Search(PluginTopic):
193
214
  )
194
215
  return self.config.metadata_mapping
195
216
 
196
- def get_sort_by_arg(self, kwargs: Dict[str, Any]) -> Optional[SortByList]:
197
- """Extract the "sort_by" argument from the kwargs or the provider default sort configuration
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
198
219
 
199
220
  :param kwargs: Search arguments
200
- :returns: The "sort_by" argument from the kwargs or the provider default sort configuration
221
+ :returns: The ``sort_by`` argument from the kwargs or the provider default sort configuration
201
222
  """
202
223
  # remove "sort_by" from search args if exists because it is not part of metadata mapping,
203
224
  # it will complete the query string or body once metadata mapping will be done
@@ -214,23 +235,23 @@ class Search(PluginTopic):
214
235
 
215
236
  def build_sort_by(
216
237
  self, sort_by_arg: SortByList
217
- ) -> Tuple[str, Dict[str, List[Dict[str, str]]]]:
238
+ ) -> tuple[str, dict[str, list[dict[str, str]]]]:
218
239
  """Build the sorting part of the query string or body by transforming
219
- the "sort_by" argument into a provider-specific string or dictionnary
240
+ the ``sort_by`` argument into a provider-specific string or dictionary
220
241
 
221
- :param sort_by_arg: the "sort_by" argument in EODAG format
222
- :returns: The "sort_by" argument in provider-specific format
242
+ :param sort_by_arg: the ``sort_by`` argument in EODAG format
243
+ :returns: The ``sort_by`` argument in provider-specific format
223
244
  """
224
245
  if not hasattr(self.config, "sort"):
225
246
  raise ValidationError(f"{self.provider} does not support sorting feature")
226
247
  # TODO: remove this code block when search args model validation is embeded
227
248
  # remove duplicates
228
- sort_by_arg = list(set(sort_by_arg))
249
+ sort_by_arg = list(dict.fromkeys(sort_by_arg))
229
250
 
230
251
  sort_by_qs: str = ""
231
- sort_by_qp: Dict[str, Any] = {}
252
+ sort_by_qp: dict[str, Any] = {}
232
253
 
233
- provider_sort_by_tuples_used: List[Tuple[str, str]] = []
254
+ provider_sort_by_tuples_used: list[tuple[str, str]] = []
234
255
  for eodag_sort_by_tuple in sort_by_arg:
235
256
  eodag_sort_param = eodag_sort_by_tuple[0]
236
257
  provider_sort_param = self.config.sort["sort_param_mapping"].get(
@@ -263,7 +284,7 @@ class Search(PluginTopic):
263
284
  if eodag_sort_order == "ASC"
264
285
  else self.config.sort["sort_order_mapping"]["descending"]
265
286
  )
266
- provider_sort_by_tuple: Tuple[str, str] = (
287
+ provider_sort_by_tuple: tuple[str, str] = (
267
288
  provider_sort_param,
268
289
  provider_sort_order,
269
290
  )
@@ -296,7 +317,7 @@ class Search(PluginTopic):
296
317
  sort_order=provider_sort_by_tuple[1],
297
318
  )
298
319
  try:
299
- parsed_sort_by_tpl_dict: Dict[str, Any] = orjson.loads(
320
+ parsed_sort_by_tpl_dict: dict[str, Any] = orjson.loads(
300
321
  parsed_sort_by_tpl
301
322
  )
302
323
  sort_by_qp = update_nested_dict(
@@ -306,35 +327,87 @@ class Search(PluginTopic):
306
327
  sort_by_qs += parsed_sort_by_tpl
307
328
  return (sort_by_qs, sort_by_qp)
308
329
 
330
+ def _get_product_type_queryables(
331
+ self, product_type: Optional[str], alias: Optional[str], filters: dict[str, Any]
332
+ ) -> QueryablesDict:
333
+ default_values: dict[str, Any] = deepcopy(
334
+ getattr(self.config, "products", {}).get(product_type, {})
335
+ )
336
+ default_values.pop("metadata_mapping", None)
337
+ try:
338
+ filters["productType"] = product_type
339
+ queryables = self.discover_queryables(**{**default_values, **filters}) or {}
340
+ except NotImplementedError:
341
+ queryables = self.queryables_from_metadata_mapping(product_type, alias)
342
+
343
+ return QueryablesDict(**queryables)
344
+
309
345
  def list_queryables(
310
346
  self,
311
- filters: Dict[str, Any],
347
+ filters: dict[str, Any],
348
+ available_product_types: list[Any],
349
+ product_type_configs: dict[str, dict[str, Any]],
312
350
  product_type: Optional[str] = None,
313
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
351
+ alias: Optional[str] = None,
352
+ ) -> QueryablesDict:
314
353
  """
315
354
  Get queryables
316
355
 
317
356
  :param filters: Additional filters for queryables.
357
+ :param available_product_types: list of available product types
358
+ :param product_type_configs: dict containing the product type information for all used product types
318
359
  :param product_type: (optional) The product type.
360
+ :param alias: (optional) alias of the product type
319
361
 
320
362
  :return: A dictionary containing the queryable properties, associating parameters to their
321
363
  annotated type.
322
364
  """
323
- default_values: Dict[str, Any] = deepcopy(
324
- getattr(self.config, "products", {}).get(product_type, {})
365
+ additional_info = (
366
+ "Please select a product type to get the possible values of the parameters!"
367
+ if not product_type
368
+ else ""
325
369
  )
326
- default_values.pop("metadata_mapping", None)
327
-
328
- queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
329
- try:
330
- queryables = self.discover_queryables(**{**default_values, **filters}) or {}
331
- except NotImplementedError:
332
- pass
370
+ if product_type or getattr(self.config, "discover_queryables", {}).get(
371
+ "fetch_url", ""
372
+ ):
373
+ if product_type:
374
+ self.config.product_type_config = product_type_configs[product_type]
375
+ queryables = self._get_product_type_queryables(product_type, alias, filters)
376
+ queryables.additional_information = additional_info
377
+
378
+ return queryables
379
+ else:
380
+ all_queryables: dict[str, Any] = {}
381
+ for pt in available_product_types:
382
+ self.config.product_type_config = product_type_configs[pt]
383
+ pt_queryables = self._get_product_type_queryables(pt, None, filters)
384
+ # only use key and type because values and defaults will vary between product types
385
+ pt_queryables_neutral = {
386
+ k: Annotated[v.__args__[0], Field(default=None)]
387
+ for k, v in pt_queryables.items()
388
+ }
389
+ all_queryables.update(pt_queryables_neutral)
390
+ return QueryablesDict(
391
+ additional_properties=True,
392
+ additional_information=additional_info,
393
+ **all_queryables,
394
+ )
333
395
 
334
- metadata_mapping: Dict[str, Any] = deepcopy(
396
+ def queryables_from_metadata_mapping(
397
+ self, product_type: Optional[str] = None, alias: Optional[str] = None
398
+ ) -> dict[str, Annotated[Any, FieldInfo]]:
399
+ """
400
+ Extract queryable parameters from product type metadata mapping.
401
+ :param product_type: product type id (optional)
402
+ :param alias: (optional) alias of the product type
403
+ :returns: dict of annotated queryables
404
+ """
405
+ metadata_mapping: dict[str, Any] = deepcopy(
335
406
  self.get_metadata_mapping(product_type)
336
407
  )
337
408
 
409
+ queryables: dict[str, Annotated[Any, FieldInfo]] = {}
410
+
338
411
  for param in list(metadata_mapping.keys()):
339
412
  if NOT_MAPPED in metadata_mapping[param] or not isinstance(
340
413
  metadata_mapping[param], list
@@ -344,28 +417,18 @@ class Search(PluginTopic):
344
417
  eodag_queryables = copy_deepcopy(
345
418
  model_fields_to_annotated(Queryables.model_fields)
346
419
  )
420
+ # add default value for product type
421
+ if alias:
422
+ eodag_queryables.pop("productType")
423
+ eodag_queryables["productType"] = Annotated[str, Field(default=alias)]
347
424
  for k, v in eodag_queryables.items():
348
425
  eodag_queryable_field_info = (
349
426
  get_args(v)[1] if len(get_args(v)) > 1 else None
350
427
  )
351
428
  if not isinstance(eodag_queryable_field_info, FieldInfo):
352
429
  continue
353
- # keep default field info of eodag queryables
354
- if k in filters and k in queryables:
355
- queryable_field_info = (
356
- get_args(queryables[k])[1]
357
- if len(get_args(queryables[k])) > 1
358
- else None
359
- )
360
- if not isinstance(queryable_field_info, FieldInfo):
361
- continue
362
- queryable_field_info.default = filters[k]
363
- continue
364
- if k in queryables:
365
- continue
366
430
  if eodag_queryable_field_info.is_required() or (
367
431
  (eodag_queryable_field_info.alias or k) in metadata_mapping
368
432
  ):
369
433
  queryables[k] = v
370
-
371
434
  return queryables