freva-client 2508.0.0__py3-none-any.whl → 2509.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 freva-client might be problematic. Click here for more details.
- freva_client/__init__.py +2 -1
- freva_client/auth.py +36 -129
- freva_client/cli/auth_cli.py +6 -0
- freva_client/cli/cli_utils.py +38 -1
- freva_client/cli/databrowser_cli.py +232 -32
- freva_client/query.py +255 -38
- freva_client/utils/auth_utils.py +297 -21
- freva_client/utils/databrowser_utils.py +118 -32
- {freva_client-2508.0.0.data → freva_client-2509.1.0.data}/data/share/freva/freva.toml +5 -0
- {freva_client-2508.0.0.dist-info → freva_client-2509.1.0.dist-info}/METADATA +1 -1
- freva_client-2509.1.0.dist-info/RECORD +20 -0
- freva_client-2508.0.0.dist-info/RECORD +0 -20
- {freva_client-2508.0.0.dist-info → freva_client-2509.1.0.dist-info}/WHEEL +0 -0
- {freva_client-2508.0.0.dist-info → freva_client-2509.1.0.dist-info}/entry_points.txt +0 -0
freva_client/utils/auth_utils.py
CHANGED
|
@@ -2,19 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
-
import
|
|
5
|
+
import random
|
|
6
6
|
import sys
|
|
7
7
|
import time
|
|
8
|
+
import webbrowser
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from dataclasses import dataclass, field
|
|
8
11
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
10
|
-
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Dict,
|
|
16
|
+
Iterator,
|
|
17
|
+
Literal,
|
|
18
|
+
Optional,
|
|
19
|
+
TypedDict,
|
|
20
|
+
Union,
|
|
21
|
+
cast,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
import rich.console
|
|
26
|
+
import rich.spinner
|
|
11
27
|
from appdirs import user_cache_dir
|
|
28
|
+
from rich.live import Live
|
|
29
|
+
|
|
30
|
+
from freva_client.utils import logger
|
|
12
31
|
|
|
13
32
|
TOKEN_EXPIRY_BUFFER = 60 # seconds
|
|
14
33
|
TOKEN_ENV_VAR = "FREVA_TOKEN_FILE"
|
|
15
34
|
|
|
16
35
|
|
|
17
|
-
|
|
36
|
+
BROWSER_MESSAGE = """Will attempt to open the auth url in your browser.
|
|
37
|
+
|
|
38
|
+
If this doesn't work, try opening the following url:
|
|
39
|
+
|
|
40
|
+
{interactive}{uri}{interactive_end}
|
|
41
|
+
|
|
42
|
+
You might have to enter the this code manually {interactive}{user_code}{interactive_end}
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
NON_INTERACTIVE_MESSAGE = """
|
|
46
|
+
|
|
47
|
+
Visit the following url to authorise your session:
|
|
48
|
+
|
|
49
|
+
{interactive}{uri}{interactive_end}
|
|
50
|
+
|
|
51
|
+
You might have to enter the this code manually {interactive}{user_code}{interactive_end}
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@contextmanager
|
|
57
|
+
def _clock(timeout: Optional[int] = None) -> Iterator[None]:
|
|
58
|
+
Console = rich.console.Console(
|
|
59
|
+
force_terminal=is_interactive_shell(), stderr=True
|
|
60
|
+
)
|
|
61
|
+
txt = f"- Timeout: {timeout:>3,.0f}s " if timeout else ""
|
|
62
|
+
interactive = int(Console.is_terminal)
|
|
63
|
+
if int(os.getenv("INTERACIVE_SESSION", str(interactive))):
|
|
64
|
+
spinner = rich.spinner.Spinner(
|
|
65
|
+
"moon", text=f"[b]Waiting for code {txt}... [/]"
|
|
66
|
+
)
|
|
67
|
+
live = Live(
|
|
68
|
+
spinner, console=Console, refresh_per_second=2.5, transient=True
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
live.start()
|
|
72
|
+
yield
|
|
73
|
+
finally:
|
|
74
|
+
live.stop()
|
|
75
|
+
else:
|
|
76
|
+
yield
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AuthError(Exception):
|
|
80
|
+
"""Athentication error."""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self, message: str, detail: Optional[Dict[str, str]] = None
|
|
84
|
+
) -> None:
|
|
85
|
+
super().__init__(message)
|
|
86
|
+
self.message = message
|
|
87
|
+
self.detail = detail or {}
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
return f"{self.message}: {self.detail}" if self.detail else self.message
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Token(TypedDict, total=False):
|
|
18
94
|
"""Token information."""
|
|
19
95
|
|
|
20
96
|
access_token: str
|
|
@@ -25,9 +101,191 @@ class Token(TypedDict):
|
|
|
25
101
|
scope: str
|
|
26
102
|
|
|
27
103
|
|
|
28
|
-
|
|
104
|
+
@dataclass
|
|
105
|
+
class DeviceAuthClient:
|
|
106
|
+
"""
|
|
107
|
+
Minimal OIDC Device Authorization Grant client.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
device_endpoint : str
|
|
112
|
+
Device endpoint URL, e.g. ``{issuer}/protocol/openid-connect/auth/device``.
|
|
113
|
+
token_endpoint : str
|
|
114
|
+
Token endpoint URL, e.g. ``{issuer}/protocol/openid-connect/token``.
|
|
115
|
+
scope : str, optional
|
|
116
|
+
Space-separated scopes; include ``offline_access`` if you need offline RTs.
|
|
117
|
+
timeout : int | None, optional
|
|
118
|
+
Overall timeout (seconds) for user approval. ``None`` waits indefinitely.
|
|
119
|
+
session : requests.Session | None, optional
|
|
120
|
+
Reusable HTTP session. A new one is created if omitted.
|
|
121
|
+
|
|
122
|
+
Notes
|
|
123
|
+
-----
|
|
124
|
+
- For a CLI UX, show both ``verification_uri_complete`` (if present) and
|
|
125
|
+
``verification_uri`` + ``user_code`` as fallback.
|
|
126
|
+
- Polling respects ``interval`` and ``slow_down`` per RFC 8628.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
device_endpoint: str
|
|
130
|
+
token_endpoint: str
|
|
131
|
+
timeout: Optional[int] = 600
|
|
132
|
+
session: requests.Session = field(default_factory=requests.Session)
|
|
133
|
+
|
|
134
|
+
# ---------- Public API ----------
|
|
135
|
+
|
|
136
|
+
def login(
|
|
137
|
+
self,
|
|
138
|
+
*,
|
|
139
|
+
auto_open: bool = True,
|
|
140
|
+
token_normalizer: Optional[Callable[[str, Dict[str, str]], Token]] = None,
|
|
141
|
+
) -> Token:
|
|
142
|
+
"""
|
|
143
|
+
Run the full device flow and return an OIDC token payload.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
auto_open : bool, optional
|
|
148
|
+
Attempt to open ``verification_uri_complete`` in a browser.
|
|
149
|
+
token_normalizer : Callable, optional
|
|
150
|
+
If provided, called as ``token_normalizer(self.token_endpoint, data)``
|
|
151
|
+
to normalize/store tokens (e.g., your existing ``self.get_token``).
|
|
152
|
+
If omitted, the raw token JSON from the token endpoint is returned.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
Token
|
|
157
|
+
The token payload. Includes a refresh token if granted and allowed.
|
|
158
|
+
|
|
159
|
+
Raises
|
|
160
|
+
------
|
|
161
|
+
AuthError
|
|
162
|
+
If the device flow fails or times out.
|
|
163
|
+
"""
|
|
164
|
+
init = self._authorize()
|
|
165
|
+
|
|
166
|
+
uri = init.get("verification_uri_complete") or init["verification_uri"]
|
|
167
|
+
user_code = init["user_code"]
|
|
168
|
+
|
|
169
|
+
Console = rich.console.Console(
|
|
170
|
+
force_terminal=is_interactive_shell(), stderr=True
|
|
171
|
+
)
|
|
172
|
+
pprint = Console.print if Console.is_terminal else print
|
|
173
|
+
interactive, interactive_end = (
|
|
174
|
+
("[b]", "[/b]") if Console.is_terminal else ("", "")
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if auto_open and init.get("verification_uri_complete"):
|
|
178
|
+
try:
|
|
179
|
+
pprint(
|
|
180
|
+
BROWSER_MESSAGE.format(
|
|
181
|
+
user_code=user_code,
|
|
182
|
+
uri=uri,
|
|
183
|
+
interactive=interactive,
|
|
184
|
+
interactive_end=interactive_end,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
webbrowser.open(init["verification_uri_complete"])
|
|
188
|
+
except Exception as error:
|
|
189
|
+
logger.warning("Could not auto-open browser: %s", error)
|
|
190
|
+
else:
|
|
191
|
+
pprint(
|
|
192
|
+
NON_INTERACTIVE_MESSAGE.format(
|
|
193
|
+
user_code=user_code,
|
|
194
|
+
uri=uri,
|
|
195
|
+
interactive=interactive,
|
|
196
|
+
interactive_end=interactive_end,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return self._poll_for_token(
|
|
200
|
+
device_code=init["device_code"],
|
|
201
|
+
base_interval=int(init.get("interval", 5)),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# ---------- Internals ----------
|
|
205
|
+
|
|
206
|
+
def _authorize(self) -> Dict[str, Any]:
|
|
207
|
+
"""Start device authorization; return the raw init payload."""
|
|
208
|
+
payload = self._post_form(self.device_endpoint)
|
|
209
|
+
# Validate essentials
|
|
210
|
+
for k in ("device_code", "user_code", "verification_uri", "expires_in"):
|
|
211
|
+
if k not in payload:
|
|
212
|
+
raise AuthError(f"Device authorization missing '{k}'")
|
|
213
|
+
return payload
|
|
214
|
+
|
|
215
|
+
def _poll_for_token(self, *, device_code: str, base_interval: int) -> Token:
|
|
216
|
+
"""
|
|
217
|
+
Poll token endpoint until approved/denied/expired; return token JSON.
|
|
218
|
+
|
|
219
|
+
Raises
|
|
220
|
+
------
|
|
221
|
+
AuthError
|
|
222
|
+
On timeout, denial, or other OAuth errors.
|
|
223
|
+
"""
|
|
224
|
+
start = time.monotonic()
|
|
225
|
+
interval = max(1, base_interval)
|
|
226
|
+
with _clock(self.timeout):
|
|
227
|
+
while True:
|
|
228
|
+
sleep = interval + random.uniform(-0.2, 0.4)
|
|
229
|
+
if (
|
|
230
|
+
self.timeout is not None
|
|
231
|
+
and time.monotonic() - start > self.timeout
|
|
232
|
+
):
|
|
233
|
+
raise AuthError(
|
|
234
|
+
"Login did not complete within the allotted time; "
|
|
235
|
+
"approve the request in your browser and try again."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
data = {"device-code": device_code}
|
|
239
|
+
try:
|
|
240
|
+
return cast(Token, self._post_form(self.token_endpoint, data))
|
|
241
|
+
except AuthError as error:
|
|
242
|
+
err = (
|
|
243
|
+
error.detail.get("error")
|
|
244
|
+
if isinstance(error.detail, dict)
|
|
245
|
+
else None
|
|
246
|
+
)
|
|
247
|
+
if err is None or "authorization_pending" in err:
|
|
248
|
+
time.sleep(sleep)
|
|
249
|
+
elif "slow_down" in err:
|
|
250
|
+
interval += 5
|
|
251
|
+
time.sleep(sleep)
|
|
252
|
+
elif "expired_token" in err or "access_denied" in err:
|
|
253
|
+
raise AuthError(f"Device flow failed: {err}")
|
|
254
|
+
else:
|
|
255
|
+
# Unknown OAuth error
|
|
256
|
+
raise # pragma: no cover
|
|
257
|
+
|
|
258
|
+
def _post_form(
|
|
259
|
+
self, url: str, data: Optional[Dict[str, str]] = None
|
|
260
|
+
) -> Dict[str, Any]:
|
|
261
|
+
"""POST x-www-form-urlencoded and return JSON or raise AuthError."""
|
|
262
|
+
resp = self.session.post(
|
|
263
|
+
url,
|
|
264
|
+
data=data,
|
|
265
|
+
headers={
|
|
266
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
267
|
+
"Connection": "close",
|
|
268
|
+
},
|
|
269
|
+
timeout=30,
|
|
270
|
+
)
|
|
271
|
+
if resp.status_code >= 400:
|
|
272
|
+
try:
|
|
273
|
+
payload = resp.json()
|
|
274
|
+
except Exception:
|
|
275
|
+
payload = {
|
|
276
|
+
"error": "http_error",
|
|
277
|
+
"error_description": resp.text[:300],
|
|
278
|
+
}
|
|
279
|
+
raise AuthError(f"{url} -> {resp.status_code}", detail=payload)
|
|
280
|
+
try:
|
|
281
|
+
return cast(Dict[str, Any], resp.json())
|
|
282
|
+
except Exception as error:
|
|
283
|
+
raise AuthError(f"Invalid JSON from {url}: {error}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def get_default_token_file(token_file: Optional[Union[str, Path]] = None) -> Path:
|
|
29
287
|
"""Get the location of the default token file."""
|
|
30
|
-
path_str = os.getenv(TOKEN_ENV_VAR, "").strip()
|
|
288
|
+
path_str = token_file or os.getenv(TOKEN_ENV_VAR, "").strip()
|
|
31
289
|
|
|
32
290
|
path = Path(
|
|
33
291
|
path_str or os.path.join(user_cache_dir("freva"), "auth-token.json")
|
|
@@ -223,18 +481,36 @@ def choose_token_strategy(
|
|
|
223
481
|
return "fail"
|
|
224
482
|
|
|
225
483
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
484
|
+
def requires_authentication(
|
|
485
|
+
flavour: Optional[str],
|
|
486
|
+
zarr: bool = False,
|
|
487
|
+
databrowser_url: Optional[str] = None,
|
|
488
|
+
) -> bool:
|
|
489
|
+
"""Check if authentication is required.
|
|
490
|
+
|
|
491
|
+
Parameters
|
|
492
|
+
----------
|
|
493
|
+
flavour : str or None
|
|
494
|
+
The data flavour to check.
|
|
495
|
+
zarr : bool, default: False
|
|
496
|
+
Whether the request is for zarr data.
|
|
497
|
+
databrowser_url : str or None
|
|
498
|
+
The URL of the databrowser to query for available flavours.
|
|
499
|
+
If None, the function will skip querying and assume authentication
|
|
500
|
+
is required for non-default flavours.
|
|
501
|
+
"""
|
|
502
|
+
if zarr:
|
|
503
|
+
return True
|
|
504
|
+
if flavour in {"freva", "cmip6", "cmip5", "cordex", "user", None}:
|
|
505
|
+
return False
|
|
506
|
+
try:
|
|
507
|
+
response = requests.get(f"{databrowser_url}/flavours", timeout=30)
|
|
508
|
+
response.raise_for_status()
|
|
509
|
+
result = {"flavours": response.json().get("flavours", [])}
|
|
510
|
+
if "flavours" in result:
|
|
511
|
+
global_flavour_names = {f["flavour_name"] for f in result["flavours"]}
|
|
512
|
+
return flavour not in global_flavour_names
|
|
513
|
+
except Exception:
|
|
514
|
+
pass
|
|
515
|
+
|
|
516
|
+
return True
|
|
@@ -40,15 +40,50 @@ class Config:
|
|
|
40
40
|
self,
|
|
41
41
|
host: Optional[str] = None,
|
|
42
42
|
uniq_key: Literal["file", "uri"] = "file",
|
|
43
|
-
flavour: str =
|
|
43
|
+
flavour: Optional[str] = None,
|
|
44
44
|
) -> None:
|
|
45
45
|
self.databrowser_url = f"{self.get_api_url(host)}/databrowser"
|
|
46
46
|
self.auth_url = f"{self.get_api_url(host)}/auth/v2"
|
|
47
|
+
self.get_api_main_url = self.get_api_url(host)
|
|
47
48
|
self.uniq_key = uniq_key
|
|
48
|
-
self.
|
|
49
|
+
self.flavour = self.get_flavour(flavour)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
@cached_property
|
|
52
|
+
def validate_server(self) -> bool:
|
|
53
|
+
"""Ping the databrowser to check if it is reachable."""
|
|
54
|
+
try:
|
|
55
|
+
res = requests.get(f"{self.get_api_main_url}/ping", timeout=15)
|
|
56
|
+
return res.status_code == 200
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Could not connect to {self.databrowser_url}: {e}"
|
|
60
|
+
) from None
|
|
61
|
+
|
|
62
|
+
def get_flavour(self, flavour: Optional[str]) -> str:
|
|
63
|
+
"""Get the current flavour."""
|
|
64
|
+
if flavour:
|
|
65
|
+
return flavour
|
|
66
|
+
else:
|
|
67
|
+
try:
|
|
68
|
+
config_flavour = self._get_databrowser_params_from_config().get(
|
|
69
|
+
"flavour", ""
|
|
70
|
+
)
|
|
71
|
+
return config_flavour or "freva"
|
|
72
|
+
except ValueError:
|
|
73
|
+
return "freva"
|
|
74
|
+
|
|
75
|
+
def _read_ini(self, path: Path) -> Dict[str, str]:
|
|
76
|
+
"""Read an ini file.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
path : Path
|
|
81
|
+
Path to the ini configuration file.
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
Dict[str, str]
|
|
85
|
+
Dictionary with the configuration.
|
|
86
|
+
"""
|
|
52
87
|
ini_parser = ConfigParser(interpolation=ExtendedInterpolation())
|
|
53
88
|
ini_parser.read_string(path.read_text())
|
|
54
89
|
config = ini_parser["evaluation_system"]
|
|
@@ -59,41 +94,99 @@ class Config:
|
|
|
59
94
|
port = port or config.get("databrowser.port", "")
|
|
60
95
|
if port:
|
|
61
96
|
host = f"{host}:{port}"
|
|
62
|
-
|
|
97
|
+
host = f"{scheme}://{host}"
|
|
98
|
+
flavour = config.get("databrowser.default_flavour", "")
|
|
99
|
+
return {
|
|
100
|
+
"host": host,
|
|
101
|
+
"flavour": flavour,
|
|
102
|
+
}
|
|
63
103
|
|
|
64
|
-
def _read_toml(self, path: Path) -> str:
|
|
65
|
-
"""Read a new style toml config file.
|
|
104
|
+
def _read_toml(self, path: Path) -> Dict[str, str]:
|
|
105
|
+
"""Read a new style toml config file.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
path : Path
|
|
110
|
+
Path to the toml configuration file.
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
Dict[str, str]
|
|
114
|
+
Dictionary with the configuration.
|
|
115
|
+
"""
|
|
66
116
|
try:
|
|
67
117
|
config = tomli.loads(path.read_text()).get("freva", {})
|
|
68
|
-
scheme, host = self._split_url(cast(str, config
|
|
118
|
+
scheme, host = self._split_url(cast(str, config.get("host", "")))
|
|
119
|
+
flavour = config.get("default_flavour", "")
|
|
69
120
|
except (tomli.TOMLDecodeError, KeyError):
|
|
70
|
-
return
|
|
121
|
+
return {}
|
|
71
122
|
host, _, port = host.partition(":")
|
|
72
123
|
if port:
|
|
73
124
|
host = f"{host}:{port}"
|
|
74
|
-
|
|
125
|
+
host = f"{scheme}://{host}"
|
|
126
|
+
return {
|
|
127
|
+
"host": host,
|
|
128
|
+
"flavour": flavour,
|
|
129
|
+
}
|
|
75
130
|
|
|
76
|
-
def _read_config(
|
|
77
|
-
|
|
131
|
+
def _read_config(
|
|
132
|
+
self, path: Path, file_type: Literal["toml", "ini"]
|
|
133
|
+
) -> Dict[str, str]:
|
|
134
|
+
"""Read the configuration.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
path : Path
|
|
139
|
+
Path to the configuration file.
|
|
140
|
+
file_type : Literal["toml", "ini"]
|
|
141
|
+
Type of the configuration file.
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
Dict[str, str]
|
|
145
|
+
Dictionary with the configuration.
|
|
146
|
+
"""
|
|
78
147
|
data_types = {"toml": self._read_toml, "ini": self._read_ini}
|
|
79
148
|
try:
|
|
80
149
|
return data_types[file_type](path)
|
|
81
150
|
except KeyError:
|
|
82
151
|
pass
|
|
83
|
-
return
|
|
152
|
+
return {}
|
|
153
|
+
|
|
154
|
+
@cached_property
|
|
155
|
+
def _get_headers(self) -> Optional[Dict[str, str]]:
|
|
156
|
+
"""Get the headers for requests."""
|
|
157
|
+
from freva_client.auth import Auth
|
|
158
|
+
|
|
159
|
+
from .auth_utils import load_token
|
|
160
|
+
auth = Auth()
|
|
161
|
+
token = auth._auth_token or load_token(auth.token_file)
|
|
162
|
+
if token and "access_token" in token:
|
|
163
|
+
return {"Authorization": f"Bearer {token['access_token']}"}
|
|
164
|
+
return None
|
|
84
165
|
|
|
85
166
|
@cached_property
|
|
86
167
|
def overview(self) -> Dict[str, Any]:
|
|
87
|
-
"""Get an overview of
|
|
168
|
+
"""Get an overview of all databrowser flavours
|
|
169
|
+
and search keys including custom flavours."""
|
|
170
|
+
headers = self._get_headers
|
|
88
171
|
try:
|
|
89
|
-
res = requests.get(
|
|
172
|
+
res = requests.get(
|
|
173
|
+
f"{self.databrowser_url}/overview",
|
|
174
|
+
headers=headers,
|
|
175
|
+
timeout=15
|
|
176
|
+
)
|
|
177
|
+
data = cast(Dict[str, Any], res.json())
|
|
178
|
+
if not headers:
|
|
179
|
+
data["Note"] = (
|
|
180
|
+
"Displaying only global flavours. "
|
|
181
|
+
"Authenticate to see custom user flavours as well."
|
|
182
|
+
)
|
|
183
|
+
return data
|
|
90
184
|
except requests.exceptions.ConnectionError:
|
|
91
185
|
raise ValueError(
|
|
92
186
|
f"Could not connect to {self.databrowser_url}"
|
|
93
187
|
) from None
|
|
94
|
-
return cast(Dict[str, Any], res.json())
|
|
95
188
|
|
|
96
|
-
def
|
|
189
|
+
def _get_databrowser_params_from_config(self) -> Dict[str, str]:
|
|
97
190
|
"""Get the config file order."""
|
|
98
191
|
|
|
99
192
|
eval_conf = self.get_dirs(user=False) / "evaluation_system.conf"
|
|
@@ -111,26 +204,20 @@ class Config:
|
|
|
111
204
|
}
|
|
112
205
|
for config_path, config_type in paths.items():
|
|
113
206
|
if config_path.is_file():
|
|
114
|
-
|
|
207
|
+
config_data = self._read_config(config_path, config_type)
|
|
208
|
+
host = config_data.get("host", "")
|
|
209
|
+
flavour = config_data.get("flavour", "")
|
|
115
210
|
if host:
|
|
116
|
-
return
|
|
211
|
+
return {
|
|
212
|
+
"host": host,
|
|
213
|
+
"flavour": flavour
|
|
214
|
+
}
|
|
117
215
|
raise ValueError(
|
|
118
216
|
"No databrowser host configured, please use a"
|
|
119
217
|
" configuration defining a databrowser host or"
|
|
120
218
|
" set a host name using the `host` key"
|
|
121
219
|
)
|
|
122
220
|
|
|
123
|
-
@cached_property
|
|
124
|
-
def flavour(self) -> str:
|
|
125
|
-
"""Get the flavour."""
|
|
126
|
-
flavours = self.overview.get("flavours", [])
|
|
127
|
-
if self._flavour not in flavours:
|
|
128
|
-
raise ValueError(
|
|
129
|
-
f"Search {self._flavour} not available, select from"
|
|
130
|
-
f" {','.join(flavours)}"
|
|
131
|
-
)
|
|
132
|
-
return self._flavour
|
|
133
|
-
|
|
134
221
|
@property
|
|
135
222
|
def search_url(self) -> str:
|
|
136
223
|
"""Define the data search endpoint."""
|
|
@@ -172,7 +259,7 @@ class Config:
|
|
|
172
259
|
|
|
173
260
|
def get_api_url(self, url: Optional[str]) -> str:
|
|
174
261
|
"""Construct the databrowser url from a given hostname."""
|
|
175
|
-
url = url or self.
|
|
262
|
+
url = url or self._get_databrowser_params_from_config().get("host", "")
|
|
176
263
|
scheme, hostname = self._split_url(url)
|
|
177
264
|
hostname, _, port = hostname.partition(":")
|
|
178
265
|
if port:
|
|
@@ -207,7 +294,6 @@ class Config:
|
|
|
207
294
|
|
|
208
295
|
class UserDataHandler:
|
|
209
296
|
"""Class for processing user data.
|
|
210
|
-
|
|
211
297
|
This class is used for processing user data and extracting metadata
|
|
212
298
|
from the data files.
|
|
213
299
|
"""
|
|
@@ -17,3 +17,8 @@
|
|
|
17
17
|
## You can set a port by separating <hostname:port>
|
|
18
18
|
## for example freva.example.org:7777
|
|
19
19
|
# host = ""
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
## The default flavour to use when accessing freva. You can simply list
|
|
23
|
+
## the flavours you want to use and choose one as default.
|
|
24
|
+
# default_flavour = "cmip6"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
freva_client/__init__.py,sha256=m2rzqsvQ80LSRnN3aBwSfirKiSIqQb6qv6O9J_WhAOo,916
|
|
2
|
+
freva_client/__main__.py,sha256=JVj12puT4o8JfhKLAggR2-NKCZa3wKwsYGi4HQ61DOQ,149
|
|
3
|
+
freva_client/auth.py,sha256=_oHcMDKJjMvP7ZDNl6NK7NKelcxYW3jVpnQm8E-uvIU,7390
|
|
4
|
+
freva_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
freva_client/query.py,sha256=Dm6Wy9U8ofzZd4v50Sjhl4Guh7njrClSnNNV5_7UdM0,49433
|
|
6
|
+
freva_client/cli/__init__.py,sha256=NgTqBZGdozmTZtJduJUMrZj-opGw2KoT20tg6sc_xqo,149
|
|
7
|
+
freva_client/cli/auth_cli.py,sha256=lM6QNcewh9Cz7DtHO4Wu6iv7GapwplyjhDRfmIlzyJ0,1950
|
|
8
|
+
freva_client/cli/cli_app.py,sha256=QH6cb9XZTx8S9L0chPWOVd9Jn4GD1hd3sENAhdzmLZU,887
|
|
9
|
+
freva_client/cli/cli_parser.py,sha256=EyzvTr70TBCD0mO2P3mVNtuEeEbNk2OdrhKSEHuu6NE,5101
|
|
10
|
+
freva_client/cli/cli_utils.py,sha256=9h2hlBQA-D3n-JlFIC1DSuzEEv73Bpu8lXLAqZBDaYI,1946
|
|
11
|
+
freva_client/cli/databrowser_cli.py,sha256=LpK6NvHWsbHVeJvOHG_su0dh9FHkLxTmY4K8P75S8A0,39397
|
|
12
|
+
freva_client/utils/__init__.py,sha256=ySHn-3CZBwfZW2s0EpZ3INxUWOw1V4LOlKIxSLYr52U,1000
|
|
13
|
+
freva_client/utils/auth_utils.py,sha256=H0knqoX2gvxM8hPzfIZQQZSKaT5tD1sRCSF2ddcdAzw,15058
|
|
14
|
+
freva_client/utils/databrowser_utils.py,sha256=Hv39Q8Crd63l2U-lBJfppMn14BUm6ECY0ASsgP_8du8,17801
|
|
15
|
+
freva_client/utils/logger.py,sha256=vjBbNb9KvyMriBPpgIoJjlQFCEj3DLqkJu8tYxfp2xI,2494
|
|
16
|
+
freva_client-2509.1.0.data/data/share/freva/freva.toml,sha256=5xXWBqw0W6JyWfRMaSNnKDOGkDznx6NFaJvggh-cUPU,899
|
|
17
|
+
freva_client-2509.1.0.dist-info/entry_points.txt,sha256=zGyEwHrH_kAGLsCXv00y7Qnp-WjXkUuIomHkfGMCxtA,53
|
|
18
|
+
freva_client-2509.1.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
19
|
+
freva_client-2509.1.0.dist-info/METADATA,sha256=S92UfQmSmjiv_Ol1g1fy9OCPeZkoZl0n3943pN1Jyoc,2487
|
|
20
|
+
freva_client-2509.1.0.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
freva_client/__init__.py,sha256=Yh9B2N-5bZJgwcHOxXDMg5hEOgetBoU3TmbsE6IVLQk,851
|
|
2
|
-
freva_client/__main__.py,sha256=JVj12puT4o8JfhKLAggR2-NKCZa3wKwsYGi4HQ61DOQ,149
|
|
3
|
-
freva_client/auth.py,sha256=p1itCygbLgEaCFTPWgqoId3S9PteP1lrziULziKzPG4,10453
|
|
4
|
-
freva_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
freva_client/query.py,sha256=SQx5c4lNwP7ZiP1u6uN7pbUyvdjv74s2vQ3kRAa_708,41906
|
|
6
|
-
freva_client/cli/__init__.py,sha256=NgTqBZGdozmTZtJduJUMrZj-opGw2KoT20tg6sc_xqo,149
|
|
7
|
-
freva_client/cli/auth_cli.py,sha256=hCgTxN930uTj5Z0NqItAyejiUI9E-1oSv_lWOVoHNUo,1780
|
|
8
|
-
freva_client/cli/cli_app.py,sha256=QH6cb9XZTx8S9L0chPWOVd9Jn4GD1hd3sENAhdzmLZU,887
|
|
9
|
-
freva_client/cli/cli_parser.py,sha256=EyzvTr70TBCD0mO2P3mVNtuEeEbNk2OdrhKSEHuu6NE,5101
|
|
10
|
-
freva_client/cli/cli_utils.py,sha256=Ev9UxM4S1ZDbZSAGHFe5dMjVGot75w3muNKH3P80bHY,842
|
|
11
|
-
freva_client/cli/databrowser_cli.py,sha256=eUd-lbSQaRyczLKQQLY_eKU3VRf6Zg5HaHMQbe2Ib5Q,32242
|
|
12
|
-
freva_client/utils/__init__.py,sha256=ySHn-3CZBwfZW2s0EpZ3INxUWOw1V4LOlKIxSLYr52U,1000
|
|
13
|
-
freva_client/utils/auth_utils.py,sha256=Tma8aPi32zhJ-z2o6gEe1LvpBFlkn-t__UgZONcuARY,6032
|
|
14
|
-
freva_client/utils/databrowser_utils.py,sha256=X9M71mBfc8lauyLh3t1CCxhu_96Ya_NVkMNdxmWTsjE,15109
|
|
15
|
-
freva_client/utils/logger.py,sha256=vjBbNb9KvyMriBPpgIoJjlQFCEj3DLqkJu8tYxfp2xI,2494
|
|
16
|
-
freva_client-2508.0.0.data/data/share/freva/freva.toml,sha256=64Rh4qvWc9TaGJMXMi8tZW14FnESt5Z24y17BfD2VyM,736
|
|
17
|
-
freva_client-2508.0.0.dist-info/entry_points.txt,sha256=zGyEwHrH_kAGLsCXv00y7Qnp-WjXkUuIomHkfGMCxtA,53
|
|
18
|
-
freva_client-2508.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
19
|
-
freva_client-2508.0.0.dist-info/METADATA,sha256=8oAcxK-rhgUnBSSmUUsVcEJTNlzOcnWCkS91UaqhMqk,2487
|
|
20
|
-
freva_client-2508.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|