eodag 2.12.1__py3-none-any.whl → 3.0.0b2__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.
- eodag/api/core.py +440 -321
- eodag/api/product/__init__.py +5 -1
- eodag/api/product/_assets.py +57 -2
- eodag/api/product/_product.py +89 -68
- eodag/api/product/metadata_mapping.py +181 -66
- eodag/api/search_result.py +48 -1
- eodag/cli.py +20 -6
- eodag/config.py +95 -6
- eodag/plugins/apis/base.py +8 -165
- eodag/plugins/apis/ecmwf.py +36 -24
- eodag/plugins/apis/usgs.py +40 -24
- eodag/plugins/authentication/aws_auth.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +13 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +256 -46
- eodag/plugins/authentication/qsauth.py +3 -0
- eodag/plugins/authentication/sas_auth.py +8 -1
- eodag/plugins/authentication/token.py +92 -46
- eodag/plugins/authentication/token_exchange.py +120 -0
- eodag/plugins/download/aws.py +86 -91
- eodag/plugins/download/base.py +72 -40
- eodag/plugins/download/http.py +607 -264
- eodag/plugins/download/s3rest.py +28 -15
- eodag/plugins/manager.py +74 -57
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +225 -18
- eodag/plugins/search/build_search_result.py +389 -32
- eodag/plugins/search/cop_marine.py +378 -0
- eodag/plugins/search/creodias_s3.py +15 -14
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +44 -20
- eodag/plugins/search/qssearch.py +508 -203
- eodag/plugins/search/static_stac_search.py +99 -36
- eodag/resources/constraints/climate-dt.json +13 -0
- eodag/resources/constraints/extremes-dt.json +8 -0
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1897 -34
- eodag/resources/providers.yml +3539 -3277
- eodag/resources/stac.yml +48 -54
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +51 -3
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +27 -0
- eodag/rest/core.py +757 -0
- eodag/rest/server.py +397 -258
- eodag/rest/stac.py +438 -307
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +232 -43
- eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
- eodag/rest/types/stac_search.py +277 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +65 -0
- eodag/types/__init__.py +99 -9
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +31 -0
- eodag/types/search_args.py +58 -7
- eodag/types/whoosh.py +81 -0
- eodag/utils/__init__.py +72 -9
- eodag/utils/constraints.py +37 -37
- eodag/utils/exceptions.py +23 -17
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +138 -0
- eodag/utils/rest.py +104 -0
- eodag/utils/stac_reader.py +100 -16
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/METADATA +65 -44
- eodag-3.0.0b2.dist-info/RECORD +110 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/WHEEL +1 -1
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/entry_points.txt +6 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/utils.py +0 -1133
- eodag-2.12.1.dist-info/RECORD +0 -94
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/LICENSE +0 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/top_level.txt +0 -0
eodag/plugins/download/base.py
CHANGED
|
@@ -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.
|
|
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 ``
|
|
90
|
-
(``hashlib.md5(
|
|
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[
|
|
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:
|
|
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)
|
|
123
|
-
:type auth:
|
|
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[
|
|
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:
|
|
153
|
-
) ->
|
|
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)
|
|
160
|
-
:type auth:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
250
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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:
|
|
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)
|
|
455
|
-
:type auth:
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|