eodag 4.0.0a5__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 (38) hide show
  1. eodag/api/collection.py +65 -1
  2. eodag/api/core.py +48 -16
  3. eodag/api/product/_assets.py +1 -1
  4. eodag/api/product/_product.py +108 -15
  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 +7 -7
  14. eodag/config.py +22 -4
  15. eodag/plugins/download/aws.py +3 -1
  16. eodag/plugins/download/http.py +4 -10
  17. eodag/plugins/search/base.py +8 -3
  18. eodag/plugins/search/build_search_result.py +108 -120
  19. eodag/plugins/search/cop_marine.py +3 -1
  20. eodag/plugins/search/qssearch.py +7 -6
  21. eodag/resources/collections.yml +255 -0
  22. eodag/resources/ext_collections.json +1 -1
  23. eodag/resources/ext_product_types.json +1 -1
  24. eodag/resources/providers.yml +60 -25
  25. eodag/resources/user_conf_template.yml +6 -0
  26. eodag/types/__init__.py +22 -16
  27. eodag/types/download_args.py +3 -1
  28. eodag/types/queryables.py +125 -55
  29. eodag/types/stac_extensions.py +408 -0
  30. eodag/types/stac_metadata.py +312 -0
  31. eodag/utils/__init__.py +42 -4
  32. eodag/utils/dates.py +202 -2
  33. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/METADATA +7 -13
  34. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/RECORD +38 -36
  35. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/WHEEL +1 -1
  36. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/entry_points.txt +1 -1
  37. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
  38. {eodag-4.0.0a5.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
@@ -154,11 +155,14 @@ class EODataAccessGateway:
154
155
  user_conf_file_path = os.getenv(env_var_name)
155
156
  if user_conf_file_path is None:
156
157
  user_conf_file_path = standard_configuration_path
157
- 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
+ ):
158
164
  shutil.copy(
159
- str(
160
- res_files("eodag") / "resources" / "user_conf_template.yml"
161
- ),
165
+ source,
162
166
  standard_configuration_path,
163
167
  )
164
168
  self._providers.update_from_config_file(user_conf_file_path)
@@ -1783,14 +1787,15 @@ class EODataAccessGateway:
1783
1787
 
1784
1788
  # remove None values and convert param names to their pydantic alias if any
1785
1789
  search_params = {}
1790
+ queryables_fields = Queryables.from_stac_models().model_fields
1786
1791
  ecmwf_queryables = [
1787
1792
  f"{ECMWF_PREFIX[:-1]}_{k}" for k in ECMWF_ALLOWED_KEYWORDS
1788
1793
  ]
1789
1794
  for param, value in kwargs.items():
1790
1795
  if value is None:
1791
1796
  continue
1792
- if param in Queryables.model_fields:
1793
- param_alias = Queryables.model_fields[param].alias or param
1797
+ if param in queryables_fields:
1798
+ param_alias = queryables_fields[param].alias or param
1794
1799
  search_params[param_alias] = value
1795
1800
  elif param in ecmwf_queryables:
1796
1801
  # alias equivalent for ECMWF queryables
@@ -1849,14 +1854,6 @@ class EODataAccessGateway:
1849
1854
  else:
1850
1855
  eo_product.collection = guesses[0].id
1851
1856
 
1852
- try:
1853
- if eo_product.collection is not None:
1854
- eo_product.collection = self.get_collection_from_alias(
1855
- eo_product.collection
1856
- )
1857
- except NoMatchingCollection:
1858
- logger.debug("collection %s not found", eo_product.collection)
1859
-
1860
1857
  if eo_product.search_intersection is not None:
1861
1858
  eo_product._register_downloader_from_manager(self._plugins_manager)
1862
1859
 
@@ -1993,8 +1990,42 @@ class EODataAccessGateway:
1993
1990
  :param filename: (optional) The name of the file to generate
1994
1991
  :returns: The name of the created file
1995
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
1996
2004
  with open(filename, "w") as fh:
1997
- 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
+
1998
2029
  return filename
1999
2030
 
2000
2031
  @staticmethod
@@ -2198,7 +2229,8 @@ class EODataAccessGateway:
2198
2229
 
2199
2230
  # use queryables aliases
2200
2231
  kwargs_alias = {**kwargs}
2201
- 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():
2202
2234
  if search_param in kwargs and field_info.alias:
2203
2235
  kwargs_alias[field_info.alias] = kwargs_alias.pop(search_param)
2204
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,
@@ -146,6 +151,13 @@ class EOProduct:
146
151
  and not key.startswith("_")
147
152
  and value is not None
148
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
149
161
  common_stac_properties = {
150
162
  key: self.properties[key]
151
163
  for key in sorted(self.properties)
@@ -205,25 +217,71 @@ class EOProduct:
205
217
  """
206
218
  search_intersection = None
207
219
  if self.search_intersection is not None:
208
- 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)
209
266
 
210
267
  geojson_repr: dict[str, Any] = {
211
268
  "type": "Feature",
212
- "geometry": geometry.mapping(self.geometry),
269
+ "geometry": orjson.loads(orjson.dumps(self.geometry.__geo_interface__)),
270
+ "bbox": list(self.geometry.bounds),
213
271
  "id": self.properties["id"],
214
- "assets": self.assets.as_dict(),
215
- "properties": {
216
- "eodag:collection": self.collection,
217
- "eodag:provider": self.provider,
218
- "eodag:search_intersection": search_intersection,
219
- **{
220
- key: value
221
- for key, value in self.properties.items()
222
- 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",
223
279
  },
224
- },
280
+ ],
281
+ "stac_extensions": list(stac_extensions),
282
+ "stac_version": STAC_VERSION,
283
+ "collection": self.collection,
225
284
  }
226
-
227
285
  return geojson_repr
228
286
 
229
287
  @classmethod
@@ -237,11 +295,11 @@ class EOProduct:
237
295
  :raises: :class:`~eodag.utils.exceptions.ValidationError`
238
296
  """
239
297
  try:
298
+ collection = feature.get("collection")
240
299
  properties = feature["properties"]
241
300
  properties["geometry"] = feature["geometry"]
242
301
  properties["id"] = feature["id"]
243
302
  provider = properties.pop("eodag:provider")
244
- collection = properties.pop("eodag:collection")
245
303
  search_intersection = properties.pop("eodag:search_intersection")
246
304
  except KeyError as e:
247
305
  raise ValidationError(
@@ -663,3 +721,38 @@ class EOProduct:
663
721
  <td {thumbnail_style} title='properties[&quot;thumbnail&quot;]'>{thumbnail_html}</td>
664
722
  </tr>
665
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
  ]
@@ -38,12 +38,13 @@ class Sentinel1Driver(DatasetDriver):
38
38
  (re.compile(r"grd", re.IGNORECASE), ""),
39
39
  (re.compile(r"slc", re.IGNORECASE), ""),
40
40
  (re.compile(r"ocn", re.IGNORECASE), ""),
41
- (re.compile(r"iw", re.IGNORECASE), ""),
42
- (re.compile(r"ew", re.IGNORECASE), ""),
41
+ (re.compile(r"(?<![A-Za-z])iw(?![A-Za-z])", re.IGNORECASE), ""),
42
+ (re.compile(r"(?<![A-Za-z])ew(?![A-Za-z])", re.IGNORECASE), ""),
43
43
  (re.compile(r"wv", re.IGNORECASE), ""),
44
- (re.compile(r"sm", re.IGNORECASE), ""),
45
- (re.compile(r"raw([-_]s)?", re.IGNORECASE), ""),
44
+ (re.compile(r"(?<![A-Za-z])sm(?![A-Za-z])", re.IGNORECASE), ""),
45
+ (re.compile(r"(?<![A-Za-z])raw([-_]s)?(?![A-Za-z])", re.IGNORECASE), ""),
46
46
  (re.compile(r"[t?0-9]{3,}", re.IGNORECASE), ""),
47
+ (re.compile(r"\b[0-9A-F]{3,}\b", re.IGNORECASE), ""),
47
48
  (re.compile(r"-+"), "-"),
48
49
  (re.compile(r"-+\."), "."),
49
50
  (re.compile(r"_+"), "_"),
@@ -55,34 +56,38 @@ class Sentinel1Driver(DatasetDriver):
55
56
  # data
56
57
  {
57
58
  "pattern": re.compile(
58
- r"^.*?([vh]{2}).*\.(?:jp2|tiff?|dat)$", re.IGNORECASE
59
+ r"^.*?([vh]{2}).*\.(?:jp2|tiff?|dat)(?:\?.*)?$", re.IGNORECASE
59
60
  ),
60
61
  "roles": ["data"],
61
62
  },
62
63
  # metadata
63
64
  {
64
65
  "pattern": re.compile(
65
- r"^(?:.*[/\\])?([^/\\]+)(\.xml|\.xsd|\.safe|\.json)$", re.IGNORECASE
66
+ r"^(?:.*[/\\])?([^/\\]+)(\.xml|\.xsd|\.safe|\.json)(?:\?.*)?$",
67
+ re.IGNORECASE,
66
68
  ),
67
69
  "roles": ["metadata"],
68
70
  },
69
71
  # thumbnail
70
72
  {
71
73
  "pattern": re.compile(
72
- r"^(?:.*[/\\])?(thumbnail)(\.jpe?g|\.png)$", re.IGNORECASE
74
+ r"^(?:.*[/\\])?(thumbnail)(\.jpe?g|\.png)(?:\?.*)?$", re.IGNORECASE
73
75
  ),
74
76
  "roles": ["thumbnail"],
75
77
  },
76
78
  # quicklook
77
79
  {
78
80
  "pattern": re.compile(
79
- r"^(?:.*[/\\])?([^/\\]+-ql|preview|quick-?look)(\.jpe?g|\.png)$",
81
+ r"^(?:.*[/\\])?([^/\\]+-ql|preview|quick-?look)(\.jpe?g|\.png)(?:\?.*)?$",
80
82
  re.IGNORECASE,
81
83
  ),
82
84
  "roles": ["overview"],
83
85
  },
84
86
  # default
85
- {"pattern": re.compile(r"^(?:.*[/\\])?([^/\\]+)$"), "roles": ["auxiliary"]},
87
+ {
88
+ "pattern": re.compile(r"^(?:.*[/\\])?([^/\\?]+)(?:\?.*)?$"),
89
+ "roles": ["auxiliary"],
90
+ },
86
91
  ]
87
92
 
88
93
  def _normalize_key(self, key: str, eo_product: EOProduct) -> str:
@@ -40,47 +40,54 @@ class Sentinel2Driver(DatasetDriver):
40
40
  ASSET_KEYS_PATTERNS_ROLES: list[AssetPatterns] = [
41
41
  # masks
42
42
  {
43
- "pattern": re.compile(r"^.*?(MSK_[^/\\]+)\.(?:jp2|tiff?)$", re.IGNORECASE),
43
+ "pattern": re.compile(
44
+ r"^.*?(MSK_[^/\\]+)\.(?:jp2|tiff?)(?:\?.*)?$", re.IGNORECASE
45
+ ),
44
46
  "roles": ["data-mask"],
45
47
  },
46
48
  # visual
47
49
  {
48
50
  "pattern": re.compile(
49
- r"^.*?(TCI)(_[0-9]+m)?\.(?:jp2|tiff?)$", re.IGNORECASE
51
+ r"^.*?(TCI)(_[0-9]+m)?\.(?:jp2|tiff?)(?:\?.*)?$", re.IGNORECASE
50
52
  ),
51
53
  "roles": ["visual"],
52
54
  },
53
55
  # bands
54
56
  {
55
57
  "pattern": re.compile(
56
- r"^.*?([A-Z]+[0-9]*[A-Z]?)(_[0-9]+m)?\.(?:jp2|tiff?)$", re.IGNORECASE
58
+ r"^.*?([A-Z]+[0-9]*[A-Z]?)(_[0-9]+m)?\.(?:jp2|tiff?)(?:\?.*)?$",
59
+ re.IGNORECASE,
57
60
  ),
58
61
  "roles": ["data"],
59
62
  },
60
63
  # metadata
61
64
  {
62
65
  "pattern": re.compile(
63
- r"^(?:.*[/\\])?([^/\\]+)(\.xml|\.xsd|\.safe|\.json)$", re.IGNORECASE
66
+ r"^(?:.*[/\\])?([^/\\]+)(\.xml|\.xsd|\.safe|\.json)(?:\?.*)?$",
67
+ re.IGNORECASE,
64
68
  ),
65
69
  "roles": ["metadata"],
66
70
  },
67
71
  # thumbnail
68
72
  {
69
73
  "pattern": re.compile(
70
- r"^(?:.*[/\\])?(thumbnail)(\.jpe?g|\.png)$", re.IGNORECASE
74
+ r"^(?:.*[/\\])?(thumbnail)(\.jpe?g|\.png)(?:\?.*)?$", re.IGNORECASE
71
75
  ),
72
76
  "roles": ["thumbnail"],
73
77
  },
74
78
  # quicklook
75
79
  {
76
80
  "pattern": re.compile(
77
- r"^(?:.*[/\\])?[^/\\]+(-ql|preview|quick-?look)(\.jpe?g|\.png)$",
81
+ r"^(?:.*[/\\])?[^/\\]+(-ql|preview|quick-?look)(\.jpe?g|\.png)(?:\?.*)?$",
78
82
  re.IGNORECASE,
79
83
  ),
80
84
  "roles": ["overview"],
81
85
  },
82
86
  # default
83
- {"pattern": re.compile(r"^(?:.*[/\\])?([^/\\]+)$"), "roles": ["auxiliary"]},
87
+ {
88
+ "pattern": re.compile(r"^(?:.*[/\\])?([^/\\?]+)(?:\?.*)?$"),
89
+ "roles": ["auxiliary"],
90
+ },
84
91
  ]
85
92
 
86
93
  def _normalize_key(self, key: str, eo_product: EOProduct) -> str:
@@ -1731,16 +1731,19 @@ def get_queryable_from_provider(
1731
1731
  mapping_values = [
1732
1732
  v[0] if isinstance(v, list) else "" for v in metadata_mapping.values()
1733
1733
  ]
1734
+ StacQueryables = Queryables.from_stac_models()
1734
1735
  if provider_queryable in mapping_values:
1735
1736
  ind = mapping_values.index(provider_queryable)
1736
- return Queryables.get_queryable_from_alias(list(metadata_mapping.keys())[ind])
1737
+ return StacQueryables.get_queryable_from_alias(
1738
+ list(metadata_mapping.keys())[ind]
1739
+ )
1737
1740
  for param, param_conf in metadata_mapping.items():
1738
1741
  if (
1739
1742
  isinstance(param_conf, list)
1740
1743
  and param_conf[0]
1741
1744
  and re.search(pattern, param_conf[0])
1742
1745
  ):
1743
- return Queryables.get_queryable_from_alias(param)
1746
+ return StacQueryables.get_queryable_from_alias(param)
1744
1747
  return None
1745
1748
 
1746
1749
 
eodag/api/provider.py CHANGED
@@ -451,6 +451,7 @@ class Provider:
451
451
  getattr(
452
452
  self.config, key
453
453
  ).credentials = conf_with_creds.credentials
454
+ return
454
455
 
455
456
  def delete_collection(self, name: str) -> None:
456
457
  """Remove a collection from this provider.
@@ -32,7 +32,7 @@ from eodag.plugins.crunch.filter_latest_intersect import FilterLatestIntersect
32
32
  from eodag.plugins.crunch.filter_latest_tpl_name import FilterLatestByName
33
33
  from eodag.plugins.crunch.filter_overlap import FilterOverlap
34
34
  from eodag.plugins.crunch.filter_property import FilterProperty
35
- from eodag.utils import GENERIC_STAC_PROVIDER, STAC_SEARCH_PLUGINS
35
+ from eodag.utils import GENERIC_STAC_PROVIDER, STAC_SEARCH_PLUGINS, STAC_VERSION
36
36
  from eodag.utils.exceptions import MisconfiguredError
37
37
 
38
38
  if TYPE_CHECKING:
@@ -206,6 +206,9 @@ class SearchResult(UserList[EOProduct]):
206
206
  "eodag:search_params": geojson_search_params or None,
207
207
  "eodag:raise_errors": self.raise_errors,
208
208
  },
209
+ "links": [],
210
+ "stac_extensions": [],
211
+ "stac_version": STAC_VERSION,
209
212
  }
210
213
 
211
214
  def as_shapely_geometry_object(self) -> GeometryCollection: