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.

@@ -2,19 +2,95 @@
2
2
 
3
3
  import json
4
4
  import os
5
- import socket
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 Literal, Optional, TypedDict, Union, cast
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
- class Token(TypedDict):
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
- def get_default_token_file() -> Path:
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 wait_for_port(host: str, port: int, timeout: float = 5.0) -> None:
227
- """Wait until a TCP port starts accepting connections."""
228
- deadline = time.time() + timeout
229
- while time.time() < deadline:
230
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
231
- sock.settimeout(0.5)
232
- try:
233
- if sock.connect_ex((host, port)) == 0:
234
- return
235
- except OSError:
236
- pass
237
- time.sleep(0.05)
238
- raise TimeoutError(
239
- f"Port {port} on {host} did not open within {timeout} seconds."
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 = "freva",
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._flavour = flavour
49
+ self.flavour = self.get_flavour(flavour)
49
50
 
50
- def _read_ini(self, path: Path) -> str:
51
- """Read an ini file."""
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
- return f"{scheme}://{host}"
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["host"]))
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
- return f"{scheme}://{host}"
125
+ host = f"{scheme}://{host}"
126
+ return {
127
+ "host": host,
128
+ "flavour": flavour,
129
+ }
75
130
 
76
- def _read_config(self, path: Path, file_type: Literal["toml", "ini"]) -> str:
77
- """Read the configuration."""
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 the all databrowser flavours and search keys."""
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(f"{self.databrowser_url}/overview", timeout=15)
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 _get_databrowser_host_from_config(self) -> str:
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
- host = self._read_config(config_path, config_type)
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 host
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._get_databrowser_host_from_config()
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freva-client
3
- Version: 2508.0.0
3
+ Version: 2509.1.0
4
4
  Summary: Search for climate data based on key-value pairs
5
5
  Author-email: "DKRZ, Clint" <freva@dkrz.de>
6
6
  Requires-Python: >=3.8
@@ -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,,