freva-client 2509.2.0__py3-none-any.whl → 2510.0.1__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 +15 -4
- freva_client/cli/databrowser_cli.py +68 -0
- freva_client/query.py +43 -3
- freva_client/utils/auth_utils.py +174 -10
- {freva_client-2509.2.0.dist-info → freva_client-2510.0.1.dist-info}/METADATA +1 -1
- {freva_client-2509.2.0.dist-info → freva_client-2510.0.1.dist-info}/RECORD +10 -10
- {freva_client-2509.2.0.data → freva_client-2510.0.1.data}/data/share/freva/freva.toml +0 -0
- {freva_client-2509.2.0.dist-info → freva_client-2510.0.1.dist-info}/WHEEL +0 -0
- {freva_client-2509.2.0.dist-info → freva_client-2510.0.1.dist-info}/entry_points.txt +0 -0
freva_client/__init__.py
CHANGED
freva_client/auth.py
CHANGED
|
@@ -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(
|
freva_client/query.py
CHANGED
|
@@ -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']}"}
|
freva_client/utils/auth_utils.py
CHANGED
|
@@ -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:
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
freva_client/__init__.py,sha256=
|
|
1
|
+
freva_client/__init__.py,sha256=0nL9Fjqi9agYmHjdH3fSs1iEtktJL9-qeqFyyC0L7VA,916
|
|
2
2
|
freva_client/__main__.py,sha256=JVj12puT4o8JfhKLAggR2-NKCZa3wKwsYGi4HQ61DOQ,149
|
|
3
|
-
freva_client/auth.py,sha256=
|
|
3
|
+
freva_client/auth.py,sha256=6pTY7lHdo0EC8pTTSpauXymd9fgN7SW2-AoQ-qKAmYY,7746
|
|
4
4
|
freva_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
freva_client/query.py,sha256=
|
|
5
|
+
freva_client/query.py,sha256=2B_8MCbGl9ASvnxGcLgP5SCqp7_0AY0UufytmidQPu0,50857
|
|
6
6
|
freva_client/cli/__init__.py,sha256=NgTqBZGdozmTZtJduJUMrZj-opGw2KoT20tg6sc_xqo,149
|
|
7
7
|
freva_client/cli/auth_cli.py,sha256=lM6QNcewh9Cz7DtHO4Wu6iv7GapwplyjhDRfmIlzyJ0,1950
|
|
8
8
|
freva_client/cli/cli_app.py,sha256=QH6cb9XZTx8S9L0chPWOVd9Jn4GD1hd3sENAhdzmLZU,887
|
|
9
9
|
freva_client/cli/cli_parser.py,sha256=EyzvTr70TBCD0mO2P3mVNtuEeEbNk2OdrhKSEHuu6NE,5101
|
|
10
10
|
freva_client/cli/cli_utils.py,sha256=9h2hlBQA-D3n-JlFIC1DSuzEEv73Bpu8lXLAqZBDaYI,1946
|
|
11
|
-
freva_client/cli/databrowser_cli.py,sha256=
|
|
11
|
+
freva_client/cli/databrowser_cli.py,sha256=0NRPlaVumEul0XSzQYuSQkfAE--G2dcJ97bZCKsbKL0,41599
|
|
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=zZhfwXSaFATFNlQwxbC41gq9AV1c1f6W2ylnrB_A5Ms,20684
|
|
14
14
|
freva_client/utils/databrowser_utils.py,sha256=LFojxncBavashqOyy5hawoQB7DkMAtzlfKhWD378eqw,18152
|
|
15
15
|
freva_client/utils/logger.py,sha256=vjBbNb9KvyMriBPpgIoJjlQFCEj3DLqkJu8tYxfp2xI,2494
|
|
16
|
-
freva_client-
|
|
17
|
-
freva_client-
|
|
18
|
-
freva_client-
|
|
19
|
-
freva_client-
|
|
20
|
-
freva_client-
|
|
16
|
+
freva_client-2510.0.1.data/data/share/freva/freva.toml,sha256=5xXWBqw0W6JyWfRMaSNnKDOGkDznx6NFaJvggh-cUPU,899
|
|
17
|
+
freva_client-2510.0.1.dist-info/entry_points.txt,sha256=zGyEwHrH_kAGLsCXv00y7Qnp-WjXkUuIomHkfGMCxtA,53
|
|
18
|
+
freva_client-2510.0.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
19
|
+
freva_client-2510.0.1.dist-info/METADATA,sha256=lAFCT4pcds8OHjFAlOPGB4XzN17dAlK0YfFiDm5TzAw,2487
|
|
20
|
+
freva_client-2510.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|