ecmwf-datastores-client 0.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.
Potentially problematic release.
This version of ecmwf-datastores-client might be problematic. Click here for more details.
- ecmwf/datastores/__init__.py +39 -0
- ecmwf/datastores/catalogue.py +206 -0
- ecmwf/datastores/client.py +400 -0
- ecmwf/datastores/config.py +45 -0
- ecmwf/datastores/legacy_client.py +275 -0
- ecmwf/datastores/processing.py +793 -0
- ecmwf/datastores/profile.py +87 -0
- ecmwf/datastores/py.typed +0 -0
- ecmwf/datastores/utils.py +9 -0
- ecmwf/datastores/version.py +2 -0
- ecmwf_datastores_client-0.1.0.dist-info/METADATA +531 -0
- ecmwf_datastores_client-0.1.0.dist-info/RECORD +15 -0
- ecmwf_datastores_client-0.1.0.dist-info/WHEEL +5 -0
- ecmwf_datastores_client-0.1.0.dist-info/licenses/LICENSE +201 -0
- ecmwf_datastores_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""ECMWF Data Stores Service (DSS) API Python client."""
|
|
2
|
+
|
|
3
|
+
# Copyright 2022, European Union.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
# NOTE: the `version.py` file must not be present in the git repository
|
|
19
|
+
# as it is generated by setuptools at install time
|
|
20
|
+
from ecmwf.datastores.version import __version__
|
|
21
|
+
except ImportError: # pragma: no cover
|
|
22
|
+
# Local copy or not installed with setuptools
|
|
23
|
+
__version__ = "999"
|
|
24
|
+
|
|
25
|
+
from .catalogue import Collection, Collections
|
|
26
|
+
from .client import Client
|
|
27
|
+
from .processing import Jobs, Process, Processes, Remote, Results
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"__version__",
|
|
31
|
+
"Client",
|
|
32
|
+
"Collection",
|
|
33
|
+
"Collections",
|
|
34
|
+
"Jobs",
|
|
35
|
+
"Process",
|
|
36
|
+
"Processes",
|
|
37
|
+
"Remote",
|
|
38
|
+
"Results",
|
|
39
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Copyright 2022, European Union.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import warnings
|
|
19
|
+
from typing import Any, Callable
|
|
20
|
+
|
|
21
|
+
import attrs
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
from ecmwf import datastores
|
|
25
|
+
from ecmwf.datastores import config, utils
|
|
26
|
+
from ecmwf.datastores.processing import ApiResponse, ApiResponsePaginated, RequestKwargs
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@attrs.define
|
|
30
|
+
class Collections(ApiResponsePaginated):
|
|
31
|
+
"""A class to interact with catalogue collections."""
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def collection_ids(self) -> list[str]:
|
|
35
|
+
"""List of collection IDs."""
|
|
36
|
+
return [collection["id"] for collection in self._json_dict["collections"]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@attrs.define
|
|
40
|
+
class Collection(ApiResponse):
|
|
41
|
+
"""A class to interact with a catalogue collection."""
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def begin_datetime(self) -> datetime.datetime | None:
|
|
45
|
+
"""Begin datetime of the collection."""
|
|
46
|
+
if (value := self._json_dict["extent"]["temporal"]["interval"][0][0]) is None:
|
|
47
|
+
return value
|
|
48
|
+
return utils.string_to_datetime(value)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def end_datetime(self) -> datetime.datetime | None:
|
|
52
|
+
"""End datetime of the collection."""
|
|
53
|
+
if (value := self._json_dict["extent"]["temporal"]["interval"][0][1]) is None:
|
|
54
|
+
return value
|
|
55
|
+
return utils.string_to_datetime(value)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def published_at(self) -> datetime.datetime:
|
|
59
|
+
"""When the collection was first published."""
|
|
60
|
+
return utils.string_to_datetime(self._json_dict["published"])
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def updated_at(self) -> datetime.datetime:
|
|
64
|
+
"""When the collection was last updated."""
|
|
65
|
+
return utils.string_to_datetime(self._json_dict["updated"])
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def title(self) -> str:
|
|
69
|
+
"""Title of the collection."""
|
|
70
|
+
value = self._json_dict["title"]
|
|
71
|
+
assert isinstance(value, str)
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def description(self) -> str:
|
|
76
|
+
"""Description of the collection."""
|
|
77
|
+
value = self._json_dict["description"]
|
|
78
|
+
assert isinstance(value, str)
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def bbox(self) -> tuple[float, float, float, float]:
|
|
83
|
+
"""Bounding box of the collection (W, S, E, N)."""
|
|
84
|
+
return tuple(self._json_dict["extent"]["spatial"]["bbox"][0])
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def id(self) -> str:
|
|
88
|
+
"""Collection ID."""
|
|
89
|
+
return str(self._json_dict["id"])
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def _process(self) -> datastores.Process:
|
|
93
|
+
url = self._get_link_href(rel="retrieve")
|
|
94
|
+
return datastores.Process.from_request("get", url, **self._request_kwargs)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def process(self) -> datastores.Process:
|
|
98
|
+
warnings.warn(
|
|
99
|
+
"`process` has been deprecated, and in the future will raise an error."
|
|
100
|
+
"Please use `submit` and `apply_constraints` from now on.",
|
|
101
|
+
DeprecationWarning,
|
|
102
|
+
stacklevel=2,
|
|
103
|
+
)
|
|
104
|
+
return self._process
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def form(self) -> list[dict[str, Any]]:
|
|
108
|
+
url = f"{self.url}/form.json"
|
|
109
|
+
return ApiResponse.from_request(
|
|
110
|
+
"get", url, log_messages=False, **self._request_kwargs
|
|
111
|
+
)._json_list
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def constraints(self) -> list[dict[str, Any]]:
|
|
115
|
+
url = f"{self.url}/constraints.json"
|
|
116
|
+
return ApiResponse.from_request(
|
|
117
|
+
"get", url, log_messages=False, **self._request_kwargs
|
|
118
|
+
)._json_list
|
|
119
|
+
|
|
120
|
+
def submit(self, request: dict[str, Any]) -> datastores.Remote:
|
|
121
|
+
"""Submit a request.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
request: dict[str,Any]
|
|
126
|
+
Request parameters.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
datastores.Remote
|
|
131
|
+
"""
|
|
132
|
+
return self._process.submit(request)
|
|
133
|
+
|
|
134
|
+
def apply_constraints(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
135
|
+
"""Apply constraints to the parameters in a request.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
request: dict[str,Any]
|
|
140
|
+
Request parameters.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
dict[str,Any]
|
|
145
|
+
Dictionary of valid values.
|
|
146
|
+
"""
|
|
147
|
+
return self._process.apply_constraints(request)
|
|
148
|
+
|
|
149
|
+
def estimate_costs(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
150
|
+
return self._process.estimate_costs(request)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@attrs.define(slots=False)
|
|
154
|
+
class Catalogue:
|
|
155
|
+
url: str
|
|
156
|
+
headers: dict[str, Any]
|
|
157
|
+
session: requests.Session
|
|
158
|
+
retry_options: dict[str, Any]
|
|
159
|
+
request_options: dict[str, Any]
|
|
160
|
+
download_options: dict[str, Any]
|
|
161
|
+
sleep_max: float
|
|
162
|
+
cleanup: bool
|
|
163
|
+
log_callback: Callable[..., None] | None
|
|
164
|
+
force_exact_url: bool = False
|
|
165
|
+
|
|
166
|
+
def __attrs_post_init__(self) -> None:
|
|
167
|
+
if not self.force_exact_url:
|
|
168
|
+
self.url += f"/{config.SUPPORTED_API_VERSION}"
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def _request_kwargs(self) -> RequestKwargs:
|
|
172
|
+
return RequestKwargs(
|
|
173
|
+
headers=self.headers,
|
|
174
|
+
session=self.session,
|
|
175
|
+
retry_options=self.retry_options,
|
|
176
|
+
request_options=self.request_options,
|
|
177
|
+
download_options=self.download_options,
|
|
178
|
+
sleep_max=self.sleep_max,
|
|
179
|
+
cleanup=self.cleanup,
|
|
180
|
+
log_callback=self.log_callback,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def get_collections(self, search_stats: bool = False, **params: Any) -> Collections:
|
|
184
|
+
url = f"{self.url}/datasets"
|
|
185
|
+
params["search_stats"] = search_stats
|
|
186
|
+
return Collections.from_request(
|
|
187
|
+
"get", url, params=params, **self._request_kwargs
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def get_collection(self, collection_id: str) -> Collection:
|
|
191
|
+
url = f"{self.url}/collections/{collection_id}"
|
|
192
|
+
return Collection.from_request("get", url, **self._request_kwargs)
|
|
193
|
+
|
|
194
|
+
def get_licenses(self, **params: Any) -> dict[str, Any]:
|
|
195
|
+
url = f"{self.url}/vocabularies/licences"
|
|
196
|
+
response = ApiResponse.from_request(
|
|
197
|
+
"get", url, params=params, **self._request_kwargs
|
|
198
|
+
)
|
|
199
|
+
return response._json_dict
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def messages(self) -> ApiResponse:
|
|
203
|
+
url = f"{self.url}/messages"
|
|
204
|
+
return ApiResponse.from_request(
|
|
205
|
+
"get", url, log_messages=False, **self._request_kwargs
|
|
206
|
+
)
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# Copyright 2022, European Union.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import warnings
|
|
19
|
+
from typing import Any, Callable, Literal
|
|
20
|
+
|
|
21
|
+
import attrs
|
|
22
|
+
import multiurl.base
|
|
23
|
+
import requests
|
|
24
|
+
|
|
25
|
+
from ecmwf import datastores
|
|
26
|
+
from ecmwf.datastores import config
|
|
27
|
+
from ecmwf.datastores.catalogue import Catalogue
|
|
28
|
+
from ecmwf.datastores.processing import Processing, RequestKwargs
|
|
29
|
+
from ecmwf.datastores.profile import Profile
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@attrs.define(slots=False)
|
|
33
|
+
class Client:
|
|
34
|
+
"""ECMWF Data Stores Service (DSS) API Python client.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
url: str or None, default: None
|
|
39
|
+
API URL. If None, infer from ECMWF_DATASTORES_URL or ECMWF_DATASTORES_RC_FILE.
|
|
40
|
+
key: str or None, default: None
|
|
41
|
+
API Key. If None, infer from ECMWF_DATASTORES_KEY or ECMWF_DATASTORES_RC_FILE.
|
|
42
|
+
verify: bool, default: True
|
|
43
|
+
Whether to verify the TLS certificate at the remote end.
|
|
44
|
+
timeout: float or tuple[float,float], default: 60
|
|
45
|
+
How many seconds to wait for the server to send data, as a float, or a (connect, read) tuple.
|
|
46
|
+
progress: bool, default: True
|
|
47
|
+
Whether to display the progress bar during download.
|
|
48
|
+
cleanup: bool, default: False
|
|
49
|
+
Whether to delete requests after completion.
|
|
50
|
+
sleep_max: float, default: 120
|
|
51
|
+
Maximum time to wait (in seconds) while checking for a status change.
|
|
52
|
+
retry_after: float, default: 120
|
|
53
|
+
Time to wait (in seconds) between retries.
|
|
54
|
+
maximum_tries: int, default: 500
|
|
55
|
+
Maximum number of retries.
|
|
56
|
+
session: requests.Session
|
|
57
|
+
Requests session.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
url: str | None = None
|
|
61
|
+
key: str | None = None
|
|
62
|
+
verify: bool = True
|
|
63
|
+
timeout: float | tuple[float, float] = 60
|
|
64
|
+
progress: bool = True
|
|
65
|
+
cleanup: bool = False
|
|
66
|
+
sleep_max: float = 120
|
|
67
|
+
retry_after: float = 120
|
|
68
|
+
maximum_tries: int = 500
|
|
69
|
+
session: requests.Session = attrs.field(factory=requests.Session)
|
|
70
|
+
_log_callback: Callable[..., None] | None = None
|
|
71
|
+
|
|
72
|
+
def __attrs_post_init__(self) -> None:
|
|
73
|
+
if self.url is None:
|
|
74
|
+
self.url = str(config.get_config("url"))
|
|
75
|
+
|
|
76
|
+
if self.key is None:
|
|
77
|
+
try:
|
|
78
|
+
self.key = str(config.get_config("key"))
|
|
79
|
+
except (KeyError, FileNotFoundError):
|
|
80
|
+
warnings.warn("The API key is missing", UserWarning)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
self._catalogue_api.messages.log_messages()
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
warnings.warn(str(exc), UserWarning)
|
|
86
|
+
|
|
87
|
+
def _get_headers(self, key_is_mandatory: bool = True) -> dict[str, str]:
|
|
88
|
+
headers = {"User-Agent": f"ecmwf-datastores-client/{datastores.__version__}"}
|
|
89
|
+
if self.key is not None:
|
|
90
|
+
headers["PRIVATE-TOKEN"] = self.key
|
|
91
|
+
elif key_is_mandatory:
|
|
92
|
+
raise ValueError("The API key is needed to access this resource")
|
|
93
|
+
return headers
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def _retry_options(self) -> dict[str, Any]:
|
|
97
|
+
return {
|
|
98
|
+
"maximum_tries": self.maximum_tries,
|
|
99
|
+
"retry_after": self.retry_after,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def _download_options(self) -> dict[str, Any]:
|
|
104
|
+
progress_bar = (
|
|
105
|
+
multiurl.base.progress_bar if self.progress else multiurl.base.NoBar
|
|
106
|
+
)
|
|
107
|
+
return {
|
|
108
|
+
"progress_bar": progress_bar,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def _request_options(self) -> dict[str, Any]:
|
|
113
|
+
return {
|
|
114
|
+
"timeout": self.timeout,
|
|
115
|
+
"verify": self.verify,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def _get_request_kwargs(self, key_is_mandatory: bool = True) -> RequestKwargs:
|
|
119
|
+
return RequestKwargs(
|
|
120
|
+
headers=self._get_headers(key_is_mandatory=key_is_mandatory),
|
|
121
|
+
session=self.session,
|
|
122
|
+
retry_options=self._retry_options,
|
|
123
|
+
request_options=self._request_options,
|
|
124
|
+
download_options=self._download_options,
|
|
125
|
+
sleep_max=self.sleep_max,
|
|
126
|
+
cleanup=self.cleanup,
|
|
127
|
+
log_callback=self._log_callback,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@functools.cached_property
|
|
131
|
+
def _catalogue_api(self) -> Catalogue:
|
|
132
|
+
return Catalogue(
|
|
133
|
+
f"{self.url}/catalogue",
|
|
134
|
+
**self._get_request_kwargs(key_is_mandatory=False),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@functools.cached_property
|
|
138
|
+
def _retrieve_api(self) -> Processing:
|
|
139
|
+
return Processing(f"{self.url}/retrieve", **self._get_request_kwargs())
|
|
140
|
+
|
|
141
|
+
@functools.cached_property
|
|
142
|
+
def _profile_api(self) -> Profile:
|
|
143
|
+
return Profile(f"{self.url}/profiles", **self._get_request_kwargs())
|
|
144
|
+
|
|
145
|
+
def accept_licence(self, licence_id: str, revision: int) -> dict[str, Any]:
|
|
146
|
+
return self._profile_api.accept_licence(licence_id, revision=revision)
|
|
147
|
+
|
|
148
|
+
def apply_constraints(
|
|
149
|
+
self, collection_id: str, request: dict[str, Any]
|
|
150
|
+
) -> dict[str, Any]:
|
|
151
|
+
"""Apply constraints to the parameters in a request.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
collection_id: str
|
|
156
|
+
Collection ID (e.g., ``"projections-cmip6"``).
|
|
157
|
+
request: dict[str,Any]
|
|
158
|
+
Request parameters.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
dict[str,Any]
|
|
163
|
+
Dictionary of valid values.
|
|
164
|
+
"""
|
|
165
|
+
return self.get_process(collection_id).apply_constraints(request)
|
|
166
|
+
|
|
167
|
+
def check_authentication(self) -> dict[str, Any]:
|
|
168
|
+
"""Verify authentication.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
dict[str,Any]
|
|
173
|
+
Content of the response.
|
|
174
|
+
|
|
175
|
+
Raises
|
|
176
|
+
------
|
|
177
|
+
requests.HTTPError
|
|
178
|
+
If the authentication fails.
|
|
179
|
+
"""
|
|
180
|
+
return self._profile_api.check_authentication()
|
|
181
|
+
|
|
182
|
+
def download_results(self, request_id: str, target: str | None = None) -> str:
|
|
183
|
+
"""Download the results of a request.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
request_id: str
|
|
188
|
+
Request ID.
|
|
189
|
+
target: str or None
|
|
190
|
+
Target path. If None, download to the working directory.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
str
|
|
195
|
+
Path to the retrieved file.
|
|
196
|
+
"""
|
|
197
|
+
return self.get_remote(request_id).download(target)
|
|
198
|
+
|
|
199
|
+
def estimate_costs(self, collection_id: str, request: Any) -> dict[str, Any]:
|
|
200
|
+
return self.get_process(collection_id).estimate_costs(request)
|
|
201
|
+
|
|
202
|
+
def get_accepted_licences(
|
|
203
|
+
self,
|
|
204
|
+
scope: Literal[None, "all", "dataset", "portal"] = None,
|
|
205
|
+
) -> list[dict[str, Any]]:
|
|
206
|
+
params = {k: v for k, v in zip(["scope"], [scope]) if v is not None}
|
|
207
|
+
licences: list[dict[str, Any]]
|
|
208
|
+
licences = self._profile_api.accepted_licences(**params).get("licences", [])
|
|
209
|
+
return licences
|
|
210
|
+
|
|
211
|
+
def get_collection(self, collection_id: str) -> datastores.Collection:
|
|
212
|
+
"""Retrieve a catalogue collection.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
collection_id: str
|
|
217
|
+
Collection ID (e.g., ``"projections-cmip6"``).
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
datastores.Collection
|
|
222
|
+
"""
|
|
223
|
+
return self._catalogue_api.get_collection(collection_id)
|
|
224
|
+
|
|
225
|
+
def get_collections(
|
|
226
|
+
self,
|
|
227
|
+
limit: int | None = None,
|
|
228
|
+
sortby: Literal[None, "id", "relevance", "title", "update"] = None,
|
|
229
|
+
query: str | None = None,
|
|
230
|
+
keywords: list[str] | None = None,
|
|
231
|
+
) -> datastores.Collections:
|
|
232
|
+
"""Retrieve catalogue collections.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
limit: int | None
|
|
237
|
+
Number of processes per page.
|
|
238
|
+
sortby: {None, 'id', 'relevance', 'title', 'update'}
|
|
239
|
+
Field to sort results by.
|
|
240
|
+
query: str or None
|
|
241
|
+
Full-text search query.
|
|
242
|
+
keywords: list[str] or None
|
|
243
|
+
Filter by keywords.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
datastores.Collections
|
|
248
|
+
"""
|
|
249
|
+
params: dict[str, Any] = {
|
|
250
|
+
k: v
|
|
251
|
+
for k, v in zip(
|
|
252
|
+
["limit", "sortby", "q", "kw"], [limit, sortby, query, keywords]
|
|
253
|
+
)
|
|
254
|
+
if v is not None
|
|
255
|
+
}
|
|
256
|
+
return self._catalogue_api.get_collections(**params)
|
|
257
|
+
|
|
258
|
+
def get_jobs(
|
|
259
|
+
self,
|
|
260
|
+
limit: int | None = None,
|
|
261
|
+
sortby: Literal[None, "created", "-created"] = None,
|
|
262
|
+
status: Literal[None, "accepted", "running", "successful", "failed"] = None,
|
|
263
|
+
) -> datastores.Jobs:
|
|
264
|
+
"""Retrieve submitted jobs.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
limit: int or None
|
|
269
|
+
Number of processes per page.
|
|
270
|
+
sortby: {None, 'created', '-created'}
|
|
271
|
+
Field to sort results by.
|
|
272
|
+
status: {None, 'accepted', 'running', 'successful', 'failed'}
|
|
273
|
+
Status of the results.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
datastores.Jobs
|
|
278
|
+
"""
|
|
279
|
+
params = {
|
|
280
|
+
k: v
|
|
281
|
+
for k, v in zip(["limit", "sortby", "status"], [limit, sortby, status])
|
|
282
|
+
if v is not None
|
|
283
|
+
}
|
|
284
|
+
return self._retrieve_api.get_jobs(**params)
|
|
285
|
+
|
|
286
|
+
def get_licences(
|
|
287
|
+
self,
|
|
288
|
+
scope: Literal[None, "all", "dataset", "portal"] = None,
|
|
289
|
+
) -> list[dict[str, Any]]:
|
|
290
|
+
params = {k: v for k, v in zip(["scope"], [scope]) if v is not None}
|
|
291
|
+
licences: list[dict[str, Any]]
|
|
292
|
+
licences = self._catalogue_api.get_licenses(**params).get("licences", [])
|
|
293
|
+
return licences
|
|
294
|
+
|
|
295
|
+
def get_process(self, collection_id: str) -> datastores.Process:
|
|
296
|
+
return self._retrieve_api.get_process(collection_id)
|
|
297
|
+
|
|
298
|
+
def get_processes(
|
|
299
|
+
self,
|
|
300
|
+
limit: int | None = None,
|
|
301
|
+
sortby: Literal[None, "id", "-id"] = None,
|
|
302
|
+
) -> datastores.Processes:
|
|
303
|
+
params = {
|
|
304
|
+
k: v for k, v in zip(["limit", "sortby"], [limit, sortby]) if v is not None
|
|
305
|
+
}
|
|
306
|
+
return self._retrieve_api.get_processes(**params)
|
|
307
|
+
|
|
308
|
+
def get_remote(self, request_id: str) -> datastores.Remote:
|
|
309
|
+
"""
|
|
310
|
+
Retrieve the remote object of a request.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
request_id: str
|
|
315
|
+
Request ID.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
datastores.Remote
|
|
320
|
+
"""
|
|
321
|
+
return self._retrieve_api.get_job(request_id).get_remote()
|
|
322
|
+
|
|
323
|
+
def get_results(self, request_id: str) -> datastores.Results:
|
|
324
|
+
"""
|
|
325
|
+
Retrieve the results of a request.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
request_id: str
|
|
330
|
+
Request ID.
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
datastores.Results
|
|
335
|
+
"""
|
|
336
|
+
return self.get_remote(request_id).get_results()
|
|
337
|
+
|
|
338
|
+
def retrieve(
|
|
339
|
+
self,
|
|
340
|
+
collection_id: str,
|
|
341
|
+
request: dict[str, Any],
|
|
342
|
+
target: str | None = None,
|
|
343
|
+
) -> str:
|
|
344
|
+
"""Submit a request and retrieve the results.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
collection_id: str
|
|
349
|
+
Collection ID (e.g., ``"projections-cmip6"``).
|
|
350
|
+
request: dict[str,Any]
|
|
351
|
+
Request parameters.
|
|
352
|
+
target: str or None
|
|
353
|
+
Target path. If None, download to the working directory.
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
str
|
|
358
|
+
Path to the retrieved file.
|
|
359
|
+
"""
|
|
360
|
+
return self.submit(collection_id, request).download(target)
|
|
361
|
+
|
|
362
|
+
def star_collection(self, collection_id: str) -> list[str]:
|
|
363
|
+
return self._profile_api.star_collection(collection_id)
|
|
364
|
+
|
|
365
|
+
def submit(self, collection_id: str, request: dict[str, Any]) -> datastores.Remote:
|
|
366
|
+
"""Submit a request.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
collection_id: str
|
|
371
|
+
Collection ID (e.g., ``"projections-cmip6"``).
|
|
372
|
+
request: dict[str,Any]
|
|
373
|
+
Request parameters.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
datastores.Remote
|
|
378
|
+
"""
|
|
379
|
+
return self._retrieve_api.submit(collection_id, request)
|
|
380
|
+
|
|
381
|
+
def submit_and_wait_on_results(
|
|
382
|
+
self, collection_id: str, request: dict[str, Any]
|
|
383
|
+
) -> datastores.Results:
|
|
384
|
+
"""Submit a request and wait for the results to be ready.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
collection_id: str
|
|
389
|
+
Collection ID (e.g., ``"projections-cmip6"``).
|
|
390
|
+
request: dict[str,Any]
|
|
391
|
+
Request parameters.
|
|
392
|
+
|
|
393
|
+
Returns
|
|
394
|
+
-------
|
|
395
|
+
datastores.Results
|
|
396
|
+
"""
|
|
397
|
+
return self._retrieve_api.submit(collection_id, request).get_results()
|
|
398
|
+
|
|
399
|
+
def unstar_collection(self, collection_id: str) -> None:
|
|
400
|
+
return self._profile_api.unstar_collection(collection_id)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright 2022, European Union.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
SUPPORTED_API_VERSION = "v1"
|
|
20
|
+
CONFIG_PREFIX = "ecmwf_datastores"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def read_config(path: str | None = None) -> dict[str, str]:
|
|
24
|
+
if path is None:
|
|
25
|
+
path = os.getenv(
|
|
26
|
+
f"{CONFIG_PREFIX}_RC_FILE".upper(),
|
|
27
|
+
f"~/.{CONFIG_PREFIX}rc".replace("_", "").lower(),
|
|
28
|
+
)
|
|
29
|
+
path = os.path.expanduser(path)
|
|
30
|
+
try:
|
|
31
|
+
config = {}
|
|
32
|
+
with open(path) as f:
|
|
33
|
+
for line in f.readlines():
|
|
34
|
+
if ":" in line:
|
|
35
|
+
key, value = line.strip().split(":", 1)
|
|
36
|
+
config[key] = value.strip()
|
|
37
|
+
return config
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
raise
|
|
40
|
+
except Exception:
|
|
41
|
+
raise ValueError(f"Failed to parse {path!r} file")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_config(key: str, config_path: str | None = None) -> str:
|
|
45
|
+
return os.getenv(f"{CONFIG_PREFIX}_{key}".upper()) or read_config(config_path)[key]
|