freva-client 2509.1.0__tar.gz → 2510.0.0__tar.gz
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-2509.1.0 → freva_client-2510.0.0}/PKG-INFO +1 -1
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/__init__.py +1 -1
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/auth.py +15 -4
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/cli/databrowser_cli.py +68 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/query.py +43 -3
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/utils/auth_utils.py +174 -10
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/utils/databrowser_utils.py +44 -33
- {freva_client-2509.1.0 → freva_client-2510.0.0}/MANIFEST.in +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/README.md +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/assets/share/freva/freva.toml +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/pyproject.toml +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/__main__.py +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/cli/__init__.py +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/cli/auth_cli.py +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/cli/cli_app.py +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/cli/cli_parser.py +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/cli/cli_utils.py +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/py.typed +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/utils/__init__.py +0 -0
- {freva_client-2509.1.0 → freva_client-2510.0.0}/src/freva_client/utils/logger.py +0 -0
|
@@ -11,6 +11,7 @@ import requests
|
|
|
11
11
|
from .utils import logger
|
|
12
12
|
from .utils.auth_utils import (
|
|
13
13
|
AuthError,
|
|
14
|
+
CodeAuthClient,
|
|
14
15
|
DeviceAuthClient,
|
|
15
16
|
Token,
|
|
16
17
|
choose_token_strategy,
|
|
@@ -72,17 +73,27 @@ class Auth:
|
|
|
72
73
|
) -> Token:
|
|
73
74
|
device_endpoint = f"{auth_url}/device"
|
|
74
75
|
token_endpoint = f"{auth_url}/token"
|
|
75
|
-
|
|
76
|
+
device_client = DeviceAuthClient(
|
|
76
77
|
device_endpoint=device_endpoint,
|
|
77
78
|
token_endpoint=token_endpoint,
|
|
78
79
|
timeout=_timeout,
|
|
79
80
|
)
|
|
81
|
+
code_client = CodeAuthClient(
|
|
82
|
+
login_endpoint=f"{auth_url}/login",
|
|
83
|
+
token_endpoint=f"{auth_url}/token",
|
|
84
|
+
port_endpoint=f"{auth_url}/auth-ports",
|
|
85
|
+
)
|
|
86
|
+
|
|
80
87
|
is_interactive_auth = int(
|
|
81
88
|
os.getenv("BROWSER_SESSION", str(int(is_interactive_auth_possible())))
|
|
82
89
|
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
try:
|
|
91
|
+
response = device_client.login(auto_open=bool(is_interactive_auth))
|
|
92
|
+
except AuthError as error:
|
|
93
|
+
if error.status_code == 503:
|
|
94
|
+
response = code_client.login()
|
|
95
|
+
else:
|
|
96
|
+
raise
|
|
86
97
|
return self.set_token(
|
|
87
98
|
access_token=response["access_token"],
|
|
88
99
|
token_type=response["token_type"],
|
|
@@ -33,6 +33,7 @@ class UniqKeys(str, Enum):
|
|
|
33
33
|
|
|
34
34
|
class FlavourAction(str, Enum):
|
|
35
35
|
add = "add"
|
|
36
|
+
update = "update"
|
|
36
37
|
delete = "delete"
|
|
37
38
|
list = "list"
|
|
38
39
|
|
|
@@ -1056,6 +1057,73 @@ def flavour_add(
|
|
|
1056
1057
|
)
|
|
1057
1058
|
|
|
1058
1059
|
|
|
1060
|
+
@flavour_app.command("update", help="Update an existing custom flavour.")
|
|
1061
|
+
@exception_handler
|
|
1062
|
+
def flavour_update(
|
|
1063
|
+
name: str = typer.Argument(..., help="Name of the flavour to update"),
|
|
1064
|
+
mapping: List[str] = typer.Option(
|
|
1065
|
+
[],
|
|
1066
|
+
"--map",
|
|
1067
|
+
"-m",
|
|
1068
|
+
help="Key-value mappings to update in the format key=value",
|
|
1069
|
+
),
|
|
1070
|
+
new_name: Optional[str] = typer.Option(
|
|
1071
|
+
None,
|
|
1072
|
+
"--new-name",
|
|
1073
|
+
help="New name for the flavour (optional)",
|
|
1074
|
+
),
|
|
1075
|
+
global_: bool = typer.Option(
|
|
1076
|
+
False,
|
|
1077
|
+
"--global",
|
|
1078
|
+
help="Update global flavour (requires admin privileges)",
|
|
1079
|
+
),
|
|
1080
|
+
host: Optional[str] = typer.Option(
|
|
1081
|
+
None,
|
|
1082
|
+
"--host",
|
|
1083
|
+
help=(
|
|
1084
|
+
"Set the hostname of the databrowser. If not set (default), "
|
|
1085
|
+
"the hostname is read from a config file."
|
|
1086
|
+
),
|
|
1087
|
+
),
|
|
1088
|
+
token_file: Optional[Path] = typer.Option(
|
|
1089
|
+
None,
|
|
1090
|
+
"--token-file",
|
|
1091
|
+
"-tf",
|
|
1092
|
+
help=(
|
|
1093
|
+
"Instead of authenticating via code based authentication flow "
|
|
1094
|
+
"you can set the path to the json file that contains a "
|
|
1095
|
+
"`refresh token` containing a refresh_token key."
|
|
1096
|
+
),
|
|
1097
|
+
),
|
|
1098
|
+
verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
|
|
1099
|
+
) -> None:
|
|
1100
|
+
"""Update an existing custom flavour in the databrowser.
|
|
1101
|
+
This command allows partial updates to flavour mappings. Only the keys
|
|
1102
|
+
provided will be updated; other keys remain unchanged."""
|
|
1103
|
+
logger.set_verbosity(verbose)
|
|
1104
|
+
logger.debug(f"Updating flavour '{name}' with mapping {mapping}")
|
|
1105
|
+
Auth(token_file).authenticate(host=host, _cli=True)
|
|
1106
|
+
|
|
1107
|
+
mapping_dict = {}
|
|
1108
|
+
for map_item in mapping:
|
|
1109
|
+
if "=" not in map_item:
|
|
1110
|
+
logger.error(
|
|
1111
|
+
f"Invalid mapping format: {map_item}. Expected format: key=value."
|
|
1112
|
+
)
|
|
1113
|
+
raise typer.Exit(code=1)
|
|
1114
|
+
key, value = map_item.split("=", 1)
|
|
1115
|
+
mapping_dict[key] = value
|
|
1116
|
+
|
|
1117
|
+
databrowser.flavour(
|
|
1118
|
+
action="update",
|
|
1119
|
+
name=name,
|
|
1120
|
+
mapping=mapping_dict if mapping_dict else None,
|
|
1121
|
+
new_name=new_name,
|
|
1122
|
+
is_global=global_,
|
|
1123
|
+
host=host,
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
|
|
1059
1127
|
@flavour_app.command("delete", help="Delete an existing custom flavour.")
|
|
1060
1128
|
@exception_handler
|
|
1061
1129
|
def flavour_delete(
|
|
@@ -721,7 +721,9 @@ class databrowser:
|
|
|
721
721
|
for k, v in self._facet_search(extended_search=True).items()
|
|
722
722
|
], columns=["facet", "values"])
|
|
723
723
|
.explode("values")
|
|
724
|
-
.groupby("facet")["values"].apply(
|
|
724
|
+
.groupby("facet")["values"].apply(
|
|
725
|
+
lambda x: [v for v in x if pd.notna(v)]
|
|
726
|
+
)
|
|
725
727
|
)
|
|
726
728
|
|
|
727
729
|
@classmethod
|
|
@@ -907,7 +909,9 @@ class databrowser:
|
|
|
907
909
|
], columns=["facet", "values"]
|
|
908
910
|
)
|
|
909
911
|
.explode("values")
|
|
910
|
-
.groupby("facet")["values"].apply(
|
|
912
|
+
.groupby("facet")["values"].apply(
|
|
913
|
+
lambda x: [v for v in x if pd.notna(v)]
|
|
914
|
+
)
|
|
911
915
|
)
|
|
912
916
|
|
|
913
917
|
@classmethod
|
|
@@ -1104,8 +1108,9 @@ class databrowser:
|
|
|
1104
1108
|
@classmethod
|
|
1105
1109
|
def flavour(
|
|
1106
1110
|
cls,
|
|
1107
|
-
action: Literal["add", "delete", "list"],
|
|
1111
|
+
action: Literal["add", "update", "delete", "list"],
|
|
1108
1112
|
name: Optional[str] = None,
|
|
1113
|
+
new_name: Optional[str] = None,
|
|
1109
1114
|
mapping: Optional[Dict[str, str]] = None,
|
|
1110
1115
|
is_global: bool = False,
|
|
1111
1116
|
host: Optional[str] = None,
|
|
@@ -1168,6 +1173,18 @@ class databrowser:
|
|
|
1168
1173
|
is_global=False
|
|
1169
1174
|
)
|
|
1170
1175
|
|
|
1176
|
+
Updating a custom flavour:
|
|
1177
|
+
|
|
1178
|
+
.. code-block:: python
|
|
1179
|
+
|
|
1180
|
+
from freva_client import databrowser
|
|
1181
|
+
databrowser.flavour(
|
|
1182
|
+
action="update",
|
|
1183
|
+
name="klimakataster",
|
|
1184
|
+
mapping={"experiment": "Experiment"},
|
|
1185
|
+
new_name="klimakataster_v2",
|
|
1186
|
+
is_global=False
|
|
1187
|
+
)
|
|
1171
1188
|
Listing all custom flavours:
|
|
1172
1189
|
|
|
1173
1190
|
.. code-block:: python
|
|
@@ -1210,6 +1227,29 @@ class databrowser:
|
|
|
1210
1227
|
msg = result.json().get("status", "Flavour added successfully")
|
|
1211
1228
|
pprint(f"[b][green] {msg} [/green][/b]")
|
|
1212
1229
|
|
|
1230
|
+
elif action == "update":
|
|
1231
|
+
token = this._auth.authenticate(config=this._cfg)
|
|
1232
|
+
headers = {"Authorization": f"Bearer {token['access_token']}"}
|
|
1233
|
+
if not name:
|
|
1234
|
+
raise ValueError("'name' is required for update action")
|
|
1235
|
+
|
|
1236
|
+
payload_update: Dict[str, Any] = {
|
|
1237
|
+
"is_global": is_global,
|
|
1238
|
+
"mapping": mapping or {},
|
|
1239
|
+
}
|
|
1240
|
+
if new_name:
|
|
1241
|
+
payload_update["flavour_name"] = new_name
|
|
1242
|
+
|
|
1243
|
+
result = this._request(
|
|
1244
|
+
"PUT",
|
|
1245
|
+
f"{this._cfg.databrowser_url}/flavours/{name}",
|
|
1246
|
+
data=payload_update,
|
|
1247
|
+
headers=headers,
|
|
1248
|
+
)
|
|
1249
|
+
if result is not None:
|
|
1250
|
+
msg = result.json().get("status", "Flavour updated successfully")
|
|
1251
|
+
pprint(f"[b][green] {msg} [/green][/b]")
|
|
1252
|
+
|
|
1213
1253
|
elif action == "delete":
|
|
1214
1254
|
token = this._auth.authenticate(config=this._cfg)
|
|
1215
1255
|
headers = {"Authorization": f"Bearer {token['access_token']}"}
|
|
@@ -3,17 +3,22 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import random
|
|
6
|
+
import socket
|
|
6
7
|
import sys
|
|
7
8
|
import time
|
|
9
|
+
import urllib.parse
|
|
8
10
|
import webbrowser
|
|
9
11
|
from contextlib import contextmanager
|
|
10
12
|
from dataclasses import dataclass, field
|
|
13
|
+
from getpass import getuser
|
|
14
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
11
15
|
from pathlib import Path
|
|
16
|
+
from threading import Event, Thread
|
|
12
17
|
from typing import (
|
|
13
18
|
Any,
|
|
14
|
-
Callable,
|
|
15
19
|
Dict,
|
|
16
20
|
Iterator,
|
|
21
|
+
List,
|
|
17
22
|
Literal,
|
|
18
23
|
Optional,
|
|
19
24
|
TypedDict,
|
|
@@ -33,6 +38,7 @@ TOKEN_EXPIRY_BUFFER = 60 # seconds
|
|
|
33
38
|
TOKEN_ENV_VAR = "FREVA_TOKEN_FILE"
|
|
34
39
|
|
|
35
40
|
|
|
41
|
+
REDIRECT_URI = "http://localhost:{port}/callback"
|
|
36
42
|
BROWSER_MESSAGE = """Will attempt to open the auth url in your browser.
|
|
37
43
|
|
|
38
44
|
If this doesn't work, try opening the following url:
|
|
@@ -80,11 +86,15 @@ class AuthError(Exception):
|
|
|
80
86
|
"""Athentication error."""
|
|
81
87
|
|
|
82
88
|
def __init__(
|
|
83
|
-
self,
|
|
89
|
+
self,
|
|
90
|
+
message: str,
|
|
91
|
+
detail: Optional[Dict[str, str]] = None,
|
|
92
|
+
status_code: int = 500,
|
|
84
93
|
) -> None:
|
|
85
94
|
super().__init__(message)
|
|
86
95
|
self.message = message
|
|
87
96
|
self.detail = detail or {}
|
|
97
|
+
self.status_code = status_code
|
|
88
98
|
|
|
89
99
|
def __str__(self) -> str:
|
|
90
100
|
return f"{self.message}: {self.detail}" if self.detail else self.message
|
|
@@ -101,10 +111,165 @@ class Token(TypedDict, total=False):
|
|
|
101
111
|
scope: str
|
|
102
112
|
|
|
103
113
|
|
|
114
|
+
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
115
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
116
|
+
logger.debug(format, *args)
|
|
117
|
+
|
|
118
|
+
def do_GET(self) -> None:
|
|
119
|
+
query = urllib.parse.urlparse(self.path).query
|
|
120
|
+
params = urllib.parse.parse_qs(query)
|
|
121
|
+
if "code" in params:
|
|
122
|
+
setattr(self.server, "auth_code", params["code"][0])
|
|
123
|
+
self.send_response(200)
|
|
124
|
+
self.end_headers()
|
|
125
|
+
self.wfile.write(b"Login successful! You can close this tab.")
|
|
126
|
+
else:
|
|
127
|
+
self.send_response(400)
|
|
128
|
+
self.end_headers()
|
|
129
|
+
self.wfile.write(b"Authorization code not found.")
|
|
130
|
+
|
|
131
|
+
|
|
104
132
|
@dataclass
|
|
105
|
-
class
|
|
133
|
+
class CodeAuthClient:
|
|
134
|
+
"""Minimal OIDC Code Authorization Grand client.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
device_endpoint : str
|
|
139
|
+
Device endpoint URL, e.g. ``{issuer}/protocol/openid-connect/auth/device``.
|
|
140
|
+
token_endpoint : str
|
|
141
|
+
Token endpoint URL, e.g. ``{issuer}/protocol/openid-connect/token``.
|
|
142
|
+
scope : str, optional
|
|
143
|
+
Space-separated scopes; include ``offline_access`` if you need offline RTs.
|
|
144
|
+
timeout : int | None, optional
|
|
145
|
+
Overall timeout (seconds) for user approval. ``None`` waits indefinitely.
|
|
146
|
+
session : requests.Session | None, optional
|
|
147
|
+
Reusable HTTP session. A new one is created if omitted.
|
|
148
|
+
|
|
106
149
|
"""
|
|
107
|
-
|
|
150
|
+
|
|
151
|
+
login_endpoint: str
|
|
152
|
+
token_endpoint: str
|
|
153
|
+
port_endpoint: str
|
|
154
|
+
timeout: Optional[int] = 600
|
|
155
|
+
session: requests.Session = field(default_factory=requests.Session)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _start_local_server(port: int, event: Event) -> HTTPServer:
|
|
159
|
+
"""Start local HTTP server to wait for a single callback."""
|
|
160
|
+
server = HTTPServer(("localhost", port), OAuthCallbackHandler)
|
|
161
|
+
|
|
162
|
+
def handle() -> None:
|
|
163
|
+
logger.info("Waiting for browser callback on port %s ...", port)
|
|
164
|
+
while not event.is_set():
|
|
165
|
+
server.handle_request()
|
|
166
|
+
if getattr(server, "auth_code", None):
|
|
167
|
+
event.set()
|
|
168
|
+
|
|
169
|
+
thread = Thread(target=handle, daemon=True)
|
|
170
|
+
thread.start()
|
|
171
|
+
return server
|
|
172
|
+
|
|
173
|
+
def _find_free_port(self) -> int:
|
|
174
|
+
"""Get a free port where we can start the test server."""
|
|
175
|
+
ports: List[int] = (
|
|
176
|
+
requests.get(self.port_endpoint).json().get("valid_ports", [])
|
|
177
|
+
)
|
|
178
|
+
for port in ports:
|
|
179
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
180
|
+
try:
|
|
181
|
+
s.bind(("localhost", port))
|
|
182
|
+
return port
|
|
183
|
+
except (OSError, PermissionError):
|
|
184
|
+
pass
|
|
185
|
+
raise OSError("No free ports available for login flow")
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _wait_for_port(host: str, port: int, timeout: float = 5.0) -> None:
|
|
189
|
+
"""Wait until a TCP port starts accepting connections."""
|
|
190
|
+
deadline = time.time() + timeout
|
|
191
|
+
while time.time() < deadline:
|
|
192
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
193
|
+
sock.settimeout(0.5)
|
|
194
|
+
try:
|
|
195
|
+
if sock.connect_ex((host, port)) == 0:
|
|
196
|
+
return
|
|
197
|
+
except OSError:
|
|
198
|
+
pass
|
|
199
|
+
time.sleep(0.05)
|
|
200
|
+
raise TimeoutError(
|
|
201
|
+
f"Port {port} on {host} did not open within {timeout} seconds."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def login(
|
|
205
|
+
self,
|
|
206
|
+
) -> Token:
|
|
207
|
+
"""Start the login flow."""
|
|
208
|
+
port = self._find_free_port()
|
|
209
|
+
redirect_uri = REDIRECT_URI.format(port=port)
|
|
210
|
+
params = {
|
|
211
|
+
"redirect_uri": redirect_uri,
|
|
212
|
+
"offline_access": "true",
|
|
213
|
+
"prompt": "consent",
|
|
214
|
+
}
|
|
215
|
+
query = urllib.parse.urlencode(params)
|
|
216
|
+
login_url = f"{self.login_endpoint}?{query}"
|
|
217
|
+
logger.info("Opening browser for login:\n%s", login_url)
|
|
218
|
+
logger.info(
|
|
219
|
+
"If you are using this on a remote host, you might need to "
|
|
220
|
+
"increase the login timeout and forward port %d:\n"
|
|
221
|
+
" ssh -L %d:localhost:%d %s@%s",
|
|
222
|
+
port,
|
|
223
|
+
port,
|
|
224
|
+
port,
|
|
225
|
+
getuser(),
|
|
226
|
+
socket.gethostname(),
|
|
227
|
+
)
|
|
228
|
+
event = Event()
|
|
229
|
+
server = self._start_local_server(port, event)
|
|
230
|
+
code: Optional[str] = None
|
|
231
|
+
reason = "Login failed."
|
|
232
|
+
try:
|
|
233
|
+
self._wait_for_port("localhost", port)
|
|
234
|
+
webbrowser.open(login_url)
|
|
235
|
+
success = event.wait(timeout=self.timeout or None)
|
|
236
|
+
if not success:
|
|
237
|
+
raise TimeoutError(
|
|
238
|
+
f"Login did not complete within {self.timeout} seconds. "
|
|
239
|
+
"Possibly headless environment."
|
|
240
|
+
)
|
|
241
|
+
code = getattr(server, "auth_code", None)
|
|
242
|
+
except Exception as error:
|
|
243
|
+
logger.warning(
|
|
244
|
+
"Could not open browser automatically. %s"
|
|
245
|
+
"Please open the URL manually.",
|
|
246
|
+
error,
|
|
247
|
+
)
|
|
248
|
+
reason = str(error)
|
|
249
|
+
|
|
250
|
+
finally:
|
|
251
|
+
logger.debug("Cleaning up login state")
|
|
252
|
+
if hasattr(server, "server_close"):
|
|
253
|
+
try:
|
|
254
|
+
server.server_close()
|
|
255
|
+
except Exception as error:
|
|
256
|
+
logger.debug("Failed to close server cleanly: %s", error)
|
|
257
|
+
if not code:
|
|
258
|
+
raise AuthError(reason)
|
|
259
|
+
data = {
|
|
260
|
+
"code": code,
|
|
261
|
+
"redirect_uri": redirect_uri,
|
|
262
|
+
"grant_type": "authorization_code",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
response = requests.post(self.token_endpoint, data=data)
|
|
266
|
+
response.raise_for_status()
|
|
267
|
+
return cast(Token, response.json())
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class DeviceAuthClient:
|
|
272
|
+
"""Minimal OIDC Device Authorization Grant client.
|
|
108
273
|
|
|
109
274
|
Parameters
|
|
110
275
|
----------
|
|
@@ -137,7 +302,6 @@ class DeviceAuthClient:
|
|
|
137
302
|
self,
|
|
138
303
|
*,
|
|
139
304
|
auto_open: bool = True,
|
|
140
|
-
token_normalizer: Optional[Callable[[str, Dict[str, str]], Token]] = None,
|
|
141
305
|
) -> Token:
|
|
142
306
|
"""
|
|
143
307
|
Run the full device flow and return an OIDC token payload.
|
|
@@ -146,10 +310,6 @@ class DeviceAuthClient:
|
|
|
146
310
|
----------
|
|
147
311
|
auto_open : bool, optional
|
|
148
312
|
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
313
|
|
|
154
314
|
Returns
|
|
155
315
|
-------
|
|
@@ -276,7 +436,11 @@ class DeviceAuthClient:
|
|
|
276
436
|
"error": "http_error",
|
|
277
437
|
"error_description": resp.text[:300],
|
|
278
438
|
}
|
|
279
|
-
raise AuthError(
|
|
439
|
+
raise AuthError(
|
|
440
|
+
f"{url} -> {resp.status_code}",
|
|
441
|
+
detail=payload,
|
|
442
|
+
status_code=resp.status_code,
|
|
443
|
+
)
|
|
280
444
|
try:
|
|
281
445
|
return cast(Dict[str, Any], resp.json())
|
|
282
446
|
except Exception as error:
|
|
@@ -42,11 +42,18 @@ class Config:
|
|
|
42
42
|
uniq_key: Literal["file", "uri"] = "file",
|
|
43
43
|
flavour: Optional[str] = None,
|
|
44
44
|
) -> None:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self.
|
|
45
|
+
config_host = host or cast(str, self._get_databrowser_param_from_config("host"))
|
|
46
|
+
|
|
47
|
+
self.databrowser_url = f"{self.get_api_url(config_host)}/databrowser"
|
|
48
|
+
self.auth_url = f"{self.get_api_url(config_host)}/auth/v2"
|
|
49
|
+
self.get_api_main_url = self.get_api_url(config_host)
|
|
48
50
|
self.uniq_key = uniq_key
|
|
49
|
-
self.
|
|
51
|
+
self._flavour = (
|
|
52
|
+
flavour or self._get_databrowser_param_from_config(
|
|
53
|
+
"flavour",
|
|
54
|
+
optional=True
|
|
55
|
+
)
|
|
56
|
+
)
|
|
50
57
|
|
|
51
58
|
@cached_property
|
|
52
59
|
def validate_server(self) -> bool:
|
|
@@ -59,18 +66,17 @@ class Config:
|
|
|
59
66
|
f"Could not connect to {self.databrowser_url}: {e}"
|
|
60
67
|
) from None
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return "freva"
|
|
69
|
+
@property
|
|
70
|
+
def flavour(self) -> str:
|
|
71
|
+
"""Get the flavour, using server default if not configured."""
|
|
72
|
+
if self._flavour:
|
|
73
|
+
return self._flavour
|
|
74
|
+
try:
|
|
75
|
+
flavours = self.overview.get("flavours", [])
|
|
76
|
+
self._flavour = flavours[0] if flavours else "freva"
|
|
77
|
+
except (ValueError, IndexError, KeyError):
|
|
78
|
+
self._flavour = "freva"
|
|
79
|
+
return self._flavour
|
|
74
80
|
|
|
75
81
|
def _read_ini(self, path: Path) -> Dict[str, str]:
|
|
76
82
|
"""Read an ini file.
|
|
@@ -115,8 +121,11 @@ class Config:
|
|
|
115
121
|
"""
|
|
116
122
|
try:
|
|
117
123
|
config = tomli.loads(path.read_text()).get("freva", {})
|
|
118
|
-
|
|
124
|
+
raw_host = cast(str, config.get("host", ""))
|
|
119
125
|
flavour = config.get("default_flavour", "")
|
|
126
|
+
if not raw_host:
|
|
127
|
+
return {"host": "", "flavour": flavour}
|
|
128
|
+
scheme, host = self._split_url(raw_host)
|
|
120
129
|
except (tomli.TOMLDecodeError, KeyError):
|
|
121
130
|
return {}
|
|
122
131
|
host, _, port = host.partition(":")
|
|
@@ -186,9 +195,10 @@ class Config:
|
|
|
186
195
|
f"Could not connect to {self.databrowser_url}"
|
|
187
196
|
) from None
|
|
188
197
|
|
|
189
|
-
def
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
def _get_databrowser_param_from_config(
|
|
199
|
+
self, key: str, optional: bool = False
|
|
200
|
+
) -> Optional[str]:
|
|
201
|
+
"""Get a single config parameter following proper precedence."""
|
|
192
202
|
eval_conf = self.get_dirs(user=False) / "evaluation_system.conf"
|
|
193
203
|
freva_config = Path(
|
|
194
204
|
os.environ.get("FREVA_CONFIG")
|
|
@@ -202,20 +212,22 @@ class Config:
|
|
|
202
212
|
os.environ.get("EVALUATION_SYSTEM_CONFIG_FILE") or eval_conf
|
|
203
213
|
): "ini",
|
|
204
214
|
}
|
|
215
|
+
|
|
205
216
|
for config_path, config_type in paths.items():
|
|
206
217
|
if config_path.is_file():
|
|
207
218
|
config_data = self._read_config(config_path, config_type)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
value = config_data.get(key, "")
|
|
220
|
+
# we cannot igonore the empty string here, because
|
|
221
|
+
# it needs to check the next config file
|
|
222
|
+
if value:
|
|
223
|
+
return value if value else None
|
|
224
|
+
if optional:
|
|
225
|
+
return None
|
|
226
|
+
|
|
215
227
|
raise ValueError(
|
|
216
|
-
"No databrowser
|
|
217
|
-
" configuration defining a databrowser
|
|
218
|
-
" set a host name using the `
|
|
228
|
+
f"No databrowser {key} configured, please use a"
|
|
229
|
+
f" configuration defining a databrowser {key} or"
|
|
230
|
+
f" set a host name using the `{key}` key"
|
|
219
231
|
)
|
|
220
232
|
|
|
221
233
|
@property
|
|
@@ -257,9 +269,8 @@ class Config:
|
|
|
257
269
|
scheme = scheme or "http"
|
|
258
270
|
return scheme, hostname
|
|
259
271
|
|
|
260
|
-
def get_api_url(self, url:
|
|
272
|
+
def get_api_url(self, url: str) -> str:
|
|
261
273
|
"""Construct the databrowser url from a given hostname."""
|
|
262
|
-
url = url or self._get_databrowser_params_from_config().get("host", "")
|
|
263
274
|
scheme, hostname = self._split_url(url)
|
|
264
275
|
hostname, _, port = hostname.partition(":")
|
|
265
276
|
if port:
|
|
@@ -385,7 +396,7 @@ class UserDataHandler:
|
|
|
385
396
|
) -> None:
|
|
386
397
|
for data in validated_userdata:
|
|
387
398
|
metadata = self._get_metadata(data)
|
|
388
|
-
if
|
|
399
|
+
if metadata == {}:
|
|
389
400
|
logger.warning("Error getting metadata: %s", metadata)
|
|
390
401
|
else:
|
|
391
402
|
self.user_metadata.append(metadata)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|