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
@@ -43,6 +43,7 @@ from eodag.utils import (
43
43
  DEFAULT_DOWNLOAD_TIMEOUT,
44
44
  DEFAULT_DOWNLOAD_WAIT,
45
45
  ProgressCallback,
46
+ StreamResponse,
46
47
  sanitize,
47
48
  uri_to_path,
48
49
  )
@@ -54,10 +55,13 @@ from eodag.utils.exceptions import (
54
55
  from eodag.utils.notebook import NotebookWidgets
55
56
 
56
57
  if TYPE_CHECKING:
58
+ from requests.auth import AuthBase
59
+
57
60
  from eodag.api.product import EOProduct
58
61
  from eodag.api.search_result import SearchResult
59
62
  from eodag.config import PluginConfig
60
- from eodag.utils import DownloadedCallback
63
+ from eodag.types.download_args import DownloadConf
64
+ from eodag.utils import DownloadedCallback, Unpack
61
65
 
62
66
 
63
67
  logger = logging.getLogger("eodag.download.base")
@@ -86,9 +90,9 @@ class Download(PluginTopic):
86
90
  (e.g. 'file:///tmp/product_folder' on Linux or
87
91
  'file:///C:/Users/username/AppData/LOcal/Temp' on Windows)
88
92
  - save a *record* file in the directory ``outputs_prefix/.downloaded`` whose name
89
- is built on the MD5 hash of the product's ``remote_location`` attribute
90
- (``hashlib.md5(remote_location.encode("utf-8")).hexdigest()``) and whose content is
91
- the product's ``remote_location`` attribute itself.
93
+ is built on the MD5 hash of the product's ``product_type`` and ``properties['id']``
94
+ attributes (``hashlib.md5((product.product_type+"-"+product.properties['id']).encode("utf-8")).hexdigest()``)
95
+ and whose content is the product's ``remote_location`` attribute itself.
92
96
  - not try to download a product whose ``location`` attribute already points to an
93
97
  existing file/directory
94
98
  - not try to download a product if its *record* file exists as long as the expected
@@ -108,19 +112,19 @@ class Download(PluginTopic):
108
112
  def download(
109
113
  self,
110
114
  product: EOProduct,
111
- auth: Optional[PluginConfig] = None,
115
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
112
116
  progress_callback: Optional[ProgressCallback] = None,
113
117
  wait: int = DEFAULT_DOWNLOAD_WAIT,
114
118
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
115
- **kwargs: Union[str, bool, Dict[str, Any]],
119
+ **kwargs: Unpack[DownloadConf],
116
120
  ) -> Optional[str]:
117
121
  r"""
118
122
  Base download method. Not available, it must be defined for each plugin.
119
123
 
120
124
  :param product: The EO product to download
121
125
  :type product: :class:`~eodag.api.product._product.EOProduct`
122
- :param auth: (optional) The configuration of a plugin of type Authentication
123
- :type auth: :class:`~eodag.config.PluginConfig`
126
+ :param auth: (optional) authenticated object
127
+ :type auth: Optional[Union[AuthBase, Dict[str, str]]]
124
128
  :param progress_callback: (optional) A progress callback
125
129
  :type progress_callback: :class:`~eodag.utils.ProgressCallback`
126
130
  :param wait: (optional) If download fails, wait time in minutes between two download tries
@@ -145,19 +149,19 @@ class Download(PluginTopic):
145
149
  def _stream_download_dict(
146
150
  self,
147
151
  product: EOProduct,
148
- auth: Optional[PluginConfig] = None,
152
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
149
153
  progress_callback: Optional[ProgressCallback] = None,
150
154
  wait: int = DEFAULT_DOWNLOAD_WAIT,
151
155
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
152
- **kwargs: Union[str, bool, Dict[str, Any]],
153
- ) -> Dict[str, Any]:
156
+ **kwargs: Unpack[DownloadConf],
157
+ ) -> StreamResponse:
154
158
  r"""
155
159
  Base _stream_download_dict method. Not available, it must be defined for each plugin.
156
160
 
157
161
  :param product: The EO product to download
158
162
  :type product: :class:`~eodag.api.product._product.EOProduct`
159
- :param auth: (optional) The configuration of a plugin of type Authentication
160
- :type auth: :class:`~eodag.config.PluginConfig`
163
+ :param auth: (optional) authenticated object
164
+ :type auth: Optional[Union[AuthBase, Dict[str, str]]]
161
165
  :param progress_callback: (optional) A progress callback
162
166
  :type progress_callback: :class:`~eodag.utils.ProgressCallback`
163
167
  :param wait: (optional) If download fails, wait time in minutes between two download tries
@@ -181,7 +185,7 @@ class Download(PluginTopic):
181
185
  self,
182
186
  product: EOProduct,
183
187
  progress_callback: Optional[ProgressCallback] = None,
184
- **kwargs: Union[str, bool, Dict[str, Any]],
188
+ **kwargs: Unpack[DownloadConf],
185
189
  ) -> Tuple[Optional[str], Optional[str]]:
186
190
  """Check if file has already been downloaded, and prepare product download
187
191
 
@@ -233,7 +237,9 @@ class Download(PluginTopic):
233
237
  prefix,
234
238
  f"{sanitize(product.properties['title'])}{collision_avoidance_suffix}{outputs_extension}",
235
239
  )
236
- fs_dir_path = fs_path.replace(outputs_extension, "")
240
+ fs_dir_path = (
241
+ fs_path.replace(outputs_extension, "") if outputs_extension else fs_path
242
+ )
237
243
  download_records_dir = os.path.join(prefix, ".downloaded")
238
244
  try:
239
245
  os.makedirs(download_records_dir)
@@ -246,8 +252,9 @@ class Download(PluginTopic):
246
252
  logger.warning(
247
253
  f"Unable to create records directory. Got:\n{tb.format_exc()}",
248
254
  )
249
- url_hash = hashlib.md5(url.encode("utf-8")).hexdigest()
250
- record_filename = os.path.join(download_records_dir, url_hash)
255
+ record_filename = os.path.join(
256
+ download_records_dir, self.generate_record_hash(product)
257
+ )
251
258
  if os.path.isfile(record_filename) and os.path.isfile(fs_path):
252
259
  logger.info(
253
260
  f"Product already downloaded: {fs_path}",
@@ -278,6 +285,21 @@ class Download(PluginTopic):
278
285
 
279
286
  return fs_path, record_filename
280
287
 
288
+ def generate_record_hash(self, product: EOProduct) -> str:
289
+ """Generate the record hash of the given product.
290
+
291
+ The MD5 hash is built from the product's ``product_type`` and ``properties['id']`` attributes
292
+ (``hashlib.md5((product.product_type+"-"+product.properties['id']).encode("utf-8")).hexdigest()``)
293
+
294
+ :param product: The product to calculate the record hash
295
+ :type product: :class:`~eodag.api.product._product.EOProduct`
296
+ :returns: The MD5 hash
297
+ :rtype: str
298
+ """
299
+ # In some unit tests, `product.product_type` is `None` and `product.properties["id"]` is `ìnt`
300
+ product_hash = str(product.product_type) + "-" + str(product.properties["id"])
301
+ return hashlib.md5(product_hash.encode("utf-8")).hexdigest()
302
+
281
303
  def _resolve_archive_depth(self, product_path: str) -> str:
282
304
  """Update product_path using archive_depth from provider configuration.
283
305
 
@@ -302,7 +324,7 @@ class Download(PluginTopic):
302
324
  self,
303
325
  fs_path: str,
304
326
  progress_callback: Optional[ProgressCallback] = None,
305
- **kwargs: Any,
327
+ **kwargs: Unpack[DownloadConf],
306
328
  ) -> str:
307
329
  """Finalize the download process.
308
330
 
@@ -332,6 +354,11 @@ class Download(PluginTopic):
332
354
  extract = (
333
355
  extract if extract is not None else getattr(self.config, "extract", True)
334
356
  )
357
+ if not extract:
358
+ logger.info("Extraction not activated. The product is available as is.")
359
+ progress_callback(1, total=1)
360
+ return fs_path
361
+
335
362
  delete_archive = kwargs.pop("delete_archive", None)
336
363
  delete_archive = (
337
364
  delete_archive
@@ -340,11 +367,6 @@ class Download(PluginTopic):
340
367
  )
341
368
  outputs_extension = kwargs.pop("outputs_extension", ".zip")
342
369
 
343
- if not extract:
344
- logger.info("Extraction not activated. The product is available as is.")
345
- progress_callback(1, total=1)
346
- return fs_path
347
-
348
370
  product_path = (
349
371
  fs_path[: fs_path.index(outputs_extension)]
350
372
  if outputs_extension in fs_path
@@ -373,7 +395,6 @@ class Download(PluginTopic):
373
395
  f"Extraction cancelled, destination directory already exists and is not empty: {product_path}"
374
396
  )
375
397
  progress_callback(1, total=1)
376
- product_path = self._resolve_archive_depth(product_path)
377
398
  return product_path
378
399
  outputs_prefix = (
379
400
  kwargs.pop("outputs_prefix", None) or self.config.outputs_prefix
@@ -402,14 +423,28 @@ class Download(PluginTopic):
402
423
  path=extraction_dir,
403
424
  )
404
425
  progress_callback(1)
405
- shutil.move(extraction_dir, outputs_dir)
426
+ # in some cases, only a lone file is extracted without being in a directory
427
+ # then, we create a directory in which we place this file
428
+ product_extraction_path = self._resolve_archive_depth(extraction_dir)
429
+ if os.path.isfile(product_extraction_path) and not os.path.isdir(
430
+ outputs_dir
431
+ ):
432
+ os.makedirs(outputs_dir)
433
+ shutil.move(product_extraction_path, outputs_dir)
406
434
 
407
- elif fs_path.endswith(".tar.gz"):
435
+ elif fs_path.endswith(".tar") or fs_path.endswith(".tar.gz"):
408
436
  with tarfile.open(fs_path, "r") as zfile:
409
437
  progress_callback.reset(total=1)
410
438
  zfile.extractall(path=extraction_dir)
411
439
  progress_callback(1)
412
- shutil.move(extraction_dir, outputs_dir)
440
+ # in some cases, only a lone file is extracted without being in a directory
441
+ # then, we create a directory in which we place this file
442
+ product_extraction_path = self._resolve_archive_depth(extraction_dir)
443
+ if os.path.isfile(product_extraction_path) and not os.path.isdir(
444
+ outputs_dir
445
+ ):
446
+ os.makedirs(outputs_dir)
447
+ shutil.move(product_extraction_path, outputs_dir)
413
448
  else:
414
449
  progress_callback(1, total=1)
415
450
 
@@ -429,19 +464,17 @@ class Download(PluginTopic):
429
464
  if close_progress_callback:
430
465
  progress_callback.close()
431
466
 
432
- product_path = self._resolve_archive_depth(product_path)
433
-
434
467
  return product_path
435
468
 
436
469
  def download_all(
437
470
  self,
438
471
  products: SearchResult,
439
- auth: Optional[PluginConfig] = None,
472
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
440
473
  downloaded_callback: Optional[DownloadedCallback] = None,
441
474
  progress_callback: Optional[ProgressCallback] = None,
442
475
  wait: int = DEFAULT_DOWNLOAD_WAIT,
443
476
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
444
- **kwargs: Union[str, bool, Dict[str, Any]],
477
+ **kwargs: Unpack[DownloadConf],
445
478
  ) -> List[str]:
446
479
  """
447
480
  Base download_all method.
@@ -451,8 +484,8 @@ class Download(PluginTopic):
451
484
 
452
485
  :param products: Products to download
453
486
  :type products: :class:`~eodag.api.search_result.SearchResult`
454
- :param auth: (optional) The configuration of a plugin of type Authentication
455
- :type auth: :class:`~eodag.config.PluginConfig`
487
+ :param auth: (optional) authenticated object
488
+ :type auth: Optional[Union[AuthBase, Dict[str, str]]]
456
489
  :param downloaded_callback: (optional) A method or a callable object which takes
457
490
  as parameter the ``product``. You can use the base class
458
491
  :class:`~eodag.api.product.DownloadedCallback` and override
@@ -610,7 +643,7 @@ class Download(PluginTopic):
610
643
  """
611
644
 
612
645
  def decorator(download: Callable[..., T]) -> Callable[..., T]:
613
- def download_and_retry(*args: Any, **kwargs: Any) -> T:
646
+ def download_and_retry(*args: Any, **kwargs: Unpack[DownloadConf]) -> T:
614
647
  # initiate retry loop
615
648
  start_time = datetime.now()
616
649
  stop_time = start_time + timedelta(minutes=timeout)
@@ -634,8 +667,7 @@ class Download(PluginTopic):
634
667
  f"Product is not available for download and order is not supported for"
635
668
  f" {self.provider}, {e}"
636
669
  )
637
- not_available_info = e
638
- pass
670
+ not_available_info = str(e)
639
671
 
640
672
  if datetime_now >= product.next_try and datetime_now < stop_time:
641
673
  wait_seconds = (
@@ -646,7 +678,7 @@ class Download(PluginTopic):
646
678
  f"[Retry #{retry_count}] Waited {wait_seconds}s, trying again to download ordered product"
647
679
  f" (retry every {wait}' for {timeout}')"
648
680
  )
649
- logger.debug(not_available_info)
681
+ logger.info(not_available_info)
650
682
  # Retry-After info from Response header
651
683
  if hasattr(self, "stream"):
652
684
  retry_server_info = self.stream.headers.get(
@@ -656,7 +688,7 @@ class Download(PluginTopic):
656
688
  logger.debug(
657
689
  f"[{self.provider} response] Retry-After: {retry_server_info}"
658
690
  )
659
- logger.info(retry_info)
691
+ logger.debug(retry_info)
660
692
  nb_info.display_html(retry_info)
661
693
  product.next_try = datetime_now
662
694
  elif datetime_now < product.next_try and datetime_now < stop_time:
@@ -668,7 +700,7 @@ class Download(PluginTopic):
668
700
  f"[Retry #{retry_count}] Waiting {wait_seconds}s until next download try"
669
701
  f" for ordered product (retry every {wait}' for {timeout}')"
670
702
  )
671
- logger.debug(not_available_info)
703
+ logger.info(not_available_info)
672
704
  # Retry-After info from Response header
673
705
  if hasattr(self, "stream"):
674
706
  retry_server_info = self.stream.headers.get(
@@ -678,7 +710,7 @@ class Download(PluginTopic):
678
710
  logger.debug(
679
711
  f"[{self.provider} response] Retry-After: {retry_server_info}"
680
712
  )
681
- logger.info(retry_info)
713
+ logger.debug(retry_info)
682
714
  nb_info.display_html(retry_info)
683
715
  sleep(wait_seconds)
684
716
  elif datetime_now >= stop_time and timeout > 0: