eodag 3.9.1__py3-none-any.whl → 3.10.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
eodag/api/core.py CHANGED
@@ -56,7 +56,7 @@ from eodag.plugins.search import PreparedSearch
56
56
  from eodag.plugins.search.build_search_result import MeteoblueSearch
57
57
  from eodag.plugins.search.qssearch import PostJsonSearch
58
58
  from eodag.types import model_fields_to_annotated
59
- from eodag.types.queryables import CommonQueryables, QueryablesDict
59
+ from eodag.types.queryables import CommonQueryables, Queryables, QueryablesDict
60
60
  from eodag.utils import (
61
61
  DEFAULT_DOWNLOAD_TIMEOUT,
62
62
  DEFAULT_DOWNLOAD_WAIT,
@@ -2318,8 +2318,14 @@ class EODataAccessGateway:
2318
2318
  plugin.provider,
2319
2319
  )
2320
2320
 
2321
+ # use queryables aliases
2322
+ kwargs_alias = {**kwargs}
2323
+ for search_param, field_info in Queryables.model_fields.items():
2324
+ if search_param in kwargs and field_info.alias:
2325
+ kwargs_alias[field_info.alias] = kwargs_alias.pop(search_param)
2326
+
2321
2327
  plugin_queryables = plugin.list_queryables(
2322
- kwargs,
2328
+ kwargs_alias,
2323
2329
  available_product_types,
2324
2330
  product_type_configs,
2325
2331
  product_type,
@@ -48,6 +48,7 @@ from eodag.api.product.metadata_mapping import (
48
48
  from eodag.utils import (
49
49
  DEFAULT_DOWNLOAD_TIMEOUT,
50
50
  DEFAULT_DOWNLOAD_WAIT,
51
+ DEFAULT_SHAPELY_GEOMETRY,
51
52
  DEFAULT_STREAM_REQUESTS_TIMEOUT,
52
53
  USER_AGENT,
53
54
  ProgressCallback,
@@ -67,12 +68,6 @@ if TYPE_CHECKING:
67
68
  from eodag.types.download_args import DownloadConf
68
69
  from eodag.utils import Unpack
69
70
 
70
- try:
71
- from shapely.errors import GEOSException
72
- except ImportError:
73
- # shapely < 2.0 compatibility
74
- from shapely.errors import TopologicalError as GEOSException
75
-
76
71
 
77
72
  logger = logging.getLogger("eodag.product")
78
73
 
@@ -136,6 +131,7 @@ class EOProduct:
136
131
  if key != "geometry"
137
132
  and value != NOT_MAPPED
138
133
  and NOT_AVAILABLE not in str(value)
134
+ and value is not None
139
135
  }
140
136
  if "geometry" not in properties or (
141
137
  (
@@ -144,17 +140,17 @@ class EOProduct:
144
140
  )
145
141
  and "defaultGeometry" not in properties
146
142
  ):
147
- raise MisconfiguredError(
148
- f"No geometry available to build EOProduct(id={properties.get('id')}, provider={provider})"
149
- )
143
+ product_geometry = DEFAULT_SHAPELY_GEOMETRY
150
144
  elif not properties["geometry"] or properties["geometry"] == NOT_AVAILABLE:
151
145
  product_geometry = properties.pop("defaultGeometry", DEFAULT_GEOMETRY)
152
146
  else:
153
147
  product_geometry = properties["geometry"]
154
148
 
155
- self.geometry = self.search_intersection = get_geometry_from_various(
156
- geometry=product_geometry
157
- )
149
+ geometry_obj = get_geometry_from_various(geometry=product_geometry)
150
+ # whole world as default geometry
151
+ if geometry_obj is None:
152
+ geometry_obj = DEFAULT_SHAPELY_GEOMETRY
153
+ self.geometry = self.search_intersection = geometry_obj
158
154
 
159
155
  self.search_kwargs = kwargs
160
156
  if self.search_kwargs.get("geometry") is not None:
@@ -163,7 +159,7 @@ class EOProduct:
163
159
  )
164
160
  try:
165
161
  self.search_intersection = self.geometry.intersection(searched_geom)
166
- except (GEOSException, ShapelyError):
162
+ except ShapelyError:
167
163
  logger.warning(
168
164
  "Unable to intersect the requested extent: %s with the product "
169
165
  "geometry: %s",
@@ -42,6 +42,7 @@ from shapely.ops import transform
42
42
  from eodag.types.queryables import Queryables
43
43
  from eodag.utils import (
44
44
  DEFAULT_PROJ,
45
+ DEFAULT_SHAPELY_GEOMETRY,
45
46
  _deprecated,
46
47
  deepcopy,
47
48
  dict_items_recursive_apply,
@@ -340,14 +341,16 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
340
341
  @staticmethod
341
342
  def convert_to_bounds(input_geom_unformatted: Any) -> list[float]:
342
343
  input_geom = get_geometry_from_various(geometry=input_geom_unformatted)
344
+ if input_geom is None:
345
+ input_geom = DEFAULT_SHAPELY_GEOMETRY
343
346
  if isinstance(input_geom, MultiPolygon):
344
347
  geoms = [geom for geom in input_geom.geoms]
345
348
  # sort with larger one at first (stac-browser only plots first one)
346
349
  geoms.sort(key=lambda x: x.area, reverse=True)
347
- min_lon = 180
348
- min_lat = 90
349
- max_lon = -180
350
- max_lat = -90
350
+ min_lon = 180.0
351
+ min_lat = 90.0
352
+ max_lon = -180.0
353
+ max_lat = -90.0
351
354
  for geom in geoms:
352
355
  min_lon = min(min_lon, geom.bounds[0])
353
356
  min_lat = min(min_lat, geom.bounds[1])
@@ -24,9 +24,11 @@ import boto3
24
24
  from botocore.exceptions import ClientError, ProfileNotFound
25
25
  from botocore.handlers import disable_signing
26
26
 
27
+ from eodag.api.product._assets import Asset
27
28
  from eodag.plugins.authentication.base import Authentication
28
29
  from eodag.types import S3SessionKwargs
29
- from eodag.utils.exceptions import AuthenticationError
30
+ from eodag.utils import get_bucket_name_and_prefix
31
+ from eodag.utils.exceptions import AuthenticationError, EodagError
30
32
 
31
33
  if TYPE_CHECKING:
32
34
  from mypy_boto3_s3 import S3Client, S3ServiceResource
@@ -283,3 +285,36 @@ class AwsAuth(Authentication):
283
285
  "session": self.s3_session,
284
286
  **rio_env_kwargs,
285
287
  }
288
+
289
+ def presign_url(
290
+ self,
291
+ asset: Asset,
292
+ expires_in: int = 3600,
293
+ ) -> str:
294
+ """This method is used to presign a url to download an asset from S3.
295
+
296
+ :param asset: asset for which the url shall be presigned
297
+ :param expires_in: expiration time of the presigned url in seconds
298
+ :returns: presigned url
299
+ :raises: :class:`~eodag.utils.exceptions.EodagError`
300
+ :raises: :class:`NotImplementedError`
301
+ """
302
+ if not getattr(self.config, "support_presign_url", True):
303
+ raise NotImplementedError(
304
+ f"presign_url is not supported for provider {self.provider}"
305
+ )
306
+
307
+ s3_client = self.get_s3_client()
308
+ bucket, prefix = get_bucket_name_and_prefix(asset["href"])
309
+ try:
310
+ presigned_url = s3_client.generate_presigned_url(
311
+ "get_object",
312
+ Params={
313
+ "Bucket": bucket,
314
+ "Key": prefix,
315
+ },
316
+ ExpiresIn=expires_in,
317
+ )
318
+ return presigned_url
319
+ except ClientError:
320
+ raise EodagError(f"Couldn't get a presigned URL for '{asset}'.")
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  from typing import TYPE_CHECKING, Any, Optional, Union
21
21
 
22
+ from eodag.api.product._assets import Asset
22
23
  from eodag.plugins.base import PluginTopic
23
24
  from eodag.utils.exceptions import MisconfiguredError
24
25
 
@@ -80,3 +81,19 @@ class Authentication(PluginTopic):
80
81
  Authenticates with s3 and retrieves the available objects
81
82
  """
82
83
  raise NotImplementedError
84
+
85
+ def presign_url(
86
+ self,
87
+ asset: Asset,
88
+ expires_in: int = 3600,
89
+ ) -> str:
90
+ """This method is used to presign a url to download an asset from S3.
91
+
92
+ :param asset: asset for which the url shall be presigned
93
+ :param expires_in: expiration time of the presigned url in seconds
94
+ :returns: presigned url
95
+ :raises: :class:`NotImplementedError`
96
+ """
97
+ raise NotImplementedError(
98
+ f"presign_url is not implemented for plugin {type(self).__name__}"
99
+ )
@@ -25,6 +25,7 @@ from typing import TYPE_CHECKING, Optional
25
25
  import requests
26
26
  from requests.auth import AuthBase
27
27
 
28
+ from eodag.api.product._assets import Asset
28
29
  from eodag.plugins.authentication.base import Authentication
29
30
  from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, deepcopy, format_dict_items
30
31
  from eodag.utils.exceptions import AuthenticationError, TimeOutError
@@ -143,3 +144,17 @@ class SASAuth(Authentication):
143
144
  ssl_verify=ssl_verify,
144
145
  matching_url=matching_url,
145
146
  )
147
+
148
+ def presign_url(
149
+ self,
150
+ asset: Asset,
151
+ expires_in: int = 3600,
152
+ ) -> str:
153
+ """This method is used to presign a url to download an asset.
154
+
155
+ :param asset: asset for which the url shall be presigned
156
+ :param expires_in: expiration time of the presigned url in seconds
157
+ :returns: presigned url
158
+ """
159
+ url = asset["href"]
160
+ return self.config.auth_uri.format(url=url)
@@ -70,6 +70,7 @@ class FilterLatestIntersect(Crunch):
70
70
  # Warning: May crash if startTimeFromAscendingNode is not in the appropriate format
71
71
  products.sort(key=self.sort_product_by_start_date, reverse=True)
72
72
  filtered: list[EOProduct] = []
73
+ search_extent: BaseGeometry
73
74
  add_to_filtered = filtered.append
74
75
  footprint: Union[dict[str, Any], BaseGeometry, Any] = search_params.get(
75
76
  "geometry"
@@ -20,15 +20,11 @@ from __future__ import annotations
20
20
  import logging
21
21
  from typing import TYPE_CHECKING, Any
22
22
 
23
+ from shapely.errors import ShapelyError
24
+
23
25
  from eodag.plugins.crunch.base import Crunch
24
26
  from eodag.utils import get_geometry_from_various
25
27
 
26
- try:
27
- from shapely.errors import GEOSException
28
- except ImportError:
29
- # shapely < 2.0 compatibility
30
- from shapely.errors import TopologicalError as GEOSException
31
-
32
28
  if TYPE_CHECKING:
33
29
  from eodag.api.product import EOProduct
34
30
 
@@ -108,7 +104,7 @@ class FilterOverlap(Crunch):
108
104
  product_geometry = product.geometry.buffer(0)
109
105
  try:
110
106
  intersection = search_geom.intersection(product_geometry)
111
- except GEOSException:
107
+ except ShapelyError:
112
108
  logger.debug(
113
109
  "Product geometry still invalid. Overlap test restricted to containment"
114
110
  )
@@ -716,7 +716,7 @@ class AwsDownload(Download):
716
716
  ignore_assets,
717
717
  product,
718
718
  )
719
- if auth and isinstance(auth, boto3.resources.base.ServiceResource):
719
+ if auth and isinstance(auth, boto3.resource("s3").__class__):
720
720
  s3_resource = auth
721
721
  else:
722
722
  s3_resource = boto3.resource(
@@ -15,12 +15,19 @@
15
15
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
+ from __future__ import annotations
19
+
18
20
  from typing import Optional
19
21
 
20
22
  from eodag import EOProduct
21
23
  from eodag.plugins.download.aws import AwsDownload
24
+ from eodag.utils import _deprecated
22
25
 
23
26
 
27
+ @_deprecated(
28
+ reason="Plugin that was used in creodias_s3 provider configuration, but not anymore",
29
+ version="3.10.0",
30
+ )
24
31
  class CreodiasS3Download(AwsDownload):
25
32
  """
26
33
  Download on creodias s3 from their VMs (extension of :class:`~eodag.plugins.download.aws.AwsDownload`)
@@ -57,10 +57,16 @@ from eodag.utils import (
57
57
  DEFAULT_SEARCH_TIMEOUT,
58
58
  deepcopy,
59
59
  dict_items_recursive_sort,
60
+ get_geometry_from_ecmwf_area,
61
+ get_geometry_from_ecmwf_feature,
60
62
  get_geometry_from_various,
61
63
  )
62
64
  from eodag.utils.cache import instance_cached_method
63
- from eodag.utils.dates import is_range_in_range
65
+ from eodag.utils.dates import (
66
+ COMPACT_DATE_RANGE_PATTERN,
67
+ DATE_RANGE_PATTERN,
68
+ is_range_in_range,
69
+ )
64
70
  from eodag.utils.exceptions import DownloadError, NotAvailableError, ValidationError
65
71
  from eodag.utils.requests import fetch_json
66
72
 
@@ -145,6 +151,7 @@ COP_DS_KEYWORDS = {
145
151
  "aerosol_type",
146
152
  "altitude",
147
153
  "product_type",
154
+ "area",
148
155
  "band",
149
156
  "cdr_type",
150
157
  "data_format",
@@ -182,6 +189,7 @@ COP_DS_KEYWORDS = {
182
189
  "region",
183
190
  "release_version",
184
191
  "satellite",
192
+ "satellite_mission",
185
193
  "sensor",
186
194
  "sensor_and_algorithm",
187
195
  "soil_level",
@@ -189,6 +197,7 @@ COP_DS_KEYWORDS = {
189
197
  "statistic",
190
198
  "system_version",
191
199
  "temporal_aggregation",
200
+ "temporal_resolution",
192
201
  "time_aggregation",
193
202
  "time_reference",
194
203
  "time_step",
@@ -607,6 +616,14 @@ class ECMWFSearch(PostJsonSearch):
607
616
  # geometry
608
617
  if "geometry" in params:
609
618
  params["geometry"] = get_geometry_from_various(geometry=params["geometry"])
619
+ # ECMWF Polytope uses non-geojson structure for features
620
+ if "feature" in params:
621
+ params["geometry"] = get_geometry_from_ecmwf_feature(params["feature"])
622
+ params.pop("feature")
623
+ # bounding box in area format
624
+ if "area" in params:
625
+ params["geometry"] = get_geometry_from_ecmwf_area(params["area"])
626
+ params.pop("area")
610
627
 
611
628
  return params
612
629
 
@@ -696,10 +713,13 @@ class ECMWFSearch(PostJsonSearch):
696
713
  if "end" in filters:
697
714
  filters[END] = filters.pop("end")
698
715
 
699
- # extract default datetime
700
- processed_filters = self._preprocess_search_params(
701
- deepcopy(filters), product_type
702
- )
716
+ # extract default datetime and convert geometry
717
+ try:
718
+ processed_filters = self._preprocess_search_params(
719
+ deepcopy(filters), product_type
720
+ )
721
+ except Exception as e:
722
+ raise ValidationError(e.args[0]) from e
703
723
 
704
724
  constraints_url = format_metadata(
705
725
  getattr(self.config, "discover_queryables", {}).get("constraints_url", ""),
@@ -714,7 +734,7 @@ class ECMWFSearch(PostJsonSearch):
714
734
  form: list[dict[str, Any]] = self._fetch_data(form_url)
715
735
 
716
736
  formated_filters = self.format_as_provider_keyword(
717
- product_type, processed_filters
737
+ product_type, deepcopy(processed_filters)
718
738
  )
719
739
  # we re-apply kwargs input to consider override of year, month, day and time.
720
740
  for k, v in {**default_values, **kwargs}.items():
@@ -723,7 +743,6 @@ class ECMWFSearch(PostJsonSearch):
723
743
  if key not in ALLOWED_KEYWORDS | {
724
744
  START,
725
745
  END,
726
- "geom",
727
746
  "geometry",
728
747
  }:
729
748
  raise ValidationError(
@@ -772,7 +791,7 @@ class ECMWFSearch(PostJsonSearch):
772
791
  # To check if all keywords are queryable parameters, we check if they are in the
773
792
  # available values or the product type config (available values calculated from the
774
793
  # constraints might not include all queryables)
775
- for keyword in filters:
794
+ for keyword in processed_filters:
776
795
  if (
777
796
  keyword
778
797
  not in available_values.keys()
@@ -780,7 +799,7 @@ class ECMWFSearch(PostJsonSearch):
780
799
  | {
781
800
  START,
782
801
  END,
783
- "geom",
802
+ "geometry",
784
803
  }
785
804
  and keyword not in [f["name"] for f in form]
786
805
  and keyword.removeprefix(ECMWF_PREFIX)
@@ -880,13 +899,21 @@ class ECMWFSearch(PostJsonSearch):
880
899
  )
881
900
 
882
901
  # We convert every single value to a list of string
883
- filter_v = values if isinstance(values, (list, tuple)) else [values]
902
+ filter_v = list(values) if isinstance(values, tuple) else values
903
+ filter_v = filter_v if isinstance(filter_v, list) else [filter_v]
884
904
 
885
905
  # We strip values of superfluous quotes (added by mapping converter to_geojson).
886
- # ECMWF accept values with /to/. We need to split it to an array
887
- # ECMWF accept values in format val1/val2. We need to split it to an array
888
- sep = re.compile(r"/to/|/")
889
- filter_v = [i for v in filter_v for i in sep.split(str(v))]
906
+ # ECMWF accept date ranges with /to/. We need to split it to an array
907
+ # ECMWF accept date ranges in format val1/val2. We need to split it to an array
908
+ date_regex = [
909
+ re.compile(p) for p in (DATE_RANGE_PATTERN, COMPACT_DATE_RANGE_PATTERN)
910
+ ]
911
+ is_date = any(
912
+ any(r.match(v) is not None for r in date_regex) for v in filter_v
913
+ )
914
+ if is_date:
915
+ sep = re.compile(r"/to/|/")
916
+ filter_v = [i for v in filter_v for i in sep.split(str(v))]
890
917
 
891
918
  # special handling for time 0000 converted to 0 by pre-formating with metadata_mapping
892
919
  if keyword.split(":")[-1] == "time":
@@ -211,9 +211,12 @@ class CopMarineSearch(StaticStacSearch):
211
211
  dataset_item: dict[str, Any],
212
212
  collection_dict: dict[str, Any],
213
213
  use_dataset_dates: bool = False,
214
+ product_id: Optional[str] = None,
214
215
  ) -> Optional[EOProduct]:
215
216
 
216
217
  item_id = os.path.splitext(item_key.split("/")[-1])[0]
218
+ if product_id and product_id != item_id:
219
+ return None
217
220
  download_url = s3_url + "/" + item_key
218
221
  geometry = (
219
222
  get_geometry_from_various(**dataset_item)
@@ -377,9 +380,12 @@ class CopMarineSearch(StaticStacSearch):
377
380
  dataset_item,
378
381
  collection_dict,
379
382
  True,
383
+ kwargs.get("id"),
380
384
  )
381
385
  if product:
382
386
  products.append(product)
387
+ if product and kwargs.get("id"):
388
+ break
383
389
  continue
384
390
 
385
391
  s3_client = _get_s3_client(endpoint_url)