eodag 3.3.1__py3-none-any.whl → 3.4.0__py3-none-any.whl

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