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.
Files changed (42) hide show
  1. eodag/api/collection.py +65 -1
  2. eodag/api/core.py +65 -19
  3. eodag/api/product/_assets.py +1 -1
  4. eodag/api/product/_product.py +133 -18
  5. eodag/api/product/drivers/__init__.py +3 -1
  6. eodag/api/product/drivers/base.py +3 -1
  7. eodag/api/product/drivers/generic.py +9 -5
  8. eodag/api/product/drivers/sentinel1.py +14 -9
  9. eodag/api/product/drivers/sentinel2.py +14 -7
  10. eodag/api/product/metadata_mapping.py +5 -2
  11. eodag/api/provider.py +1 -0
  12. eodag/api/search_result.py +4 -1
  13. eodag/cli.py +17 -8
  14. eodag/config.py +22 -4
  15. eodag/plugins/apis/ecmwf.py +3 -24
  16. eodag/plugins/apis/usgs.py +3 -24
  17. eodag/plugins/download/aws.py +85 -44
  18. eodag/plugins/download/base.py +117 -41
  19. eodag/plugins/download/http.py +88 -65
  20. eodag/plugins/search/base.py +8 -3
  21. eodag/plugins/search/build_search_result.py +108 -120
  22. eodag/plugins/search/cop_marine.py +3 -1
  23. eodag/plugins/search/qssearch.py +7 -6
  24. eodag/resources/collections.yml +255 -0
  25. eodag/resources/ext_collections.json +1 -1
  26. eodag/resources/ext_product_types.json +1 -1
  27. eodag/resources/providers.yml +62 -25
  28. eodag/resources/user_conf_template.yml +6 -0
  29. eodag/types/__init__.py +22 -16
  30. eodag/types/download_args.py +3 -1
  31. eodag/types/queryables.py +125 -55
  32. eodag/types/stac_extensions.py +408 -0
  33. eodag/types/stac_metadata.py +312 -0
  34. eodag/utils/__init__.py +42 -4
  35. eodag/utils/dates.py +202 -2
  36. eodag/utils/s3.py +4 -4
  37. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/METADATA +7 -13
  38. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/RECORD +42 -40
  39. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/WHEEL +1 -1
  40. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/entry_points.txt +1 -1
  41. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
  42. {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
- if not os.path.isfile(standard_configuration_path):
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
- str(
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 Queryables.model_fields:
1789
- param_alias = Queryables.model_fields[param].alias or param
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 product from several provider
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(search_result.as_geojson_object(), fh)
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, wait=wait, timeout=timeout, **kwargs
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
- for search_param, field_info in Queryables.model_fields.items():
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
 
@@ -129,7 +129,7 @@ class AssetsDict(UserDict):
129
129
  ...
130
130
  }}
131
131
  </summary>
132
- {dict_to_html_table(v, depth=1)}
132
+ {dict_to_html_table(v, depth=2)}
133
133
  </details>
134
134
  </td></tr>
135
135
  """
@@ -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 = geometry.mapping(self.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": geometry.mapping(self.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": self.assets.as_dict(),
212
- "properties": {
213
- "eodag:collection": self.collection,
214
- "eodag:provider": self.provider,
215
- "eodag:search_intersection": search_intersection,
216
- **{
217
- key: value
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, progress_callback: Optional[ProgressCallback]
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=1)
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[&quot;thumbnail&quot;]'>{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, TypedDict
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, TypedDict
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)$", re.IGNORECASE
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)$", re.IGNORECASE
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
- {"pattern": re.compile(r"^(?:.*[/\\])?([^/\\]+)$"), "roles": ["auxiliary"]},
69
+ {
70
+ "pattern": re.compile(r"^(?:.*[/\\])?([^/\\?]+)(?:\?.*)?$"),
71
+ "roles": ["auxiliary"],
72
+ },
69
73
  ]