eodag 3.0.0b3__py3-none-any.whl → 3.1.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.
Files changed (94) hide show
  1. eodag/api/core.py +347 -247
  2. eodag/api/product/_assets.py +44 -15
  3. eodag/api/product/_product.py +58 -47
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +129 -93
  10. eodag/api/search_result.py +28 -12
  11. eodag/cli.py +61 -24
  12. eodag/config.py +457 -167
  13. eodag/plugins/apis/base.py +10 -4
  14. eodag/plugins/apis/ecmwf.py +53 -23
  15. eodag/plugins/apis/usgs.py +41 -17
  16. eodag/plugins/authentication/aws_auth.py +30 -18
  17. eodag/plugins/authentication/base.py +14 -3
  18. eodag/plugins/authentication/generic.py +14 -3
  19. eodag/plugins/authentication/header.py +14 -6
  20. eodag/plugins/authentication/keycloak.py +44 -25
  21. eodag/plugins/authentication/oauth.py +18 -4
  22. eodag/plugins/authentication/openid_connect.py +192 -171
  23. eodag/plugins/authentication/qsauth.py +12 -4
  24. eodag/plugins/authentication/sas_auth.py +22 -5
  25. eodag/plugins/authentication/token.py +95 -17
  26. eodag/plugins/authentication/token_exchange.py +19 -19
  27. eodag/plugins/base.py +4 -4
  28. eodag/plugins/crunch/base.py +8 -5
  29. eodag/plugins/crunch/filter_date.py +9 -6
  30. eodag/plugins/crunch/filter_latest_intersect.py +9 -8
  31. eodag/plugins/crunch/filter_latest_tpl_name.py +8 -8
  32. eodag/plugins/crunch/filter_overlap.py +9 -11
  33. eodag/plugins/crunch/filter_property.py +10 -10
  34. eodag/plugins/download/aws.py +181 -105
  35. eodag/plugins/download/base.py +49 -67
  36. eodag/plugins/download/creodias_s3.py +40 -2
  37. eodag/plugins/download/http.py +247 -223
  38. eodag/plugins/download/s3rest.py +29 -28
  39. eodag/plugins/manager.py +176 -41
  40. eodag/plugins/search/__init__.py +6 -5
  41. eodag/plugins/search/base.py +123 -60
  42. eodag/plugins/search/build_search_result.py +1046 -355
  43. eodag/plugins/search/cop_marine.py +132 -39
  44. eodag/plugins/search/creodias_s3.py +19 -68
  45. eodag/plugins/search/csw.py +48 -8
  46. eodag/plugins/search/data_request_search.py +124 -23
  47. eodag/plugins/search/qssearch.py +531 -310
  48. eodag/plugins/search/stac_list_assets.py +85 -0
  49. eodag/plugins/search/static_stac_search.py +23 -24
  50. eodag/resources/ext_product_types.json +1 -1
  51. eodag/resources/product_types.yml +1295 -355
  52. eodag/resources/providers.yml +1819 -3010
  53. eodag/resources/stac.yml +3 -163
  54. eodag/resources/stac_api.yml +2 -2
  55. eodag/resources/user_conf_template.yml +115 -99
  56. eodag/rest/cache.py +2 -2
  57. eodag/rest/config.py +3 -4
  58. eodag/rest/constants.py +0 -1
  59. eodag/rest/core.py +157 -117
  60. eodag/rest/errors.py +181 -0
  61. eodag/rest/server.py +57 -339
  62. eodag/rest/stac.py +133 -581
  63. eodag/rest/types/collections_search.py +3 -3
  64. eodag/rest/types/eodag_search.py +41 -30
  65. eodag/rest/types/queryables.py +42 -32
  66. eodag/rest/types/stac_search.py +15 -16
  67. eodag/rest/utils/__init__.py +14 -21
  68. eodag/rest/utils/cql_evaluate.py +6 -6
  69. eodag/rest/utils/rfc3339.py +2 -2
  70. eodag/types/__init__.py +153 -32
  71. eodag/types/bbox.py +2 -2
  72. eodag/types/download_args.py +4 -4
  73. eodag/types/queryables.py +183 -73
  74. eodag/types/search_args.py +6 -6
  75. eodag/types/whoosh.py +127 -3
  76. eodag/utils/__init__.py +228 -106
  77. eodag/utils/exceptions.py +47 -26
  78. eodag/utils/import_system.py +2 -2
  79. eodag/utils/logging.py +37 -77
  80. eodag/utils/repr.py +65 -6
  81. eodag/utils/requests.py +13 -15
  82. eodag/utils/rest.py +2 -2
  83. eodag/utils/s3.py +231 -0
  84. eodag/utils/stac_reader.py +11 -11
  85. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/METADATA +81 -81
  86. eodag-3.1.0.dist-info/RECORD +113 -0
  87. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  88. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +5 -2
  89. eodag/resources/constraints/climate-dt.json +0 -13
  90. eodag/resources/constraints/extremes-dt.json +0 -8
  91. eodag/utils/constraints.py +0 -244
  92. eodag-3.0.0b3.dist-info/RECORD +0 -110
  93. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  94. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
eodag/utils/exceptions.py CHANGED
@@ -17,24 +17,18 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING
20
+ from typing import TYPE_CHECKING, Annotated
21
21
 
22
22
  if TYPE_CHECKING:
23
- from typing import Optional, Set, Tuple
23
+ from typing import Optional
24
+
25
+ from typing_extensions import Doc, Self
24
26
 
25
27
 
26
28
  class EodagError(Exception):
27
29
  """General EODAG error"""
28
30
 
29
31
 
30
- class ValidationError(EodagError):
31
- """Error validating data"""
32
-
33
- def __init__(self, message: str, parameters: Set[str] = set()) -> None:
34
- self.message = message
35
- self.parameters = parameters
36
-
37
-
38
32
  class PluginNotFoundError(EodagError):
39
33
  """Error when looking for a plugin class that was not defined"""
40
34
 
@@ -72,35 +66,62 @@ class AuthenticationError(EodagError):
72
66
  authenticating a user"""
73
67
 
74
68
 
75
- class DownloadError(EodagError):
76
- """An error indicating something wrong with the download process"""
77
-
78
-
79
69
  class NotAvailableError(EodagError):
80
70
  """An error indicating that the product is not available for download"""
81
71
 
82
72
 
73
+ class NoMatchingProductType(EodagError):
74
+ """An error indicating that eodag was unable to derive a product type from a set
75
+ of search parameters"""
76
+
77
+
78
+ class STACOpenerError(EodagError):
79
+ """An error indicating that a STAC file could not be opened"""
80
+
81
+
83
82
  class RequestError(EodagError):
84
83
  """An error indicating that a request has failed. Usually eodag functions
85
84
  and methods should catch and skip this"""
86
85
 
87
- history: Set[Tuple[str, Exception]] = set()
88
- parameters: Set[str] = set()
86
+ status_code: Annotated[Optional[int], Doc("HTTP status code")] = None
89
87
 
90
- def __str__(self):
91
- repr = super().__str__()
92
- for err_tuple in self.history:
93
- repr += f"- {str(err_tuple)}"
94
- return repr
88
+ @classmethod
89
+ def from_error(cls, error: Exception, msg: Optional[str] = None) -> Self:
90
+ """Generate a RequestError from an Exception"""
91
+ status_code = getattr(error, "code", None)
92
+ text = getattr(error, "msg", None)
95
93
 
94
+ response = getattr(error, "response", None)
95
+ # Explicitly test for None because response objects are considered false if they
96
+ # have a status code other than 200
97
+ if response is not None:
98
+ status_code = response.status_code
99
+ text = " ".join([text or "", response.text])
96
100
 
97
- class NoMatchingProductType(EodagError):
98
- """An error indicating that eodag was unable to derive a product type from a set
99
- of search parameters"""
101
+ text = text or str(error)
100
102
 
103
+ e = cls(msg, text) if msg else cls(text)
104
+ e.status_code = status_code
105
+ return e
101
106
 
102
- class STACOpenerError(EodagError):
103
- """An error indicating that a STAC file could not be opened"""
107
+
108
+ class ValidationError(RequestError):
109
+ """Error validating data"""
110
+
111
+ def __init__(self, message: str, parameters: set[str] = set()) -> None:
112
+ self.message = message
113
+ self.parameters = parameters
114
+
115
+ @classmethod
116
+ def from_error(cls, error: Exception, msg: Optional[str] = None) -> Self:
117
+ """Override parent from_error to handle ValidationError specificities."""
118
+ setattr(error, "msg", msg)
119
+ validation_error = super().from_error(error)
120
+ return validation_error
121
+
122
+
123
+ class DownloadError(RequestError):
124
+ """An error indicating something wrong with the download process"""
104
125
 
105
126
 
106
127
  class TimeOutError(RequestError):
@@ -21,14 +21,14 @@ import importlib
21
21
  import pkgutil
22
22
  from contextlib import contextmanager
23
23
  from functools import partial
24
- from typing import TYPE_CHECKING, Any, Generator, Tuple
24
+ from typing import TYPE_CHECKING, Any, Generator
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from types import ModuleType
28
28
 
29
29
 
30
30
  def import_all_modules(
31
- base_package: ModuleType, depth: int = 1, exclude: Tuple[str, ...] = ()
31
+ base_package: ModuleType, depth: int = 1, exclude: tuple[str, ...] = ()
32
32
  ) -> None:
33
33
  """Import all modules in base_package, including modules in the sub-packages up to `depth` and excluding modules in
34
34
  `exclude`.
eodag/utils/logging.py CHANGED
@@ -37,88 +37,48 @@ def setup_logging(verbose: int, no_progress_bar: bool = False) -> None:
37
37
  global disable_tqdm
38
38
  disable_tqdm = no_progress_bar
39
39
 
40
+ if verbose > 3:
41
+ raise ValueError("'verbose' must be one of: 0, 1, 2, 3")
42
+
40
43
  if verbose < 1:
41
44
  disable_tqdm = True
42
45
 
43
- if verbose <= 1:
44
- logging.config.dictConfig(
45
- {
46
- "version": 1,
47
- "disable_existing_loggers": False,
48
- "handlers": {
49
- "null": {"level": "DEBUG", "class": "logging.NullHandler"}
50
- },
51
- "loggers": {
52
- "eodag": {"handlers": ["null"], "propagate": True, "level": "INFO"}
53
- },
54
- }
55
- )
56
- elif verbose == 2:
57
- logging.config.dictConfig(
58
- {
59
- "version": 1,
60
- "disable_existing_loggers": False,
61
- "formatters": {
62
- "standard": {
63
- "format": "%(asctime)-15s %(name)-32s [%(levelname)-8s] %(message)s"
64
- }
65
- },
66
- "handlers": {
67
- "console": {
68
- "level": "DEBUG",
69
- "class": "logging.StreamHandler",
70
- "formatter": "standard",
71
- }
72
- },
73
- "loggers": {
74
- "eodag": {
75
- "handlers": ["console"],
76
- "propagate": True,
77
- "level": "INFO",
78
- },
79
- "sentinelsat": {
80
- "handlers": ["console"],
81
- "propagate": True,
82
- "level": "INFO",
83
- },
84
- },
85
- }
86
- )
87
- elif verbose == 3:
88
- logging.config.dictConfig(
89
- {
90
- "version": 1,
91
- "disable_existing_loggers": False,
92
- "formatters": {
93
- "verbose": {
94
- "format": (
95
- "%(asctime)-15s %(name)-32s [%(levelname)-8s] (tid=%(thread)d) %(message)s"
96
- )
97
- }
98
- },
99
- "handlers": {
100
- "console": {
101
- "level": "DEBUG",
102
- "class": "logging.StreamHandler",
103
- "formatter": "verbose",
104
- }
46
+ level = "DEBUG" if verbose == 3 else "INFO"
47
+
48
+ handlers = {
49
+ "console": {
50
+ "level": level,
51
+ "class": "logging.StreamHandler",
52
+ "formatter": "standard",
53
+ },
54
+ "null": {"level": level, "class": "logging.NullHandler"},
55
+ }
56
+ handler = "console" if verbose > 1 else "null"
57
+
58
+ logging.config.dictConfig(
59
+ {
60
+ "version": 1,
61
+ "disable_existing_loggers": False,
62
+ "formatters": {
63
+ "standard": {
64
+ "format": "%(asctime)-15s %(name)-32s [%(levelname)-8s] %(message)s"
65
+ }
66
+ },
67
+ "handlers": handlers,
68
+ "loggers": {
69
+ "eodag": {
70
+ "handlers": [handler],
71
+ "propagate": True,
72
+ "level": f"{level}",
105
73
  },
106
- "loggers": {
107
- "eodag": {
108
- "handlers": ["console"],
109
- "propagate": True,
110
- "level": "DEBUG",
111
- },
112
- "sentinelsat": {
113
- "handlers": ["console"],
114
- "propagate": True,
115
- "level": "DEBUG",
116
- },
74
+ "eodag-cube": {
75
+ "handlers": [handler],
76
+ "propagate": True,
77
+ "level": f"{level}",
117
78
  },
118
- }
119
- )
120
- else:
121
- raise ValueError("'verbose' must be one of: 0, 1, 2, 3")
79
+ },
80
+ }
81
+ )
122
82
 
123
83
 
124
84
  def get_logging_verbose() -> Optional[int]:
eodag/utils/repr.py CHANGED
@@ -18,12 +18,21 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import collections.abc
21
+ import re
21
22
  from typing import Any, Optional
22
23
  from urllib.parse import urlparse
23
24
 
24
25
 
25
26
  def str_as_href(link: str) -> str:
26
- """URL to html link"""
27
+ """URL to html link
28
+
29
+ :param link: URL to format
30
+ :returns: HMLT formatted link
31
+
32
+ >>> str_as_href("http://foo.bar")
33
+ "<a href='http://foo.bar' target='_blank'>http://foo.bar</a>"
34
+
35
+ """
27
36
  if urlparse(link).scheme in ("file", "http", "https", "s3"):
28
37
  return f"<a href='{link}' target='_blank'>{link}</a>"
29
38
  else:
@@ -31,7 +40,13 @@ def str_as_href(link: str) -> str:
31
40
 
32
41
 
33
42
  def html_table(input: Any, depth: Optional[int] = None) -> str:
34
- """Transform input to HTML table"""
43
+ """Transform input object to HTML table
44
+
45
+ :param input: input object to represent
46
+ :param depth: maximum depth level until which nested objects should be represented
47
+ in new tables (unlimited by default)
48
+ :returns: HTML table
49
+ """
35
50
  if isinstance(input, collections.abc.Mapping):
36
51
  return dict_to_html_table(input, depth=depth)
37
52
  elif isinstance(input, collections.abc.Sequence) and not isinstance(input, str):
@@ -47,7 +62,14 @@ def dict_to_html_table(
47
62
  depth: Optional[int] = None,
48
63
  brackets: bool = True,
49
64
  ) -> str:
50
- """Transform input dict to HTML table"""
65
+ """Transform input dict to HTML table
66
+
67
+ :param input_dict: input dict to represent
68
+ :param depth: maximum depth level until which nested objects should be represented
69
+ in new tables (unlimited by default)
70
+ :param brackets: whether surrounding brackets should be displayed or not
71
+ :returns: HTML table
72
+ """
51
73
  opening_bracket = "<span style='color: grey;'>{</span>" if brackets else ""
52
74
  closing_bracket = "<span style='color: grey;'>}</span>" if brackets else ""
53
75
  indent = "10px" if brackets else "0"
@@ -90,7 +112,13 @@ def dict_to_html_table(
90
112
  def list_to_html_table(
91
113
  input_list: collections.abc.Sequence, depth: Optional[int] = None
92
114
  ) -> str:
93
- """Transform input list to HTML table"""
115
+ """Transform input list to HTML table
116
+
117
+ :param input_list: input list to represent
118
+ :param depth: maximum depth level until which nested objects should be represented
119
+ in new tables (unlimited by default)
120
+ :returns: HTML table
121
+ """
94
122
  if depth is not None:
95
123
  depth -= 1
96
124
  separator = (
@@ -103,11 +131,42 @@ def list_to_html_table(
103
131
  + separator.join(
104
132
  [
105
133
  f"""<span style='text-align: left;'>{
106
- html_table(v, depth=depth)
107
- }</span>
134
+ html_table(v, depth=depth)
135
+ }</span>
108
136
  """
109
137
  for v in input_list
110
138
  ]
111
139
  )
112
140
  + "<span style='color: grey;'>]</span>"
113
141
  )
142
+
143
+
144
+ def remove_class_repr(type_repr: str) -> str:
145
+ """Removes class tag from type representation
146
+
147
+ :param type_repr: input type representation
148
+ :returns: type without class tag
149
+
150
+ >>> remove_class_repr(str(type("foo")))
151
+ 'str'
152
+ """
153
+ return re.sub(r"<class '(\w+)'>", r"\1", type_repr)
154
+
155
+
156
+ def shorter_type_repr(long_type: str) -> str:
157
+ """Shorten long type representation
158
+
159
+ :param long_type: long type representation
160
+ :returns: type reprensentation shortened
161
+
162
+ >>> import typing
163
+ >>> shorter_type_repr(str(typing.Literal["foo", "bar"]))
164
+ "Literal['foo', ...]"
165
+ """
166
+ # shorten lists
167
+ shorter = re.sub(r",[^\[^\]]+\]", ", ...]", str(long_type))
168
+ # remove class prefix
169
+ shorter = remove_class_repr(shorter)
170
+ # remove parent objects
171
+ shorter = re.sub(r"\w+\.", "", shorter)
172
+ return shorter
eodag/utils/requests.py CHANGED
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import logging
21
21
  import os
22
- from typing import Any, Optional, Tuple
22
+ from typing import Any, Optional
23
23
 
24
24
  import requests
25
25
 
@@ -30,7 +30,7 @@ logger = logging.getLogger("eodag.utils.requests")
30
30
 
31
31
 
32
32
  def fetch_json(
33
- file_url: str,
33
+ url: str,
34
34
  req_session: Optional[requests.Session] = None,
35
35
  auth: Optional[requests.auth.AuthBase] = None,
36
36
  timeout: float = HTTP_REQ_TIMEOUT,
@@ -38,34 +38,32 @@ def fetch_json(
38
38
  """
39
39
  Fetches http/distant or local json file
40
40
 
41
- :param file_url: url from which the file can be fetched
41
+ :param url: url from which the file can be fetched
42
42
  :param req_session: (optional) requests session
43
43
  :param auth: (optional) authenticated object if request needs authentication
44
44
  :param timeout: (optional) authenticated object
45
45
  :returns: json file content
46
46
  """
47
47
  if req_session is None:
48
- req_session = requests.Session()
48
+ req_session = requests.sessions.Session()
49
49
  try:
50
- if not file_url.lower().startswith("http"):
51
- file_url = path_to_uri(os.path.abspath(file_url))
50
+ if not url.lower().startswith("http"):
51
+ url = path_to_uri(os.path.abspath(url))
52
52
  req_session.mount("file://", LocalFileAdapter())
53
53
 
54
54
  headers = USER_AGENT
55
- logger.debug(f"fetching {file_url}")
55
+ logger.debug(f"fetching {url}")
56
56
  res = req_session.get(
57
- file_url,
57
+ url,
58
58
  headers=headers,
59
59
  auth=auth,
60
60
  timeout=timeout,
61
61
  )
62
62
  res.raise_for_status()
63
63
  except requests.exceptions.Timeout as exc:
64
- raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
64
+ raise TimeOutError(exc, timeout=timeout) from exc
65
65
  except requests.exceptions.RequestException as exc:
66
- raise RequestError(
67
- f"Unable to fetch {file_url}: {str(exc)}",
68
- ) from exc
66
+ raise RequestError.from_error(exc, f"Unable to fetch {url}") from exc
69
67
  else:
70
68
  return res.json()
71
69
 
@@ -77,7 +75,7 @@ class LocalFileAdapter(requests.adapters.BaseAdapter):
77
75
  """
78
76
 
79
77
  @staticmethod
80
- def _chkpath(method: str, path: str) -> Tuple[int, str]:
78
+ def _chkpath(method: str, path: str) -> tuple[int, str]:
81
79
  """Return an HTTP status for the given filesystem path.
82
80
 
83
81
  :param method: method of the request
@@ -102,8 +100,8 @@ class LocalFileAdapter(requests.adapters.BaseAdapter):
102
100
  ) -> requests.Response:
103
101
  """Wraps a file, described in request, in a Response object.
104
102
 
105
- :param req: The PreparedRequest being "sent".
106
- :param kwargs: (not used) additionnal arguments of the request
103
+ :param request: The PreparedRequest being "sent".
104
+ :param kwargs: (not used) additional arguments of the request
107
105
  :returns: a Response object containing the file
108
106
  """
109
107
  response = requests.Response()
eodag/utils/rest.py CHANGED
@@ -21,7 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  import datetime
23
23
  import re
24
- from typing import Any, Dict, Optional, Tuple
24
+ from typing import Any, Optional
25
25
 
26
26
  import dateutil.parser
27
27
  from dateutil import tz
@@ -35,7 +35,7 @@ RFC3339_PATTERN = (
35
35
  )
36
36
 
37
37
 
38
- def get_datetime(arguments: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
38
+ def get_datetime(arguments: dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
39
39
  """Get start and end dates from a dict containing `/` separated dates in `datetime` item
40
40
 
41
41
  :param arguments: dict containing a single date or `/` separated dates in `datetime` item