eodag 2.12.1__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 -165
  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.1.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.1.dist-info → eodag-3.0.0b1.dist-info}/WHEEL +1 -1
  72. {eodag-2.12.1.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.1.dist-info/RECORD +0 -94
  76. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/LICENSE +0 -0
  77. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/top_level.txt +0 -0
@@ -17,16 +17,16 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- import hashlib
21
20
  import logging
22
21
  import os
23
22
  import os.path
24
- from typing import TYPE_CHECKING, Any, Dict, Optional, Union
23
+ from typing import TYPE_CHECKING, Dict, Optional, Union
25
24
  from xml.dom import minidom
26
25
  from xml.parsers.expat import ExpatError
27
26
 
28
27
  import requests
29
28
  from requests import RequestException
29
+ from requests.auth import AuthBase
30
30
 
31
31
  from eodag.api.product.metadata_mapping import OFFLINE_STATUS, ONLINE_STATUS
32
32
  from eodag.plugins.download.base import Download
@@ -46,6 +46,7 @@ from eodag.utils import (
46
46
  from eodag.utils.exceptions import (
47
47
  AuthenticationError,
48
48
  DownloadError,
49
+ MisconfiguredError,
49
50
  NotAvailableError,
50
51
  RequestError,
51
52
  )
@@ -53,6 +54,8 @@ from eodag.utils.exceptions import (
53
54
  if TYPE_CHECKING:
54
55
  from eodag.api.product import EOProduct
55
56
  from eodag.config import PluginConfig
57
+ from eodag.types.download_args import DownloadConf
58
+ from eodag.utils import Unpack
56
59
 
57
60
  logger = logging.getLogger("eodag.download.s3rest")
58
61
 
@@ -76,10 +79,7 @@ class S3RestDownload(Download):
76
79
  * ``config.order_method`` (str) - (optional) HTTP request method, GET (default) or POST
77
80
  * ``config.order_headers`` (dict) - (optional) order request headers
78
81
  * ``config.order_on_response`` (dict) - (optional) edit or add new product properties
79
- * ``config.order_status_method`` (str) - (optional) status HTTP request method, GET (default) or POST
80
- * ``config.order_status_percent`` (str) - (optional) progress percentage key in obtained status response
81
- * ``config.order_status_success`` (dict) - (optional) key/value identifying an error success
82
- * ``config.order_status_on_success`` (dict) - (optional) edit or add new product properties
82
+ * ``config.order_status`` (:class:`~eodag.config.PluginConfig.OrderStatus`) - Order status handling
83
83
 
84
84
  :type config: :class:`~eodag.config.PluginConfig`
85
85
  """
@@ -91,18 +91,18 @@ class S3RestDownload(Download):
91
91
  def download(
92
92
  self,
93
93
  product: EOProduct,
94
- auth: Optional[PluginConfig] = None,
94
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
95
95
  progress_callback: Optional[ProgressCallback] = None,
96
96
  wait: int = DEFAULT_DOWNLOAD_WAIT,
97
97
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
98
- **kwargs: Union[str, bool, Dict[str, Any]],
98
+ **kwargs: Unpack[DownloadConf],
99
99
  ) -> Optional[str]:
100
100
  """Download method for S3 REST API.
101
101
 
102
102
  :param product: The EO product to download
103
103
  :type product: :class:`~eodag.api.product._product.EOProduct`
104
- :param auth: (optional) The configuration of a plugin of type Authentication
105
- :type auth: :class:`~eodag.config.PluginConfig`
104
+ :param auth: (optional) authenticated object
105
+ :type auth: Optional[Union[AuthBase, Dict[str, str]]]
106
106
  :param progress_callback: (optional) A method or a callable object
107
107
  which takes a current size and a maximum
108
108
  size as inputs and handle progress bar
@@ -117,6 +117,9 @@ class S3RestDownload(Download):
117
117
  :returns: The absolute path to the downloaded product in the local filesystem
118
118
  :rtype: str
119
119
  """
120
+ if auth is not None and not isinstance(auth, AuthBase):
121
+ raise MisconfiguredError(f"Incompatible auth plugin: {type(auth)}")
122
+
120
123
  if progress_callback is None:
121
124
  logger.info(
122
125
  "Progress bar unavailable, please call product.download() instead of plugin.download()"
@@ -135,10 +138,10 @@ class S3RestDownload(Download):
135
138
  @self._download_retry(product, wait, timeout)
136
139
  def download_request(
137
140
  product: EOProduct,
138
- auth: PluginConfig,
141
+ auth: AuthBase,
139
142
  progress_callback: ProgressCallback,
140
143
  ordered_message: str,
141
- **kwargs: Any,
144
+ **kwargs: Unpack[DownloadConf],
142
145
  ):
143
146
  # check order status
144
147
  if product.properties.get("orderStatusLink", None):
@@ -172,8 +175,16 @@ class S3RestDownload(Download):
172
175
 
173
176
  # get nodes/files list contained in the bucket
174
177
  logger.debug("Retrieving product content from %s", nodes_list_url)
178
+
179
+ ssl_verify = getattr(self.config, "ssl_verify", True)
180
+ timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
181
+
175
182
  bucket_contents = requests.get(
176
- nodes_list_url, auth=auth, headers=USER_AGENT, timeout=HTTP_REQ_TIMEOUT
183
+ nodes_list_url,
184
+ auth=auth,
185
+ headers=USER_AGENT,
186
+ timeout=timeout,
187
+ verify=ssl_verify,
177
188
  )
178
189
  try:
179
190
  bucket_contents.raise_for_status()
@@ -252,8 +263,9 @@ class S3RestDownload(Download):
252
263
  "Unable to create records directory. Got:\n%s", tb.format_exc()
253
264
  )
254
265
  # check if product has already been downloaded
255
- url_hash = hashlib.md5(product.remote_location.encode("utf-8")).hexdigest()
256
- record_filename = os.path.join(download_records_dir, url_hash)
266
+ record_filename = os.path.join(
267
+ download_records_dir, self.generate_record_hash(product)
268
+ )
257
269
  if os.path.isfile(record_filename) and os.path.exists(product_local_path):
258
270
  product.location = path_to_uri(product_local_path)
259
271
  return product_local_path
@@ -303,6 +315,7 @@ class S3RestDownload(Download):
303
315
  auth=auth,
304
316
  headers=USER_AGENT,
305
317
  timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
318
+ verify=ssl_verify,
306
319
  ) as stream:
307
320
  try:
308
321
  stream.raise_for_status()
eodag/plugins/manager.py CHANGED
@@ -43,7 +43,7 @@ from eodag.plugins.crunch.base import Crunch
43
43
  from eodag.plugins.download.base import Download
44
44
  from eodag.plugins.search.base import Search
45
45
  from eodag.utils import GENERIC_PRODUCT_TYPE
46
- from eodag.utils.exceptions import UnsupportedProvider
46
+ from eodag.utils.exceptions import MisconfiguredError, UnsupportedProvider
47
47
 
48
48
  if TYPE_CHECKING:
49
49
  from eodag.api.product import EOProduct
@@ -73,7 +73,12 @@ class PluginManager:
73
73
 
74
74
  supported_topics = {"search", "download", "crunch", "auth", "api"}
75
75
 
76
+ product_type_to_provider_config_map: Dict[str, List[ProviderConfig]]
77
+
78
+ skipped_plugins: List[str]
79
+
76
80
  def __init__(self, providers_config: Dict[str, ProviderConfig]) -> None:
81
+ self.skipped_plugins = []
77
82
  self.providers_config = providers_config
78
83
  # Load all the plugins. This will make all plugin classes of a particular
79
84
  # type to be available in the base plugin class's 'plugins' attribute.
@@ -90,6 +95,13 @@ class PluginManager:
90
95
  ):
91
96
  try:
92
97
  entry_point.load()
98
+ except pkg_resources.DistributionNotFound:
99
+ logger.debug(
100
+ "%s plugin skipped, eodag[%s] or eodag[all] needed",
101
+ entry_point.name,
102
+ ",".join(entry_point.extras),
103
+ )
104
+ self.skipped_plugins.append(entry_point.name)
93
105
  except ImportError:
94
106
  import traceback as tb
95
107
 
@@ -116,7 +128,9 @@ class PluginManager:
116
128
  self.providers_config = plugin_providers_config
117
129
  self.rebuild()
118
130
 
119
- def rebuild(self, providers_config=None):
131
+ def rebuild(
132
+ self, providers_config: Optional[Dict[str, ProviderConfig]] = None
133
+ ) -> None:
120
134
  """(Re)Build plugin manager mapping and cache"""
121
135
  if providers_config is not None:
122
136
  self.providers_config = providers_config
@@ -126,13 +140,13 @@ class PluginManager:
126
140
 
127
141
  def build_product_type_to_provider_config_map(self) -> None:
128
142
  """Build mapping conf between product types and providers"""
129
- self.product_type_to_provider_config_map: Dict[str, List[ProviderConfig]] = {}
143
+ self.product_type_to_provider_config_map = {}
130
144
  for provider in list(self.providers_config):
131
145
  provider_config = self.providers_config[provider]
132
146
  if not hasattr(provider_config, "products") or not provider_config.products:
133
147
  logger.info(
134
- "%s: provider has no product configured and will be skipped"
135
- % provider
148
+ "%s: provider has no product configured and will be skipped",
149
+ provider,
136
150
  )
137
151
  self.providers_config.pop(provider)
138
152
  continue
@@ -159,56 +173,57 @@ class PluginManager:
159
173
  :param product_type: (optional) The product type that the constructed plugins
160
174
  must support
161
175
  :type product_type: str
162
- :param provider: (optional) The provider on which to get the search plugin
176
+ :param provider: (optional) The provider or the provider group on which to get
177
+ the search plugins
163
178
  :type provider: str
164
179
  :returns: All the plugins supporting the product type, one by one (a generator
165
180
  object)
166
- :rtype: types.GeneratorType(:class:`~eodag.plugins.search.Search` or :class:`~eodag.plugins.download.Api`)
181
+ :rtype: types.GeneratorType(:class:`~eodag.plugins.search.Search`
182
+ or :class:`~eodag.plugins.download.Api`)
167
183
  :raises: :class:`~eodag.utils.exceptions.UnsupportedProvider`
168
- :raises: :class:`~eodag.utils.exceptions.UnsupportedProductType`
169
184
  """
170
185
 
171
186
  def get_plugin() -> Union[Search, Api]:
172
187
  plugin: Union[Search, Api]
173
- try:
188
+ if search := getattr(config, "search", None):
174
189
  config.search.products = config.products
175
190
  config.search.priority = config.priority
176
- plugin = cast(
177
- Search, self._build_plugin(config.name, config.search, Search)
178
- )
179
- except AttributeError:
191
+ plugin = cast(Search, self._build_plugin(config.name, search, Search))
192
+ elif api := getattr(config, "api", None):
180
193
  config.api.products = config.products
181
194
  config.api.priority = config.priority
182
- plugin = cast(Api, self._build_plugin(config.name, config.api, Api))
195
+ plugin = cast(Api, self._build_plugin(config.name, api, Api))
196
+ else:
197
+ raise MisconfiguredError(
198
+ f"No search plugin configureed for {config.name}."
199
+ )
183
200
  return plugin
184
201
 
185
- if provider is not None:
186
- try:
187
- config = self.providers_config[provider]
188
- except KeyError:
189
- raise UnsupportedProvider
190
- yield get_plugin()
191
- # Signal the end of iteration as we already have what we wanted (see PEP-479)
192
- return
193
-
194
- if product_type is None:
195
- for config in sorted(
196
- self.providers_config.values(), key=attrgetter("priority"), reverse=True
197
- ):
198
- yield get_plugin()
199
- # Signal the end of iteration as we already have what we wanted (see PEP-479)
200
- return
201
- try:
202
- for config in self.product_type_to_provider_config_map[product_type]:
203
- yield get_plugin()
204
- except KeyError:
205
- logger.info(
206
- "UnsupportedProductType: %s, using generic settings", product_type
202
+ configs: Optional[List[ProviderConfig]]
203
+ if product_type:
204
+ configs = self.product_type_to_provider_config_map.get(product_type)
205
+ if not configs:
206
+ logger.info(
207
+ "UnsupportedProductType: %s, using generic settings", product_type
208
+ )
209
+ configs = self.product_type_to_provider_config_map[GENERIC_PRODUCT_TYPE]
210
+ else:
211
+ configs = list(self.providers_config.values())
212
+
213
+ if provider:
214
+ configs = [
215
+ c for c in configs if provider in [getattr(c, "group", None), c.name]
216
+ ]
217
+
218
+ if not configs and product_type:
219
+ raise UnsupportedProvider(
220
+ f"{provider} is not (yet) supported for {product_type}"
207
221
  )
208
- for config in self.product_type_to_provider_config_map[
209
- GENERIC_PRODUCT_TYPE
210
- ]:
211
- yield get_plugin()
222
+ if not configs:
223
+ raise UnsupportedProvider(f"{provider} is not (yet) supported")
224
+
225
+ for config in sorted(configs, key=attrgetter("priority"), reverse=True):
226
+ yield get_plugin()
212
227
 
213
228
  def get_download_plugin(self, product: EOProduct) -> Union[Download, Api]:
214
229
  """Build and return the download plugin capable of downloading the given
@@ -220,19 +235,20 @@ class PluginManager:
220
235
  :rtype: :class:`~eodag.plugins.download.Download` or :class:`~eodag.plugins.download.Api`
221
236
  """
222
237
  plugin_conf = self.providers_config[product.provider]
223
- try:
238
+ if download := getattr(plugin_conf, "download", None):
224
239
  plugin_conf.download.priority = plugin_conf.priority
225
240
  plugin = cast(
226
241
  Download,
227
- self._build_plugin(product.provider, plugin_conf.download, Download),
242
+ self._build_plugin(product.provider, download, Download),
228
243
  )
229
- return plugin
230
- except AttributeError:
244
+ elif api := getattr(plugin_conf, "api", None):
231
245
  plugin_conf.api.priority = plugin_conf.priority
232
- plugin = cast(
233
- Api, self._build_plugin(product.provider, plugin_conf.api, Api)
246
+ plugin = cast(Api, self._build_plugin(product.provider, api, Api))
247
+ else:
248
+ raise MisconfiguredError(
249
+ f"No download plugin configured for provider {plugin_conf.name}."
234
250
  )
235
- return plugin
251
+ return plugin
236
252
 
237
253
  def get_auth_plugin(self, provider: str) -> Optional[Authentication]:
238
254
  """Build and return the authentication plugin for the given product_type and
@@ -244,17 +260,17 @@ class PluginManager:
244
260
  :rtype: :class:`~eodag.plugins.authentication.Authentication`
245
261
  """
246
262
  plugin_conf = self.providers_config[provider]
247
- try:
248
- plugin_conf.auth.priority = plugin_conf.priority
249
- plugin = cast(
250
- Authentication,
251
- self._build_plugin(provider, plugin_conf.auth, Authentication),
252
- )
253
- return plugin
254
- except AttributeError:
263
+ auth: Optional[PluginConfig] = getattr(plugin_conf, "auth", None)
264
+ if not auth:
255
265
  # We guess the plugin being built is of type Api, therefore no need
256
266
  # for an Auth plugin.
257
267
  return None
268
+ auth.priority = plugin_conf.priority
269
+ plugin = cast(
270
+ Authentication,
271
+ self._build_plugin(provider, auth, Authentication),
272
+ )
273
+ return plugin
258
274
 
259
275
  @staticmethod
260
276
  def get_crunch_plugin(name: str, **options: Any) -> Crunch:
@@ -268,8 +284,8 @@ class PluginManager:
268
284
  :returns: The cruncher named `name`
269
285
  :rtype: :class:`~eodag.plugins.crunch.Crunch`
270
286
  """
271
- Klass = Crunch.get_plugin_by_class_name(name)
272
- return Klass(options)
287
+ klass = Crunch.get_plugin_by_class_name(name)
288
+ return klass(options)
273
289
 
274
290
  def sort_providers(self) -> None:
275
291
  """Sort providers taking into account current priority order"""
@@ -16,3 +16,39 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
  """EODAG search package"""
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass, field
22
+ from typing import TYPE_CHECKING
23
+
24
+ from eodag.utils import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE
25
+
26
+ if TYPE_CHECKING:
27
+ from typing import Any, Dict, List, Optional, Union
28
+
29
+ from requests.auth import AuthBase
30
+
31
+ from eodag.plugins.authentication.base import Authentication
32
+
33
+
34
+ @dataclass
35
+ class PreparedSearch:
36
+ """An object collecting needed information for search."""
37
+
38
+ product_type: Optional[str] = None
39
+ page: int = DEFAULT_PAGE
40
+ items_per_page: int = DEFAULT_ITEMS_PER_PAGE
41
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None
42
+ auth_plugin: Optional[Authentication] = None
43
+ count: bool = True
44
+ url: Optional[str] = None
45
+ info_message: Optional[str] = None
46
+ exception_message: Optional[str] = None
47
+
48
+ need_count: bool = field(init=False)
49
+ query_params: Dict[str, Any] = field(init=False)
50
+ query_string: str = field(init=False)
51
+ search_urls: List[str] = field(init=False)
52
+ product_type_def_params: Dict[str, Any] = field(init=False)
53
+ total_items_nb: int = field(init=False)
54
+ sort_by_qs: str = field(init=False)