eodag 3.9.1__py3-none-any.whl → 4.0.0a1__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 (71) hide show
  1. eodag/api/core.py +378 -419
  2. eodag/api/product/__init__.py +3 -3
  3. eodag/api/product/_product.py +68 -40
  4. eodag/api/product/drivers/__init__.py +3 -5
  5. eodag/api/product/drivers/base.py +1 -18
  6. eodag/api/product/metadata_mapping.py +151 -215
  7. eodag/api/search_result.py +13 -7
  8. eodag/cli.py +72 -395
  9. eodag/config.py +46 -50
  10. eodag/plugins/apis/base.py +2 -2
  11. eodag/plugins/apis/ecmwf.py +20 -21
  12. eodag/plugins/apis/usgs.py +37 -33
  13. eodag/plugins/authentication/aws_auth.py +36 -1
  14. eodag/plugins/authentication/base.py +18 -3
  15. eodag/plugins/authentication/sas_auth.py +15 -0
  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 +45 -41
  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 +3 -4
  24. eodag/plugins/search/base.py +128 -77
  25. eodag/plugins/search/build_search_result.py +105 -107
  26. eodag/plugins/search/cop_marine.py +44 -47
  27. eodag/plugins/search/csw.py +33 -33
  28. eodag/plugins/search/qssearch.py +335 -354
  29. eodag/plugins/search/stac_list_assets.py +1 -1
  30. eodag/plugins/search/static_stac_search.py +31 -31
  31. eodag/resources/{product_types.yml → collections.yml} +2353 -2429
  32. eodag/resources/ext_collections.json +1 -0
  33. eodag/resources/ext_product_types.json +1 -1
  34. eodag/resources/providers.yml +2432 -2714
  35. eodag/resources/stac_provider.yml +46 -90
  36. eodag/types/queryables.py +55 -91
  37. eodag/types/search_args.py +1 -1
  38. eodag/utils/__init__.py +94 -21
  39. eodag/utils/exceptions.py +6 -6
  40. eodag/utils/free_text_search.py +3 -3
  41. {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/METADATA +11 -88
  42. eodag-4.0.0a1.dist-info/RECORD +92 -0
  43. {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/entry_points.txt +0 -4
  44. eodag/plugins/authentication/oauth.py +0 -60
  45. eodag/plugins/download/creodias_s3.py +0 -64
  46. eodag/plugins/download/s3rest.py +0 -351
  47. eodag/plugins/search/data_request_search.py +0 -565
  48. eodag/resources/stac.yml +0 -294
  49. eodag/resources/stac_api.yml +0 -2105
  50. eodag/rest/__init__.py +0 -24
  51. eodag/rest/cache.py +0 -70
  52. eodag/rest/config.py +0 -67
  53. eodag/rest/constants.py +0 -26
  54. eodag/rest/core.py +0 -764
  55. eodag/rest/errors.py +0 -210
  56. eodag/rest/server.py +0 -604
  57. eodag/rest/server.wsgi +0 -6
  58. eodag/rest/stac.py +0 -1032
  59. eodag/rest/templates/README +0 -1
  60. eodag/rest/types/__init__.py +0 -18
  61. eodag/rest/types/collections_search.py +0 -44
  62. eodag/rest/types/eodag_search.py +0 -386
  63. eodag/rest/types/queryables.py +0 -174
  64. eodag/rest/types/stac_search.py +0 -272
  65. eodag/rest/utils/__init__.py +0 -207
  66. eodag/rest/utils/cql_evaluate.py +0 -119
  67. eodag/rest/utils/rfc3339.py +0 -64
  68. eodag-3.9.1.dist-info/RECORD +0 -115
  69. {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/WHEEL +0 -0
  70. {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
  71. {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/top_level.txt +0 -0
@@ -21,10 +21,10 @@ import logging
21
21
  from typing import TYPE_CHECKING, Annotated, get_args
22
22
 
23
23
  import orjson
24
+ from pydantic import ValidationError as PydanticValidationError
24
25
  from pydantic.fields import Field, FieldInfo
25
26
 
26
27
  from eodag.api.product.metadata_mapping import (
27
- DEFAULT_METADATA_MAPPING,
28
28
  NOT_AVAILABLE,
29
29
  NOT_MAPPED,
30
30
  mtd_cfg_as_conversion_and_querypath,
@@ -35,10 +35,12 @@ from eodag.types import model_fields_to_annotated
35
35
  from eodag.types.queryables import Queryables, QueryablesDict
36
36
  from eodag.types.search_args import SortByList
37
37
  from eodag.utils import (
38
- GENERIC_PRODUCT_TYPE,
38
+ GENERIC_COLLECTION,
39
39
  copy_deepcopy,
40
40
  deepcopy,
41
41
  format_dict_items,
42
+ format_pydantic_error,
43
+ get_collection_dates,
42
44
  string_to_jsonpath,
43
45
  update_nested_dict,
44
46
  )
@@ -52,7 +54,6 @@ if TYPE_CHECKING:
52
54
 
53
55
  from eodag.api.product import EOProduct
54
56
  from eodag.config import PluginConfig
55
- from eodag.types import S3SessionKwargs
56
57
 
57
58
  logger = logging.getLogger("eodag.search.base")
58
59
 
@@ -64,18 +65,17 @@ class Search(PluginTopic):
64
65
  :param config: An EODAG plugin configuration
65
66
  """
66
67
 
67
- auth: Union[AuthBase, S3SessionKwargs, S3ServiceResource]
68
+ auth: Union[AuthBase, S3ServiceResource]
68
69
  next_page_url: Optional[str]
69
70
  next_page_query_obj: Optional[dict[str, Any]]
70
71
  total_items_nb: int
71
72
  need_count: bool
72
- _request: Any # needed by deprecated load_stac_items
73
73
 
74
74
  def __init__(self, provider: str, config: PluginConfig) -> None:
75
75
  super(Search, self).__init__(provider, config)
76
76
  # Prepare the metadata mapping
77
77
  # Do a shallow copy, the structure is flat enough for this to be sufficient
78
- metas: dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
78
+ metas: dict[str, Any] = {}
79
79
  # Update the defaults with the mapping value. This will add any new key
80
80
  # added by the provider mapping that is not in the default metadata
81
81
  if self.config.metadata_mapping:
@@ -85,6 +85,9 @@ class Search(PluginTopic):
85
85
  self.config.metadata_mapping,
86
86
  result_type=getattr(self.config, "result_type", "json"),
87
87
  )
88
+ # set default metadata prefix for discover_metadata if not already set
89
+ if hasattr(self.config, "discover_metadata"):
90
+ self.config.discover_metadata.setdefault("metadata_prefix", provider)
88
91
 
89
92
  def clear(self) -> None:
90
93
  """Method used to clear a search context between two searches."""
@@ -103,8 +106,8 @@ class Search(PluginTopic):
103
106
  """
104
107
  raise NotImplementedError("A Search plugin must implement a method named query")
105
108
 
106
- def discover_product_types(self, **kwargs: Any) -> Optional[dict[str, Any]]:
107
- """Fetch product types list from provider using `discover_product_types` conf"""
109
+ def discover_collections(self, **kwargs: Any) -> Optional[dict[str, Any]]:
110
+ """Fetch collections list from provider using `discover_collections` conf"""
108
111
  return None
109
112
 
110
113
  def discover_queryables(
@@ -112,7 +115,7 @@ class Search(PluginTopic):
112
115
  ) -> Optional[dict[str, Annotated[Any, FieldInfo]]]:
113
116
  """Fetch queryables list from provider using :attr:`~eodag.config.PluginConfig.discover_queryables` conf
114
117
 
115
- :param kwargs: additional filters for queryables (``productType`` and other search
118
+ :param kwargs: additional filters for queryables (``collection`` and other search
116
119
  arguments)
117
120
  :returns: fetched queryable parameters dict
118
121
  """
@@ -121,15 +124,15 @@ class Search(PluginTopic):
121
124
  )
122
125
 
123
126
  def _get_defaults_as_queryables(
124
- self, product_type: str
127
+ self, collection: str
125
128
  ) -> dict[str, Annotated[Any, FieldInfo]]:
126
129
  """
127
- Return given product type default settings as queryables
130
+ Return given collection default settings as queryables
128
131
 
129
- :param product_type: given product type
132
+ :param collection: given collection
130
133
  :returns: queryable parameters dict
131
134
  """
132
- defaults = deepcopy(self.config.products.get(product_type, {}))
135
+ defaults = deepcopy(self.config.products.get(collection, {}))
133
136
  defaults.pop("metadata_mapping", None)
134
137
 
135
138
  queryables: dict[str, Annotated[Any, FieldInfo]] = {}
@@ -137,53 +140,51 @@ class Search(PluginTopic):
137
140
  queryables[parameter] = Annotated[type(value), Field(default=value)]
138
141
  return queryables
139
142
 
140
- def map_product_type(
141
- self, product_type: Optional[str], **kwargs: Any
142
- ) -> Optional[str]:
143
- """Get the provider product type from eodag product type
143
+ def map_collection(self, collection: Optional[str], **kwargs: Any) -> Optional[str]:
144
+ """Get the provider collection from eodag collection
144
145
 
145
- :param product_type: eodag product type
146
- :returns: provider product type
146
+ :param collection: eodag collection
147
+ :returns: provider collection
147
148
  """
148
- if product_type is None:
149
+ if collection is None:
149
150
  return None
150
- logger.debug("Mapping eodag product type to provider product type")
151
- return self.config.products.get(product_type, {}).get(
152
- "productType", GENERIC_PRODUCT_TYPE
151
+ logger.debug("Mapping eodag collection to provider collection")
152
+ return self.config.products.get(collection, {}).get(
153
+ "_collection", GENERIC_COLLECTION
153
154
  )
154
155
 
155
- def get_product_type_def_params(
156
- self, product_type: str, format_variables: Optional[dict[str, Any]] = None
156
+ def get_collection_def_params(
157
+ self, collection: str, format_variables: Optional[dict[str, Any]] = None
157
158
  ) -> dict[str, Any]:
158
- """Get the provider product type definition parameters and specific settings
159
+ """Get the provider collection definition parameters and specific settings
159
160
 
160
- :param product_type: the desired product type
161
- :returns: The product type definition parameters
161
+ :param collection: the desired collection
162
+ :returns: The collection definition parameters
162
163
  """
163
- if product_type in self.config.products.keys():
164
- return self.config.products[product_type]
165
- elif GENERIC_PRODUCT_TYPE in self.config.products.keys():
164
+ if collection in self.config.products.keys():
165
+ return self.config.products[collection]
166
+ elif GENERIC_COLLECTION in self.config.products.keys():
166
167
  logger.debug(
167
- "Getting generic provider product type definition parameters for %s",
168
- product_type,
168
+ "Getting generic provider collection definition parameters for %s",
169
+ collection,
169
170
  )
170
171
  return {
171
172
  k: v
172
173
  for k, v in format_dict_items(
173
- self.config.products[GENERIC_PRODUCT_TYPE],
174
- **(format_variables or {}),
174
+ self.config.products[GENERIC_COLLECTION],
175
+ **({"collection": collection} | (format_variables or {})),
175
176
  ).items()
176
177
  if v
177
178
  }
178
179
  else:
179
180
  return {}
180
181
 
181
- def get_product_type_cfg_value(self, key: str, default: Any = None) -> Any:
182
+ def get_collection_cfg_value(self, key: str, default: Any = None) -> Any:
182
183
  """
183
- Get the value of a configuration option specific to the current product type.
184
+ Get the value of a configuration option specific to the current collection.
184
185
 
185
186
  This method retrieves the value of a configuration option from the
186
- ``product_type_config`` attribute. If the option is not found, the provided
187
+ ``collection_config`` attribute. If the option is not found, the provided
187
188
  default value is returned.
188
189
 
189
190
  :param key: The configuration option key.
@@ -194,21 +195,39 @@ class Search(PluginTopic):
194
195
  :return: The value of the specified configuration option or the default value.
195
196
  :rtype: Any
196
197
  """
197
- product_type_cfg = getattr(self.config, "product_type_config", {})
198
- non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
198
+ collection_cfg = getattr(self.config, "collection_config", {})
199
+ non_none_cfg = {k: v for k, v in collection_cfg.items() if v}
199
200
 
200
201
  return non_none_cfg.get(key, default)
201
202
 
203
+ def get_collection_cfg_dates(
204
+ self, start_default: Optional[str] = None, end_default: Optional[str] = None
205
+ ) -> tuple[Optional[str], Optional[str]]:
206
+ """
207
+ Get start and end dates from the collection configuration.
208
+
209
+ Extracts dates from the extent.temporal.interval structure in the collection
210
+ configuration, falling back to provided defaults if dates are not available.
211
+
212
+ :param start_default: Default value to return for start date if not found in config
213
+ :param end_default: Default value to return for end date if not found in config
214
+ :returns: Tuple of (mission_start_date, mission_end_date) as ISO strings or defaults
215
+ """
216
+ collection_cfg = getattr(self.config, "collection_config", {})
217
+ col_start, col_end = get_collection_dates(collection_cfg)
218
+
219
+ return col_start or start_default, col_end or end_default
220
+
202
221
  def get_metadata_mapping(
203
- self, product_type: Optional[str] = None
222
+ self, collection: Optional[str] = None
204
223
  ) -> dict[str, Union[str, list[str]]]:
205
- """Get the plugin metadata mapping configuration (product type specific if exists)
224
+ """Get the plugin metadata mapping configuration (collection specific if exists)
206
225
 
207
- :param product_type: the desired product type
208
- :returns: The product type specific metadata-mapping
226
+ :param collection: the desired collection
227
+ :returns: The collection specific metadata-mapping
209
228
  """
210
- if product_type:
211
- return self.config.products.get(product_type, {}).get(
229
+ if collection:
230
+ return self.config.products.get(collection, {}).get(
212
231
  "metadata_mapping", self.config.metadata_mapping
213
232
  )
214
233
  return self.config.metadata_mapping
@@ -327,68 +346,68 @@ class Search(PluginTopic):
327
346
  sort_by_qs += parsed_sort_by_tpl
328
347
  return (sort_by_qs, sort_by_qp)
329
348
 
330
- def _get_product_type_queryables(
331
- self, product_type: Optional[str], alias: Optional[str], filters: dict[str, Any]
349
+ def _get_collection_queryables(
350
+ self, collection: Optional[str], alias: Optional[str], filters: dict[str, Any]
332
351
  ) -> QueryablesDict:
333
352
  default_values: dict[str, Any] = deepcopy(
334
- getattr(self.config, "products", {}).get(product_type, {})
353
+ getattr(self.config, "products", {}).get(collection, {})
335
354
  )
336
355
  default_values.pop("metadata_mapping", None)
337
356
  try:
338
- filters["productType"] = product_type
357
+ filters["collection"] = collection
339
358
  queryables = self.discover_queryables(**{**default_values, **filters}) or {}
340
359
  except NotImplementedError as e:
341
360
  if str(e):
342
361
  logger.debug(str(e))
343
- queryables = self.queryables_from_metadata_mapping(product_type, alias)
362
+ queryables = self.queryables_from_metadata_mapping(collection, alias)
344
363
 
345
364
  return QueryablesDict(**queryables)
346
365
 
347
366
  def list_queryables(
348
367
  self,
349
368
  filters: dict[str, Any],
350
- available_product_types: list[Any],
351
- product_type_configs: dict[str, dict[str, Any]],
352
- product_type: Optional[str] = None,
369
+ available_collections: list[Any],
370
+ collection_configs: dict[str, dict[str, Any]],
371
+ collection: Optional[str] = None,
353
372
  alias: Optional[str] = None,
354
373
  ) -> QueryablesDict:
355
374
  """
356
375
  Get queryables
357
376
 
358
377
  :param filters: Additional filters for queryables.
359
- :param available_product_types: list of available product types
360
- :param product_type_configs: dict containing the product type information for all used product types
361
- :param product_type: (optional) The product type.
362
- :param alias: (optional) alias of the product type
378
+ :param available_collections: list of available collections
379
+ :param collection_configs: dict containing the collection information for all used collections
380
+ :param collection: (optional) The collection.
381
+ :param alias: (optional) alias of the collection
363
382
 
364
383
  :return: A dictionary containing the queryable properties, associating parameters to their
365
384
  annotated type.
366
385
  """
367
386
  additional_info = (
368
- "Please select a product type to get the possible values of the parameters!"
369
- if not product_type
387
+ "Please select a collection to get the possible values of the parameters!"
388
+ if not collection
370
389
  else ""
371
390
  )
372
391
  discover_metadata = getattr(self.config, "discover_metadata", {})
373
392
  auto_discovery = discover_metadata.get("auto_discovery", False)
374
393
 
375
- if product_type or getattr(self.config, "discover_queryables", {}).get(
394
+ if collection or getattr(self.config, "discover_queryables", {}).get(
376
395
  "fetch_url", ""
377
396
  ):
378
- if product_type:
379
- self.config.product_type_config = product_type_configs[product_type]
380
- queryables = self._get_product_type_queryables(product_type, alias, filters)
397
+ if collection:
398
+ self.config.collection_config = collection_configs[collection]
399
+ queryables = self._get_collection_queryables(collection, alias, filters)
381
400
  queryables.additional_information = additional_info
382
401
  queryables.additional_properties = auto_discovery
383
402
 
384
403
  return queryables
385
404
  else:
386
405
  all_queryables: dict[str, Any] = {}
387
- for pt in available_product_types:
388
- self.config.product_type_config = product_type_configs[pt]
389
- pt_queryables = self._get_product_type_queryables(pt, None, filters)
406
+ for pt in available_collections:
407
+ self.config.collection_config = collection_configs[pt]
408
+ pt_queryables = self._get_collection_queryables(pt, None, filters)
390
409
  all_queryables.update(pt_queryables)
391
- # reset defaults because they may vary between product types
410
+ # reset defaults because they may vary between collections
392
411
  for k, v in all_queryables.items():
393
412
  v.__metadata__[0].default = getattr(
394
413
  Queryables.model_fields.get(k, Field(None)), "default", None
@@ -399,17 +418,46 @@ class Search(PluginTopic):
399
418
  **all_queryables,
400
419
  )
401
420
 
421
+ def validate(
422
+ self,
423
+ search_params: dict[str, Any],
424
+ auth: Optional[Union[AuthBase, S3ServiceResource]],
425
+ ) -> None:
426
+ """Validate a search request.
427
+
428
+ :param search_params: Arguments of the search request
429
+ :param auth: Authentication object
430
+ :raises: :class:`~eodag.utils.exceptions.ValidationError`
431
+ """
432
+ logger.debug("Validate request")
433
+ # attach authentication if required
434
+ if getattr(self.config, "need_auth", False) and auth:
435
+ self.auth = auth
436
+ try:
437
+ collection = search_params.get("collection")
438
+ if not collection:
439
+ raise ValidationError("Field required: collection")
440
+ self.list_queryables(
441
+ filters=search_params,
442
+ available_collections=[collection],
443
+ collection_configs={collection: self.config.collection_config},
444
+ collection=collection,
445
+ alias=collection,
446
+ ).get_model().model_validate(search_params)
447
+ except PydanticValidationError as e:
448
+ raise ValidationError(format_pydantic_error(e)) from e
449
+
402
450
  def queryables_from_metadata_mapping(
403
- self, product_type: Optional[str] = None, alias: Optional[str] = None
451
+ self, collection: Optional[str] = None, alias: Optional[str] = None
404
452
  ) -> dict[str, Annotated[Any, FieldInfo]]:
405
453
  """
406
- Extract queryable parameters from product type metadata mapping.
407
- :param product_type: product type id (optional)
408
- :param alias: (optional) alias of the product type
454
+ Extract queryable parameters from collection metadata mapping.
455
+ :param collection: collection id (optional)
456
+ :param alias: (optional) alias of the collection
409
457
  :returns: dict of annotated queryables
410
458
  """
411
459
  metadata_mapping: dict[str, Any] = deepcopy(
412
- self.get_metadata_mapping(product_type)
460
+ self.get_metadata_mapping(collection)
413
461
  )
414
462
 
415
463
  queryables: dict[str, Annotated[Any, FieldInfo]] = {}
@@ -423,10 +471,13 @@ class Search(PluginTopic):
423
471
  eodag_queryables = copy_deepcopy(
424
472
  model_fields_to_annotated(Queryables.model_fields)
425
473
  )
426
- # add default value for product type
427
- if alias:
428
- eodag_queryables.pop("productType")
429
- eodag_queryables["productType"] = Annotated[str, Field(default=alias)]
474
+ queryables["collection"] = eodag_queryables.pop("collection")
475
+ # add default value for collection
476
+ if collection_or_alias := alias or collection:
477
+ queryables["collection"] = Annotated[
478
+ str, Field(default=collection_or_alias)
479
+ ]
480
+
430
481
  for k, v in eodag_queryables.items():
431
482
  eodag_queryable_field_info = (
432
483
  get_args(v)[1] if len(get_args(v)) > 1 else None