eodag 3.0.0b2__py3-none-any.whl → 3.0.1__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 (84) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +295 -287
  3. eodag/api/product/__init__.py +10 -4
  4. eodag/api/product/_assets.py +2 -14
  5. eodag/api/product/_product.py +16 -30
  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 +12 -31
  9. eodag/api/search_result.py +33 -12
  10. eodag/cli.py +35 -19
  11. eodag/config.py +455 -155
  12. eodag/plugins/apis/base.py +13 -7
  13. eodag/plugins/apis/ecmwf.py +16 -7
  14. eodag/plugins/apis/usgs.py +68 -16
  15. eodag/plugins/authentication/aws_auth.py +25 -7
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +14 -3
  18. eodag/plugins/authentication/header.py +12 -4
  19. eodag/plugins/authentication/keycloak.py +41 -22
  20. eodag/plugins/authentication/oauth.py +11 -1
  21. eodag/plugins/authentication/openid_connect.py +183 -167
  22. eodag/plugins/authentication/qsauth.py +12 -4
  23. eodag/plugins/authentication/sas_auth.py +19 -2
  24. eodag/plugins/authentication/token.py +59 -11
  25. eodag/plugins/authentication/token_exchange.py +19 -19
  26. eodag/plugins/crunch/base.py +7 -2
  27. eodag/plugins/crunch/filter_date.py +8 -11
  28. eodag/plugins/crunch/filter_latest_intersect.py +5 -7
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +2 -5
  30. eodag/plugins/crunch/filter_overlap.py +9 -15
  31. eodag/plugins/crunch/filter_property.py +9 -14
  32. eodag/plugins/download/aws.py +84 -99
  33. eodag/plugins/download/base.py +36 -77
  34. eodag/plugins/download/creodias_s3.py +11 -2
  35. eodag/plugins/download/http.py +134 -109
  36. eodag/plugins/download/s3rest.py +37 -43
  37. eodag/plugins/manager.py +173 -41
  38. eodag/plugins/search/__init__.py +9 -9
  39. eodag/plugins/search/base.py +35 -35
  40. eodag/plugins/search/build_search_result.py +55 -64
  41. eodag/plugins/search/cop_marine.py +113 -32
  42. eodag/plugins/search/creodias_s3.py +20 -8
  43. eodag/plugins/search/csw.py +41 -1
  44. eodag/plugins/search/data_request_search.py +119 -14
  45. eodag/plugins/search/qssearch.py +619 -197
  46. eodag/plugins/search/static_stac_search.py +25 -23
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +211 -56
  49. eodag/resources/providers.yml +1762 -1809
  50. eodag/resources/stac.yml +3 -163
  51. eodag/resources/user_conf_template.yml +134 -119
  52. eodag/rest/config.py +1 -2
  53. eodag/rest/constants.py +0 -1
  54. eodag/rest/core.py +70 -92
  55. eodag/rest/errors.py +181 -0
  56. eodag/rest/server.py +24 -330
  57. eodag/rest/stac.py +105 -630
  58. eodag/rest/types/eodag_search.py +17 -15
  59. eodag/rest/types/queryables.py +5 -14
  60. eodag/rest/types/stac_search.py +18 -13
  61. eodag/rest/utils/rfc3339.py +0 -1
  62. eodag/types/__init__.py +24 -6
  63. eodag/types/download_args.py +14 -5
  64. eodag/types/queryables.py +1 -2
  65. eodag/types/search_args.py +10 -11
  66. eodag/types/whoosh.py +0 -2
  67. eodag/utils/__init__.py +97 -136
  68. eodag/utils/constraints.py +0 -8
  69. eodag/utils/exceptions.py +23 -9
  70. eodag/utils/import_system.py +0 -4
  71. eodag/utils/logging.py +37 -80
  72. eodag/utils/notebook.py +4 -4
  73. eodag/utils/requests.py +13 -23
  74. eodag/utils/rest.py +0 -4
  75. eodag/utils/stac_reader.py +3 -15
  76. {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/METADATA +41 -24
  77. eodag-3.0.1.dist-info/RECORD +109 -0
  78. {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/WHEEL +1 -1
  79. {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/entry_points.txt +1 -0
  80. eodag/resources/constraints/climate-dt.json +0 -13
  81. eodag/resources/constraints/extremes-dt.json +0 -8
  82. eodag-3.0.0b2.dist-info/RECORD +0 -110
  83. {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/LICENSE +0 -0
  84. {eodag-3.0.0b2.dist-info → eodag-3.0.1.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
@@ -35,11 +35,9 @@ from eodag.types.queryables import Queryables
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
@@ -59,9 +57,7 @@ class Search(PluginTopic):
59
57
  """Base Search Plugin.
60
58
 
61
59
  :param provider: An EODAG provider name
62
- :type provider: str
63
60
  :param config: An EODAG plugin configuration
64
- :type config: :class:`~eodag.config.PluginConfig`
65
61
  """
66
62
 
67
63
  auth: Union[AuthBase, Dict[str, str]]
@@ -97,9 +93,9 @@ class Search(PluginTopic):
97
93
  ) -> Tuple[List[EOProduct], Optional[int]]:
98
94
  """Implementation of how the products must be searched goes here.
99
95
 
100
- This method must return a tuple with (1) a list of EOProduct instances (see eodag.api.product module)
101
- which will be processed by a Download plugin (2) and the total number of products matching
102
- the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
96
+ This method must return a tuple with (1) a list of :class:`~eodag.api.product._product.EOProduct` instances
97
+ which will be processed by a :class:`~eodag.plugins.download.base.Download` plugin (2) and the total number of
98
+ products matching the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
103
99
  """
104
100
  raise NotImplementedError("A Search plugin must implement a method named query")
105
101
 
@@ -110,13 +106,11 @@ class Search(PluginTopic):
110
106
  def discover_queryables(
111
107
  self, **kwargs: Any
112
108
  ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
113
- """Fetch queryables list from provider using `discover_queryables` conf
109
+ """Fetch queryables list from provider using :attr:`~eodag.config.PluginConfig.discover_queryables` conf
114
110
 
115
- :param kwargs: additional filters for queryables (`productType` and other search
111
+ :param kwargs: additional filters for queryables (``productType`` and other search
116
112
  arguments)
117
- :type kwargs: Any
118
113
  :returns: fetched queryable parameters dict
119
- :rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
120
114
  """
121
115
  raise NotImplementedError(
122
116
  f"discover_queryables is not implemeted for plugin {self.__class__.__name__}"
@@ -129,9 +123,7 @@ class Search(PluginTopic):
129
123
  Return given product type default settings as queryables
130
124
 
131
125
  :param product_type: given product type
132
- :type product_type: str
133
126
  :returns: queryable parameters dict
134
- :rtype: Dict[str, Annotated[Any, FieldInfo]]
135
127
  """
136
128
  defaults = deepcopy(self.config.products.get(product_type, {}))
137
129
  defaults.pop("metadata_mapping", None)
@@ -147,9 +139,7 @@ class Search(PluginTopic):
147
139
  """Get the provider product type from eodag product type
148
140
 
149
141
  :param product_type: eodag product type
150
- :type product_type: str
151
142
  :returns: provider product type
152
- :rtype: str
153
143
  """
154
144
  if product_type is None:
155
145
  return None
@@ -164,9 +154,7 @@ class Search(PluginTopic):
164
154
  """Get the provider product type definition parameters and specific settings
165
155
 
166
156
  :param product_type: the desired product type
167
- :type product_type: str
168
157
  :returns: The product type definition parameters
169
- :rtype: dict
170
158
  """
171
159
  if product_type in self.config.products.keys():
172
160
  logger.debug(
@@ -189,15 +177,34 @@ class Search(PluginTopic):
189
177
  else:
190
178
  return {}
191
179
 
180
+ def get_product_type_cfg_value(self, key: str, default: Any = None) -> Any:
181
+ """
182
+ Get the value of a configuration option specific to the current product type.
183
+
184
+ This method retrieves the value of a configuration option from the
185
+ ``product_type_config`` attribute. If the option is not found, the provided
186
+ default value is returned.
187
+
188
+ :param key: The configuration option key.
189
+ :type key: str
190
+ :param default: The default value to be returned if the option is not found (default is None).
191
+ :type default: Any
192
+
193
+ :return: The value of the specified configuration option or the default value.
194
+ :rtype: Any
195
+ """
196
+ product_type_cfg = getattr(self.config, "product_type_config", {})
197
+ non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
198
+
199
+ return non_none_cfg.get(key, default)
200
+
192
201
  def get_metadata_mapping(
193
202
  self, product_type: Optional[str] = None
194
203
  ) -> Dict[str, Union[str, List[str]]]:
195
204
  """Get the plugin metadata mapping configuration (product type specific if exists)
196
205
 
197
206
  :param product_type: the desired product type
198
- :type product_type: str
199
207
  :returns: The product type specific metadata-mapping
200
- :rtype: dict
201
208
  """
202
209
  if product_type:
203
210
  return self.config.products.get(product_type, {}).get(
@@ -206,16 +213,14 @@ class Search(PluginTopic):
206
213
  return self.config.metadata_mapping
207
214
 
208
215
  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
216
+ """Extract the ``sort_by`` argument from the kwargs or the provider default sort configuration
210
217
 
211
218
  :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`
219
+ :returns: The ``sort_by`` argument from the kwargs or the provider default sort configuration
215
220
  """
216
- # remove "sortBy" from search args if exists because it is not part of metadata mapping,
221
+ # remove "sort_by" from search args if exists because it is not part of metadata mapping,
217
222
  # it will complete the query string or body once metadata mapping will be done
218
- sort_by_arg_tmp = kwargs.pop("sortBy", None)
223
+ sort_by_arg_tmp = kwargs.pop("sort_by", None)
219
224
  sort_by_arg = sort_by_arg_tmp or getattr(self.config, "sort", {}).get(
220
225
  "sort_by_default", None
221
226
  )
@@ -230,18 +235,16 @@ class Search(PluginTopic):
230
235
  self, sort_by_arg: SortByList
231
236
  ) -> Tuple[str, Dict[str, List[Dict[str, str]]]]:
232
237
  """Build the sorting part of the query string or body by transforming
233
- the "sortBy" argument into a provider-specific string or dictionnary
238
+ the ``sort_by`` argument into a provider-specific string or dictionary
234
239
 
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]]]]
240
+ :param sort_by_arg: the ``sort_by`` argument in EODAG format
241
+ :returns: The ``sort_by`` argument in provider-specific format
239
242
  """
240
243
  if not hasattr(self.config, "sort"):
241
244
  raise ValidationError(f"{self.provider} does not support sorting feature")
242
245
  # TODO: remove this code block when search args model validation is embeded
243
246
  # remove duplicates
244
- sort_by_arg = list(set(sort_by_arg))
247
+ sort_by_arg = list(dict.fromkeys(sort_by_arg))
245
248
 
246
249
  sort_by_qs: str = ""
247
250
  sort_by_qp: Dict[str, Any] = {}
@@ -331,13 +334,10 @@ class Search(PluginTopic):
331
334
  Get queryables
332
335
 
333
336
  :param filters: Additional filters for queryables.
334
- :type filters: Dict[str, Any]
335
337
  :param product_type: (optional) The product type.
336
- :type product_type: Optional[str]
337
338
 
338
339
  :return: A dictionary containing the queryable properties, associating parameters to their
339
340
  annotated type.
340
- :rtype: Dict[str, Annotated[Any, FieldInfo]]
341
341
  """
342
342
  default_values: Dict[str, Any] = deepcopy(
343
343
  getattr(self.config, "products", {}).get(product_type, {})
@@ -20,16 +20,27 @@ from __future__ import annotations
20
20
  import hashlib
21
21
  import logging
22
22
  from datetime import datetime, timedelta, timezone
23
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, cast
23
+ from typing import (
24
+ TYPE_CHECKING,
25
+ Annotated,
26
+ Any,
27
+ Dict,
28
+ List,
29
+ Optional,
30
+ Set,
31
+ Tuple,
32
+ cast,
33
+ get_args,
34
+ )
24
35
  from urllib.parse import quote_plus, unquote_plus
25
36
 
26
37
  import geojson
27
38
  import orjson
28
39
  from dateutil.parser import isoparse
40
+ from dateutil.tz import tzutc
29
41
  from jsonpath_ng import Child, Fields, Root
30
42
  from pydantic import create_model
31
43
  from pydantic.fields import FieldInfo
32
- from typing_extensions import get_args
33
44
 
34
45
  from eodag.api.product import EOProduct
35
46
  from eodag.api.product.metadata_mapping import (
@@ -47,7 +58,6 @@ from eodag.types import json_field_definition_to_python, model_fields_to_annotat
47
58
  from eodag.types.queryables import CommonQueryables
48
59
  from eodag.utils import (
49
60
  DEFAULT_MISSION_START_DATE,
50
- Annotated,
51
61
  deepcopy,
52
62
  dict_items_recursive_sort,
53
63
  get_geometry_from_various,
@@ -71,23 +81,16 @@ class BuildPostSearchResult(PostJsonSearch):
71
81
  performs a POST request and uses its result to build a single :class:`~eodag.api.search_result.SearchResult`
72
82
  object.
73
83
 
74
- The available configuration parameters inherits from parent classes, with particularly
75
- for this plugin:
76
-
77
- - **api_endpoint**: (mandatory) The endpoint of the provider's search interface
84
+ The available configuration parameters are inherited from parent classes
85
+ (:class:`~eodag.plugins.search.qssearch.PostJsonSearch` and
86
+ :class:`~eodag.plugins.search.qssearch.QueryStringSearch`), with particularly for this plugin:
78
87
 
79
- - **pagination**: The configuration of how the pagination is done
80
- on the provider. It is a tree with the following nodes:
88
+ :param provider: provider name
89
+ :param config: Search plugin configuration:
81
90
 
82
- - *next_page_query_obj*: (optional) The additional parameters needed to perform
83
- search. These paramaters won't be included in result. This must be a json dict
84
- formatted like `{{"foo":"bar"}}` because it will be passed to a `.format()`
85
- method before being loaded as json.
91
+ * :attr:`~eodag.config.PluginConfig.remove_from_query` (``List[str]``): List of parameters
92
+ used to parse metadata but that must not be included to the query
86
93
 
87
- :param provider: An eodag providers configuration dictionary
88
- :type provider: dict
89
- :param config: Path to the user configuration file
90
- :type config: str
91
94
  """
92
95
 
93
96
  def count_hits(
@@ -112,8 +115,8 @@ class BuildPostSearchResult(PostJsonSearch):
112
115
  prep.url = prep.search_urls[0]
113
116
  prep.info_message = f"Sending search request: {prep.url}"
114
117
  prep.exception_message = (
115
- f"Skipping error while searching for {self.provider} "
116
- f"{self.__class__.__name__} instance:"
118
+ f"Skipping error while searching for {self.provider}"
119
+ f" {self.__class__.__name__} instance"
117
120
  )
118
121
  response = self._request(prep)
119
122
 
@@ -125,11 +128,8 @@ class BuildPostSearchResult(PostJsonSearch):
125
128
  """Build :class:`~eodag.api.product._product.EOProduct` from provider result
126
129
 
127
130
  :param results: Raw provider result as single dict in list
128
- :type results: list
129
131
  :param kwargs: Search arguments
130
- :type kwargs: Union[int, str, bool, dict, list]
131
132
  :returns: list of single :class:`~eodag.api.product._product.EOProduct`
132
- :rtype: list
133
133
  """
134
134
  product_type = kwargs.get("productType")
135
135
 
@@ -183,7 +183,7 @@ class BuildPostSearchResult(PostJsonSearch):
183
183
  result.update(results.product_type_def_params)
184
184
  result = dict(result, **{k: v for k, v in kwargs.items() if v is not None})
185
185
 
186
- # parse porperties
186
+ # parse properties
187
187
  parsed_properties = properties_from_json(
188
188
  result,
189
189
  self.config.metadata_mapping,
@@ -244,21 +244,18 @@ class BuildSearchResult(BuildPostSearchResult):
244
244
  This plugin builds a single :class:`~eodag.api.search_result.SearchResult` object
245
245
  using given query parameters as product properties.
246
246
 
247
- The available configuration parameters inherits from parent classes, with particularly
248
- for this plugin:
247
+ The available configuration parameters inherits from parent classes
248
+ (:class:`~eodag.plugins.search.build_search_result.BuildPostSearchResult`,
249
+ :class:`~eodag.plugins.search.qssearch.PostJsonSearch` and
250
+ :class:`~eodag.plugins.search.qssearch.QueryStringSearch`), with particularly for this plugin:
249
251
 
250
- - **end_date_excluded**: Set to `False` if provider does not include end date to
251
- search
252
+ :param provider: provider name
253
+ :param config: Search plugin configuration:
252
254
 
253
- - **remove_from_query**: List of parameters used to parse metadata but that must
254
- not be included to the query
255
+ * :attr:`~eodag.config.PluginConfig.end_date_excluded` (``bool``): Set to ``False`` if provider
256
+ does not include end date in the search request; In this case, if the end date is at midnight,
257
+ the previous day will be used. default: ``True``
255
258
 
256
- - **constraints_file_url**: url of the constraint file used to build queryables
257
-
258
- :param provider: An eodag providers configuration dictionary
259
- :type provider: dict
260
- :param config: Path to the user configuration file
261
- :type config: str
262
259
  """
263
260
 
264
261
  def __init__(self, provider: str, config: PluginConfig) -> None:
@@ -346,27 +343,6 @@ class BuildSearchResult(BuildPostSearchResult):
346
343
  self, product_type=product_type, **available_properties
347
344
  )
348
345
 
349
- def get_product_type_cfg(self, key: str, default: Any = None) -> Any:
350
- """
351
- Get the value of a configuration option specific to the current product type.
352
-
353
- This method retrieves the value of a configuration option from the
354
- `_product_type_config` attribute. If the option is not found, the provided
355
- default value is returned.
356
-
357
- :param key: The configuration option key.
358
- :type key: str
359
- :param default: The default value to be returned if the option is not found (default is None).
360
- :type default: Any
361
-
362
- :return: The value of the specified configuration option or the default value.
363
- :rtype: Any
364
- """
365
- product_type_cfg = getattr(self.config, "product_type_config", {})
366
- non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
367
-
368
- return non_none_cfg.get(key, default)
369
-
370
346
  def _preprocess_search_params(self, params: Dict[str, Any]) -> None:
371
347
  """Preprocess search parameters before making a request to the CDS API.
372
348
 
@@ -376,7 +352,6 @@ class BuildSearchResult(BuildPostSearchResult):
376
352
  in the input parameters, default values or values from the configuration are used.
377
353
 
378
354
  :param params: Search parameters to be preprocessed.
379
- :type params: dict
380
355
  """
381
356
  _dc_qs = params.get("_dc_qs", None)
382
357
  if _dc_qs is not None:
@@ -408,7 +383,7 @@ class BuildSearchResult(BuildPostSearchResult):
408
383
 
409
384
  # dates
410
385
  mission_start_dt = datetime.fromisoformat(
411
- self.get_product_type_cfg(
386
+ self.get_product_type_cfg_value(
412
387
  "missionStartDate", DEFAULT_MISSION_START_DATE
413
388
  ).replace(
414
389
  "Z", "+00:00"
@@ -434,11 +409,29 @@ class BuildSearchResult(BuildPostSearchResult):
434
409
  "completionTimeFromAscendingNode", default_end_str
435
410
  )
436
411
 
437
- # temporary _date parameter mixing start & end
412
+ # adapt end date if it is midnight
438
413
  end_date_excluded = getattr(self.config, "end_date_excluded", True)
439
- end_date = isoparse(params["completionTimeFromAscendingNode"])
440
- if not end_date_excluded and end_date == end_date.replace(
441
- hour=0, minute=0, second=0, microsecond=0
414
+ is_datetime = True
415
+ try:
416
+ end_date = datetime.strptime(
417
+ params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%SZ"
418
+ )
419
+ end_date = end_date.replace(tzinfo=tzutc())
420
+ except ValueError:
421
+ try:
422
+ end_date = datetime.strptime(
423
+ params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%S.%fZ"
424
+ )
425
+ end_date = end_date.replace(tzinfo=tzutc())
426
+ except ValueError:
427
+ end_date = isoparse(params["completionTimeFromAscendingNode"])
428
+ is_datetime = False
429
+ start_date = isoparse(params["startTimeFromAscendingNode"])
430
+ if (
431
+ not end_date_excluded
432
+ and is_datetime
433
+ and end_date > start_date
434
+ and end_date == end_date.replace(hour=0, minute=0, second=0, microsecond=0)
442
435
  ):
443
436
  end_date += timedelta(days=-1)
444
437
  params["completionTimeFromAscendingNode"] = end_date.isoformat()
@@ -454,9 +447,7 @@ class BuildSearchResult(BuildPostSearchResult):
454
447
 
455
448
  :param kwargs: additional filters for queryables (`productType` and other search
456
449
  arguments)
457
- :type kwargs: Any
458
450
  :returns: fetched queryable parameters dict
459
- :rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
460
451
  """
461
452
  constraints_file_url = getattr(self.config, "constraints_file_url", "")
462
453
  if not constraints_file_url:
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import copy
21
21
  import logging
22
+ import os
22
23
  import re
23
24
  from datetime import datetime
24
25
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
@@ -37,7 +38,7 @@ from eodag.config import PluginConfig
37
38
  from eodag.plugins.search import PreparedSearch
38
39
  from eodag.plugins.search.static_stac_search import StaticStacSearch
39
40
  from eodag.utils import get_bucket_name_and_prefix
40
- from eodag.utils.exceptions import UnsupportedProductType, ValidationError
41
+ from eodag.utils.exceptions import RequestError, UnsupportedProductType, ValidationError
41
42
 
42
43
  if TYPE_CHECKING:
43
44
  from mypy_boto3_s3 import S3Client
@@ -67,6 +68,21 @@ def _get_date_from_yyyymmdd(date_str: str, item_key: str) -> Optional[datetime]:
67
68
  return date
68
69
 
69
70
 
71
+ def _get_dates_from_dataset_data(
72
+ dataset_item: Dict[str, Any]
73
+ ) -> Optional[Dict[str, str]]:
74
+ dates = {}
75
+ if "start_datetime" in dataset_item["properties"]:
76
+ dates["start"] = dataset_item["properties"]["start_datetime"]
77
+ dates["end"] = dataset_item["properties"]["end_datetime"]
78
+ elif "datetime" in dataset_item["properties"]:
79
+ dates["start"] = dataset_item["properties"]["datetime"]
80
+ dates["end"] = dataset_item["properties"]["datetime"]
81
+ else:
82
+ return None
83
+ return dates
84
+
85
+
70
86
  def _get_s3_client(endpoint_url: str) -> S3Client:
71
87
  s3_session = boto3.Session()
72
88
  return s3_session.client(
@@ -94,7 +110,21 @@ def _check_int_values_properties(properties: Dict[str, Any]):
94
110
 
95
111
 
96
112
  class CopMarineSearch(StaticStacSearch):
97
- """class that implements search for the Copernicus Marine provider"""
113
+ """class that implements search for the Copernicus Marine provider
114
+
115
+ It calls :meth:`~eodag.plugins.search.static_stac_search.StaticStacSearch.discover_product_types`
116
+ inherited from :class:`~eodag.plugins.search.static_stac_search.StaticStacSearch`
117
+ but for the actual search a special method which fetches the urls of the available products from an S3 storage and
118
+ filters them has been written.
119
+
120
+ The configuration parameters are inherited from the parent and grand-parent classes. The
121
+ :attr:`~eodag.config.PluginConfig.DiscoverMetadata.auto_discovery` parameter in the
122
+ :attr:`~eodag.config.PluginConfig.discover_metadata` section has to be set to ``false`` and the
123
+ :attr:`~eodag.config.PluginConfig.DiscoverQueryables.fetch_url` in the
124
+ :attr:`~eodag.config.PluginConfig.discover_queryables` queryables section has to be set to ``null`` to
125
+ overwrite the default config from the stac provider configuration because those functionalities
126
+ are not available.
127
+ """
98
128
 
99
129
  def __init__(self, provider: str, config: PluginConfig):
100
130
  original_metadata_mapping = copy.deepcopy(config.metadata_mapping)
@@ -107,12 +137,10 @@ class CopMarineSearch(StaticStacSearch):
107
137
  ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
108
138
  """Fetch product type and associated datasets info"""
109
139
 
110
- fetch_url = cast(
111
- str,
112
- self.config.discover_product_types["fetch_url"].format(
113
- **self.config.__dict__
114
- ),
140
+ fetch_url = cast(str, self.config.discover_product_types["fetch_url"]).format(
141
+ **self.config.__dict__
115
142
  )
143
+
116
144
  logger.debug("fetch data for collection %s", product_type)
117
145
  provider_product_type = self.config.products.get(product_type, {}).get(
118
146
  "productType", None
@@ -125,9 +153,14 @@ class CopMarineSearch(StaticStacSearch):
125
153
  )
126
154
  try:
127
155
  collection_data = requests.get(collection_url).json()
128
- except requests.RequestException:
156
+ except requests.RequestException as exc:
157
+ if exc.errno == 404:
158
+ logger.error("product %s not found", product_type)
159
+ raise UnsupportedProductType(product_type)
129
160
  logger.error("data for product %s could not be fetched", product_type)
130
- raise UnsupportedProductType(product_type)
161
+ raise RequestError.from_error(
162
+ exc, f"data for product {product_type} could not be fetched"
163
+ ) from exc
131
164
 
132
165
  datasets = []
133
166
  for link in [li for li in collection_data["links"] if li["rel"] == "item"]:
@@ -170,7 +203,7 @@ class CopMarineSearch(StaticStacSearch):
170
203
  use_dataset_dates: bool = False,
171
204
  ) -> Optional[EOProduct]:
172
205
 
173
- item_id = item_key.split("/")[-1].split(".")[0]
206
+ item_id = os.path.splitext(item_key.split("/")[-1])[0]
174
207
  download_url = s3_url + "/" + item_key
175
208
  properties = {
176
209
  "id": item_id,
@@ -180,20 +213,16 @@ class CopMarineSearch(StaticStacSearch):
180
213
  "dataset": dataset_item["id"],
181
214
  }
182
215
  if use_dataset_dates:
183
- if "start_datetime" in dataset_item:
184
- properties["startTimeFromAscendingNode"] = dataset_item[
185
- "start_datetime"
186
- ]
187
- properties["completionTimeFromAscendingNode"] = dataset_item[
188
- "end_datetime"
189
- ]
190
- elif "datetime" in dataset_item:
191
- properties["startTimeFromAscendingNode"] = dataset_item["datetime"]
192
- properties["completionTimeFromAscendingNode"] = dataset_item["datetime"]
216
+ dates = _get_dates_from_dataset_data(dataset_item)
217
+ if not dates:
218
+ return None
219
+ properties["startTimeFromAscendingNode"] = dates["start"]
220
+ properties["completionTimeFromAscendingNode"] = dates["end"]
193
221
  else:
194
- item_dates = re.findall(r"\d{8}", item_key)
222
+ item_dates = re.findall(r"(\d{4})(0[1-9]|1[0-2])([0-3]\d)", item_id)
195
223
  if not item_dates:
196
- item_dates = re.findall(r"\d{6}", item_key)
224
+ item_dates = re.findall(r"_(\d{4})(0[1-9]|1[0-2])", item_id)
225
+ item_dates = ["".join(row) for row in item_dates]
197
226
  item_start = _get_date_from_yyyymmdd(item_dates[0], item_key)
198
227
  if not item_start: # identified pattern was not a valid datetime
199
228
  return None
@@ -209,11 +238,26 @@ class CopMarineSearch(StaticStacSearch):
209
238
  ).strftime("%Y-%m-%dT%H:%M:%SZ")
210
239
 
211
240
  for key, value in collection_dict["properties"].items():
212
- if key not in ["id", "title", "start_datetime", "end_datetime"]:
241
+ if key not in ["id", "title", "start_datetime", "end_datetime", "datetime"]:
213
242
  properties[key] = value
214
243
  for key, value in dataset_item["properties"].items():
215
- if key not in ["id", "title", "start_datetime", "end_datetime"]:
244
+ if key not in ["id", "title", "start_datetime", "end_datetime", "datetime"]:
216
245
  properties[key] = value
246
+
247
+ code_mapping = self.config.products.get(product_type, {}).get(
248
+ "code_mapping", None
249
+ )
250
+ if code_mapping:
251
+ id_parts = item_id.split("_")
252
+ if len(id_parts) > code_mapping["index"]:
253
+ code = id_parts[code_mapping["index"]]
254
+ if "pattern" not in code_mapping:
255
+ properties[code_mapping["param"]] = code
256
+ elif re.findall(code_mapping["pattern"], code):
257
+ properties[code_mapping["param"]] = re.findall(
258
+ code_mapping["pattern"], code
259
+ )[0]
260
+
217
261
  _check_int_values_properties(properties)
218
262
 
219
263
  properties["thumbnail"] = collection_dict["assets"]["thumbnail"]["href"]
@@ -238,16 +282,14 @@ class CopMarineSearch(StaticStacSearch):
238
282
  """
239
283
  Implementation of search for the Copernicus Marine provider
240
284
  :param prep: object containing search parameterds
241
- :type prep: PreparedSearch
242
285
  :param kwargs: additional search arguments
243
286
  :returns: list of products and total number of products
244
- :rtype: Tuple[List[EOProduct], Optional[int]]
245
287
  """
246
288
  page = prep.page
247
289
  items_per_page = prep.items_per_page
248
290
 
249
291
  # only return 1 page if pagination is disabled
250
- if page > 1 and items_per_page <= 0:
292
+ if page is None or items_per_page is None or page > 1 and items_per_page <= 0:
251
293
  return ([], 0) if prep.count else ([], None)
252
294
 
253
295
  product_type = kwargs.get("productType", prep.product_type)
@@ -350,16 +392,54 @@ class CopMarineSearch(StaticStacSearch):
350
392
 
351
393
  for obj in s3_objects["Contents"]:
352
394
  item_key = obj["Key"]
395
+ item_id = os.path.splitext(item_key.split("/")[-1])[0]
353
396
  # filter according to date(s) in item id
354
- item_dates = re.findall(r"\d{8}", item_key)
397
+ item_dates = re.findall(r"(\d{4})(0[1-9]|1[0-2])([0-3]\d)", item_id)
355
398
  if not item_dates:
356
- item_dates = re.findall(r"\d{6}", item_key)
357
- item_start = _get_date_from_yyyymmdd(item_dates[0], item_key)
358
- if not item_start: # identified pattern was not a valid datetime
399
+ item_dates = re.findall(r"_(\d{4})(0[1-9]|1[0-2])", item_id)
400
+ item_dates = [
401
+ "".join(row) for row in item_dates
402
+ ] # join tuples returned by findall
403
+ item_start = None
404
+ item_end = None
405
+ use_dataset_dates = False
406
+ if item_dates:
407
+ item_start = _get_date_from_yyyymmdd(item_dates[0], item_key)
408
+ if len(item_dates) > 2: # start, end and created_at timestamps
409
+ item_end = _get_date_from_yyyymmdd(item_dates[1], item_key)
410
+ if not item_start:
411
+ # no valid datetime given in id
412
+ use_dataset_dates = True
413
+ dates = _get_dates_from_dataset_data(dataset_item)
414
+ if dates:
415
+ item_start_str = dates["start"].replace("Z", "+0000")
416
+ item_end_str = dates["end"].replace("Z", "+0000")
417
+ try:
418
+ item_start = datetime.strptime(
419
+ item_start_str, "%Y-%m-%dT%H:%M:%S.%f%z"
420
+ )
421
+ item_end = datetime.strptime(
422
+ item_end_str, "%Y-%m-%dT%H:%M:%S.%f%z"
423
+ )
424
+ except ValueError:
425
+ item_start = datetime.strptime(
426
+ item_start_str, "%Y-%m-%dT%H:%M:%S%z"
427
+ )
428
+ item_end = datetime.strptime(
429
+ item_end_str, "%Y-%m-%dT%H:%M:%S%z"
430
+ )
431
+ if not item_start:
432
+ # no valid datetime in id and dataset data
359
433
  continue
360
434
  if item_start > end_date:
361
435
  stop_search = True
362
- if not item_dates or (start_date <= item_start <= end_date):
436
+ if (
437
+ (start_date <= item_start <= end_date)
438
+ or (item_end and start_date <= item_end <= end_date)
439
+ or (
440
+ item_end and item_start < start_date and item_end > end_date
441
+ )
442
+ ):
363
443
  num_total += 1
364
444
  if num_total < start_index:
365
445
  continue
@@ -370,6 +450,7 @@ class CopMarineSearch(StaticStacSearch):
370
450
  endpoint_url + "/" + bucket,
371
451
  dataset_item,
372
452
  collection_dict,
453
+ use_dataset_dates,
373
454
  )
374
455
  if product:
375
456
  products.append(product)