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.

@@ -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, use_cftime=True, lock=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: 2502.0.0
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: flit 3.11.0
2
+ Generator: flit 3.12.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,