eodag 2.12.1__py3-none-any.whl → 3.0.0b2__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 +440 -321
- eodag/api/product/__init__.py +5 -1
- eodag/api/product/_assets.py +57 -2
- eodag/api/product/_product.py +89 -68
- eodag/api/product/metadata_mapping.py +181 -66
- eodag/api/search_result.py +48 -1
- eodag/cli.py +20 -6
- eodag/config.py +95 -6
- eodag/plugins/apis/base.py +8 -165
- eodag/plugins/apis/ecmwf.py +36 -24
- eodag/plugins/apis/usgs.py +40 -24
- eodag/plugins/authentication/aws_auth.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +13 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +256 -46
- eodag/plugins/authentication/qsauth.py +3 -0
- eodag/plugins/authentication/sas_auth.py +8 -1
- eodag/plugins/authentication/token.py +92 -46
- eodag/plugins/authentication/token_exchange.py +120 -0
- eodag/plugins/download/aws.py +86 -91
- eodag/plugins/download/base.py +72 -40
- eodag/plugins/download/http.py +607 -264
- eodag/plugins/download/s3rest.py +28 -15
- eodag/plugins/manager.py +74 -57
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +225 -18
- eodag/plugins/search/build_search_result.py +389 -32
- eodag/plugins/search/cop_marine.py +378 -0
- eodag/plugins/search/creodias_s3.py +15 -14
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +44 -20
- eodag/plugins/search/qssearch.py +508 -203
- eodag/plugins/search/static_stac_search.py +99 -36
- eodag/resources/constraints/climate-dt.json +13 -0
- eodag/resources/constraints/extremes-dt.json +8 -0
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1897 -34
- eodag/resources/providers.yml +3539 -3277
- eodag/resources/stac.yml +48 -54
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +51 -3
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +27 -0
- eodag/rest/core.py +757 -0
- eodag/rest/server.py +397 -258
- eodag/rest/stac.py +438 -307
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +232 -43
- eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
- eodag/rest/types/stac_search.py +277 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +65 -0
- eodag/types/__init__.py +99 -9
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +31 -0
- eodag/types/search_args.py +58 -7
- eodag/types/whoosh.py +81 -0
- eodag/utils/__init__.py +72 -9
- eodag/utils/constraints.py +37 -37
- eodag/utils/exceptions.py +23 -17
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +138 -0
- eodag/utils/rest.py +104 -0
- eodag/utils/stac_reader.py +100 -16
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/METADATA +65 -44
- eodag-3.0.0b2.dist-info/RECORD +110 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/WHEEL +1 -1
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/entry_points.txt +6 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/utils.py +0 -1133
- eodag-2.12.1.dist-info/RECORD +0 -94
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/LICENSE +0 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/top_level.txt +0 -0
eodag/utils/constraints.py
CHANGED
|
@@ -19,13 +19,14 @@ import copy
|
|
|
19
19
|
import logging
|
|
20
20
|
from typing import Any, Dict, List, Set, Union
|
|
21
21
|
|
|
22
|
-
import
|
|
22
|
+
from requests.auth import AuthBase
|
|
23
23
|
|
|
24
24
|
from eodag.api.product.metadata_mapping import get_provider_queryable_key
|
|
25
25
|
from eodag.plugins.apis.base import Api
|
|
26
26
|
from eodag.plugins.search.base import Search
|
|
27
|
-
from eodag.utils import
|
|
28
|
-
from eodag.utils.exceptions import
|
|
27
|
+
from eodag.utils import deepcopy
|
|
28
|
+
from eodag.utils.exceptions import RequestError, ValidationError
|
|
29
|
+
from eodag.utils.requests import fetch_json
|
|
29
30
|
|
|
30
31
|
logger = logging.getLogger("eodag.constraints")
|
|
31
32
|
|
|
@@ -73,8 +74,20 @@ def get_constraint_queryables_with_additional_params(
|
|
|
73
74
|
if provider_key and provider_key in constraint:
|
|
74
75
|
eodag_provider_key_mapping[provider_key] = param
|
|
75
76
|
params_available[param] = True
|
|
76
|
-
if
|
|
77
|
+
if (
|
|
78
|
+
isinstance(value, list)
|
|
79
|
+
and all([v in constraint[provider_key] for v in value])
|
|
80
|
+
or not isinstance(value, list)
|
|
81
|
+
and value in constraint[provider_key]
|
|
82
|
+
):
|
|
77
83
|
params_matched[param] = True
|
|
84
|
+
elif isinstance(value, str):
|
|
85
|
+
# for Copernicus providers, values can be multiple and represented with a string
|
|
86
|
+
# separated by slashes (example: time = "0000/0100/0200")
|
|
87
|
+
values = value.split("/")
|
|
88
|
+
params_matched[param] = all(
|
|
89
|
+
[v in constraint[provider_key] for v in values]
|
|
90
|
+
)
|
|
78
91
|
values_available[param].update(constraint[provider_key])
|
|
79
92
|
# match with default values of params
|
|
80
93
|
for default_param, default_value in defaults.items():
|
|
@@ -167,42 +180,29 @@ def fetch_constraints(
|
|
|
167
180
|
:returns: list of constraints fetched from the provider
|
|
168
181
|
:rtype: List[Dict[Any, Any]]
|
|
169
182
|
"""
|
|
183
|
+
auth = (
|
|
184
|
+
plugin.auth
|
|
185
|
+
if hasattr(plugin, "auth") and isinstance(plugin.auth, AuthBase)
|
|
186
|
+
else None
|
|
187
|
+
)
|
|
170
188
|
try:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
res.raise_for_status()
|
|
185
|
-
except requests.exceptions.Timeout as exc:
|
|
186
|
-
raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
|
|
187
|
-
except requests.exceptions.HTTPError as err:
|
|
188
|
-
logger.error(
|
|
189
|
-
"constraints could not be fetched from %s, error: %s",
|
|
190
|
-
constraints_url,
|
|
191
|
-
str(err),
|
|
192
|
-
)
|
|
189
|
+
constraints_data = fetch_json(constraints_url, auth=auth)
|
|
190
|
+
except RequestError as err:
|
|
191
|
+
logger.error(str(err))
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
config = plugin.config.__dict__
|
|
195
|
+
if (
|
|
196
|
+
"constraints_entry" in config
|
|
197
|
+
and config["constraints_entry"]
|
|
198
|
+
and config["constraints_entry"] in constraints_data
|
|
199
|
+
):
|
|
200
|
+
constraints = constraints_data[config["constraints_entry"]]
|
|
201
|
+
elif config.get("stop_without_constraints_entry_key", False):
|
|
193
202
|
return []
|
|
194
203
|
else:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
"constraints_entry" in config
|
|
199
|
-
and config["constraints_entry"]
|
|
200
|
-
and config["constraints_entry"] in constraints_data
|
|
201
|
-
):
|
|
202
|
-
constraints = constraints_data[config["constraints_entry"]]
|
|
203
|
-
else:
|
|
204
|
-
constraints = constraints_data
|
|
205
|
-
return constraints
|
|
204
|
+
constraints = constraints_data
|
|
205
|
+
return constraints
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
def _get_other_possible_values_for_values_with_defaults(
|
eodag/utils/exceptions.py
CHANGED
|
@@ -23,77 +23,83 @@ if TYPE_CHECKING:
|
|
|
23
23
|
from typing import Optional, Set, Tuple
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class
|
|
26
|
+
class EodagError(Exception):
|
|
27
|
+
"""General EODAG error"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ValidationError(EodagError):
|
|
27
31
|
"""Error validating data"""
|
|
28
32
|
|
|
29
|
-
def __init__(self, message: str) -> None:
|
|
33
|
+
def __init__(self, message: str, parameters: Set[str] = set()) -> None:
|
|
30
34
|
self.message = message
|
|
35
|
+
self.parameters = parameters
|
|
31
36
|
|
|
32
37
|
|
|
33
|
-
class PluginNotFoundError(
|
|
38
|
+
class PluginNotFoundError(EodagError):
|
|
34
39
|
"""Error when looking for a plugin class that was not defined"""
|
|
35
40
|
|
|
36
41
|
|
|
37
|
-
class PluginImplementationError(
|
|
42
|
+
class PluginImplementationError(EodagError):
|
|
38
43
|
"""Error when a plugin does not behave as expected"""
|
|
39
44
|
|
|
40
45
|
|
|
41
|
-
class MisconfiguredError(
|
|
46
|
+
class MisconfiguredError(EodagError):
|
|
42
47
|
"""An error indicating a Search Plugin that is not well configured"""
|
|
43
48
|
|
|
44
49
|
|
|
45
|
-
class AddressNotFound(
|
|
50
|
+
class AddressNotFound(EodagError):
|
|
46
51
|
"""An error indicating the address of a subdataset was not found"""
|
|
47
52
|
|
|
48
53
|
|
|
49
|
-
class UnsupportedProvider(
|
|
54
|
+
class UnsupportedProvider(EodagError):
|
|
50
55
|
"""An error indicating that eodag does not support a provider"""
|
|
51
56
|
|
|
52
57
|
|
|
53
|
-
class UnsupportedProductType(
|
|
58
|
+
class UnsupportedProductType(EodagError):
|
|
54
59
|
"""An error indicating that eodag does not support a product type"""
|
|
55
60
|
|
|
56
61
|
def __init__(self, product_type: str) -> None:
|
|
57
62
|
self.product_type = product_type
|
|
58
63
|
|
|
59
64
|
|
|
60
|
-
class UnsupportedDatasetAddressScheme(
|
|
65
|
+
class UnsupportedDatasetAddressScheme(EodagError):
|
|
61
66
|
"""An error indicating that eodag does not yet support an address scheme for
|
|
62
67
|
accessing raster subdatasets"""
|
|
63
68
|
|
|
64
69
|
|
|
65
|
-
class AuthenticationError(
|
|
70
|
+
class AuthenticationError(EodagError):
|
|
66
71
|
"""An error indicating that an authentication plugin did not succeeded
|
|
67
72
|
authenticating a user"""
|
|
68
73
|
|
|
69
74
|
|
|
70
|
-
class DownloadError(
|
|
75
|
+
class DownloadError(EodagError):
|
|
71
76
|
"""An error indicating something wrong with the download process"""
|
|
72
77
|
|
|
73
78
|
|
|
74
|
-
class NotAvailableError(
|
|
79
|
+
class NotAvailableError(EodagError):
|
|
75
80
|
"""An error indicating that the product is not available for download"""
|
|
76
81
|
|
|
77
82
|
|
|
78
|
-
class RequestError(
|
|
83
|
+
class RequestError(EodagError):
|
|
79
84
|
"""An error indicating that a request has failed. Usually eodag functions
|
|
80
85
|
and methods should catch and skip this"""
|
|
81
86
|
|
|
82
|
-
history: Set[Tuple[
|
|
87
|
+
history: Set[Tuple[str, Exception]] = set()
|
|
88
|
+
parameters: Set[str] = set()
|
|
83
89
|
|
|
84
90
|
def __str__(self):
|
|
85
91
|
repr = super().__str__()
|
|
86
92
|
for err_tuple in self.history:
|
|
87
|
-
repr += f"
|
|
93
|
+
repr += f"- {str(err_tuple)}"
|
|
88
94
|
return repr
|
|
89
95
|
|
|
90
96
|
|
|
91
|
-
class NoMatchingProductType(
|
|
97
|
+
class NoMatchingProductType(EodagError):
|
|
92
98
|
"""An error indicating that eodag was unable to derive a product type from a set
|
|
93
99
|
of search parameters"""
|
|
94
100
|
|
|
95
101
|
|
|
96
|
-
class STACOpenerError(
|
|
102
|
+
class STACOpenerError(EodagError):
|
|
97
103
|
"""An error indicating that a STAC file could not be opened"""
|
|
98
104
|
|
|
99
105
|
|
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,138 @@
|
|
|
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.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
|
+
:type file_url: str
|
|
43
|
+
:param req_session: (optional) requests session
|
|
44
|
+
:type req_session: requests.Session
|
|
45
|
+
:param auth: (optional) authenticated object if request needs authentication
|
|
46
|
+
:type auth: Optional[requests.AuthBase]
|
|
47
|
+
:param timeout: (optional) authenticated object
|
|
48
|
+
:type timeout: float
|
|
49
|
+
:returns: json file content
|
|
50
|
+
:rtype: Any
|
|
51
|
+
"""
|
|
52
|
+
if req_session is None:
|
|
53
|
+
req_session = requests.Session()
|
|
54
|
+
try:
|
|
55
|
+
if not file_url.lower().startswith("http"):
|
|
56
|
+
file_url = path_to_uri(os.path.abspath(file_url))
|
|
57
|
+
req_session.mount("file://", LocalFileAdapter())
|
|
58
|
+
|
|
59
|
+
headers = USER_AGENT
|
|
60
|
+
logger.debug(f"fetching {file_url}")
|
|
61
|
+
res = req_session.get(
|
|
62
|
+
file_url,
|
|
63
|
+
headers=headers,
|
|
64
|
+
auth=auth,
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
)
|
|
67
|
+
res.raise_for_status()
|
|
68
|
+
except requests.exceptions.Timeout as exc:
|
|
69
|
+
raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
|
|
70
|
+
except requests.exceptions.RequestException as exc:
|
|
71
|
+
raise RequestError(
|
|
72
|
+
f"Unable to fetch {file_url}: {str(exc)}",
|
|
73
|
+
) from exc
|
|
74
|
+
else:
|
|
75
|
+
return res.json()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class LocalFileAdapter(requests.adapters.BaseAdapter):
|
|
79
|
+
"""Protocol Adapter to allow Requests to GET file:// URLs inspired
|
|
80
|
+
by https://stackoverflow.com/questions/10123929/fetch-a-file-from-a-local-url-with-python-requests/27786580
|
|
81
|
+
`LocalFileAdapter` class available for the moment (on the 2024-04-22)
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _chkpath(method: str, path: str) -> Tuple[int, str]:
|
|
86
|
+
"""Return an HTTP status for the given filesystem path.
|
|
87
|
+
|
|
88
|
+
:param method: method of the request
|
|
89
|
+
:type method: str
|
|
90
|
+
:param path: path of the given file
|
|
91
|
+
:type path: str
|
|
92
|
+
:returns: HTTP status and its associated message
|
|
93
|
+
:rtype: Tuple[int, str]
|
|
94
|
+
"""
|
|
95
|
+
if method.lower() in ("put", "delete"):
|
|
96
|
+
return 501, "Not Implemented" # TODO
|
|
97
|
+
elif method.lower() not in ("get", "head"):
|
|
98
|
+
return 405, "Method Not Allowed"
|
|
99
|
+
elif os.path.isdir(path):
|
|
100
|
+
return 400, "Path Not A File"
|
|
101
|
+
elif not os.path.isfile(path):
|
|
102
|
+
return 404, "File Not Found"
|
|
103
|
+
elif not os.access(path, os.R_OK):
|
|
104
|
+
return 403, "Access Denied"
|
|
105
|
+
else:
|
|
106
|
+
return 200, "OK"
|
|
107
|
+
|
|
108
|
+
def send(self, req: requests.PreparedRequest, **kwargs: Any) -> requests.Response:
|
|
109
|
+
"""Wraps a file, described in request, in a Response object.
|
|
110
|
+
|
|
111
|
+
:param req: The PreparedRequest being "sent".
|
|
112
|
+
:type req: :class:`~requests.PreparedRequest`
|
|
113
|
+
:param kwargs: (not used) additionnal arguments of the request
|
|
114
|
+
:type kwargs: Any
|
|
115
|
+
:returns: a Response object containing the file
|
|
116
|
+
:rtype: :class:`~requests.Response`
|
|
117
|
+
"""
|
|
118
|
+
response = requests.Response()
|
|
119
|
+
|
|
120
|
+
path_url = uri_to_path(req.url)
|
|
121
|
+
|
|
122
|
+
if req.method is None or req.url is None:
|
|
123
|
+
raise RequestError("Method or url of the request is missing")
|
|
124
|
+
response.status_code, response.reason = self._chkpath(req.method, path_url)
|
|
125
|
+
if response.status_code == 200 and req.method.lower() != "head":
|
|
126
|
+
try:
|
|
127
|
+
response.raw = open(path_url, "rb")
|
|
128
|
+
except (OSError, IOError) as err:
|
|
129
|
+
response.status_code = 500
|
|
130
|
+
response.reason = str(err)
|
|
131
|
+
response.url = req.url
|
|
132
|
+
response.request = req
|
|
133
|
+
|
|
134
|
+
return response
|
|
135
|
+
|
|
136
|
+
def close(self):
|
|
137
|
+
"""Closes without cleaning up adapter specific items."""
|
|
138
|
+
pass
|
eodag/utils/rest.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
:type arguments: dict
|
|
43
|
+
:returns: Start date and end date from datetime string (duplicate value if only one date as input)
|
|
44
|
+
:rtype: Tuple[Optional[str], Optional[str]]
|
|
45
|
+
"""
|
|
46
|
+
datetime_str = arguments.pop("datetime", None)
|
|
47
|
+
|
|
48
|
+
if datetime_str:
|
|
49
|
+
datetime_split = datetime_str.split("/")
|
|
50
|
+
if len(datetime_split) > 1:
|
|
51
|
+
dtstart = datetime_split[0] if datetime_split[0] != ".." else None
|
|
52
|
+
dtend = datetime_split[1] if datetime_split[1] != ".." else None
|
|
53
|
+
elif len(datetime_split) == 1:
|
|
54
|
+
# same time for start & end if only one is given
|
|
55
|
+
dtstart, dtend = datetime_split[0:1] * 2
|
|
56
|
+
else:
|
|
57
|
+
return None, None
|
|
58
|
+
|
|
59
|
+
return get_date(dtstart), get_date(dtend)
|
|
60
|
+
|
|
61
|
+
else:
|
|
62
|
+
# return already set (dtstart, dtend) or None
|
|
63
|
+
dtstart = get_date(arguments.pop("dtstart", None))
|
|
64
|
+
dtend = get_date(arguments.pop("dtend", None))
|
|
65
|
+
return get_date(dtstart), get_date(dtend)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_date(date: Optional[str]) -> Optional[str]:
|
|
69
|
+
"""Check if the input date can be parsed as a date"""
|
|
70
|
+
|
|
71
|
+
if not date:
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
return (
|
|
75
|
+
dateutil.parser.parse(date)
|
|
76
|
+
.replace(tzinfo=tz.UTC)
|
|
77
|
+
.isoformat()
|
|
78
|
+
.replace("+00:00", "")
|
|
79
|
+
)
|
|
80
|
+
except ValueError as e:
|
|
81
|
+
exc = ValidationError("invalid input date: %s" % e)
|
|
82
|
+
raise exc
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def rfc3339_str_to_datetime(s: str) -> datetime.datetime:
|
|
86
|
+
"""Convert a string conforming to RFC 3339 to a :class:`datetime.datetime`.
|
|
87
|
+
|
|
88
|
+
:param s: The string to convert to :class:`datetime.datetime`
|
|
89
|
+
:type s: str
|
|
90
|
+
|
|
91
|
+
:returns: The datetime represented by the ISO8601 (RFC 3339) formatted string
|
|
92
|
+
:rtype: :class:`datetime.datetime`
|
|
93
|
+
|
|
94
|
+
raises: :class:`ValidationError`
|
|
95
|
+
"""
|
|
96
|
+
# Uppercase the string
|
|
97
|
+
s = s.upper()
|
|
98
|
+
|
|
99
|
+
# Match against RFC3339 regex.
|
|
100
|
+
result = re.match(RFC3339_PATTERN, s)
|
|
101
|
+
if not result:
|
|
102
|
+
raise ValidationError("Invalid RFC3339 datetime.")
|
|
103
|
+
|
|
104
|
+
return dateutil.parser.isoparse(s).replace(tzinfo=datetime.timezone.utc)
|