eodag 3.3.1__py3-none-any.whl → 3.4.0__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/product/_product.py +73 -36
- eodag/plugins/manager.py +8 -15
- eodag/plugins/search/cop_marine.py +12 -2
- eodag/plugins/search/qssearch.py +25 -6
- eodag/plugins/search/static_stac_search.py +31 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/providers.yml +6 -3
- {eodag-3.3.1.dist-info → eodag-3.4.0.dist-info}/METADATA +3 -3
- {eodag-3.3.1.dist-info → eodag-3.4.0.dist-info}/RECORD +13 -13
- {eodag-3.3.1.dist-info → eodag-3.4.0.dist-info}/WHEEL +1 -1
- {eodag-3.3.1.dist-info → eodag-3.4.0.dist-info}/entry_points.txt +0 -0
- {eodag-3.3.1.dist-info → eodag-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.3.1.dist-info → eodag-3.4.0.dist-info}/top_level.txt +0 -0
eodag/api/product/_product.py
CHANGED
|
@@ -313,7 +313,6 @@ class EOProduct:
|
|
|
313
313
|
"EO product is unable to download itself due to lacking of a "
|
|
314
314
|
"download plugin"
|
|
315
315
|
)
|
|
316
|
-
|
|
317
316
|
auth = (
|
|
318
317
|
self.downloader_auth.authenticate()
|
|
319
318
|
if self.downloader_auth is not None
|
|
@@ -323,7 +322,6 @@ class EOProduct:
|
|
|
323
322
|
progress_callback, close_progress_callback = self._init_progress_bar(
|
|
324
323
|
progress_callback
|
|
325
324
|
)
|
|
326
|
-
|
|
327
325
|
fs_path = self.downloader.download(
|
|
328
326
|
self,
|
|
329
327
|
auth=auth,
|
|
@@ -369,6 +367,47 @@ class EOProduct:
|
|
|
369
367
|
progress_callback.refresh()
|
|
370
368
|
return (progress_callback, close_progress_callback)
|
|
371
369
|
|
|
370
|
+
def _download_quicklook(
|
|
371
|
+
self,
|
|
372
|
+
quicklook_file: str,
|
|
373
|
+
progress_callback: ProgressCallback,
|
|
374
|
+
ssl_verify: Optional[bool] = None,
|
|
375
|
+
auth: Optional[AuthBase] = None,
|
|
376
|
+
):
|
|
377
|
+
|
|
378
|
+
"""Download the quicklook image from the EOProduct's quicklook URL.
|
|
379
|
+
|
|
380
|
+
This method performs an HTTP GET request to retrieve the quicklook image and saves it
|
|
381
|
+
locally at the specified path. It optionally verifies SSL certificates, uses HTTP
|
|
382
|
+
authentication, and can display a download progress if a callback is provided.
|
|
383
|
+
|
|
384
|
+
:param quicklook_file: The full path (including filename) where the quicklook will be saved.
|
|
385
|
+
:param progress_callback: A callable that accepts the current and total download sizes
|
|
386
|
+
to display or log the download progress. It must support `reset(total)`
|
|
387
|
+
and be callable with downloaded chunk sizes.
|
|
388
|
+
:param ssl_verify: (optional) Whether to verify SSL certificates. Defaults to True.
|
|
389
|
+
:param auth: (optional) Authentication credentials (e.g., tuple or object) used for the
|
|
390
|
+
HTTP request if the resource requires authentication.
|
|
391
|
+
:raises HTTPError: If the HTTP request to the quicklook URL fails.
|
|
392
|
+
"""
|
|
393
|
+
with requests.get(
|
|
394
|
+
self.properties["quicklook"],
|
|
395
|
+
stream=True,
|
|
396
|
+
auth=auth,
|
|
397
|
+
headers=USER_AGENT,
|
|
398
|
+
timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
|
|
399
|
+
verify=ssl_verify,
|
|
400
|
+
) as stream:
|
|
401
|
+
stream.raise_for_status()
|
|
402
|
+
stream_size = int(stream.headers.get("content-length", 0))
|
|
403
|
+
progress_callback.reset(stream_size)
|
|
404
|
+
with open(quicklook_file, "wb") as fhandle:
|
|
405
|
+
for chunk in stream.iter_content(chunk_size=64 * 1024):
|
|
406
|
+
if chunk:
|
|
407
|
+
fhandle.write(chunk)
|
|
408
|
+
progress_callback(len(chunk))
|
|
409
|
+
logger.info("Download recorded in %s", quicklook_file)
|
|
410
|
+
|
|
372
411
|
def get_quicklook(
|
|
373
412
|
self,
|
|
374
413
|
filename: Optional[str] = None,
|
|
@@ -378,6 +417,9 @@ class EOProduct:
|
|
|
378
417
|
"""Download the quicklook image of a given EOProduct from its provider if it
|
|
379
418
|
exists.
|
|
380
419
|
|
|
420
|
+
This method retrieves the quicklook URL from the EOProduct metadata and delegates
|
|
421
|
+
the download to the internal `download_quicklook` method.
|
|
422
|
+
|
|
381
423
|
:param filename: (optional) The name to give to the downloaded quicklook. If not
|
|
382
424
|
given, it defaults to the product's ID (without file extension).
|
|
383
425
|
:param output_dir: (optional) The absolute path of the directory where to store
|
|
@@ -403,18 +445,6 @@ class EOProduct:
|
|
|
403
445
|
}
|
|
404
446
|
)
|
|
405
447
|
|
|
406
|
-
# progress bar init
|
|
407
|
-
if progress_callback is None:
|
|
408
|
-
progress_callback = ProgressCallback()
|
|
409
|
-
# one shot progress callback to close after download
|
|
410
|
-
close_progress_callback = True
|
|
411
|
-
else:
|
|
412
|
-
close_progress_callback = False
|
|
413
|
-
# update units as bar may have been previously used for extraction
|
|
414
|
-
progress_callback.unit = "B"
|
|
415
|
-
progress_callback.unit_scale = True
|
|
416
|
-
progress_callback.desc = "quicklooks/%s" % self.properties.get("id", "")
|
|
417
|
-
|
|
418
448
|
if self.properties.get("quicklook", None) is None:
|
|
419
449
|
logger.warning(
|
|
420
450
|
"Missing information to retrieve quicklook for EO product: %s",
|
|
@@ -442,6 +472,18 @@ class EOProduct:
|
|
|
442
472
|
)
|
|
443
473
|
|
|
444
474
|
if not os.path.isfile(quicklook_file):
|
|
475
|
+
|
|
476
|
+
# progress bar init
|
|
477
|
+
if progress_callback is None:
|
|
478
|
+
progress_callback = ProgressCallback()
|
|
479
|
+
# one shot progress callback to close after download
|
|
480
|
+
close_progress_callback = True
|
|
481
|
+
else:
|
|
482
|
+
close_progress_callback = False
|
|
483
|
+
# update units as bar may have been previously used for extraction
|
|
484
|
+
progress_callback.unit = "B"
|
|
485
|
+
progress_callback.unit_scale = True
|
|
486
|
+
progress_callback.desc = "quicklooks/%s" % self.properties.get("id", "")
|
|
445
487
|
# VERY SPECIAL CASE (introduced by the onda provider): first check if
|
|
446
488
|
# it is a HTTP URL. If not, we assume it is a base64 string, in which case
|
|
447
489
|
# we just decode the content, write it into the quicklook_file and return it.
|
|
@@ -468,30 +510,25 @@ class EOProduct:
|
|
|
468
510
|
if self.downloader
|
|
469
511
|
else True
|
|
470
512
|
)
|
|
471
|
-
|
|
472
|
-
self.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
|
|
477
|
-
verify=ssl_verify,
|
|
478
|
-
) as stream:
|
|
479
|
-
try:
|
|
480
|
-
stream.raise_for_status()
|
|
481
|
-
except RequestException as e:
|
|
482
|
-
import traceback as tb
|
|
513
|
+
try:
|
|
514
|
+
self._download_quicklook(
|
|
515
|
+
quicklook_file, progress_callback, ssl_verify, auth
|
|
516
|
+
)
|
|
517
|
+
except RequestException as e:
|
|
483
518
|
|
|
484
|
-
|
|
519
|
+
logger.debug(
|
|
520
|
+
f"Error while getting resource with authentication. {e} \nTrying without authentication..."
|
|
521
|
+
)
|
|
522
|
+
try:
|
|
523
|
+
self._download_quicklook(
|
|
524
|
+
quicklook_file, progress_callback, ssl_verify, None
|
|
525
|
+
)
|
|
526
|
+
except RequestException as e_no_auth:
|
|
527
|
+
logger.error(
|
|
528
|
+
f"Failed to get resource with authentication: {e} \n \
|
|
529
|
+
Failed to get resource even without authentication. {e_no_auth}"
|
|
530
|
+
)
|
|
485
531
|
return str(e)
|
|
486
|
-
else:
|
|
487
|
-
stream_size = int(stream.headers.get("content-length", 0))
|
|
488
|
-
progress_callback.reset(stream_size)
|
|
489
|
-
with open(quicklook_file, "wb") as fhandle:
|
|
490
|
-
for chunk in stream.iter_content(chunk_size=64 * 1024):
|
|
491
|
-
if chunk:
|
|
492
|
-
fhandle.write(chunk)
|
|
493
|
-
progress_callback(len(chunk))
|
|
494
|
-
logger.info("Download recorded in %s", quicklook_file)
|
|
495
532
|
|
|
496
533
|
# close progress bar if needed
|
|
497
534
|
if close_progress_callback:
|
eodag/plugins/manager.py
CHANGED
|
@@ -20,10 +20,9 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import re
|
|
22
22
|
from operator import attrgetter
|
|
23
|
-
from pathlib import Path
|
|
24
23
|
from typing import TYPE_CHECKING, Any, Iterator, Optional, Union, cast
|
|
25
24
|
|
|
26
|
-
import
|
|
25
|
+
import importlib_metadata
|
|
27
26
|
|
|
28
27
|
from eodag.config import (
|
|
29
28
|
AUTH_TOPIC_KEYS,
|
|
@@ -91,12 +90,12 @@ class PluginManager:
|
|
|
91
90
|
# have it discovered as long as they declare an entry point of the type
|
|
92
91
|
# 'eodag.plugins.search' for example in its setup script. See the setup
|
|
93
92
|
# script of eodag for an example of how to do this.
|
|
94
|
-
for entry_point in
|
|
95
|
-
"eodag.plugins.{}".format(topic)
|
|
93
|
+
for entry_point in importlib_metadata.entry_points(
|
|
94
|
+
group="eodag.plugins.{}".format(topic)
|
|
96
95
|
):
|
|
97
96
|
try:
|
|
98
97
|
entry_point.load()
|
|
99
|
-
except
|
|
98
|
+
except ModuleNotFoundError:
|
|
100
99
|
logger.debug(
|
|
101
100
|
"%s plugin skipped, eodag[%s] or eodag[all] needed",
|
|
102
101
|
entry_point.name,
|
|
@@ -112,18 +111,12 @@ class PluginManager:
|
|
|
112
111
|
"Check that the plugin module (%s) is importable",
|
|
113
112
|
entry_point.module_name,
|
|
114
113
|
)
|
|
115
|
-
if
|
|
116
|
-
entry_point.dist
|
|
117
|
-
and entry_point.dist.key != "eodag"
|
|
118
|
-
and entry_point.dist.location is not None
|
|
119
|
-
):
|
|
114
|
+
if entry_point.dist and entry_point.dist.name != "eodag":
|
|
120
115
|
# use plugin providers if any
|
|
116
|
+
name = entry_point.dist.name
|
|
117
|
+
dist = entry_point.dist
|
|
121
118
|
plugin_providers_config_path = [
|
|
122
|
-
str(x)
|
|
123
|
-
for x in Path(
|
|
124
|
-
entry_point.dist.location,
|
|
125
|
-
pkg_resources.to_filename(entry_point.dist.key),
|
|
126
|
-
).rglob("providers.yml")
|
|
119
|
+
str(x) for x in dist.locate_file(name).rglob("providers.yml")
|
|
127
120
|
]
|
|
128
121
|
if plugin_providers_config_path:
|
|
129
122
|
plugin_providers_config = load_config(
|
|
@@ -37,7 +37,7 @@ from eodag.api.product import AssetsDict
|
|
|
37
37
|
from eodag.config import PluginConfig
|
|
38
38
|
from eodag.plugins.search import PreparedSearch
|
|
39
39
|
from eodag.plugins.search.static_stac_search import StaticStacSearch
|
|
40
|
-
from eodag.utils import get_bucket_name_and_prefix
|
|
40
|
+
from eodag.utils import get_bucket_name_and_prefix, get_geometry_from_various
|
|
41
41
|
from eodag.utils.exceptions import RequestError, UnsupportedProductType, ValidationError
|
|
42
42
|
|
|
43
43
|
if TYPE_CHECKING:
|
|
@@ -215,10 +215,14 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
215
215
|
|
|
216
216
|
item_id = os.path.splitext(item_key.split("/")[-1])[0]
|
|
217
217
|
download_url = s3_url + "/" + item_key
|
|
218
|
+
geometry = (
|
|
219
|
+
get_geometry_from_various(**dataset_item)
|
|
220
|
+
or self.config.metadata_mapping["defaultGeometry"]
|
|
221
|
+
)
|
|
218
222
|
properties = {
|
|
219
223
|
"id": item_id,
|
|
220
224
|
"title": item_id,
|
|
221
|
-
"geometry":
|
|
225
|
+
"geometry": geometry,
|
|
222
226
|
"downloadLink": download_url,
|
|
223
227
|
"dataset": dataset_item["id"],
|
|
224
228
|
}
|
|
@@ -308,10 +312,16 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
308
312
|
"parameter product type is required for search with cop_marine provider"
|
|
309
313
|
)
|
|
310
314
|
collection_dict, datasets_items_list = self._get_product_type_info(product_type)
|
|
315
|
+
geometry = kwargs.pop("geometry", None)
|
|
311
316
|
products: list[EOProduct] = []
|
|
312
317
|
start_index = items_per_page * (page - 1) + 1
|
|
313
318
|
num_total = 0
|
|
314
319
|
for i, dataset_item in enumerate(datasets_items_list):
|
|
320
|
+
# Filter by geometry
|
|
321
|
+
if "id" not in kwargs and geometry:
|
|
322
|
+
dataset_geom = get_geometry_from_various(**dataset_item)
|
|
323
|
+
if dataset_geom and not dataset_geom.intersects(geometry):
|
|
324
|
+
continue
|
|
315
325
|
try:
|
|
316
326
|
logger.debug("searching data for dataset %s", dataset_item["id"])
|
|
317
327
|
|
eodag/plugins/search/qssearch.py
CHANGED
|
@@ -70,6 +70,7 @@ from eodag.api.search_result import RawSearchResult
|
|
|
70
70
|
from eodag.plugins.search import PreparedSearch
|
|
71
71
|
from eodag.plugins.search.base import Search
|
|
72
72
|
from eodag.types import json_field_definition_to_python, model_fields_to_annotated
|
|
73
|
+
from eodag.types.queryables import Queryables
|
|
73
74
|
from eodag.types.search_args import SortByList
|
|
74
75
|
from eodag.utils import (
|
|
75
76
|
DEFAULT_SEARCH_TIMEOUT,
|
|
@@ -80,6 +81,7 @@ from eodag.utils import (
|
|
|
80
81
|
REQ_RETRY_TOTAL,
|
|
81
82
|
USER_AGENT,
|
|
82
83
|
_deprecated,
|
|
84
|
+
copy_deepcopy,
|
|
83
85
|
deepcopy,
|
|
84
86
|
dict_items_recursive_apply,
|
|
85
87
|
format_dict_items,
|
|
@@ -1896,30 +1898,47 @@ class StacSearch(PostJsonSearch):
|
|
|
1896
1898
|
"No queryable found for %s on %s", product_type, self.provider
|
|
1897
1899
|
)
|
|
1898
1900
|
return None
|
|
1899
|
-
|
|
1900
1901
|
# convert json results to pydantic model fields
|
|
1901
1902
|
field_definitions: dict[str, Any] = dict()
|
|
1903
|
+
STAC_TO_EODAG_QUERYABLES = {
|
|
1904
|
+
"start_datetime": "start",
|
|
1905
|
+
"end_datetime": "end",
|
|
1906
|
+
"datetime": None,
|
|
1907
|
+
"bbox": "geom",
|
|
1908
|
+
}
|
|
1902
1909
|
for json_param, json_mtd in json_queryables.items():
|
|
1903
|
-
param = (
|
|
1910
|
+
param = STAC_TO_EODAG_QUERYABLES.get(
|
|
1911
|
+
json_param,
|
|
1904
1912
|
get_queryable_from_provider(
|
|
1905
1913
|
json_param, self.get_metadata_mapping(product_type)
|
|
1906
1914
|
)
|
|
1907
|
-
or json_param
|
|
1915
|
+
or json_param,
|
|
1908
1916
|
)
|
|
1917
|
+
if param is None:
|
|
1918
|
+
continue
|
|
1909
1919
|
|
|
1910
|
-
default = kwargs.get(param,
|
|
1920
|
+
default = kwargs.get(param, json_mtd.get("default"))
|
|
1911
1921
|
annotated_def = json_field_definition_to_python(
|
|
1912
1922
|
json_mtd, default_value=default
|
|
1913
1923
|
)
|
|
1914
1924
|
field_definitions[param] = get_args(annotated_def)
|
|
1915
1925
|
|
|
1916
1926
|
python_queryables = create_model("m", **field_definitions).model_fields
|
|
1917
|
-
# replace geometry by geom
|
|
1918
1927
|
geom_queryable = python_queryables.pop("geometry", None)
|
|
1919
1928
|
if geom_queryable:
|
|
1920
1929
|
python_queryables["geom"] = geom_queryable
|
|
1921
1930
|
|
|
1922
|
-
|
|
1931
|
+
queryables_dict = model_fields_to_annotated(python_queryables)
|
|
1932
|
+
|
|
1933
|
+
# append "datetime" as "start" & "end" if needed
|
|
1934
|
+
if "datetime" in json_queryables:
|
|
1935
|
+
eodag_queryables = copy_deepcopy(
|
|
1936
|
+
model_fields_to_annotated(Queryables.model_fields)
|
|
1937
|
+
)
|
|
1938
|
+
queryables_dict.setdefault("start", eodag_queryables["start"])
|
|
1939
|
+
queryables_dict.setdefault("end", eodag_queryables["end"])
|
|
1940
|
+
|
|
1941
|
+
return queryables_dict
|
|
1923
1942
|
|
|
1924
1943
|
|
|
1925
1944
|
class PostJsonSearchWithStacQueryables(StacSearch, PostJsonSearch):
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
-
from typing import TYPE_CHECKING, Any, Optional
|
|
21
|
+
from typing import TYPE_CHECKING, Annotated, Any, Optional
|
|
22
22
|
from unittest import mock
|
|
23
23
|
|
|
24
24
|
import geojson
|
|
25
|
+
from pydantic.fields import FieldInfo
|
|
25
26
|
|
|
26
27
|
from eodag.api.product.metadata_mapping import get_metadata_path_value
|
|
27
28
|
from eodag.api.search_result import SearchResult
|
|
@@ -30,6 +31,7 @@ from eodag.plugins.crunch.filter_overlap import FilterOverlap
|
|
|
30
31
|
from eodag.plugins.crunch.filter_property import FilterProperty
|
|
31
32
|
from eodag.plugins.search import PreparedSearch
|
|
32
33
|
from eodag.plugins.search.qssearch import StacSearch
|
|
34
|
+
from eodag.types.queryables import Queryables
|
|
33
35
|
from eodag.utils import HTTP_REQ_TIMEOUT, MockResponse
|
|
34
36
|
from eodag.utils.stac_reader import fetch_stac_collections, fetch_stac_items
|
|
35
37
|
|
|
@@ -123,6 +125,34 @@ class StaticStacSearch(StacSearch):
|
|
|
123
125
|
|
|
124
126
|
return conf_update_dict
|
|
125
127
|
|
|
128
|
+
def discover_queryables(
|
|
129
|
+
self, **kwargs: Any
|
|
130
|
+
) -> dict[str, Annotated[Any, FieldInfo]]:
|
|
131
|
+
"""Set static available queryables for :class:`~eodag.plugins.search.static_stac_search.StaticStacSearch`
|
|
132
|
+
search plugin
|
|
133
|
+
|
|
134
|
+
:param kwargs: additional filters for queryables (`productType` and other search
|
|
135
|
+
arguments)
|
|
136
|
+
:returns: queryable parameters dict
|
|
137
|
+
"""
|
|
138
|
+
return {
|
|
139
|
+
"productType": Queryables.get_with_default(
|
|
140
|
+
"productType", kwargs.get("productType")
|
|
141
|
+
),
|
|
142
|
+
"id": Queryables.get_with_default("id", kwargs.get("id")),
|
|
143
|
+
"start": Queryables.get_with_default(
|
|
144
|
+
"start", kwargs.get("start") or kwargs.get("startTimeFromAscendingNode")
|
|
145
|
+
),
|
|
146
|
+
"end": Queryables.get_with_default(
|
|
147
|
+
"end",
|
|
148
|
+
kwargs.get("end") or kwargs.get("completionTimeFromAscendingNode"),
|
|
149
|
+
),
|
|
150
|
+
"geom": Queryables.get_with_default(
|
|
151
|
+
"geom",
|
|
152
|
+
kwargs.get("geom") or kwargs.get("geometry") or kwargs.get("area"),
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
|
|
126
156
|
def query(
|
|
127
157
|
self,
|
|
128
158
|
prep: PreparedSearch = PreparedSearch(),
|