eodag 4.0.0a4__py3-none-any.whl → 4.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.
- eodag/api/collection.py +65 -1
- eodag/api/core.py +65 -19
- eodag/api/product/_assets.py +1 -1
- eodag/api/product/_product.py +133 -18
- eodag/api/product/drivers/__init__.py +3 -1
- eodag/api/product/drivers/base.py +3 -1
- eodag/api/product/drivers/generic.py +9 -5
- eodag/api/product/drivers/sentinel1.py +14 -9
- eodag/api/product/drivers/sentinel2.py +14 -7
- eodag/api/product/metadata_mapping.py +5 -2
- eodag/api/provider.py +1 -0
- eodag/api/search_result.py +4 -1
- eodag/cli.py +17 -8
- eodag/config.py +22 -4
- eodag/plugins/apis/ecmwf.py +3 -24
- eodag/plugins/apis/usgs.py +3 -24
- eodag/plugins/download/aws.py +85 -44
- eodag/plugins/download/base.py +117 -41
- eodag/plugins/download/http.py +88 -65
- eodag/plugins/search/base.py +8 -3
- eodag/plugins/search/build_search_result.py +108 -120
- eodag/plugins/search/cop_marine.py +3 -1
- eodag/plugins/search/qssearch.py +7 -6
- eodag/resources/collections.yml +255 -0
- eodag/resources/ext_collections.json +1 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/providers.yml +62 -25
- eodag/resources/user_conf_template.yml +6 -0
- eodag/types/__init__.py +22 -16
- eodag/types/download_args.py +3 -1
- eodag/types/queryables.py +125 -55
- eodag/types/stac_extensions.py +408 -0
- eodag/types/stac_metadata.py +312 -0
- eodag/utils/__init__.py +42 -4
- eodag/utils/dates.py +202 -2
- eodag/utils/s3.py +4 -4
- {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/METADATA +7 -13
- {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/RECORD +42 -40
- {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/WHEEL +1 -1
- {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/entry_points.txt +1 -1
- {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/top_level.txt +0 -0
eodag/api/collection.py
CHANGED
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import re
|
|
22
22
|
from collections import UserDict, UserList
|
|
23
|
-
from typing import TYPE_CHECKING, Any, Optional
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Optional, cast
|
|
24
24
|
|
|
25
25
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
26
26
|
from pydantic import ValidationError as PydanticValidationError
|
|
@@ -29,6 +29,9 @@ from pydantic_core import ErrorDetails, InitErrorDetails, PydanticCustomError
|
|
|
29
29
|
from stac_pydantic.collection import Extent, Provider, SpatialExtent, TimeInterval
|
|
30
30
|
from stac_pydantic.links import Links
|
|
31
31
|
|
|
32
|
+
from eodag.types.queryables import CommonStacMetadata
|
|
33
|
+
from eodag.types.stac_metadata import create_stac_metadata_model
|
|
34
|
+
from eodag.utils import STAC_VERSION
|
|
32
35
|
from eodag.utils.env import is_env_var_true
|
|
33
36
|
from eodag.utils.exceptions import ValidationError
|
|
34
37
|
from eodag.utils.repr import dict_to_html_table
|
|
@@ -91,6 +94,11 @@ class Collection(BaseModel):
|
|
|
91
94
|
repr=False,
|
|
92
95
|
)
|
|
93
96
|
|
|
97
|
+
# path to external collection metadata file (required by stac-fastapi-eodag)
|
|
98
|
+
eodag_stac_collection: Optional[str] = Field(
|
|
99
|
+
default=None, alias="stacCollection", exclude=True, repr=False
|
|
100
|
+
)
|
|
101
|
+
|
|
94
102
|
# Private property to store the eodag internal id value. Not part of the model schema.
|
|
95
103
|
_id: str = PrivateAttr()
|
|
96
104
|
_dag: Optional[EODataAccessGateway] = PrivateAttr(default=None)
|
|
@@ -276,6 +284,62 @@ class Collection(BaseModel):
|
|
|
276
284
|
|
|
277
285
|
return dag.list_queryables(collection=self.id, **kwargs)
|
|
278
286
|
|
|
287
|
+
def serialize(self) -> dict[str, Any]:
|
|
288
|
+
"""Serialize the Collection instance to a STAC dictionary.
|
|
289
|
+
|
|
290
|
+
:returns: A STAC dictionary representation of the Collection instance.
|
|
291
|
+
"""
|
|
292
|
+
stac_dict: dict[str, Any] = {
|
|
293
|
+
"stac_version": STAC_VERSION,
|
|
294
|
+
"type": "Collection",
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
stac_dict |= self.model_dump(mode="json", exclude_none=True, exclude={"alias"})
|
|
298
|
+
|
|
299
|
+
stac_dict.setdefault("links", [])
|
|
300
|
+
stac_dict.setdefault("providers", [])
|
|
301
|
+
|
|
302
|
+
not_in_summaries = [
|
|
303
|
+
"stac_version",
|
|
304
|
+
"type",
|
|
305
|
+
"id",
|
|
306
|
+
"title",
|
|
307
|
+
"description",
|
|
308
|
+
"extent",
|
|
309
|
+
"keywords",
|
|
310
|
+
"license",
|
|
311
|
+
"links",
|
|
312
|
+
"providers",
|
|
313
|
+
]
|
|
314
|
+
summaries = dict()
|
|
315
|
+
for k, v in stac_dict.items():
|
|
316
|
+
if k not in not_in_summaries:
|
|
317
|
+
if isinstance(v, list):
|
|
318
|
+
summaries[k] = v
|
|
319
|
+
elif isinstance(v, str):
|
|
320
|
+
summaries[k] = v.split(",")
|
|
321
|
+
else:
|
|
322
|
+
summaries[k] = [v]
|
|
323
|
+
stac_dict["summaries"] = summaries
|
|
324
|
+
|
|
325
|
+
# Remove empty items and items moved to summaries
|
|
326
|
+
keys_to_remove = [
|
|
327
|
+
k
|
|
328
|
+
for k in stac_dict.keys()
|
|
329
|
+
if k not in not_in_summaries and k != "summaries"
|
|
330
|
+
]
|
|
331
|
+
for k in keys_to_remove:
|
|
332
|
+
del stac_dict[k]
|
|
333
|
+
|
|
334
|
+
# add extensions
|
|
335
|
+
summaries_model = cast(CommonStacMetadata, create_stac_metadata_model())
|
|
336
|
+
summaries_validated = summaries_model.model_construct(
|
|
337
|
+
_fields_set=None, **summaries
|
|
338
|
+
)
|
|
339
|
+
stac_dict["stac_extensions"] = summaries_validated.get_conformance_classes()
|
|
340
|
+
|
|
341
|
+
return stac_dict
|
|
342
|
+
|
|
279
343
|
|
|
280
344
|
class CollectionsDict(UserDict[str, Collection]):
|
|
281
345
|
"""A UserDict object which values are :class:`~eodag.api.collection.Collection` objects, keyed by provider ``id``.
|
eodag/api/core.py
CHANGED
|
@@ -30,6 +30,7 @@ from copy import deepcopy
|
|
|
30
30
|
from importlib.metadata import version
|
|
31
31
|
from importlib.resources import files as res_files
|
|
32
32
|
from operator import attrgetter, itemgetter
|
|
33
|
+
from pathlib import Path
|
|
33
34
|
from typing import TYPE_CHECKING, Any, Iterator, Optional, Union, cast
|
|
34
35
|
|
|
35
36
|
import geojson
|
|
@@ -87,6 +88,7 @@ from eodag.utils.free_text_search import compile_free_text_query
|
|
|
87
88
|
from eodag.utils.stac_reader import fetch_stac_items
|
|
88
89
|
|
|
89
90
|
if TYPE_CHECKING:
|
|
91
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
90
92
|
from shapely.geometry.base import BaseGeometry
|
|
91
93
|
|
|
92
94
|
from eodag.api.product import EOProduct
|
|
@@ -153,11 +155,14 @@ class EODataAccessGateway:
|
|
|
153
155
|
user_conf_file_path = os.getenv(env_var_name)
|
|
154
156
|
if user_conf_file_path is None:
|
|
155
157
|
user_conf_file_path = standard_configuration_path
|
|
156
|
-
|
|
158
|
+
source = str(
|
|
159
|
+
res_files("eodag") / "resources" / "user_conf_template.yml"
|
|
160
|
+
)
|
|
161
|
+
if os.path.isfile(source) and not os.path.isfile(
|
|
162
|
+
standard_configuration_path
|
|
163
|
+
):
|
|
157
164
|
shutil.copy(
|
|
158
|
-
|
|
159
|
-
res_files("eodag") / "resources" / "user_conf_template.yml"
|
|
160
|
-
),
|
|
165
|
+
source,
|
|
161
166
|
standard_configuration_path,
|
|
162
167
|
)
|
|
163
168
|
self._providers.update_from_config_file(user_conf_file_path)
|
|
@@ -452,6 +457,9 @@ class EODataAccessGateway:
|
|
|
452
457
|
if locations_conf_path is None:
|
|
453
458
|
locations_conf_path = os.path.join(self.conf_dir, "locations.yml")
|
|
454
459
|
if not os.path.isfile(locations_conf_path):
|
|
460
|
+
# Ensure the directory exists
|
|
461
|
+
os.makedirs(os.path.dirname(locations_conf_path), exist_ok=True)
|
|
462
|
+
|
|
455
463
|
# copy locations conf file and replace path example
|
|
456
464
|
locations_conf_template = str(
|
|
457
465
|
res_files("eodag") / "resources" / "locations_conf_template.yml"
|
|
@@ -1779,14 +1787,15 @@ class EODataAccessGateway:
|
|
|
1779
1787
|
|
|
1780
1788
|
# remove None values and convert param names to their pydantic alias if any
|
|
1781
1789
|
search_params = {}
|
|
1790
|
+
queryables_fields = Queryables.from_stac_models().model_fields
|
|
1782
1791
|
ecmwf_queryables = [
|
|
1783
1792
|
f"{ECMWF_PREFIX[:-1]}_{k}" for k in ECMWF_ALLOWED_KEYWORDS
|
|
1784
1793
|
]
|
|
1785
1794
|
for param, value in kwargs.items():
|
|
1786
1795
|
if value is None:
|
|
1787
1796
|
continue
|
|
1788
|
-
if param in
|
|
1789
|
-
param_alias =
|
|
1797
|
+
if param in queryables_fields:
|
|
1798
|
+
param_alias = queryables_fields[param].alias or param
|
|
1790
1799
|
search_params[param_alias] = value
|
|
1791
1800
|
elif param in ecmwf_queryables:
|
|
1792
1801
|
# alias equivalent for ECMWF queryables
|
|
@@ -1845,14 +1854,6 @@ class EODataAccessGateway:
|
|
|
1845
1854
|
else:
|
|
1846
1855
|
eo_product.collection = guesses[0].id
|
|
1847
1856
|
|
|
1848
|
-
try:
|
|
1849
|
-
if eo_product.collection is not None:
|
|
1850
|
-
eo_product.collection = self.get_collection_from_alias(
|
|
1851
|
-
eo_product.collection
|
|
1852
|
-
)
|
|
1853
|
-
except NoMatchingCollection:
|
|
1854
|
-
logger.debug("collection %s not found", eo_product.collection)
|
|
1855
|
-
|
|
1856
1857
|
if eo_product.search_intersection is not None:
|
|
1857
1858
|
eo_product._register_downloader_from_manager(self._plugins_manager)
|
|
1858
1859
|
|
|
@@ -1919,6 +1920,7 @@ class EODataAccessGateway:
|
|
|
1919
1920
|
search_result: SearchResult,
|
|
1920
1921
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
1921
1922
|
progress_callback: Optional[ProgressCallback] = None,
|
|
1923
|
+
executor: Optional[ThreadPoolExecutor] = None,
|
|
1922
1924
|
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
1923
1925
|
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
1924
1926
|
**kwargs: Unpack[DownloadConf],
|
|
@@ -1936,6 +1938,8 @@ class EODataAccessGateway:
|
|
|
1936
1938
|
size as inputs and handle progress bar
|
|
1937
1939
|
creation and update to give the user a
|
|
1938
1940
|
feedback on the download progress
|
|
1941
|
+
:param executor: (optional) An executor to download EO products of ``search_result`` in parallel
|
|
1942
|
+
which will also be reused to download assets of these products in parallel.
|
|
1939
1943
|
:param wait: (optional) If download fails, wait time in minutes between
|
|
1940
1944
|
two download tries of the same product
|
|
1941
1945
|
:param timeout: (optional) If download fails, maximum time in minutes
|
|
@@ -1956,8 +1960,7 @@ class EODataAccessGateway:
|
|
|
1956
1960
|
paths = []
|
|
1957
1961
|
if search_result:
|
|
1958
1962
|
logger.info("Downloading %s products", len(search_result))
|
|
1959
|
-
# Get download plugin using first product assuming
|
|
1960
|
-
# aren't mixed into a search result
|
|
1963
|
+
# Get download plugin using first product assuming all plugins use base.Download.download_all
|
|
1961
1964
|
download_plugin = self._plugins_manager.get_download_plugin(
|
|
1962
1965
|
search_result[0]
|
|
1963
1966
|
)
|
|
@@ -1965,6 +1968,7 @@ class EODataAccessGateway:
|
|
|
1965
1968
|
search_result,
|
|
1966
1969
|
downloaded_callback=downloaded_callback,
|
|
1967
1970
|
progress_callback=progress_callback,
|
|
1971
|
+
executor=executor,
|
|
1968
1972
|
wait=wait,
|
|
1969
1973
|
timeout=timeout,
|
|
1970
1974
|
**kwargs,
|
|
@@ -1986,8 +1990,42 @@ class EODataAccessGateway:
|
|
|
1986
1990
|
:param filename: (optional) The name of the file to generate
|
|
1987
1991
|
:returns: The name of the created file
|
|
1988
1992
|
"""
|
|
1993
|
+
search_result_dict = search_result.as_geojson_object()
|
|
1994
|
+
# add self link
|
|
1995
|
+
search_result_dict.setdefault("links", [])
|
|
1996
|
+
search_result_dict["links"].append(
|
|
1997
|
+
{
|
|
1998
|
+
"rel": "self",
|
|
1999
|
+
"href": f"{filename}",
|
|
2000
|
+
"type": "application/json",
|
|
2001
|
+
},
|
|
2002
|
+
)
|
|
2003
|
+
# write search results
|
|
1989
2004
|
with open(filename, "w") as fh:
|
|
1990
|
-
geojson.dump(
|
|
2005
|
+
geojson.dump(search_result_dict, fh)
|
|
2006
|
+
logger.debug("Search results saved to %s", filename)
|
|
2007
|
+
# write collection(s)
|
|
2008
|
+
if search_result._dag is None:
|
|
2009
|
+
return filename
|
|
2010
|
+
collections = set(p.collection for p in search_result)
|
|
2011
|
+
for collection in collections:
|
|
2012
|
+
collection_obj = search_result._dag.collections_config.get(
|
|
2013
|
+
collection, Collection(id=collection)
|
|
2014
|
+
)
|
|
2015
|
+
collection_dict = collection_obj.serialize()
|
|
2016
|
+
# add links
|
|
2017
|
+
collection_dict.setdefault("links", [])
|
|
2018
|
+
collection_dict["links"].append(
|
|
2019
|
+
{
|
|
2020
|
+
"rel": "self",
|
|
2021
|
+
"href": f"{collection}.json",
|
|
2022
|
+
"type": "application/json",
|
|
2023
|
+
},
|
|
2024
|
+
)
|
|
2025
|
+
with open(Path(filename).parent / f"{collection}.json", "w") as fh:
|
|
2026
|
+
geojson.dump(collection_dict, fh)
|
|
2027
|
+
logger.debug("Collection '%s' saved to %s", collection, fh.name)
|
|
2028
|
+
|
|
1991
2029
|
return filename
|
|
1992
2030
|
|
|
1993
2031
|
@staticmethod
|
|
@@ -2026,6 +2064,7 @@ class EODataAccessGateway:
|
|
|
2026
2064
|
self,
|
|
2027
2065
|
product: EOProduct,
|
|
2028
2066
|
progress_callback: Optional[ProgressCallback] = None,
|
|
2067
|
+
executor: Optional[ThreadPoolExecutor] = None,
|
|
2029
2068
|
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
2030
2069
|
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
2031
2070
|
**kwargs: Unpack[DownloadConf],
|
|
@@ -2056,6 +2095,8 @@ class EODataAccessGateway:
|
|
|
2056
2095
|
size as inputs and handle progress bar
|
|
2057
2096
|
creation and update to give the user a
|
|
2058
2097
|
feedback on the download progress
|
|
2098
|
+
:param executor: (optional) An executor to download assets of ``product`` in parallel if it has any. If ``None``
|
|
2099
|
+
, a default executor will be created
|
|
2059
2100
|
:param wait: (optional) If download fails, wait time in minutes between
|
|
2060
2101
|
two download tries
|
|
2061
2102
|
:param timeout: (optional) If download fails, maximum time in minutes
|
|
@@ -2080,7 +2121,11 @@ class EODataAccessGateway:
|
|
|
2080
2121
|
return uri_to_path(product.location)
|
|
2081
2122
|
self._setup_downloader(product)
|
|
2082
2123
|
path = product.download(
|
|
2083
|
-
progress_callback=progress_callback,
|
|
2124
|
+
progress_callback=progress_callback,
|
|
2125
|
+
executor=executor,
|
|
2126
|
+
wait=wait,
|
|
2127
|
+
timeout=timeout,
|
|
2128
|
+
**kwargs,
|
|
2084
2129
|
)
|
|
2085
2130
|
|
|
2086
2131
|
return path
|
|
@@ -2184,7 +2229,8 @@ class EODataAccessGateway:
|
|
|
2184
2229
|
|
|
2185
2230
|
# use queryables aliases
|
|
2186
2231
|
kwargs_alias = {**kwargs}
|
|
2187
|
-
|
|
2232
|
+
queryables_fields = Queryables.from_stac_models().model_fields
|
|
2233
|
+
for search_param, field_info in queryables_fields.items():
|
|
2188
2234
|
if search_param in kwargs and field_info.alias:
|
|
2189
2235
|
kwargs_alias[field_info.alias] = kwargs_alias.pop(search_param)
|
|
2190
2236
|
|
eodag/api/product/_assets.py
CHANGED
eodag/api/product/_product.py
CHANGED
|
@@ -23,14 +23,18 @@ import os
|
|
|
23
23
|
import re
|
|
24
24
|
import tempfile
|
|
25
25
|
from datetime import datetime
|
|
26
|
-
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast
|
|
27
27
|
|
|
28
|
+
import orjson
|
|
28
29
|
import requests
|
|
29
30
|
from requests import RequestException
|
|
30
31
|
from requests.auth import AuthBase
|
|
31
32
|
from shapely import geometry
|
|
32
33
|
from shapely.errors import ShapelyError
|
|
33
34
|
|
|
35
|
+
from eodag.types.queryables import CommonStacMetadata
|
|
36
|
+
from eodag.types.stac_metadata import create_stac_metadata_model
|
|
37
|
+
|
|
34
38
|
try:
|
|
35
39
|
# import from eodag-cube if installed
|
|
36
40
|
from eodag_cube.api.product import ( # pyright: ignore[reportMissingImports]
|
|
@@ -52,6 +56,7 @@ from eodag.utils import (
|
|
|
52
56
|
DEFAULT_DOWNLOAD_WAIT,
|
|
53
57
|
DEFAULT_SHAPELY_GEOMETRY,
|
|
54
58
|
DEFAULT_STREAM_REQUESTS_TIMEOUT,
|
|
59
|
+
STAC_VERSION,
|
|
55
60
|
USER_AGENT,
|
|
56
61
|
ProgressCallback,
|
|
57
62
|
format_string,
|
|
@@ -61,6 +66,7 @@ from eodag.utils.exceptions import DownloadError, MisconfiguredError, Validation
|
|
|
61
66
|
from eodag.utils.repr import dict_to_html_table
|
|
62
67
|
|
|
63
68
|
if TYPE_CHECKING:
|
|
69
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
64
70
|
from shapely.geometry.base import BaseGeometry
|
|
65
71
|
|
|
66
72
|
from eodag.api.product.drivers.base import DatasetDriver
|
|
@@ -122,6 +128,8 @@ class EOProduct:
|
|
|
122
128
|
search_kwargs: Any
|
|
123
129
|
#: Datetime for download next try
|
|
124
130
|
next_try: datetime
|
|
131
|
+
#: Stream for requests
|
|
132
|
+
_stream: requests.Response
|
|
125
133
|
|
|
126
134
|
def __init__(
|
|
127
135
|
self, provider: str, properties: dict[str, Any], **kwargs: Any
|
|
@@ -143,6 +151,13 @@ class EOProduct:
|
|
|
143
151
|
and not key.startswith("_")
|
|
144
152
|
and value is not None
|
|
145
153
|
}
|
|
154
|
+
self.properties.setdefault(
|
|
155
|
+
"datetime",
|
|
156
|
+
self.properties.get("start_datetime")
|
|
157
|
+
or self.properties.get("end_datetime"),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# sort properties to have common stac properties first
|
|
146
161
|
common_stac_properties = {
|
|
147
162
|
key: self.properties[key]
|
|
148
163
|
for key in sorted(self.properties)
|
|
@@ -202,25 +217,71 @@ class EOProduct:
|
|
|
202
217
|
"""
|
|
203
218
|
search_intersection = None
|
|
204
219
|
if self.search_intersection is not None:
|
|
205
|
-
search_intersection =
|
|
220
|
+
search_intersection = orjson.loads(
|
|
221
|
+
orjson.dumps(self.search_intersection.__geo_interface__)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# product properties
|
|
225
|
+
stac_properties = {
|
|
226
|
+
**{
|
|
227
|
+
key: value
|
|
228
|
+
for key, value in self.properties.items()
|
|
229
|
+
if key not in ("geometry", "id")
|
|
230
|
+
},
|
|
231
|
+
"eodag:provider": self.provider,
|
|
232
|
+
"eodag:search_intersection": search_intersection,
|
|
233
|
+
}
|
|
234
|
+
stac_providers = self.properties.get("providers", [])
|
|
235
|
+
if not any("host" in p.get("roles", []) for p in stac_providers):
|
|
236
|
+
stac_providers.append({"name": self.provider, "roles": ["host"]})
|
|
237
|
+
stac_properties["providers"] = stac_providers
|
|
238
|
+
|
|
239
|
+
props_model = cast(type[CommonStacMetadata], create_stac_metadata_model())
|
|
240
|
+
props_validated = props_model.safe_validate(stac_properties)
|
|
241
|
+
stac_extensions: set[str] = set(props_validated.get_conformance_classes())
|
|
242
|
+
|
|
243
|
+
# skip invalid properties
|
|
244
|
+
invalid_properties = {
|
|
245
|
+
k
|
|
246
|
+
for k in stac_properties.keys()
|
|
247
|
+
if k not in props_validated.to_dict() and props_model.has_field(k)
|
|
248
|
+
}
|
|
249
|
+
for key in invalid_properties:
|
|
250
|
+
stac_properties.pop(key, None)
|
|
251
|
+
|
|
252
|
+
# get conformance classes for assets properties
|
|
253
|
+
assets_dict = {**self.assets.as_dict()}
|
|
254
|
+
for asset_key, asset_properties in self.assets.as_dict().items():
|
|
255
|
+
asset_props_validated = props_model.safe_validate(asset_properties)
|
|
256
|
+
stac_extensions.update(asset_props_validated.get_conformance_classes())
|
|
257
|
+
|
|
258
|
+
# skip invalid assets properties
|
|
259
|
+
invalid_asset_properties = {
|
|
260
|
+
k
|
|
261
|
+
for k in asset_properties.keys()
|
|
262
|
+
if k not in asset_props_validated.to_dict() and props_model.has_field(k)
|
|
263
|
+
}
|
|
264
|
+
for key in invalid_asset_properties:
|
|
265
|
+
assets_dict[asset_key].pop(key, None)
|
|
206
266
|
|
|
207
267
|
geojson_repr: dict[str, Any] = {
|
|
208
268
|
"type": "Feature",
|
|
209
|
-
"geometry":
|
|
269
|
+
"geometry": orjson.loads(orjson.dumps(self.geometry.__geo_interface__)),
|
|
270
|
+
"bbox": list(self.geometry.bounds),
|
|
210
271
|
"id": self.properties["id"],
|
|
211
|
-
"assets":
|
|
212
|
-
"properties":
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for key, value in self.properties.items()
|
|
219
|
-
if key not in ("geometry", "id")
|
|
272
|
+
"assets": assets_dict,
|
|
273
|
+
"properties": stac_properties,
|
|
274
|
+
"links": [
|
|
275
|
+
{
|
|
276
|
+
"rel": "collection",
|
|
277
|
+
"href": f"{self.collection}.json",
|
|
278
|
+
"type": "application/json",
|
|
220
279
|
},
|
|
221
|
-
|
|
280
|
+
],
|
|
281
|
+
"stac_extensions": list(stac_extensions),
|
|
282
|
+
"stac_version": STAC_VERSION,
|
|
283
|
+
"collection": self.collection,
|
|
222
284
|
}
|
|
223
|
-
|
|
224
285
|
return geojson_repr
|
|
225
286
|
|
|
226
287
|
@classmethod
|
|
@@ -234,11 +295,11 @@ class EOProduct:
|
|
|
234
295
|
:raises: :class:`~eodag.utils.exceptions.ValidationError`
|
|
235
296
|
"""
|
|
236
297
|
try:
|
|
298
|
+
collection = feature.get("collection")
|
|
237
299
|
properties = feature["properties"]
|
|
238
300
|
properties["geometry"] = feature["geometry"]
|
|
239
301
|
properties["id"] = feature["id"]
|
|
240
302
|
provider = properties.pop("eodag:provider")
|
|
241
|
-
collection = properties.pop("eodag:collection")
|
|
242
303
|
search_intersection = properties.pop("eodag:search_intersection")
|
|
243
304
|
except KeyError as e:
|
|
244
305
|
raise ValidationError(
|
|
@@ -337,6 +398,7 @@ class EOProduct:
|
|
|
337
398
|
def download(
|
|
338
399
|
self,
|
|
339
400
|
progress_callback: Optional[ProgressCallback] = None,
|
|
401
|
+
executor: Optional[ThreadPoolExecutor] = None,
|
|
340
402
|
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
341
403
|
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
342
404
|
**kwargs: Unpack[DownloadConf],
|
|
@@ -353,6 +415,8 @@ class EOProduct:
|
|
|
353
415
|
size as inputs and handle progress bar
|
|
354
416
|
creation and update to give the user a
|
|
355
417
|
feedback on the download progress
|
|
418
|
+
:param executor: (optional) An executor to download assets of the product in parallel if it has any. If ``None``
|
|
419
|
+
, a default executor will be created
|
|
356
420
|
:param wait: (optional) If download fails, wait time in minutes between
|
|
357
421
|
two download tries
|
|
358
422
|
:param timeout: (optional) If download fails, maximum time in minutes
|
|
@@ -377,17 +441,26 @@ class EOProduct:
|
|
|
377
441
|
)
|
|
378
442
|
|
|
379
443
|
progress_callback, close_progress_callback = self._init_progress_bar(
|
|
380
|
-
progress_callback
|
|
444
|
+
progress_callback, executor
|
|
381
445
|
)
|
|
446
|
+
|
|
382
447
|
fs_path = self.downloader.download(
|
|
383
448
|
self,
|
|
384
449
|
auth=auth,
|
|
385
450
|
progress_callback=progress_callback,
|
|
451
|
+
executor=executor,
|
|
386
452
|
wait=wait,
|
|
387
453
|
timeout=timeout,
|
|
388
454
|
**kwargs,
|
|
389
455
|
)
|
|
390
456
|
|
|
457
|
+
# shutdown executor if it was not created during parallel product downloads
|
|
458
|
+
if (
|
|
459
|
+
executor is not None
|
|
460
|
+
and executor._thread_name_prefix != "eodag-download-all"
|
|
461
|
+
):
|
|
462
|
+
executor.shutdown(wait=True)
|
|
463
|
+
|
|
391
464
|
# close progress bar if needed
|
|
392
465
|
if close_progress_callback:
|
|
393
466
|
progress_callback.close()
|
|
@@ -408,15 +481,22 @@ class EOProduct:
|
|
|
408
481
|
return fs_path
|
|
409
482
|
|
|
410
483
|
def _init_progress_bar(
|
|
411
|
-
self,
|
|
484
|
+
self,
|
|
485
|
+
progress_callback: Optional[ProgressCallback],
|
|
486
|
+
executor: Optional[ThreadPoolExecutor],
|
|
412
487
|
) -> tuple[ProgressCallback, bool]:
|
|
488
|
+
# determine position of the progress bar with a counter of executor passings
|
|
489
|
+
# to avoid bar overwriting in case of parallel downloads
|
|
490
|
+
count = executor._counter() if executor is not None else 1 # type: ignore
|
|
491
|
+
|
|
413
492
|
# progress bar init
|
|
414
493
|
if progress_callback is None:
|
|
415
|
-
progress_callback = ProgressCallback(position=
|
|
494
|
+
progress_callback = ProgressCallback(position=count)
|
|
416
495
|
# one shot progress callback to close after download
|
|
417
496
|
close_progress_callback = True
|
|
418
497
|
else:
|
|
419
498
|
close_progress_callback = False
|
|
499
|
+
progress_callback.pos = count
|
|
420
500
|
# update units as bar may have been previously used for extraction
|
|
421
501
|
progress_callback.unit = "B"
|
|
422
502
|
progress_callback.unit_scale = True
|
|
@@ -641,3 +721,38 @@ class EOProduct:
|
|
|
641
721
|
<td {thumbnail_style} title='properties["thumbnail"]'>{thumbnail_html}</td>
|
|
642
722
|
</tr>
|
|
643
723
|
</table>"""
|
|
724
|
+
|
|
725
|
+
def to_xarray(
|
|
726
|
+
self,
|
|
727
|
+
asset_key: Optional[str] = None,
|
|
728
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
729
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
730
|
+
roles: Iterable[str] = {"data", "data-mask"},
|
|
731
|
+
**xarray_kwargs: Any,
|
|
732
|
+
):
|
|
733
|
+
"""
|
|
734
|
+
Return product data as a dictionary of :class:`xarray.Dataset`.
|
|
735
|
+
|
|
736
|
+
:param asset_key: (optional) key of the asset. If not specified the whole
|
|
737
|
+
product data will be retrieved
|
|
738
|
+
:param wait: (optional) If order is needed, wait time in minutes between two
|
|
739
|
+
order status check
|
|
740
|
+
:param timeout: (optional) If order is needed, maximum time in minutes before
|
|
741
|
+
stop checking order status
|
|
742
|
+
:param roles: (optional) roles of assets that must be fetched
|
|
743
|
+
:param xarray_kwargs: (optional) keyword arguments passed to :func:`xarray.open_dataset`
|
|
744
|
+
:returns: a dictionary of :class:`xarray.Dataset`
|
|
745
|
+
"""
|
|
746
|
+
raise NotImplementedError("Install eodag-cube to make this method available.")
|
|
747
|
+
|
|
748
|
+
def augment_from_xarray(
|
|
749
|
+
self,
|
|
750
|
+
roles: Iterable[str] = {"data", "data-mask"},
|
|
751
|
+
) -> EOProduct:
|
|
752
|
+
"""
|
|
753
|
+
Annotate the product properties and assets with STAC metadata got by fetching its xarray representation.
|
|
754
|
+
|
|
755
|
+
:param roles: (optional) roles of assets that must be fetched
|
|
756
|
+
:returns: updated EOProduct
|
|
757
|
+
"""
|
|
758
|
+
raise NotImplementedError("Install eodag-cube to make this method available.")
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"""EODAG drivers package"""
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
-
from typing import Callable
|
|
21
|
+
from typing import Callable
|
|
22
|
+
|
|
23
|
+
from typing_extensions import TypedDict
|
|
22
24
|
|
|
23
25
|
from eodag.api.product.drivers.base import DatasetDriver
|
|
24
26
|
from eodag.api.product.drivers.generic import GenericDriver
|
|
@@ -19,7 +19,9 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import re
|
|
22
|
-
from typing import TYPE_CHECKING, Optional
|
|
22
|
+
from typing import TYPE_CHECKING, Optional
|
|
23
|
+
|
|
24
|
+
from typing_extensions import TypedDict
|
|
23
25
|
|
|
24
26
|
if TYPE_CHECKING:
|
|
25
27
|
from eodag.api.product import EOProduct
|
|
@@ -36,7 +36,7 @@ class GenericDriver(DatasetDriver):
|
|
|
36
36
|
(
|
|
37
37
|
r"^(?:.*[/\\])?([^/\\]+)"
|
|
38
38
|
r"(\.jp2|\.tiff?|\.dat|\.nc|\.grib2?|"
|
|
39
|
-
r"\.zarr|\.nat|\.covjson|\.parquet|\.zip|\.tar|\.gz)
|
|
39
|
+
r"\.zarr|\.nat|\.covjson|\.parquet|\.zip|\.tar|\.gz)(?:\?.*)?$"
|
|
40
40
|
),
|
|
41
41
|
re.IGNORECASE,
|
|
42
42
|
),
|
|
@@ -45,25 +45,29 @@ class GenericDriver(DatasetDriver):
|
|
|
45
45
|
# metadata
|
|
46
46
|
{
|
|
47
47
|
"pattern": re.compile(
|
|
48
|
-
r"^(?:.*[/\\])?([^/\\]+)(\.xml|\.xsd|\.safe|\.json)
|
|
48
|
+
r"^(?:.*[/\\])?([^/\\]+)(\.xml|\.xsd|\.safe|\.json)(?:\?.*)?$",
|
|
49
|
+
re.IGNORECASE,
|
|
49
50
|
),
|
|
50
51
|
"roles": ["metadata"],
|
|
51
52
|
},
|
|
52
53
|
# thumbnail
|
|
53
54
|
{
|
|
54
55
|
"pattern": re.compile(
|
|
55
|
-
r"^(?:.*[/\\])?(thumbnail)(\.jpg|\.jpeg|\.png)
|
|
56
|
+
r"^(?:.*[/\\])?(thumbnail)(\.jpg|\.jpeg|\.png)(?:\?.*)?$", re.IGNORECASE
|
|
56
57
|
),
|
|
57
58
|
"roles": ["thumbnail"],
|
|
58
59
|
},
|
|
59
60
|
# quicklook
|
|
60
61
|
{
|
|
61
62
|
"pattern": re.compile(
|
|
62
|
-
r"^(?:.*[/\\])?([^/\\]+-ql|preview|quick-?look)(\.jpg|\.jpeg|\.png)
|
|
63
|
+
r"^(?:.*[/\\])?([^/\\]+-ql|preview|quick-?look)(\.jpg|\.jpeg|\.png)(?:\?.*)?$",
|
|
63
64
|
re.IGNORECASE,
|
|
64
65
|
),
|
|
65
66
|
"roles": ["overview"],
|
|
66
67
|
},
|
|
67
68
|
# default
|
|
68
|
-
{
|
|
69
|
+
{
|
|
70
|
+
"pattern": re.compile(r"^(?:.*[/\\])?([^/\\?]+)(?:\?.*)?$"),
|
|
71
|
+
"roles": ["auxiliary"],
|
|
72
|
+
},
|
|
69
73
|
]
|