freva-client 2508.0.0__tar.gz → 2509.0.0__tar.gz
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-2508.0.0 → freva_client-2509.0.0}/PKG-INFO +1 -1
- {freva_client-2508.0.0 → freva_client-2509.0.0}/assets/share/freva/freva.toml +5 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/__init__.py +2 -1
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/auth.py +21 -12
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/cli/auth_cli.py +6 -0
- freva_client-2509.0.0/src/freva_client/cli/cli_utils.py +69 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/cli/databrowser_cli.py +232 -32
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/query.py +255 -38
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/utils/auth_utils.py +38 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/utils/databrowser_utils.py +118 -32
- freva_client-2508.0.0/src/freva_client/cli/cli_utils.py +0 -32
- {freva_client-2508.0.0 → freva_client-2509.0.0}/MANIFEST.in +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/README.md +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/pyproject.toml +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/__main__.py +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/cli/__init__.py +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/cli/cli_app.py +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/cli/cli_parser.py +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/py.typed +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/utils/__init__.py +0 -0
- {freva_client-2508.0.0 → freva_client-2509.0.0}/src/freva_client/utils/logger.py +0 -0
|
@@ -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"
|
|
@@ -12,10 +12,11 @@ science community. With help of Freva researchers can:
|
|
|
12
12
|
The code described here is currently in testing phase. The client and server
|
|
13
13
|
library described in the documentation only support searching for data. If you
|
|
14
14
|
need to apply data analysis plugins, please visit the
|
|
15
|
+
official documentation: https://freva-org.github.io/freva-legacy
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
from .auth import authenticate
|
|
18
19
|
from .query import databrowser
|
|
19
20
|
|
|
20
|
-
__version__ = "
|
|
21
|
+
__version__ = "2509.0.0"
|
|
21
22
|
__all__ = ["authenticate", "databrowser", "__version__"]
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import socket
|
|
6
6
|
import urllib.parse
|
|
7
7
|
import webbrowser
|
|
8
|
+
from getpass import getuser
|
|
8
9
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from threading import Event, Lock, Thread
|
|
@@ -113,7 +114,7 @@ class Auth:
|
|
|
113
114
|
self,
|
|
114
115
|
auth_url: str,
|
|
115
116
|
force: bool = False,
|
|
116
|
-
_timeout: int = 30,
|
|
117
|
+
_timeout: Optional[int] = 30,
|
|
117
118
|
) -> Token:
|
|
118
119
|
login_endpoint = f"{auth_url}/login"
|
|
119
120
|
token_endpoint = f"{auth_url}/token"
|
|
@@ -121,6 +122,7 @@ class Auth:
|
|
|
121
122
|
redirect_uri = REDIRECT_URI.format(port=port)
|
|
122
123
|
params = {
|
|
123
124
|
"redirect_uri": redirect_uri,
|
|
125
|
+
"offline_access": "true",
|
|
124
126
|
}
|
|
125
127
|
if force:
|
|
126
128
|
params["prompt"] = "login"
|
|
@@ -129,11 +131,13 @@ class Auth:
|
|
|
129
131
|
logger.info("Opening browser for login:\n%s", login_url)
|
|
130
132
|
logger.info(
|
|
131
133
|
"If you are using this on a remote host, you might need to "
|
|
132
|
-
"forward port %d:\n"
|
|
133
|
-
" ssh -L %d:localhost:%d
|
|
134
|
+
"increase the login timeout and forward port %d:\n"
|
|
135
|
+
" ssh -L %d:localhost:%d %s@%s",
|
|
134
136
|
port,
|
|
135
137
|
port,
|
|
136
138
|
port,
|
|
139
|
+
getuser(),
|
|
140
|
+
socket.gethostname(),
|
|
137
141
|
)
|
|
138
142
|
event = Event()
|
|
139
143
|
server = start_local_server(port, event)
|
|
@@ -142,7 +146,7 @@ class Auth:
|
|
|
142
146
|
try:
|
|
143
147
|
wait_for_port("localhost", port)
|
|
144
148
|
webbrowser.open(login_url)
|
|
145
|
-
success = event.wait(timeout=_timeout)
|
|
149
|
+
success = event.wait(timeout=_timeout or None)
|
|
146
150
|
if not success:
|
|
147
151
|
raise TimeoutError(
|
|
148
152
|
f"Login did not complete within {_timeout} seconds. "
|
|
@@ -223,9 +227,7 @@ class Auth:
|
|
|
223
227
|
return self._auth_token
|
|
224
228
|
|
|
225
229
|
def _refresh(
|
|
226
|
-
self,
|
|
227
|
-
url: str,
|
|
228
|
-
refresh_token: str,
|
|
230
|
+
self, url: str, refresh_token: str, timeout: Optional[int] = 30
|
|
229
231
|
) -> Token:
|
|
230
232
|
"""Refresh the access_token with a refresh token."""
|
|
231
233
|
try:
|
|
@@ -235,7 +237,7 @@ class Auth:
|
|
|
235
237
|
)
|
|
236
238
|
except (AuthError, KeyError) as error:
|
|
237
239
|
logger.warning("Failed to refresh token: %s", error)
|
|
238
|
-
return self._login(url)
|
|
240
|
+
return self._login(url, _timeout=timeout)
|
|
239
241
|
|
|
240
242
|
def authenticate(
|
|
241
243
|
self,
|
|
@@ -243,6 +245,7 @@ class Auth:
|
|
|
243
245
|
config: Optional[Config] = None,
|
|
244
246
|
*,
|
|
245
247
|
force: bool = False,
|
|
248
|
+
timeout: Optional[int] = 30,
|
|
246
249
|
_cli: bool = False,
|
|
247
250
|
) -> Token:
|
|
248
251
|
"""Authenticate the user to the host."""
|
|
@@ -258,9 +261,11 @@ class Auth:
|
|
|
258
261
|
return self._auth_token
|
|
259
262
|
try:
|
|
260
263
|
if strategy == "refresh_token" and token:
|
|
261
|
-
return self._refresh(
|
|
264
|
+
return self._refresh(
|
|
265
|
+
cfg.auth_url, token["refresh_token"], timeout=timeout
|
|
266
|
+
)
|
|
262
267
|
if strategy == "browser_auth":
|
|
263
|
-
return self._login(cfg.auth_url, force=force)
|
|
268
|
+
return self._login(cfg.auth_url, force=force, _timeout=timeout)
|
|
264
269
|
except AuthError as error:
|
|
265
270
|
reason = str(error)
|
|
266
271
|
|
|
@@ -287,6 +292,7 @@ def authenticate(
|
|
|
287
292
|
token_file: Optional[Union[Path, str]] = None,
|
|
288
293
|
host: Optional[str] = None,
|
|
289
294
|
force: bool = False,
|
|
295
|
+
timeout: Optional[int] = 30,
|
|
290
296
|
) -> Token:
|
|
291
297
|
"""Authenticate to the host.
|
|
292
298
|
|
|
@@ -294,13 +300,15 @@ def authenticate(
|
|
|
294
300
|
|
|
295
301
|
Parameters
|
|
296
302
|
----------
|
|
297
|
-
|
|
303
|
+
token_file: str, optional
|
|
298
304
|
Instead of setting a password, you can set a refresh token to refresh
|
|
299
305
|
the access token. This is recommended for non-interactive environments.
|
|
300
306
|
host: str, optional
|
|
301
307
|
The hostname of the REST server.
|
|
302
308
|
force: bool, default: False
|
|
303
309
|
Force token recreation, even if current token is still valid.
|
|
310
|
+
timeout: int, default: 30
|
|
311
|
+
Set the timeout, None for indefinate.
|
|
304
312
|
|
|
305
313
|
Returns
|
|
306
314
|
-------
|
|
@@ -313,7 +321,7 @@ def authenticate(
|
|
|
313
321
|
.. code-block:: python
|
|
314
322
|
|
|
315
323
|
from freva_client import authenticate
|
|
316
|
-
token = authenticate()
|
|
324
|
+
token = authenticate(timeout=120)
|
|
317
325
|
print(token)
|
|
318
326
|
|
|
319
327
|
Batch mode authentication with a refresh token:
|
|
@@ -327,4 +335,5 @@ def authenticate(
|
|
|
327
335
|
return auth.authenticate(
|
|
328
336
|
host=host,
|
|
329
337
|
force=force,
|
|
338
|
+
timeout=timeout,
|
|
330
339
|
)
|
|
@@ -44,6 +44,11 @@ def authenticate_cli(
|
|
|
44
44
|
"-f",
|
|
45
45
|
help="Force token recreation, even if current token is still valid.",
|
|
46
46
|
),
|
|
47
|
+
timeout: int = typer.Option(
|
|
48
|
+
30,
|
|
49
|
+
"--timeout",
|
|
50
|
+
help="Set the timeout for login in secdonds, 0 for indefinate",
|
|
51
|
+
),
|
|
47
52
|
verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
|
|
48
53
|
version: Optional[bool] = typer.Option(
|
|
49
54
|
False,
|
|
@@ -59,5 +64,6 @@ def authenticate_cli(
|
|
|
59
64
|
host=host,
|
|
60
65
|
force=force,
|
|
61
66
|
_cli=True,
|
|
67
|
+
timeout=timeout,
|
|
62
68
|
)
|
|
63
69
|
print(json.dumps(token, indent=3))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Utilities for the command line interface."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import typer
|
|
7
|
+
from rich import print as pprint
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from freva_client import __version__
|
|
12
|
+
from freva_client.utils import logger
|
|
13
|
+
|
|
14
|
+
APP_NAME: str = "freva-client"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def version_callback(version: bool) -> None:
|
|
18
|
+
"""Print the version and exit."""
|
|
19
|
+
if version:
|
|
20
|
+
pprint(f"{APP_NAME}: {__version__}")
|
|
21
|
+
raise typer.Exit()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_cli_args(cli_args: List[str]) -> Dict[str, List[str]]:
|
|
25
|
+
"""Convert the cli arguments to a dictionary."""
|
|
26
|
+
logger.debug("parsing command line arguments.")
|
|
27
|
+
kwargs = {}
|
|
28
|
+
for entry in cli_args:
|
|
29
|
+
key, _, value = entry.partition("=")
|
|
30
|
+
if value and key not in kwargs:
|
|
31
|
+
kwargs[key] = [value]
|
|
32
|
+
elif value:
|
|
33
|
+
kwargs[key].append(value)
|
|
34
|
+
logger.debug(kwargs)
|
|
35
|
+
return kwargs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _summarize(val: Any, max_items: int = 6) -> str:
|
|
39
|
+
"""Summarize values for table display, truncating long lists."""
|
|
40
|
+
n = len(val)
|
|
41
|
+
head = ", ".join(map(str, val[:max_items]))
|
|
42
|
+
if n > max_items:
|
|
43
|
+
return f"{head} … (+{n - max_items} more)"
|
|
44
|
+
return head
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def print_df(s: pd.Series, max_items: int = 6) -> None:
|
|
48
|
+
"""Print a pandas Series as a rich table.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
s : pd.Series
|
|
53
|
+
The pandas Series to print.
|
|
54
|
+
max_items : int, optional
|
|
55
|
+
Maximum number of items to display for list-like values,
|
|
56
|
+
by default 6.
|
|
57
|
+
"""
|
|
58
|
+
left_col: str = s.index.name or "index"
|
|
59
|
+
right_col: str = s.name or "value"
|
|
60
|
+
|
|
61
|
+
table: Table = Table(show_header=True, header_style="bold magenta")
|
|
62
|
+
table.add_column(left_col, style="cyan", no_wrap=True)
|
|
63
|
+
table.add_column(right_col, style="green")
|
|
64
|
+
|
|
65
|
+
for key, val in s.items():
|
|
66
|
+
table.add_row(str(key), _summarize(val, max_items=max_items))
|
|
67
|
+
|
|
68
|
+
console: Console = Console()
|
|
69
|
+
console.print(table)
|
|
@@ -7,16 +7,21 @@ import json
|
|
|
7
7
|
from enum import Enum
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from tempfile import NamedTemporaryFile
|
|
10
|
-
from typing import Dict, List, Optional, Tuple, Union, cast
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
|
+
import typer.models
|
|
13
14
|
import xarray as xr
|
|
14
15
|
|
|
16
|
+
# import pprinr from rich
|
|
17
|
+
from rich import print as pprint
|
|
18
|
+
|
|
15
19
|
from freva_client import databrowser
|
|
16
20
|
from freva_client.auth import Auth
|
|
17
21
|
from freva_client.utils import exception_handler, logger
|
|
22
|
+
from freva_client.utils.auth_utils import requires_authentication
|
|
18
23
|
|
|
19
|
-
from .cli_utils import parse_cli_args, version_callback
|
|
24
|
+
from .cli_utils import parse_cli_args, print_df, version_callback
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
class UniqKeys(str, Enum):
|
|
@@ -26,14 +31,18 @@ class UniqKeys(str, Enum):
|
|
|
26
31
|
uri = "uri"
|
|
27
32
|
|
|
28
33
|
|
|
29
|
-
class
|
|
30
|
-
|
|
34
|
+
class FlavourAction(str, Enum):
|
|
35
|
+
add = "add"
|
|
36
|
+
delete = "delete"
|
|
37
|
+
list = "list"
|
|
38
|
+
|
|
31
39
|
|
|
40
|
+
class BuiltinFlavours(str, Enum):
|
|
41
|
+
"""Literal implementation for the cli."""
|
|
32
42
|
freva = "freva"
|
|
33
43
|
cmip6 = "cmip6"
|
|
34
44
|
cmip5 = "cmip5"
|
|
35
45
|
cordex = "cordex"
|
|
36
|
-
nextgems = "nextgems"
|
|
37
46
|
user = "user"
|
|
38
47
|
|
|
39
48
|
|
|
@@ -131,10 +140,9 @@ def metadata_search(
|
|
|
131
140
|
" containing era5, regardless of project, product etc."
|
|
132
141
|
),
|
|
133
142
|
),
|
|
134
|
-
flavour:
|
|
135
|
-
|
|
143
|
+
flavour: Optional[str] = typer.Option(
|
|
144
|
+
None,
|
|
136
145
|
"--flavour",
|
|
137
|
-
"-f",
|
|
138
146
|
help=(
|
|
139
147
|
"The Data Reference Syntax (DRS) standard specifying the type "
|
|
140
148
|
"of climate datasets to query."
|
|
@@ -200,6 +208,16 @@ def metadata_search(
|
|
|
200
208
|
parse_json: bool = typer.Option(
|
|
201
209
|
False, "-j", "--json", help="Parse output in json format."
|
|
202
210
|
),
|
|
211
|
+
token_file: Optional[Path] = typer.Option(
|
|
212
|
+
None,
|
|
213
|
+
"--token-file",
|
|
214
|
+
"-tf",
|
|
215
|
+
help=(
|
|
216
|
+
"Instead of authenticating via code based authentication flow "
|
|
217
|
+
"you can set the path to the json file that contains a "
|
|
218
|
+
"`refresh token` containing a refresh_token key."
|
|
219
|
+
),
|
|
220
|
+
),
|
|
203
221
|
verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
|
|
204
222
|
version: Optional[bool] = typer.Option(
|
|
205
223
|
False,
|
|
@@ -219,13 +237,15 @@ def metadata_search(
|
|
|
219
237
|
"""
|
|
220
238
|
logger.set_verbosity(verbose)
|
|
221
239
|
logger.debug("Search the databrowser")
|
|
240
|
+
if requires_authentication(flavour=flavour, databrowser_url=host):
|
|
241
|
+
Auth(token_file).authenticate(host=host, _cli=True)
|
|
222
242
|
result = databrowser.metadata_search(
|
|
223
243
|
*(facets or []),
|
|
224
244
|
time=time or "",
|
|
225
245
|
time_select=time_select.value,
|
|
226
246
|
bbox=bbox or None,
|
|
227
247
|
bbox_select=bbox_select.value,
|
|
228
|
-
flavour=flavour
|
|
248
|
+
flavour=flavour,
|
|
229
249
|
host=host,
|
|
230
250
|
extended_search=extended_search,
|
|
231
251
|
multiversion=multiversion,
|
|
@@ -233,10 +253,9 @@ def metadata_search(
|
|
|
233
253
|
**(parse_cli_args(search_keys or [])),
|
|
234
254
|
)
|
|
235
255
|
if parse_json:
|
|
236
|
-
print(
|
|
256
|
+
print(result.to_json())
|
|
237
257
|
return
|
|
238
|
-
|
|
239
|
-
print(f"{key}: {', '.join(values)}")
|
|
258
|
+
print_df(result)
|
|
240
259
|
|
|
241
260
|
|
|
242
261
|
@databrowser_app.command(
|
|
@@ -270,10 +289,9 @@ def data_search(
|
|
|
270
289
|
"based on file paths or Uniform Resource Identifiers"
|
|
271
290
|
),
|
|
272
291
|
),
|
|
273
|
-
flavour:
|
|
274
|
-
|
|
292
|
+
flavour: Optional[str] = typer.Option(
|
|
293
|
+
None,
|
|
275
294
|
"--flavour",
|
|
276
|
-
"-f",
|
|
277
295
|
help=(
|
|
278
296
|
"The Data Reference Syntax (DRS) standard specifying the type "
|
|
279
297
|
"of climate datasets to query."
|
|
@@ -361,13 +379,14 @@ def data_search(
|
|
|
361
379
|
"""
|
|
362
380
|
logger.set_verbosity(verbose)
|
|
363
381
|
logger.debug("Search the databrowser")
|
|
382
|
+
|
|
364
383
|
result = databrowser(
|
|
365
384
|
*(facets or []),
|
|
366
385
|
time=time or "",
|
|
367
386
|
time_select=time_select.value,
|
|
368
387
|
bbox=bbox or None,
|
|
369
388
|
bbox_select=bbox_select.value,
|
|
370
|
-
flavour=flavour
|
|
389
|
+
flavour=flavour,
|
|
371
390
|
uniq_key=uniq_key.value,
|
|
372
391
|
host=host,
|
|
373
392
|
fail_on_error=False,
|
|
@@ -375,7 +394,7 @@ def data_search(
|
|
|
375
394
|
stream_zarr=zarr,
|
|
376
395
|
**(parse_cli_args(search_keys or [])),
|
|
377
396
|
)
|
|
378
|
-
if zarr:
|
|
397
|
+
if requires_authentication(flavour=flavour, zarr=zarr, databrowser_url=host):
|
|
379
398
|
Auth(token_file).authenticate(host=host, _cli=True)
|
|
380
399
|
if parse_json:
|
|
381
400
|
print(json.dumps(sorted(result)))
|
|
@@ -415,10 +434,9 @@ def intake_catalogue(
|
|
|
415
434
|
"based on file paths or Uniform Resource Identifiers"
|
|
416
435
|
),
|
|
417
436
|
),
|
|
418
|
-
flavour:
|
|
419
|
-
|
|
437
|
+
flavour: Optional[str] = typer.Option(
|
|
438
|
+
None,
|
|
420
439
|
"--flavour",
|
|
421
|
-
"-f",
|
|
422
440
|
help=(
|
|
423
441
|
"The Data Reference Syntax (DRS) standard specifying the type "
|
|
424
442
|
"of climate datasets to query."
|
|
@@ -508,7 +526,7 @@ def intake_catalogue(
|
|
|
508
526
|
) -> None:
|
|
509
527
|
"""Create an intake catalogue for climate datasets based on the specified "
|
|
510
528
|
"Data Reference Syntax (DRS) standard (flavour) and the type of search "
|
|
511
|
-
result (uniq_key), which can be either
|
|
529
|
+
result (uniq_key), which can be either "file" or "uri"."""
|
|
512
530
|
logger.set_verbosity(verbose)
|
|
513
531
|
logger.debug("Search the databrowser")
|
|
514
532
|
result = databrowser(
|
|
@@ -517,7 +535,7 @@ def intake_catalogue(
|
|
|
517
535
|
time_select=time_select.value,
|
|
518
536
|
bbox=bbox or None,
|
|
519
537
|
bbox_select=bbox_select.value,
|
|
520
|
-
flavour=flavour
|
|
538
|
+
flavour=flavour,
|
|
521
539
|
uniq_key=uniq_key.value,
|
|
522
540
|
host=host,
|
|
523
541
|
fail_on_error=False,
|
|
@@ -525,7 +543,7 @@ def intake_catalogue(
|
|
|
525
543
|
stream_zarr=zarr,
|
|
526
544
|
**(parse_cli_args(search_keys or [])),
|
|
527
545
|
)
|
|
528
|
-
if zarr:
|
|
546
|
+
if requires_authentication(flavour=flavour, zarr=zarr, databrowser_url=host):
|
|
529
547
|
Auth(token_file).authenticate(host=host, _cli=True)
|
|
530
548
|
with NamedTemporaryFile(suffix=".json") as temp_f:
|
|
531
549
|
result._create_intake_catalogue_file(str(filename or temp_f.name))
|
|
@@ -564,10 +582,9 @@ def stac_catalogue(
|
|
|
564
582
|
"based on file paths or Uniform Resource Identifiers"
|
|
565
583
|
),
|
|
566
584
|
),
|
|
567
|
-
flavour:
|
|
568
|
-
|
|
585
|
+
flavour: Optional[str] = typer.Option(
|
|
586
|
+
None,
|
|
569
587
|
"--flavour",
|
|
570
|
-
"-f",
|
|
571
588
|
help=(
|
|
572
589
|
"The Data Reference Syntax (DRS) standard specifying the type "
|
|
573
590
|
"of climate datasets to query."
|
|
@@ -619,6 +636,16 @@ def stac_catalogue(
|
|
|
619
636
|
"the hostname is read from a config file"
|
|
620
637
|
),
|
|
621
638
|
),
|
|
639
|
+
token_file: Optional[Path] = typer.Option(
|
|
640
|
+
None,
|
|
641
|
+
"--token-file",
|
|
642
|
+
"-tf",
|
|
643
|
+
help=(
|
|
644
|
+
"Instead of authenticating via code based authentication flow "
|
|
645
|
+
"you can set the path to the json file that contains a "
|
|
646
|
+
"`refresh token` containing a refresh_token key."
|
|
647
|
+
),
|
|
648
|
+
),
|
|
622
649
|
verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
|
|
623
650
|
multiversion: bool = typer.Option(
|
|
624
651
|
False,
|
|
@@ -654,7 +681,7 @@ def stac_catalogue(
|
|
|
654
681
|
time_select=time_select.value,
|
|
655
682
|
bbox=bbox or None,
|
|
656
683
|
bbox_select=bbox_select.value,
|
|
657
|
-
flavour=flavour
|
|
684
|
+
flavour=flavour,
|
|
658
685
|
uniq_key=uniq_key.value,
|
|
659
686
|
host=host,
|
|
660
687
|
fail_on_error=False,
|
|
@@ -662,6 +689,8 @@ def stac_catalogue(
|
|
|
662
689
|
stream_zarr=False,
|
|
663
690
|
**(parse_cli_args(search_keys or [])),
|
|
664
691
|
)
|
|
692
|
+
if requires_authentication(flavour=flavour, databrowser_url=host):
|
|
693
|
+
Auth(token_file).authenticate(host=host, _cli=True)
|
|
665
694
|
print(result.stac_catalogue(filename=filename))
|
|
666
695
|
|
|
667
696
|
|
|
@@ -692,10 +721,9 @@ def count_values(
|
|
|
692
721
|
"-d",
|
|
693
722
|
help=("Separate the count by search facets."),
|
|
694
723
|
),
|
|
695
|
-
flavour:
|
|
696
|
-
|
|
724
|
+
flavour: Optional[str] = typer.Option(
|
|
725
|
+
None,
|
|
697
726
|
"--flavour",
|
|
698
|
-
"-f",
|
|
699
727
|
help=(
|
|
700
728
|
"The Data Reference Syntax (DRS) standard specifying the type "
|
|
701
729
|
"of climate datasets to query."
|
|
@@ -761,6 +789,16 @@ def count_values(
|
|
|
761
789
|
parse_json: bool = typer.Option(
|
|
762
790
|
False, "-j", "--json", help="Parse output in json format."
|
|
763
791
|
),
|
|
792
|
+
token_file: Optional[Path] = typer.Option(
|
|
793
|
+
None,
|
|
794
|
+
"--token-file",
|
|
795
|
+
"-tf",
|
|
796
|
+
help=(
|
|
797
|
+
"Instead of authenticating via code based authentication flow "
|
|
798
|
+
"you can set the path to the json file that contains a "
|
|
799
|
+
"`refresh token` containing a refresh_token key."
|
|
800
|
+
),
|
|
801
|
+
),
|
|
764
802
|
verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
|
|
765
803
|
version: Optional[bool] = typer.Option(
|
|
766
804
|
False,
|
|
@@ -788,6 +826,8 @@ def count_values(
|
|
|
788
826
|
bbox or search_kws.pop("bbox", None),
|
|
789
827
|
)
|
|
790
828
|
facets = facets or []
|
|
829
|
+
if requires_authentication(flavour=flavour, databrowser_url=host):
|
|
830
|
+
Auth(token_file).authenticate(host=host, _cli=True)
|
|
791
831
|
if detail:
|
|
792
832
|
result = databrowser.count_values(
|
|
793
833
|
*facets,
|
|
@@ -795,7 +835,7 @@ def count_values(
|
|
|
795
835
|
time_select=time_select.value,
|
|
796
836
|
bbox=bbox or None,
|
|
797
837
|
bbox_select=bbox_select.value,
|
|
798
|
-
flavour=flavour
|
|
838
|
+
flavour=flavour,
|
|
799
839
|
host=host,
|
|
800
840
|
extended_search=extended_search,
|
|
801
841
|
multiversion=multiversion,
|
|
@@ -810,7 +850,7 @@ def count_values(
|
|
|
810
850
|
time_select=time_select.value,
|
|
811
851
|
bbox=bbox or None,
|
|
812
852
|
bbox_select=bbox_select.value,
|
|
813
|
-
flavour=flavour
|
|
853
|
+
flavour=flavour,
|
|
814
854
|
host=host,
|
|
815
855
|
multiversion=multiversion,
|
|
816
856
|
fail_on_error=False,
|
|
@@ -945,3 +985,163 @@ def user_data_delete(
|
|
|
945
985
|
key, value = search_key.split("=", 1)
|
|
946
986
|
search_key_dict[key] = value
|
|
947
987
|
databrowser.userdata(action="delete", metadata=search_key_dict, host=host)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
flavour_app = typer.Typer(help="Manage custom flavours")
|
|
991
|
+
databrowser_app.add_typer(flavour_app, name="flavour")
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
@flavour_app.command("add", help="Add a new custom flavour.")
|
|
995
|
+
@exception_handler
|
|
996
|
+
def flavour_add(
|
|
997
|
+
name: str = typer.Argument(..., help="Name of the flavour to add"),
|
|
998
|
+
mapping: List[str] = typer.Option(
|
|
999
|
+
[],
|
|
1000
|
+
"--map",
|
|
1001
|
+
"-m",
|
|
1002
|
+
help="Key-value mappings in the format key=value",
|
|
1003
|
+
),
|
|
1004
|
+
global_: bool = typer.Option(
|
|
1005
|
+
False,
|
|
1006
|
+
"--global",
|
|
1007
|
+
help="Make the flavour available to all users (requires admin privileges)",
|
|
1008
|
+
),
|
|
1009
|
+
host: Optional[str] = typer.Option(
|
|
1010
|
+
None,
|
|
1011
|
+
"--host",
|
|
1012
|
+
help=(
|
|
1013
|
+
"Set the hostname of the databrowser. If not set (default), "
|
|
1014
|
+
"the hostname is read from a config file."
|
|
1015
|
+
),
|
|
1016
|
+
),
|
|
1017
|
+
token_file: Optional[Path] = typer.Option(
|
|
1018
|
+
None,
|
|
1019
|
+
"--token-file",
|
|
1020
|
+
"-tf",
|
|
1021
|
+
help=(
|
|
1022
|
+
"Instead of authenticating via code based authentication flow "
|
|
1023
|
+
"you can set the path to the json file that contains a "
|
|
1024
|
+
"`refresh token` containing a refresh_token key."
|
|
1025
|
+
),
|
|
1026
|
+
),
|
|
1027
|
+
verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
|
|
1028
|
+
) -> None:
|
|
1029
|
+
"""Add a new custom flavour to the databrowser.
|
|
1030
|
+
This command allows user to create custom DRS (Data Reference Syntax)
|
|
1031
|
+
flavours that define how search facets are mapped to different data
|
|
1032
|
+
standards."""
|
|
1033
|
+
logger.set_verbosity(verbose)
|
|
1034
|
+
logger.debug(f"Adding flavour '{name}' with mapping {mapping} and global={global_}")
|
|
1035
|
+
Auth(token_file).authenticate(host=host, _cli=True)
|
|
1036
|
+
mapping_dict = {}
|
|
1037
|
+
for map_item in mapping:
|
|
1038
|
+
if "=" not in map_item:
|
|
1039
|
+
logger.error(
|
|
1040
|
+
f"Invalid mapping format: {map_item}. Expected format: key=value."
|
|
1041
|
+
)
|
|
1042
|
+
raise typer.Exit(code=1)
|
|
1043
|
+
key, value = map_item.split("=", 1)
|
|
1044
|
+
mapping_dict[key] = value
|
|
1045
|
+
|
|
1046
|
+
if not mapping_dict:
|
|
1047
|
+
logger.error("At least one mapping must be provided using --map")
|
|
1048
|
+
raise typer.Exit(code=1)
|
|
1049
|
+
|
|
1050
|
+
databrowser.flavour(
|
|
1051
|
+
action="add",
|
|
1052
|
+
name=name,
|
|
1053
|
+
mapping=mapping_dict,
|
|
1054
|
+
is_global=global_,
|
|
1055
|
+
host=host,
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
@flavour_app.command("delete", help="Delete an existing custom flavour.")
|
|
1060
|
+
@exception_handler
|
|
1061
|
+
def flavour_delete(
|
|
1062
|
+
name: str = typer.Argument(..., help="Name of the flavour to delete"),
|
|
1063
|
+
global_: bool = typer.Option(
|
|
1064
|
+
False,
|
|
1065
|
+
"--global",
|
|
1066
|
+
help="Delete global flavour (requires admin privileges)",
|
|
1067
|
+
),
|
|
1068
|
+
host: Optional[str] = typer.Option(
|
|
1069
|
+
None,
|
|
1070
|
+
"--host",
|
|
1071
|
+
help=(
|
|
1072
|
+
"Set the hostname of the databrowser. If not set (default), "
|
|
1073
|
+
"the hostname is read from a config file."
|
|
1074
|
+
),
|
|
1075
|
+
),
|
|
1076
|
+
token_file: Optional[Path] = typer.Option(
|
|
1077
|
+
None,
|
|
1078
|
+
"--token-file",
|
|
1079
|
+
"-tf",
|
|
1080
|
+
help=(
|
|
1081
|
+
"Instead of authenticating via code based authentication flow "
|
|
1082
|
+
"you can set the path to the json file that contains a "
|
|
1083
|
+
"`refresh token` containing a refresh_token key."
|
|
1084
|
+
),
|
|
1085
|
+
),
|
|
1086
|
+
verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
|
|
1087
|
+
) -> None:
|
|
1088
|
+
"""Delete a custom flavour from the databrowser.
|
|
1089
|
+
This command removes a custom flavour that was previously created.
|
|
1090
|
+
You can only delete flavours that you own, unless you are an admin
|
|
1091
|
+
user who can delete global flavours."""
|
|
1092
|
+
logger.set_verbosity(verbose)
|
|
1093
|
+
logger.debug(f"Deleting flavour '{name}'")
|
|
1094
|
+
Auth(token_file).authenticate(host=host, _cli=True)
|
|
1095
|
+
|
|
1096
|
+
databrowser.flavour(
|
|
1097
|
+
action="delete",
|
|
1098
|
+
name=name,
|
|
1099
|
+
is_global=global_,
|
|
1100
|
+
host=host,
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
@flavour_app.command("list", help="List all available custom flavours.")
|
|
1105
|
+
@exception_handler
|
|
1106
|
+
def flavour_list(
|
|
1107
|
+
host: Optional[str] = typer.Option(
|
|
1108
|
+
None,
|
|
1109
|
+
"--host",
|
|
1110
|
+
help=(
|
|
1111
|
+
"Set the hostname of the databrowser. If not set (default), "
|
|
1112
|
+
"the hostname is read from a config file."
|
|
1113
|
+
),
|
|
1114
|
+
),
|
|
1115
|
+
parse_json: bool = typer.Option(
|
|
1116
|
+
False, "-j", "--json", help="Parse output in json format."
|
|
1117
|
+
),
|
|
1118
|
+
token_file: Optional[Path] = typer.Option(
|
|
1119
|
+
None,
|
|
1120
|
+
"--token-file",
|
|
1121
|
+
"-tf",
|
|
1122
|
+
help=(
|
|
1123
|
+
"Instead of authenticating via code based authentication flow "
|
|
1124
|
+
"you can set the path to the json file that contains a "
|
|
1125
|
+
"`refresh token` containing a refresh_token key."
|
|
1126
|
+
),
|
|
1127
|
+
),
|
|
1128
|
+
verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
|
|
1129
|
+
) -> None:
|
|
1130
|
+
"""List all custom flavours available to the current user.
|
|
1131
|
+
This command shows both personal flavours created by the user and
|
|
1132
|
+
global flavours available to all users."""
|
|
1133
|
+
logger.set_verbosity(verbose)
|
|
1134
|
+
logger.debug("Listing custom flavours")
|
|
1135
|
+
results = cast(Dict[str, Any], databrowser.flavour(action="list", host=host))
|
|
1136
|
+
flavours_json = json.dumps(results["flavours"], indent=2)
|
|
1137
|
+
if parse_json:
|
|
1138
|
+
print(flavours_json)
|
|
1139
|
+
else:
|
|
1140
|
+
pprint(f"[yellow]{results.get('Note', '')}[/yellow]")
|
|
1141
|
+
print("🎨 Available Data Reference Syntax (DRS) Flavours")
|
|
1142
|
+
print("=" * 55)
|
|
1143
|
+
for flavour in results["flavours"]:
|
|
1144
|
+
owner_icon = "🌐" if flavour['owner'] == "global" else "👤"
|
|
1145
|
+
print(f" {owner_icon} {flavour['flavour_name']} (by {flavour['owner']})")
|
|
1146
|
+
print(f" Mapping: {flavour['mapping']}")
|
|
1147
|
+
print()
|