freva-client 2502.0.0__py3-none-any.whl → 2506.0.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 +234 -102
- freva_client/cli/auth_cli.py +13 -19
- freva_client/cli/cli_parser.py +2 -0
- freva_client/cli/databrowser_cli.py +304 -82
- freva_client/query.py +201 -47
- freva_client/utils/auth_utils.py +240 -0
- freva_client/utils/databrowser_utils.py +8 -3
- {freva_client-2502.0.0.dist-info → freva_client-2506.0.0.dist-info}/METADATA +1 -2
- freva_client-2506.0.0.dist-info/RECORD +20 -0
- {freva_client-2502.0.0.dist-info → freva_client-2506.0.0.dist-info}/WHEEL +1 -1
- freva_client-2502.0.0.dist-info/RECORD +0 -19
- {freva_client-2502.0.0.data → freva_client-2506.0.0.data}/data/share/freva/freva.toml +0 -0
- {freva_client-2502.0.0.dist-info → freva_client-2506.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Helper functions for authentication."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal, Optional, TypedDict, Union, cast
|
|
10
|
+
|
|
11
|
+
from appdirs import user_cache_dir
|
|
12
|
+
|
|
13
|
+
TOKEN_EXPIRY_BUFFER = 60 # seconds
|
|
14
|
+
TOKEN_ENV_VAR = "FREVA_TOKEN_FILE"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Token(TypedDict):
|
|
18
|
+
"""Token information."""
|
|
19
|
+
|
|
20
|
+
access_token: str
|
|
21
|
+
token_type: str
|
|
22
|
+
expires: int
|
|
23
|
+
refresh_token: str
|
|
24
|
+
refresh_expires: int
|
|
25
|
+
scope: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_default_token_file() -> Path:
|
|
29
|
+
"""Get the location of the default token file."""
|
|
30
|
+
path_str = os.getenv(TOKEN_ENV_VAR, "").strip()
|
|
31
|
+
|
|
32
|
+
path = Path(
|
|
33
|
+
path_str or os.path.join(user_cache_dir("freva"), "auth-token.json")
|
|
34
|
+
)
|
|
35
|
+
path.parent.mkdir(exist_ok=True, parents=True)
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_job_env() -> bool:
|
|
40
|
+
"""Detect whether we are running in a batch or job-managed environment.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
bool
|
|
45
|
+
True if common batch or workload manager environment variables are present.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
job_env_vars = [
|
|
49
|
+
# Slurm, PBS, Moab
|
|
50
|
+
"SLURM_JOB_ID",
|
|
51
|
+
"SLURM_NODELIST",
|
|
52
|
+
"PBS_JOBID",
|
|
53
|
+
"PBS_ENVIRONMENT",
|
|
54
|
+
"PBS_NODEFILE",
|
|
55
|
+
# SGE
|
|
56
|
+
"JOB_ID",
|
|
57
|
+
"SGE_TASK_ID",
|
|
58
|
+
"PE_HOSTFILE",
|
|
59
|
+
# LSF
|
|
60
|
+
"LSB_JOBID",
|
|
61
|
+
"LSB_HOSTS",
|
|
62
|
+
# OAR
|
|
63
|
+
"OAR_JOB_ID",
|
|
64
|
+
"OAR_NODEFILE",
|
|
65
|
+
# MPI
|
|
66
|
+
"OMPI_COMM_WORLD_SIZE",
|
|
67
|
+
"PMI_RANK",
|
|
68
|
+
"MPI_LOCALRANKID",
|
|
69
|
+
# Kubernetes
|
|
70
|
+
"KUBERNETES_SERVICE_HOST",
|
|
71
|
+
"KUBERNETES_PORT",
|
|
72
|
+
# FREVA BATCH MODE
|
|
73
|
+
"FREVA_BATCH_JOB",
|
|
74
|
+
# JHUB SESSION
|
|
75
|
+
"JUPYTERHUB_USER",
|
|
76
|
+
]
|
|
77
|
+
return any(var in os.environ for var in job_env_vars)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_jupyter_notebook() -> bool:
|
|
81
|
+
"""Check if running in a Jupyter notebook.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
bool
|
|
86
|
+
True if inside a Jupyter notebook or Jupyter kernel.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
from IPython import get_ipython
|
|
90
|
+
|
|
91
|
+
return get_ipython() is not None # pragma: no cover
|
|
92
|
+
except Exception:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_interactive_shell() -> bool:
|
|
97
|
+
"""Check whether we are running in an interactive terminal.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
bool
|
|
102
|
+
True if stdin and stdout are TTYs.
|
|
103
|
+
"""
|
|
104
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_interactive_auth_possible() -> bool:
|
|
108
|
+
"""Decide if an interactive browser-based auth flow is possible.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
bool
|
|
113
|
+
True if not in a batch/job/JupyterHub context and either in a TTY or
|
|
114
|
+
local Jupyter.
|
|
115
|
+
"""
|
|
116
|
+
return (is_interactive_shell() or is_jupyter_notebook()) and not (
|
|
117
|
+
is_job_env()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def resolve_token_path(custom_path: Optional[Union[str, Path]] = None) -> Path:
|
|
122
|
+
"""Resolve the path to the token file.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
custom_path : str or None
|
|
127
|
+
Optional path override.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
Path
|
|
132
|
+
The resolved path to the token file.
|
|
133
|
+
"""
|
|
134
|
+
if custom_path:
|
|
135
|
+
return Path(custom_path).expanduser().absolute()
|
|
136
|
+
path = get_default_token_file()
|
|
137
|
+
return path.expanduser().absolute()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_token(path: Optional[Union[str, Path]]) -> Optional[Token]:
|
|
141
|
+
"""Load a token dictionary from the given file path.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
path : Path or None
|
|
146
|
+
Path to the token file.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
dict or None
|
|
151
|
+
Parsed token dict or None if load fails.
|
|
152
|
+
"""
|
|
153
|
+
path = resolve_token_path(path)
|
|
154
|
+
try:
|
|
155
|
+
token: Token = json.loads(path.read_text())
|
|
156
|
+
return token
|
|
157
|
+
except Exception:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_token_valid(
|
|
162
|
+
token: Optional[Token], token_type: Literal["access_token", "refresh_token"]
|
|
163
|
+
) -> bool:
|
|
164
|
+
"""Check if a refresh token is available.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
token : dict
|
|
169
|
+
Token dictionary.
|
|
170
|
+
typken_type: str
|
|
171
|
+
What type of token to check for.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
bool
|
|
176
|
+
True if a refresh token is present.
|
|
177
|
+
"""
|
|
178
|
+
exp = cast(
|
|
179
|
+
Literal["refresh_expires", "expires"],
|
|
180
|
+
{
|
|
181
|
+
"refresh_token": "refresh_expires",
|
|
182
|
+
"access_token": "expires",
|
|
183
|
+
}[token_type],
|
|
184
|
+
)
|
|
185
|
+
return cast(
|
|
186
|
+
bool,
|
|
187
|
+
(
|
|
188
|
+
token
|
|
189
|
+
and token_type in token
|
|
190
|
+
and exp in token
|
|
191
|
+
and (time.time() + TOKEN_EXPIRY_BUFFER < token[exp])
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def choose_token_strategy(
|
|
197
|
+
token: Optional[Token] = None, token_file: Optional[Path] = None
|
|
198
|
+
) -> Literal["use_token", "refresh_token", "browser_auth", "fail"]:
|
|
199
|
+
"""Decide what action to take based on token state and environment.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
token : dict|None, default: None
|
|
204
|
+
Token dictionary or None if no token file found.
|
|
205
|
+
token_file: Path|None, default: None
|
|
206
|
+
Path to the file holding token information.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
str
|
|
211
|
+
One of:
|
|
212
|
+
- "use_token" : Access token is valid and usable.
|
|
213
|
+
- "refresh_token" : Refresh token should be used to get new access token.
|
|
214
|
+
- "browser_auth" : Interactive login via browser is allowed.
|
|
215
|
+
- "fail" : No way to log in in current environment.
|
|
216
|
+
"""
|
|
217
|
+
if is_token_valid(token, "access_token"):
|
|
218
|
+
return "use_token"
|
|
219
|
+
if is_token_valid(token, "refresh_token"):
|
|
220
|
+
return "refresh_token"
|
|
221
|
+
if is_interactive_auth_possible():
|
|
222
|
+
return "browser_auth"
|
|
223
|
+
return "fail"
|
|
224
|
+
|
|
225
|
+
|
|
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
|
+
)
|
|
@@ -141,13 +141,18 @@ class Config:
|
|
|
141
141
|
@property
|
|
142
142
|
def zarr_loader_url(self) -> str:
|
|
143
143
|
"""Define the url for getting zarr files."""
|
|
144
|
-
return f"{self.databrowser_url}/load/{self.flavour}
|
|
144
|
+
return f"{self.databrowser_url}/load/{self.flavour}"
|
|
145
145
|
|
|
146
146
|
@property
|
|
147
147
|
def intake_url(self) -> str:
|
|
148
148
|
"""Define the url for creating intake catalogues."""
|
|
149
149
|
return f"{self.databrowser_url}/intake-catalogue/{self.flavour}/{self.uniq_key}"
|
|
150
150
|
|
|
151
|
+
@property
|
|
152
|
+
def stac_url(self) -> str:
|
|
153
|
+
"""Define the url for creating stac catalogue."""
|
|
154
|
+
return f"{self.databrowser_url}/stac-catalogue/{self.flavour}/{self.uniq_key}"
|
|
155
|
+
|
|
151
156
|
@property
|
|
152
157
|
def metadata_url(self) -> str:
|
|
153
158
|
"""Define the endpoint for the metadata search."""
|
|
@@ -325,13 +330,13 @@ class UserDataHandler:
|
|
|
325
330
|
self, path: Union[os.PathLike[str], xr.Dataset]
|
|
326
331
|
) -> Dict[str, Union[str, List[str], Dict[str, str]]]:
|
|
327
332
|
"""Get metadata from a path or xarray dataset."""
|
|
328
|
-
|
|
333
|
+
time_coder = xr.coders.CFDatetimeCoder(use_cftime=True)
|
|
329
334
|
try:
|
|
330
335
|
dset = (
|
|
331
336
|
path
|
|
332
337
|
if isinstance(path, xr.Dataset)
|
|
333
338
|
else xr.open_mfdataset(
|
|
334
|
-
str(path), parallel=False,
|
|
339
|
+
str(path), parallel=False, decode_times=time_coder, lock=False
|
|
335
340
|
)
|
|
336
341
|
)
|
|
337
342
|
time_freq = dset.attrs.get("frequency", "")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freva-client
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2506.0.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
|
|
@@ -19,7 +19,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Requires-Dist: appdirs
|
|
21
21
|
Requires-Dist: pyyaml
|
|
22
|
-
Requires-Dist: authlib
|
|
23
22
|
Requires-Dist: requests
|
|
24
23
|
Requires-Dist: intake_esm
|
|
25
24
|
Requires-Dist: rich
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
freva_client/__init__.py,sha256=l4En1aqpj7TMWuOdt6kCtQLwoOGPYx7qMrNqwCKKMpU,851
|
|
2
|
+
freva_client/__main__.py,sha256=JVj12puT4o8JfhKLAggR2-NKCZa3wKwsYGi4HQ61DOQ,149
|
|
3
|
+
freva_client/auth.py,sha256=o3s82ImfgFAkg61hcputU0rOSnfTuAJIfKwMYxlP0yM,10610
|
|
4
|
+
freva_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
freva_client/query.py,sha256=cKgY2i3r-JriayMWLI8JqIn7F_TRasl4GPvZ_YVGWmw,40640
|
|
6
|
+
freva_client/cli/__init__.py,sha256=NgTqBZGdozmTZtJduJUMrZj-opGw2KoT20tg6sc_xqo,149
|
|
7
|
+
freva_client/cli/auth_cli.py,sha256=O4GPgGdCYnJz9rTUR27cSEy801Z91_ICGYaduIhkBfY,1801
|
|
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=-KqROP0H12weHrK6tTgqzjjuRrleYPoPfP8N1n_lAGo,32196
|
|
12
|
+
freva_client/utils/__init__.py,sha256=ySHn-3CZBwfZW2s0EpZ3INxUWOw1V4LOlKIxSLYr52U,1000
|
|
13
|
+
freva_client/utils/auth_utils.py,sha256=QXT9gitISj22rrQ3I9DBg4LZz6t5oGbdG59ChUsu8io,6002
|
|
14
|
+
freva_client/utils/databrowser_utils.py,sha256=9w9zu5Uc-QWCOXqMnwa1yv7gyHLIKN4STAjjXJkklgg,14440
|
|
15
|
+
freva_client/utils/logger.py,sha256=xd_3jjbsD1UBWlZZe8OUtKLpG7lbLcH46yiJ_bftyKg,2464
|
|
16
|
+
freva_client-2506.0.0.data/data/share/freva/freva.toml,sha256=64Rh4qvWc9TaGJMXMi8tZW14FnESt5Z24y17BfD2VyM,736
|
|
17
|
+
freva_client-2506.0.0.dist-info/entry_points.txt,sha256=zGyEwHrH_kAGLsCXv00y7Qnp-WjXkUuIomHkfGMCxtA,53
|
|
18
|
+
freva_client-2506.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
19
|
+
freva_client-2506.0.0.dist-info/METADATA,sha256=UZe4YEIaydXRaQ--r4dIc7cjaIL-gzk7wRQWeEB24rI,2503
|
|
20
|
+
freva_client-2506.0.0.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
freva_client/__init__.py,sha256=T8gGhWl4PHUFdDJ0kLyxUZhTVwxzwMB6IUY0f6OOfcc,851
|
|
2
|
-
freva_client/__main__.py,sha256=JVj12puT4o8JfhKLAggR2-NKCZa3wKwsYGi4HQ61DOQ,149
|
|
3
|
-
freva_client/auth.py,sha256=o33EKI5CxMOBY0nWFLmZT-V6v2U9L1qGjLcE7y4PIoE,6814
|
|
4
|
-
freva_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
freva_client/query.py,sha256=ZxGPkU9B5JROCMIQ_AmhTKLlVPOCE2ENs4p7o9Jxge0,34095
|
|
6
|
-
freva_client/cli/__init__.py,sha256=NgTqBZGdozmTZtJduJUMrZj-opGw2KoT20tg6sc_xqo,149
|
|
7
|
-
freva_client/cli/auth_cli.py,sha256=3sThrF0YUgSzdwzzc5ww_GJlzUDK3jx-gQG9Z0hneRk,1843
|
|
8
|
-
freva_client/cli/cli_app.py,sha256=QH6cb9XZTx8S9L0chPWOVd9Jn4GD1hd3sENAhdzmLZU,887
|
|
9
|
-
freva_client/cli/cli_parser.py,sha256=c-_WG56g6arLvnayb9SO1OeHt7AjBV4egI_vTEYee4I,5042
|
|
10
|
-
freva_client/cli/cli_utils.py,sha256=Ev9UxM4S1ZDbZSAGHFe5dMjVGot75w3muNKH3P80bHY,842
|
|
11
|
-
freva_client/cli/databrowser_cli.py,sha256=r_Tl1yxLBZPMqY8vmJiOAvF_envTtf6c6KwQKjwdAq8,24387
|
|
12
|
-
freva_client/utils/__init__.py,sha256=ySHn-3CZBwfZW2s0EpZ3INxUWOw1V4LOlKIxSLYr52U,1000
|
|
13
|
-
freva_client/utils/databrowser_utils.py,sha256=kGTYjeHWBOIEcLkFuXhZ7ai6_j_DUzVq7hq7HEOYaho,14179
|
|
14
|
-
freva_client/utils/logger.py,sha256=xd_3jjbsD1UBWlZZe8OUtKLpG7lbLcH46yiJ_bftyKg,2464
|
|
15
|
-
freva_client-2502.0.0.data/data/share/freva/freva.toml,sha256=64Rh4qvWc9TaGJMXMi8tZW14FnESt5Z24y17BfD2VyM,736
|
|
16
|
-
freva_client-2502.0.0.dist-info/entry_points.txt,sha256=zGyEwHrH_kAGLsCXv00y7Qnp-WjXkUuIomHkfGMCxtA,53
|
|
17
|
-
freva_client-2502.0.0.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
|
|
18
|
-
freva_client-2502.0.0.dist-info/METADATA,sha256=G2VWBzP6OFpIOpCY-tPE2mipQVoYvaBF9gO-nrG-yEs,2526
|
|
19
|
-
freva_client-2502.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|