freva-client 2509.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 +1 -1
- freva_client/auth.py +23 -125
- freva_client/utils/auth_utils.py +266 -28
- {freva_client-2509.0.0.dist-info → freva_client-2509.1.0.dist-info}/METADATA +1 -1
- {freva_client-2509.0.0.dist-info → freva_client-2509.1.0.dist-info}/RECORD +8 -8
- {freva_client-2509.0.0.data → freva_client-2509.1.0.data}/data/share/freva/freva.toml +0 -0
- {freva_client-2509.0.0.dist-info → freva_client-2509.1.0.dist-info}/WHEEL +0 -0
- {freva_client-2509.0.0.dist-info → freva_client-2509.1.0.dist-info}/entry_points.txt +0 -0
freva_client/__init__.py
CHANGED
freva_client/auth.py
CHANGED
|
@@ -2,28 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
4
|
import json
|
|
5
|
-
import
|
|
6
|
-
import urllib.parse
|
|
7
|
-
import webbrowser
|
|
8
|
-
from getpass import getuser
|
|
9
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
5
|
+
import os
|
|
10
6
|
from pathlib import Path
|
|
11
|
-
from
|
|
12
|
-
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
from typing import Any, Dict, Optional, Union
|
|
13
8
|
|
|
14
9
|
import requests
|
|
15
10
|
|
|
16
11
|
from .utils import logger
|
|
17
12
|
from .utils.auth_utils import (
|
|
13
|
+
AuthError,
|
|
14
|
+
DeviceAuthClient,
|
|
18
15
|
Token,
|
|
19
16
|
choose_token_strategy,
|
|
20
17
|
get_default_token_file,
|
|
18
|
+
is_interactive_auth_possible,
|
|
21
19
|
load_token,
|
|
22
|
-
wait_for_port,
|
|
23
20
|
)
|
|
24
21
|
from .utils.databrowser_utils import Config
|
|
25
22
|
|
|
26
|
-
REDIRECT_URI = "http://localhost:{port}/callback"
|
|
27
23
|
AUTH_FAILED_MSG = """Login failed or no valid token found in this environment.
|
|
28
24
|
If you appear to be in a non-interactive or remote session create a new token
|
|
29
25
|
via:
|
|
@@ -35,52 +31,11 @@ Then pass the token file using:"
|
|
|
35
31
|
" {token_path} or set the $TOKEN_ENV_VAR env. variable."""
|
|
36
32
|
|
|
37
33
|
|
|
38
|
-
class AuthError(Exception):
|
|
39
|
-
"""Athentication error."""
|
|
40
|
-
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
45
|
-
def log_message(self, format: str, *args: object) -> None:
|
|
46
|
-
logger.debug(format, *args)
|
|
47
|
-
|
|
48
|
-
def do_GET(self) -> None:
|
|
49
|
-
query = urllib.parse.urlparse(self.path).query
|
|
50
|
-
params = urllib.parse.parse_qs(query)
|
|
51
|
-
if "code" in params:
|
|
52
|
-
setattr(self.server, "auth_code", params["code"][0])
|
|
53
|
-
self.send_response(200)
|
|
54
|
-
self.end_headers()
|
|
55
|
-
self.wfile.write(b"Login successful! You can close this tab.")
|
|
56
|
-
else:
|
|
57
|
-
self.send_response(400)
|
|
58
|
-
self.end_headers()
|
|
59
|
-
self.wfile.write(b"Authorization code not found.")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def start_local_server(port: int, event: Event) -> HTTPServer:
|
|
63
|
-
"""Start local HTTP server to wait for a single callback."""
|
|
64
|
-
server = HTTPServer(("localhost", port), OAuthCallbackHandler)
|
|
65
|
-
|
|
66
|
-
def handle() -> None:
|
|
67
|
-
logger.info("Waiting for browser callback on port %s ...", port)
|
|
68
|
-
while not event.is_set():
|
|
69
|
-
server.handle_request()
|
|
70
|
-
if getattr(server, "auth_code", None):
|
|
71
|
-
event.set()
|
|
72
|
-
|
|
73
|
-
thread = Thread(target=handle, daemon=True)
|
|
74
|
-
thread.start()
|
|
75
|
-
return server
|
|
76
|
-
|
|
77
|
-
|
|
78
34
|
class Auth:
|
|
79
35
|
"""Helper class for authentication."""
|
|
80
36
|
|
|
81
37
|
_instance: Optional["Auth"] = None
|
|
82
38
|
_auth_token: Optional[Token] = None
|
|
83
|
-
_thread_lock: Lock = Lock()
|
|
84
39
|
|
|
85
40
|
def __new__(cls, *args: Any, **kwargs: Any) -> "Auth":
|
|
86
41
|
if cls._instance is None:
|
|
@@ -113,86 +68,29 @@ class Auth:
|
|
|
113
68
|
def _login(
|
|
114
69
|
self,
|
|
115
70
|
auth_url: str,
|
|
116
|
-
force: bool = False,
|
|
117
71
|
_timeout: Optional[int] = 30,
|
|
118
72
|
) -> Token:
|
|
119
|
-
|
|
73
|
+
device_endpoint = f"{auth_url}/device"
|
|
120
74
|
token_endpoint = f"{auth_url}/token"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"offline_access": "true",
|
|
126
|
-
}
|
|
127
|
-
if force:
|
|
128
|
-
params["prompt"] = "login"
|
|
129
|
-
query = urllib.parse.urlencode(params)
|
|
130
|
-
login_url = f"{login_endpoint}?{query}"
|
|
131
|
-
logger.info("Opening browser for login:\n%s", login_url)
|
|
132
|
-
logger.info(
|
|
133
|
-
"If you are using this on a remote host, you might need to "
|
|
134
|
-
"increase the login timeout and forward port %d:\n"
|
|
135
|
-
" ssh -L %d:localhost:%d %s@%s",
|
|
136
|
-
port,
|
|
137
|
-
port,
|
|
138
|
-
port,
|
|
139
|
-
getuser(),
|
|
140
|
-
socket.gethostname(),
|
|
75
|
+
client = DeviceAuthClient(
|
|
76
|
+
device_endpoint=device_endpoint,
|
|
77
|
+
token_endpoint=token_endpoint,
|
|
78
|
+
timeout=_timeout,
|
|
141
79
|
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
code: Optional[str] = None
|
|
145
|
-
reason = "Login failed."
|
|
146
|
-
try:
|
|
147
|
-
wait_for_port("localhost", port)
|
|
148
|
-
webbrowser.open(login_url)
|
|
149
|
-
success = event.wait(timeout=_timeout or None)
|
|
150
|
-
if not success:
|
|
151
|
-
raise TimeoutError(
|
|
152
|
-
f"Login did not complete within {_timeout} seconds. "
|
|
153
|
-
"Possibly headless environment."
|
|
154
|
-
)
|
|
155
|
-
code = getattr(server, "auth_code", None)
|
|
156
|
-
except Exception as error:
|
|
157
|
-
logger.warning(
|
|
158
|
-
"Could not open browser automatically. %s"
|
|
159
|
-
"Please open the URL manually.",
|
|
160
|
-
error,
|
|
161
|
-
)
|
|
162
|
-
reason = str(error)
|
|
163
|
-
|
|
164
|
-
finally:
|
|
165
|
-
logger.debug("Cleaning up login state")
|
|
166
|
-
if hasattr(server, "server_close"):
|
|
167
|
-
try:
|
|
168
|
-
server.server_close()
|
|
169
|
-
except Exception as error:
|
|
170
|
-
logger.debug("Failed to close server cleanly: %s", error)
|
|
171
|
-
if not code:
|
|
172
|
-
raise AuthError(reason)
|
|
173
|
-
return self.get_token(
|
|
174
|
-
token_endpoint,
|
|
175
|
-
data={
|
|
176
|
-
"code": code,
|
|
177
|
-
"redirect_uri": redirect_uri,
|
|
178
|
-
"grant_type": "authorization_code",
|
|
179
|
-
},
|
|
80
|
+
is_interactive_auth = int(
|
|
81
|
+
os.getenv("BROWSER_SESSION", str(int(is_interactive_auth_possible())))
|
|
180
82
|
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
83
|
+
response = client.login(
|
|
84
|
+
token_normalizer=self.get_token, auto_open=bool(is_interactive_auth)
|
|
85
|
+
)
|
|
86
|
+
return self.set_token(
|
|
87
|
+
access_token=response["access_token"],
|
|
88
|
+
token_type=response["token_type"],
|
|
89
|
+
expires=response["expires"],
|
|
90
|
+
refresh_token=response["refresh_token"],
|
|
91
|
+
refresh_expires=response["refresh_expires"],
|
|
92
|
+
scope=response["scope"],
|
|
187
93
|
)
|
|
188
|
-
for port in ports:
|
|
189
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
190
|
-
try:
|
|
191
|
-
s.bind(("localhost", port))
|
|
192
|
-
return port
|
|
193
|
-
except (OSError, PermissionError):
|
|
194
|
-
pass
|
|
195
|
-
raise OSError("No free ports available for login flow")
|
|
196
94
|
|
|
197
95
|
def set_token(
|
|
198
96
|
self,
|
|
@@ -265,7 +163,7 @@ class Auth:
|
|
|
265
163
|
cfg.auth_url, token["refresh_token"], timeout=timeout
|
|
266
164
|
)
|
|
267
165
|
if strategy == "browser_auth":
|
|
268
|
-
return self._login(cfg.auth_url,
|
|
166
|
+
return self._login(cfg.auth_url, _timeout=timeout)
|
|
269
167
|
except AuthError as error:
|
|
270
168
|
reason = str(error)
|
|
271
169
|
|
freva_client/utils/auth_utils.py
CHANGED
|
@@ -2,20 +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
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Dict,
|
|
16
|
+
Iterator,
|
|
17
|
+
Literal,
|
|
18
|
+
Optional,
|
|
19
|
+
TypedDict,
|
|
20
|
+
Union,
|
|
21
|
+
cast,
|
|
22
|
+
)
|
|
10
23
|
|
|
11
24
|
import requests
|
|
25
|
+
import rich.console
|
|
26
|
+
import rich.spinner
|
|
12
27
|
from appdirs import user_cache_dir
|
|
28
|
+
from rich.live import Live
|
|
29
|
+
|
|
30
|
+
from freva_client.utils import logger
|
|
13
31
|
|
|
14
32
|
TOKEN_EXPIRY_BUFFER = 60 # seconds
|
|
15
33
|
TOKEN_ENV_VAR = "FREVA_TOKEN_FILE"
|
|
16
34
|
|
|
17
35
|
|
|
18
|
-
|
|
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):
|
|
19
94
|
"""Token information."""
|
|
20
95
|
|
|
21
96
|
access_token: str
|
|
@@ -26,9 +101,191 @@ class Token(TypedDict):
|
|
|
26
101
|
scope: str
|
|
27
102
|
|
|
28
103
|
|
|
29
|
-
|
|
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:
|
|
30
287
|
"""Get the location of the default token file."""
|
|
31
|
-
path_str = os.getenv(TOKEN_ENV_VAR, "").strip()
|
|
288
|
+
path_str = token_file or os.getenv(TOKEN_ENV_VAR, "").strip()
|
|
32
289
|
|
|
33
290
|
path = Path(
|
|
34
291
|
path_str or os.path.join(user_cache_dir("freva"), "auth-token.json")
|
|
@@ -224,27 +481,10 @@ def choose_token_strategy(
|
|
|
224
481
|
return "fail"
|
|
225
482
|
|
|
226
483
|
|
|
227
|
-
def wait_for_port(host: str, port: int, timeout: float = 5.0) -> None:
|
|
228
|
-
"""Wait until a TCP port starts accepting connections."""
|
|
229
|
-
deadline = time.time() + timeout
|
|
230
|
-
while time.time() < deadline:
|
|
231
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
232
|
-
sock.settimeout(0.5)
|
|
233
|
-
try:
|
|
234
|
-
if sock.connect_ex((host, port)) == 0:
|
|
235
|
-
return
|
|
236
|
-
except OSError:
|
|
237
|
-
pass
|
|
238
|
-
time.sleep(0.05)
|
|
239
|
-
raise TimeoutError(
|
|
240
|
-
f"Port {port} on {host} did not open within {timeout} seconds."
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
484
|
def requires_authentication(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
485
|
+
flavour: Optional[str],
|
|
486
|
+
zarr: bool = False,
|
|
487
|
+
databrowser_url: Optional[str] = None,
|
|
248
488
|
) -> bool:
|
|
249
489
|
"""Check if authentication is required.
|
|
250
490
|
|
|
@@ -268,9 +508,7 @@ def requires_authentication(
|
|
|
268
508
|
response.raise_for_status()
|
|
269
509
|
result = {"flavours": response.json().get("flavours", [])}
|
|
270
510
|
if "flavours" in result:
|
|
271
|
-
global_flavour_names = {
|
|
272
|
-
f["flavour_name"] for f in result["flavours"]
|
|
273
|
-
}
|
|
511
|
+
global_flavour_names = {f["flavour_name"] for f in result["flavours"]}
|
|
274
512
|
return flavour not in global_flavour_names
|
|
275
513
|
except Exception:
|
|
276
514
|
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
freva_client/__init__.py,sha256=
|
|
1
|
+
freva_client/__init__.py,sha256=m2rzqsvQ80LSRnN3aBwSfirKiSIqQb6qv6O9J_WhAOo,916
|
|
2
2
|
freva_client/__main__.py,sha256=JVj12puT4o8JfhKLAggR2-NKCZa3wKwsYGi4HQ61DOQ,149
|
|
3
|
-
freva_client/auth.py,sha256=
|
|
3
|
+
freva_client/auth.py,sha256=_oHcMDKJjMvP7ZDNl6NK7NKelcxYW3jVpnQm8E-uvIU,7390
|
|
4
4
|
freva_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
freva_client/query.py,sha256=Dm6Wy9U8ofzZd4v50Sjhl4Guh7njrClSnNNV5_7UdM0,49433
|
|
6
6
|
freva_client/cli/__init__.py,sha256=NgTqBZGdozmTZtJduJUMrZj-opGw2KoT20tg6sc_xqo,149
|
|
@@ -10,11 +10,11 @@ freva_client/cli/cli_parser.py,sha256=EyzvTr70TBCD0mO2P3mVNtuEeEbNk2OdrhKSEHuu6N
|
|
|
10
10
|
freva_client/cli/cli_utils.py,sha256=9h2hlBQA-D3n-JlFIC1DSuzEEv73Bpu8lXLAqZBDaYI,1946
|
|
11
11
|
freva_client/cli/databrowser_cli.py,sha256=LpK6NvHWsbHVeJvOHG_su0dh9FHkLxTmY4K8P75S8A0,39397
|
|
12
12
|
freva_client/utils/__init__.py,sha256=ySHn-3CZBwfZW2s0EpZ3INxUWOw1V4LOlKIxSLYr52U,1000
|
|
13
|
-
freva_client/utils/auth_utils.py,sha256=
|
|
13
|
+
freva_client/utils/auth_utils.py,sha256=H0knqoX2gvxM8hPzfIZQQZSKaT5tD1sRCSF2ddcdAzw,15058
|
|
14
14
|
freva_client/utils/databrowser_utils.py,sha256=Hv39Q8Crd63l2U-lBJfppMn14BUm6ECY0ASsgP_8du8,17801
|
|
15
15
|
freva_client/utils/logger.py,sha256=vjBbNb9KvyMriBPpgIoJjlQFCEj3DLqkJu8tYxfp2xI,2494
|
|
16
|
-
freva_client-2509.
|
|
17
|
-
freva_client-2509.
|
|
18
|
-
freva_client-2509.
|
|
19
|
-
freva_client-2509.
|
|
20
|
-
freva_client-2509.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|