eodag 2.12.1__py3-none-any.whl → 3.0.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.
- eodag/__init__.py +6 -8
- eodag/api/core.py +654 -538
- eodag/api/product/__init__.py +12 -2
- eodag/api/product/_assets.py +59 -16
- eodag/api/product/_product.py +100 -93
- eodag/api/product/drivers/__init__.py +7 -2
- eodag/api/product/drivers/base.py +0 -3
- eodag/api/product/metadata_mapping.py +192 -96
- eodag/api/search_result.py +69 -10
- eodag/cli.py +55 -25
- eodag/config.py +391 -116
- eodag/plugins/apis/base.py +11 -168
- eodag/plugins/apis/ecmwf.py +36 -25
- eodag/plugins/apis/usgs.py +80 -35
- eodag/plugins/authentication/aws_auth.py +13 -4
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +17 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +268 -49
- eodag/plugins/authentication/qsauth.py +4 -1
- eodag/plugins/authentication/sas_auth.py +9 -2
- eodag/plugins/authentication/token.py +98 -47
- eodag/plugins/authentication/token_exchange.py +122 -0
- eodag/plugins/crunch/base.py +3 -1
- eodag/plugins/crunch/filter_date.py +3 -9
- eodag/plugins/crunch/filter_latest_intersect.py +0 -3
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
- eodag/plugins/crunch/filter_overlap.py +4 -8
- eodag/plugins/crunch/filter_property.py +5 -11
- eodag/plugins/download/aws.py +149 -185
- eodag/plugins/download/base.py +88 -97
- eodag/plugins/download/creodias_s3.py +1 -1
- eodag/plugins/download/http.py +638 -310
- eodag/plugins/download/s3rest.py +47 -45
- eodag/plugins/manager.py +228 -88
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +239 -30
- eodag/plugins/search/build_search_result.py +382 -37
- eodag/plugins/search/cop_marine.py +441 -0
- eodag/plugins/search/creodias_s3.py +25 -20
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +61 -30
- eodag/plugins/search/qssearch.py +713 -255
- eodag/plugins/search/static_stac_search.py +106 -40
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1921 -34
- eodag/resources/providers.yml +4091 -3655
- eodag/resources/stac.yml +50 -216
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +89 -32
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +26 -0
- eodag/rest/core.py +735 -0
- eodag/rest/errors.py +178 -0
- eodag/rest/server.py +264 -431
- eodag/rest/stac.py +442 -836
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +238 -47
- eodag/rest/types/queryables.py +164 -0
- eodag/rest/types/stac_search.py +273 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +64 -0
- eodag/types/__init__.py +106 -10
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +40 -0
- eodag/types/search_args.py +57 -7
- eodag/types/whoosh.py +79 -0
- eodag/utils/__init__.py +110 -91
- eodag/utils/constraints.py +37 -45
- eodag/utils/exceptions.py +39 -22
- eodag/utils/import_system.py +0 -4
- eodag/utils/logging.py +37 -80
- eodag/utils/notebook.py +4 -4
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +128 -0
- eodag/utils/rest.py +100 -0
- eodag/utils/stac_reader.py +93 -21
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
- eodag-3.0.0.dist-info/RECORD +109 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/types/stac_queryables.py +0 -134
- eodag/rest/utils.py +0 -1133
- eodag-2.12.1.dist-info/RECORD +0 -94
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
eodag/utils/exceptions.py
CHANGED
|
@@ -20,80 +20,97 @@ from __future__ import annotations
|
|
|
20
20
|
from typing import TYPE_CHECKING
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
|
-
from typing import Optional, Set
|
|
23
|
+
from typing import Optional, Set
|
|
24
24
|
|
|
25
|
+
from typing_extensions import Annotated, Doc
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
class EodagError(Exception):
|
|
29
|
+
"""General EODAG error"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ValidationError(EodagError):
|
|
27
33
|
"""Error validating data"""
|
|
28
34
|
|
|
29
|
-
def __init__(self, message: str) -> None:
|
|
35
|
+
def __init__(self, message: str, parameters: Set[str] = set()) -> None:
|
|
30
36
|
self.message = message
|
|
37
|
+
self.parameters = parameters
|
|
31
38
|
|
|
32
39
|
|
|
33
|
-
class PluginNotFoundError(
|
|
40
|
+
class PluginNotFoundError(EodagError):
|
|
34
41
|
"""Error when looking for a plugin class that was not defined"""
|
|
35
42
|
|
|
36
43
|
|
|
37
|
-
class PluginImplementationError(
|
|
44
|
+
class PluginImplementationError(EodagError):
|
|
38
45
|
"""Error when a plugin does not behave as expected"""
|
|
39
46
|
|
|
40
47
|
|
|
41
|
-
class MisconfiguredError(
|
|
48
|
+
class MisconfiguredError(EodagError):
|
|
42
49
|
"""An error indicating a Search Plugin that is not well configured"""
|
|
43
50
|
|
|
44
51
|
|
|
45
|
-
class AddressNotFound(
|
|
52
|
+
class AddressNotFound(EodagError):
|
|
46
53
|
"""An error indicating the address of a subdataset was not found"""
|
|
47
54
|
|
|
48
55
|
|
|
49
|
-
class UnsupportedProvider(
|
|
56
|
+
class UnsupportedProvider(EodagError):
|
|
50
57
|
"""An error indicating that eodag does not support a provider"""
|
|
51
58
|
|
|
52
59
|
|
|
53
|
-
class UnsupportedProductType(
|
|
60
|
+
class UnsupportedProductType(EodagError):
|
|
54
61
|
"""An error indicating that eodag does not support a product type"""
|
|
55
62
|
|
|
56
63
|
def __init__(self, product_type: str) -> None:
|
|
57
64
|
self.product_type = product_type
|
|
58
65
|
|
|
59
66
|
|
|
60
|
-
class UnsupportedDatasetAddressScheme(
|
|
67
|
+
class UnsupportedDatasetAddressScheme(EodagError):
|
|
61
68
|
"""An error indicating that eodag does not yet support an address scheme for
|
|
62
69
|
accessing raster subdatasets"""
|
|
63
70
|
|
|
64
71
|
|
|
65
|
-
class AuthenticationError(
|
|
72
|
+
class AuthenticationError(EodagError):
|
|
66
73
|
"""An error indicating that an authentication plugin did not succeeded
|
|
67
74
|
authenticating a user"""
|
|
68
75
|
|
|
69
76
|
|
|
70
|
-
class DownloadError(
|
|
77
|
+
class DownloadError(EodagError):
|
|
71
78
|
"""An error indicating something wrong with the download process"""
|
|
72
79
|
|
|
73
80
|
|
|
74
|
-
class NotAvailableError(
|
|
81
|
+
class NotAvailableError(EodagError):
|
|
75
82
|
"""An error indicating that the product is not available for download"""
|
|
76
83
|
|
|
77
84
|
|
|
78
|
-
class RequestError(
|
|
85
|
+
class RequestError(EodagError):
|
|
79
86
|
"""An error indicating that a request has failed. Usually eodag functions
|
|
80
87
|
and methods should catch and skip this"""
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
status_code: Annotated[Optional[int], Doc("HTTP status code")] = None
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_error(cls, error: Exception, msg: Optional[str] = None):
|
|
93
|
+
"""Generate a RequestError from an Exception"""
|
|
94
|
+
status_code = getattr(error, "code", None)
|
|
95
|
+
text = getattr(error, "msg", None)
|
|
96
|
+
|
|
97
|
+
if response := getattr(error, "response", None):
|
|
98
|
+
status_code = response.status_code
|
|
99
|
+
text = response.text
|
|
100
|
+
|
|
101
|
+
text = text or str(error)
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
repr += f"\n- {str(err_tuple)}"
|
|
88
|
-
return repr
|
|
103
|
+
e = cls(msg, text) if msg else cls(text)
|
|
104
|
+
e.status_code = status_code
|
|
105
|
+
return e
|
|
89
106
|
|
|
90
107
|
|
|
91
|
-
class NoMatchingProductType(
|
|
108
|
+
class NoMatchingProductType(EodagError):
|
|
92
109
|
"""An error indicating that eodag was unable to derive a product type from a set
|
|
93
110
|
of search parameters"""
|
|
94
111
|
|
|
95
112
|
|
|
96
|
-
class STACOpenerError(
|
|
113
|
+
class STACOpenerError(EodagError):
|
|
97
114
|
"""An error indicating that a STAC file could not be opened"""
|
|
98
115
|
|
|
99
116
|
|
eodag/utils/import_system.py
CHANGED
|
@@ -52,12 +52,9 @@ def import_all_modules(
|
|
|
52
52
|
import_all_modules(base_package)
|
|
53
53
|
|
|
54
54
|
:param base_package: The package from where we must import all the modules
|
|
55
|
-
:type base_package: `module`
|
|
56
55
|
:param depth: (optional) If `base_package` has sub packages, import all the modules recursively up to this level.
|
|
57
56
|
Defaults to 1 (limits to the level of `base_package`)
|
|
58
|
-
:type depth: int
|
|
59
57
|
:param exclude: (optional) The sub packages and modules to ignore while importing. Empty by default
|
|
60
|
-
:type exclude: tuple(str, ...)
|
|
61
58
|
|
|
62
59
|
.. note::
|
|
63
60
|
if `package` and `subpackage` have a module of the same name and this name is included in the exclude
|
|
@@ -88,7 +85,6 @@ def patch_owslib_requests(verify: bool = True) -> Generator[None, Any, None]:
|
|
|
88
85
|
these functions in `owslib <https://geopython.github.io/OWSLib/>`_.
|
|
89
86
|
|
|
90
87
|
:param verify: (optional) Whether to verify the use of https or not
|
|
91
|
-
:type verify: bool
|
|
92
88
|
"""
|
|
93
89
|
from owslib.util import requests
|
|
94
90
|
|
eodag/utils/logging.py
CHANGED
|
@@ -32,95 +32,53 @@ def setup_logging(verbose: int, no_progress_bar: bool = False) -> None:
|
|
|
32
32
|
* 1: no logging but still displays progress bars
|
|
33
33
|
* 2: INFO level
|
|
34
34
|
* 3: DEBUG level
|
|
35
|
-
:type verbose: int
|
|
36
35
|
:param no_progress_bar: (optional) Disable progress bars
|
|
37
|
-
:type no_progress_bar: bool
|
|
38
36
|
"""
|
|
39
37
|
global disable_tqdm
|
|
40
38
|
disable_tqdm = no_progress_bar
|
|
41
39
|
|
|
40
|
+
if verbose > 3:
|
|
41
|
+
raise ValueError("'verbose' must be one of: 0, 1, 2, 3")
|
|
42
|
+
|
|
42
43
|
if verbose < 1:
|
|
43
44
|
disable_tqdm = True
|
|
44
45
|
|
|
45
|
-
if verbose
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"formatter": "standard",
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
"loggers": {
|
|
76
|
-
"eodag": {
|
|
77
|
-
"handlers": ["console"],
|
|
78
|
-
"propagate": True,
|
|
79
|
-
"level": "INFO",
|
|
80
|
-
},
|
|
81
|
-
"sentinelsat": {
|
|
82
|
-
"handlers": ["console"],
|
|
83
|
-
"propagate": True,
|
|
84
|
-
"level": "INFO",
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
}
|
|
88
|
-
)
|
|
89
|
-
elif verbose == 3:
|
|
90
|
-
logging.config.dictConfig(
|
|
91
|
-
{
|
|
92
|
-
"version": 1,
|
|
93
|
-
"disable_existing_loggers": False,
|
|
94
|
-
"formatters": {
|
|
95
|
-
"verbose": {
|
|
96
|
-
"format": (
|
|
97
|
-
"%(asctime)-15s %(name)-32s [%(levelname)-8s] (tid=%(thread)d) %(message)s"
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
"handlers": {
|
|
102
|
-
"console": {
|
|
103
|
-
"level": "DEBUG",
|
|
104
|
-
"class": "logging.StreamHandler",
|
|
105
|
-
"formatter": "verbose",
|
|
106
|
-
}
|
|
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}",
|
|
107
73
|
},
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"level": "DEBUG",
|
|
113
|
-
},
|
|
114
|
-
"sentinelsat": {
|
|
115
|
-
"handlers": ["console"],
|
|
116
|
-
"propagate": True,
|
|
117
|
-
"level": "DEBUG",
|
|
118
|
-
},
|
|
74
|
+
"eodag-cube": {
|
|
75
|
+
"handlers": [handler],
|
|
76
|
+
"propagate": True,
|
|
77
|
+
"level": f"{level}",
|
|
119
78
|
},
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
raise ValueError("'verbose' must be one of: 0, 1, 2, 3")
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
)
|
|
124
82
|
|
|
125
83
|
|
|
126
84
|
def get_logging_verbose() -> Optional[int]:
|
|
@@ -142,7 +100,6 @@ def get_logging_verbose() -> Optional[int]:
|
|
|
142
100
|
3
|
|
143
101
|
|
|
144
102
|
:returns: Verbose level in ``[0, 1, 2, 3]`` or None if not set
|
|
145
|
-
:rtype: int or None
|
|
146
103
|
"""
|
|
147
104
|
global disable_tqdm
|
|
148
105
|
logger = logging.getLogger("eodag")
|
eodag/utils/notebook.py
CHANGED
|
@@ -23,7 +23,7 @@ from typing import Any, Optional
|
|
|
23
23
|
def check_ipython() -> bool:
|
|
24
24
|
"""Check if called from ipython"""
|
|
25
25
|
try:
|
|
26
|
-
__IPYTHON__
|
|
26
|
+
__IPYTHON__ # type: ignore[name-defined]
|
|
27
27
|
return True
|
|
28
28
|
except NameError:
|
|
29
29
|
return False
|
|
@@ -32,7 +32,7 @@ def check_ipython() -> bool:
|
|
|
32
32
|
def check_notebook() -> bool:
|
|
33
33
|
"""Check if called from a notebook"""
|
|
34
34
|
try:
|
|
35
|
-
shell = get_ipython().__class__.__name__
|
|
35
|
+
shell = get_ipython().__class__.__name__ # type: ignore[name-defined]
|
|
36
36
|
if shell == "ZMQInteractiveShell":
|
|
37
37
|
return True # Jupyter notebook or qtconsole
|
|
38
38
|
elif shell == "TerminalInteractiveShell":
|
|
@@ -69,7 +69,7 @@ class NotebookWidgets:
|
|
|
69
69
|
if not self.is_notebook:
|
|
70
70
|
return None
|
|
71
71
|
|
|
72
|
-
self.html_box
|
|
72
|
+
setattr(self.html_box, "data", html_value)
|
|
73
73
|
|
|
74
74
|
if not self.html_box_shown:
|
|
75
75
|
self._html_handle = self.display(self.html_box, display_id=True)
|
|
@@ -83,5 +83,5 @@ class NotebookWidgets:
|
|
|
83
83
|
if not self.is_notebook:
|
|
84
84
|
return None
|
|
85
85
|
|
|
86
|
-
self.html_box
|
|
86
|
+
setattr(self.html_box, "data", "")
|
|
87
87
|
self._update_display(self.html_box, display_id=self._html_handle.display_id)
|
eodag/utils/repr.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
|
|
3
|
+
#
|
|
4
|
+
# This file is part of EODAG project
|
|
5
|
+
# https://www.github.com/CS-SI/EODAG
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import collections.abc
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
from urllib.parse import urlparse
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def str_as_href(link: str) -> str:
|
|
26
|
+
"""URL to html link"""
|
|
27
|
+
if urlparse(link).scheme in ("file", "http", "https", "s3"):
|
|
28
|
+
return f"<a href='{link}' target='_blank'>{link}</a>"
|
|
29
|
+
else:
|
|
30
|
+
return link
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def html_table(input: Any, depth: Optional[int] = None) -> str:
|
|
34
|
+
"""Transform input to HTML table"""
|
|
35
|
+
if isinstance(input, collections.abc.Mapping):
|
|
36
|
+
return dict_to_html_table(input, depth=depth)
|
|
37
|
+
elif isinstance(input, collections.abc.Sequence) and not isinstance(input, str):
|
|
38
|
+
return list_to_html_table(input, depth=depth)
|
|
39
|
+
elif isinstance(input, str):
|
|
40
|
+
return f"'{str_as_href(input)}'"
|
|
41
|
+
else:
|
|
42
|
+
return str_as_href(str(input))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def dict_to_html_table(
|
|
46
|
+
input_dict: collections.abc.Mapping,
|
|
47
|
+
depth: Optional[int] = None,
|
|
48
|
+
brackets: bool = True,
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Transform input dict to HTML table"""
|
|
51
|
+
opening_bracket = "<span style='color: grey;'>{</span>" if brackets else ""
|
|
52
|
+
closing_bracket = "<span style='color: grey;'>}</span>" if brackets else ""
|
|
53
|
+
indent = "10px" if brackets else "0"
|
|
54
|
+
|
|
55
|
+
if depth is not None:
|
|
56
|
+
depth -= 1
|
|
57
|
+
|
|
58
|
+
if depth is None or depth >= 0:
|
|
59
|
+
return (
|
|
60
|
+
f"{opening_bracket}<table style='margin: 0;'>"
|
|
61
|
+
+ "".join(
|
|
62
|
+
[
|
|
63
|
+
f"""<tr style='background-color: transparent;'>
|
|
64
|
+
<td style='padding: 5px 0 0 {indent}; text-align: left; color: grey; vertical-align:top;'>{k}:</td>
|
|
65
|
+
<td style='padding: 5px 0 0 10px; text-align: left;'>{
|
|
66
|
+
html_table(v, depth=depth)
|
|
67
|
+
},</td>
|
|
68
|
+
</tr>
|
|
69
|
+
"""
|
|
70
|
+
for k, v in input_dict.items()
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
+ f"</table>{closing_bracket}"
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
return (
|
|
77
|
+
f"{opening_bracket}"
|
|
78
|
+
+ ", ".join(
|
|
79
|
+
[
|
|
80
|
+
f"""<span style='text-align: left;'>
|
|
81
|
+
'{k}': {html_table(v, depth=depth)}
|
|
82
|
+
</span>"""
|
|
83
|
+
for k, v in input_dict.items()
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
+ f"{closing_bracket}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def list_to_html_table(
|
|
91
|
+
input_list: collections.abc.Sequence, depth: Optional[int] = None
|
|
92
|
+
) -> str:
|
|
93
|
+
"""Transform input list to HTML table"""
|
|
94
|
+
if depth is not None:
|
|
95
|
+
depth -= 1
|
|
96
|
+
separator = (
|
|
97
|
+
",<br />"
|
|
98
|
+
if any(isinstance(v, collections.abc.Mapping) for v in input_list)
|
|
99
|
+
else ", "
|
|
100
|
+
)
|
|
101
|
+
return (
|
|
102
|
+
"<span style='color: grey;'>[</span>"
|
|
103
|
+
+ separator.join(
|
|
104
|
+
[
|
|
105
|
+
f"""<span style='text-align: left;'>{
|
|
106
|
+
html_table(v, depth=depth)
|
|
107
|
+
}</span>
|
|
108
|
+
"""
|
|
109
|
+
for v in input_list
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
+ "<span style='color: grey;'>]</span>"
|
|
113
|
+
)
|
eodag/utils/requests.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
|
|
3
|
+
#
|
|
4
|
+
# This file is part of EODAG project
|
|
5
|
+
# https://www.github.com/CS-SI/EODAG
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from typing import Any, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
|
|
26
|
+
from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, path_to_uri, uri_to_path
|
|
27
|
+
from eodag.utils.exceptions import RequestError, TimeOutError
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("eodag.utils.requests")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fetch_json(
|
|
33
|
+
file_url: str,
|
|
34
|
+
req_session: Optional[requests.Session] = None,
|
|
35
|
+
auth: Optional[requests.auth.AuthBase] = None,
|
|
36
|
+
timeout: float = HTTP_REQ_TIMEOUT,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""
|
|
39
|
+
Fetches http/distant or local json file
|
|
40
|
+
|
|
41
|
+
:param file_url: url from which the file can be fetched
|
|
42
|
+
:param req_session: (optional) requests session
|
|
43
|
+
:param auth: (optional) authenticated object if request needs authentication
|
|
44
|
+
:param timeout: (optional) authenticated object
|
|
45
|
+
:returns: json file content
|
|
46
|
+
"""
|
|
47
|
+
if req_session is None:
|
|
48
|
+
req_session = requests.Session()
|
|
49
|
+
try:
|
|
50
|
+
if not file_url.lower().startswith("http"):
|
|
51
|
+
file_url = path_to_uri(os.path.abspath(file_url))
|
|
52
|
+
req_session.mount("file://", LocalFileAdapter())
|
|
53
|
+
|
|
54
|
+
headers = USER_AGENT
|
|
55
|
+
logger.debug(f"fetching {file_url}")
|
|
56
|
+
res = req_session.get(
|
|
57
|
+
file_url,
|
|
58
|
+
headers=headers,
|
|
59
|
+
auth=auth,
|
|
60
|
+
timeout=timeout,
|
|
61
|
+
)
|
|
62
|
+
res.raise_for_status()
|
|
63
|
+
except requests.exceptions.Timeout as exc:
|
|
64
|
+
raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
|
|
65
|
+
except requests.exceptions.RequestException as exc:
|
|
66
|
+
raise RequestError.from_error(exc, f"Unable to fetch {file_url}") from exc
|
|
67
|
+
else:
|
|
68
|
+
return res.json()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class LocalFileAdapter(requests.adapters.BaseAdapter):
|
|
72
|
+
"""Protocol Adapter to allow Requests to GET file:// URLs inspired
|
|
73
|
+
by https://stackoverflow.com/questions/10123929/fetch-a-file-from-a-local-url-with-python-requests/27786580
|
|
74
|
+
`LocalFileAdapter` class available for the moment (on the 2024-04-22)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _chkpath(method: str, path: str) -> Tuple[int, str]:
|
|
79
|
+
"""Return an HTTP status for the given filesystem path.
|
|
80
|
+
|
|
81
|
+
:param method: method of the request
|
|
82
|
+
:param path: path of the given file
|
|
83
|
+
:returns: HTTP status and its associated message
|
|
84
|
+
"""
|
|
85
|
+
if method.lower() in ("put", "delete"):
|
|
86
|
+
return 501, "Not Implemented" # TODO
|
|
87
|
+
elif method.lower() not in ("get", "head"):
|
|
88
|
+
return 405, "Method Not Allowed"
|
|
89
|
+
elif os.path.isdir(path):
|
|
90
|
+
return 400, "Path Not A File"
|
|
91
|
+
elif not os.path.isfile(path):
|
|
92
|
+
return 404, "File Not Found"
|
|
93
|
+
elif not os.access(path, os.R_OK):
|
|
94
|
+
return 403, "Access Denied"
|
|
95
|
+
else:
|
|
96
|
+
return 200, "OK"
|
|
97
|
+
|
|
98
|
+
def send(
|
|
99
|
+
self, request: requests.PreparedRequest, *args: Any, **kwargs: Any
|
|
100
|
+
) -> requests.Response:
|
|
101
|
+
"""Wraps a file, described in request, in a Response object.
|
|
102
|
+
|
|
103
|
+
:param req: The PreparedRequest being "sent".
|
|
104
|
+
:param kwargs: (not used) additionnal arguments of the request
|
|
105
|
+
:returns: a Response object containing the file
|
|
106
|
+
"""
|
|
107
|
+
response = requests.Response()
|
|
108
|
+
|
|
109
|
+
if request.method is None or request.url is None:
|
|
110
|
+
raise RequestError("Method or url of the request is missing")
|
|
111
|
+
|
|
112
|
+
path_url = uri_to_path(request.url)
|
|
113
|
+
|
|
114
|
+
response.status_code, response.reason = self._chkpath(request.method, path_url)
|
|
115
|
+
if response.status_code == 200 and request.method.lower() != "head":
|
|
116
|
+
try:
|
|
117
|
+
response.raw = open(path_url, "rb")
|
|
118
|
+
except (OSError, IOError) as err:
|
|
119
|
+
response.status_code = 500
|
|
120
|
+
response.reason = str(err)
|
|
121
|
+
response.url = request.url
|
|
122
|
+
response.request = request
|
|
123
|
+
|
|
124
|
+
return response
|
|
125
|
+
|
|
126
|
+
def close(self):
|
|
127
|
+
"""Closes without cleaning up adapter specific items."""
|
|
128
|
+
pass
|
eodag/utils/rest.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2018, CS GROUP - France, https://www.csgroup.eu/
|
|
3
|
+
#
|
|
4
|
+
# This file is part of EODAG project
|
|
5
|
+
# https://www.github.com/CS-SI/EODAG
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
"""eodag.rest.utils methods that must be importable without eodag[server] installed"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import datetime
|
|
23
|
+
import re
|
|
24
|
+
from typing import Any, Dict, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
import dateutil.parser
|
|
27
|
+
from dateutil import tz
|
|
28
|
+
|
|
29
|
+
from eodag.utils.exceptions import ValidationError
|
|
30
|
+
|
|
31
|
+
RFC3339_PATTERN = (
|
|
32
|
+
r"^(\d{4})-(\d{2})-(\d{2})"
|
|
33
|
+
r"(?:T(\d{2}):(\d{2}):(\d{2})(\.\d+)?"
|
|
34
|
+
r"(Z|([+-])(\d{2}):(\d{2}))?)?$"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_datetime(arguments: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
|
|
39
|
+
"""Get start and end dates from a dict containing `/` separated dates in `datetime` item
|
|
40
|
+
|
|
41
|
+
:param arguments: dict containing a single date or `/` separated dates in `datetime` item
|
|
42
|
+
:returns: Start date and end date from datetime string (duplicate value if only one date as input)
|
|
43
|
+
"""
|
|
44
|
+
datetime_str = arguments.pop("datetime", None)
|
|
45
|
+
|
|
46
|
+
if datetime_str:
|
|
47
|
+
datetime_split = datetime_str.split("/")
|
|
48
|
+
if len(datetime_split) > 1:
|
|
49
|
+
dtstart = datetime_split[0] if datetime_split[0] != ".." else None
|
|
50
|
+
dtend = datetime_split[1] if datetime_split[1] != ".." else None
|
|
51
|
+
elif len(datetime_split) == 1:
|
|
52
|
+
# same time for start & end if only one is given
|
|
53
|
+
dtstart, dtend = datetime_split[0:1] * 2
|
|
54
|
+
else:
|
|
55
|
+
return None, None
|
|
56
|
+
|
|
57
|
+
return get_date(dtstart), get_date(dtend)
|
|
58
|
+
|
|
59
|
+
else:
|
|
60
|
+
# return already set (dtstart, dtend) or None
|
|
61
|
+
dtstart = get_date(arguments.pop("dtstart", None))
|
|
62
|
+
dtend = get_date(arguments.pop("dtend", None))
|
|
63
|
+
return get_date(dtstart), get_date(dtend)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_date(date: Optional[str]) -> Optional[str]:
|
|
67
|
+
"""Check if the input date can be parsed as a date"""
|
|
68
|
+
|
|
69
|
+
if not date:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
return (
|
|
73
|
+
dateutil.parser.parse(date)
|
|
74
|
+
.replace(tzinfo=tz.UTC)
|
|
75
|
+
.isoformat()
|
|
76
|
+
.replace("+00:00", "")
|
|
77
|
+
)
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
exc = ValidationError("invalid input date: %s" % e)
|
|
80
|
+
raise exc
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def rfc3339_str_to_datetime(s: str) -> datetime.datetime:
|
|
84
|
+
"""Convert a string conforming to RFC 3339 to a :class:`datetime.datetime`.
|
|
85
|
+
|
|
86
|
+
:param s: The string to convert to :class:`datetime.datetime`
|
|
87
|
+
|
|
88
|
+
:returns: The datetime represented by the ISO8601 (RFC 3339) formatted string
|
|
89
|
+
|
|
90
|
+
raises: :class:`ValidationError`
|
|
91
|
+
"""
|
|
92
|
+
# Uppercase the string
|
|
93
|
+
s = s.upper()
|
|
94
|
+
|
|
95
|
+
# Match against RFC3339 regex.
|
|
96
|
+
result = re.match(RFC3339_PATTERN, s)
|
|
97
|
+
if not result:
|
|
98
|
+
raise ValidationError("Invalid RFC3339 datetime.")
|
|
99
|
+
|
|
100
|
+
return dateutil.parser.isoparse(s).replace(tzinfo=datetime.timezone.utc)
|