freva-client 2404.0.1__py3-none-any.whl → 2408.0.0.dev2__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.
- freva_client/__init__.py +3 -2
- freva_client/auth.py +197 -0
- freva_client/cli/auth_cli.py +71 -0
- freva_client/cli/cli_app.py +22 -9
- freva_client/cli/cli_parser.py +152 -0
- freva_client/cli/cli_utils.py +13 -147
- freva_client/cli/databrowser_cli.py +177 -9
- freva_client/query.py +166 -23
- freva_client/utils/__init__.py +1 -1
- freva_client/utils/databrowser_utils.py +26 -9
- freva_client/utils/logger.py +2 -2
- {freva_client-2404.0.1.dist-info → freva_client-2408.0.0.dev2.dist-info}/METADATA +3 -1
- freva_client-2408.0.0.dev2.dist-info/RECORD +19 -0
- freva_client-2404.0.1.dist-info/RECORD +0 -16
- {freva_client-2404.0.1.data → freva_client-2408.0.0.dev2.data}/data/share/freva/freva.toml +0 -0
- {freva_client-2404.0.1.dist-info → freva_client-2408.0.0.dev2.dist-info}/WHEEL +0 -0
- {freva_client-2404.0.1.dist-info → freva_client-2408.0.0.dev2.dist-info}/entry_points.txt +0 -0
freva_client/__init__.py
CHANGED
|
@@ -14,7 +14,8 @@ library described in the documentation only support searching for data. If you
|
|
|
14
14
|
need to apply data analysis plugins, please visit the
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
from .auth import authenticate
|
|
17
18
|
from .query import databrowser
|
|
18
19
|
|
|
19
|
-
__version__ = "
|
|
20
|
-
__all__ = ["databrowser", "__version__"]
|
|
20
|
+
__version__ = "2408.0.0.dev2"
|
|
21
|
+
__all__ = ["authenticate", "databrowser", "__version__"]
|
freva_client/auth.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Module that handles the authentication at the rest service."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from getpass import getpass, getuser
|
|
5
|
+
from typing import Optional, TypedDict
|
|
6
|
+
|
|
7
|
+
from authlib.integrations.requests_client import OAuth2Session
|
|
8
|
+
|
|
9
|
+
from .utils import logger
|
|
10
|
+
from .utils.databrowser_utils import Config
|
|
11
|
+
|
|
12
|
+
Token = TypedDict(
|
|
13
|
+
"Token",
|
|
14
|
+
{
|
|
15
|
+
"access_token": str,
|
|
16
|
+
"token_type": str,
|
|
17
|
+
"expires": float,
|
|
18
|
+
"refresh_token": str,
|
|
19
|
+
"refresh_expires": float,
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Auth:
|
|
25
|
+
"""Helper class for authentication."""
|
|
26
|
+
|
|
27
|
+
_instance: Optional["Auth"] = None
|
|
28
|
+
_auth_token: Optional[Token] = None
|
|
29
|
+
|
|
30
|
+
def __new__(cls) -> "Auth":
|
|
31
|
+
if cls._instance is None:
|
|
32
|
+
cls._instance = super().__new__(cls)
|
|
33
|
+
return cls._instance
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._auth_cls = OAuth2Session()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def token_expiration_time(self) -> datetime:
|
|
40
|
+
"""Get the expiration time of an access token."""
|
|
41
|
+
if self._auth_token is None:
|
|
42
|
+
exp = 0.0
|
|
43
|
+
else:
|
|
44
|
+
exp = self._auth_token["expires"]
|
|
45
|
+
return datetime.fromtimestamp(exp)
|
|
46
|
+
|
|
47
|
+
def set_token(
|
|
48
|
+
self,
|
|
49
|
+
access_token: str,
|
|
50
|
+
refresh_token: Optional[str] = None,
|
|
51
|
+
expires_in: int = 10,
|
|
52
|
+
refresh_expires_in: int = 10,
|
|
53
|
+
expires: Optional[float] = None,
|
|
54
|
+
refresh_expires: Optional[float] = None,
|
|
55
|
+
token_type: str = "Bearer",
|
|
56
|
+
) -> Token:
|
|
57
|
+
"""Override the existing auth token."""
|
|
58
|
+
now = datetime.now().timestamp()
|
|
59
|
+
|
|
60
|
+
self._auth_token = Token(
|
|
61
|
+
access_token=access_token or "",
|
|
62
|
+
refresh_token=refresh_token or "",
|
|
63
|
+
token_type=token_type,
|
|
64
|
+
expires=expires or now + expires_in,
|
|
65
|
+
refresh_expires=refresh_expires or now + refresh_expires_in,
|
|
66
|
+
)
|
|
67
|
+
return self._auth_token
|
|
68
|
+
|
|
69
|
+
def _refresh(
|
|
70
|
+
self, url: str, refresh_token: str, username: Optional[str] = None
|
|
71
|
+
) -> Token:
|
|
72
|
+
"""Refresh the access_token with a refresh token."""
|
|
73
|
+
auth = self._auth_cls.refresh_token(f"{url}/token", refresh_token or " ")
|
|
74
|
+
try:
|
|
75
|
+
return self.set_token(
|
|
76
|
+
access_token=auth["access_token"],
|
|
77
|
+
token_type=auth["token_type"],
|
|
78
|
+
expires_in=auth["expires_in"],
|
|
79
|
+
refresh_token=auth["refresh_token"],
|
|
80
|
+
refresh_expires_in=auth["refresh_expires_in"],
|
|
81
|
+
)
|
|
82
|
+
except KeyError:
|
|
83
|
+
logger.warning("Failed to refresh token: %s", auth.get("detail", ""))
|
|
84
|
+
if username:
|
|
85
|
+
return self._login_with_password(url, username)
|
|
86
|
+
raise ValueError("Could not use refresh token") from None
|
|
87
|
+
|
|
88
|
+
def check_authentication(self, auth_url: Optional[str] = None) -> Token:
|
|
89
|
+
"""Check the status of the authentication.
|
|
90
|
+
|
|
91
|
+
Raises
|
|
92
|
+
------
|
|
93
|
+
ValueError: If user isn't or is no longer authenticated.
|
|
94
|
+
"""
|
|
95
|
+
if not self._auth_token:
|
|
96
|
+
raise ValueError("You must authenticate first.")
|
|
97
|
+
now = datetime.now().timestamp()
|
|
98
|
+
if now > self._auth_token["refresh_expires"]:
|
|
99
|
+
raise ValueError("Refresh token has expired.")
|
|
100
|
+
if now > self._auth_token["expires"] and auth_url:
|
|
101
|
+
self._refresh(auth_url, self._auth_token["refresh_token"])
|
|
102
|
+
return self._auth_token
|
|
103
|
+
|
|
104
|
+
def _login_with_password(self, auth_url: str, username: str) -> Token:
|
|
105
|
+
"""Create a new token."""
|
|
106
|
+
pw_msg = "Give password for server authentication: "
|
|
107
|
+
auth = self._auth_cls.fetch_token(
|
|
108
|
+
f"{auth_url}/token", username=username, password=getpass(pw_msg)
|
|
109
|
+
)
|
|
110
|
+
try:
|
|
111
|
+
return self.set_token(
|
|
112
|
+
access_token=auth["access_token"],
|
|
113
|
+
token_type=auth["token_type"],
|
|
114
|
+
expires_in=auth["expires_in"],
|
|
115
|
+
refresh_token=auth["refresh_token"],
|
|
116
|
+
refresh_expires_in=auth["refresh_expires_in"],
|
|
117
|
+
)
|
|
118
|
+
except KeyError:
|
|
119
|
+
logger.error("Failed to authenticate: %s", auth.get("detail", ""))
|
|
120
|
+
raise ValueError("Token creation failed") from None
|
|
121
|
+
|
|
122
|
+
def authenticate(
|
|
123
|
+
self,
|
|
124
|
+
host: Optional[str] = None,
|
|
125
|
+
refresh_token: Optional[str] = None,
|
|
126
|
+
username: Optional[str] = None,
|
|
127
|
+
force: bool = False,
|
|
128
|
+
) -> Token:
|
|
129
|
+
"""Authenticate the user to the host."""
|
|
130
|
+
cfg = Config(host)
|
|
131
|
+
if refresh_token:
|
|
132
|
+
try:
|
|
133
|
+
return self._refresh(cfg.auth_url, refresh_token)
|
|
134
|
+
except ValueError:
|
|
135
|
+
logger.warning(
|
|
136
|
+
(
|
|
137
|
+
"Could not use refresh token, falling back "
|
|
138
|
+
"to username/password"
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
username = username or getuser()
|
|
142
|
+
if self._auth_token is None or force:
|
|
143
|
+
return self._login_with_password(cfg.auth_url, username)
|
|
144
|
+
if self.token_expiration_time < datetime.now():
|
|
145
|
+
self._refresh(cfg.auth_url, self._auth_token["refresh_token"], username)
|
|
146
|
+
return self._auth_token
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def authenticate(
|
|
150
|
+
*,
|
|
151
|
+
refresh_token: Optional[str] = None,
|
|
152
|
+
username: Optional[str] = None,
|
|
153
|
+
host: Optional[str] = None,
|
|
154
|
+
force: bool = False,
|
|
155
|
+
) -> Token:
|
|
156
|
+
"""Authenticate to the host.
|
|
157
|
+
|
|
158
|
+
This method generates a new access token that should be used for restricted methods.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
refresh_token: str, optional
|
|
163
|
+
Instead of setting a password, you can set a refresh token to refresh
|
|
164
|
+
the access token. This is recommended for non-interactive environments.
|
|
165
|
+
username: str, optional
|
|
166
|
+
The username used for authentication. By default, the current
|
|
167
|
+
system username is used.
|
|
168
|
+
host: str, optional
|
|
169
|
+
The hostname of the REST server.
|
|
170
|
+
force: bool, default: False
|
|
171
|
+
Force token recreation, even if current token is still valid.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
Token: The authentication token.
|
|
176
|
+
|
|
177
|
+
Examples
|
|
178
|
+
--------
|
|
179
|
+
Interactive authentication:
|
|
180
|
+
|
|
181
|
+
.. code-block:: python
|
|
182
|
+
|
|
183
|
+
from freva_client import authenticate
|
|
184
|
+
token = authenticate(username="janedoe")
|
|
185
|
+
print(token)
|
|
186
|
+
|
|
187
|
+
Batch mode authentication with a refresh token:
|
|
188
|
+
|
|
189
|
+
.. code-block:: python
|
|
190
|
+
|
|
191
|
+
from freva_client import authenticate
|
|
192
|
+
token = authenticate(refresh_token="MYTOKEN")
|
|
193
|
+
"""
|
|
194
|
+
auth = Auth()
|
|
195
|
+
return auth.authenticate(
|
|
196
|
+
host=host, username=username, refresh_token=refresh_token, force=force
|
|
197
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Command line interface for authentication."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from getpass import getuser
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from freva_client import authenticate
|
|
9
|
+
from freva_client.utils import exception_handler, logger
|
|
10
|
+
|
|
11
|
+
from .cli_utils import version_callback
|
|
12
|
+
|
|
13
|
+
auth_app = typer.Typer(
|
|
14
|
+
name="auth",
|
|
15
|
+
help="Create OAuth2 access and refresh token.",
|
|
16
|
+
pretty_exceptions_short=False,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@exception_handler
|
|
21
|
+
def authenticate_cli(
|
|
22
|
+
host: Optional[str] = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--host",
|
|
25
|
+
help=(
|
|
26
|
+
"Set the hostname of the databrowser, if not set (default) "
|
|
27
|
+
"the hostname is read from a config file"
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
username: str = typer.Option(
|
|
31
|
+
getuser(),
|
|
32
|
+
"--username",
|
|
33
|
+
"-u",
|
|
34
|
+
help="The username used for authentication.",
|
|
35
|
+
),
|
|
36
|
+
refresh_token: Optional[str] = typer.Option(
|
|
37
|
+
None,
|
|
38
|
+
"--refresh-token",
|
|
39
|
+
"-r",
|
|
40
|
+
help=(
|
|
41
|
+
"Instead of using a password, you can use a refresh token. "
|
|
42
|
+
"refresh the access token. This is recommended for non-interactive"
|
|
43
|
+
" environments."
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
force: bool = typer.Option(
|
|
47
|
+
False,
|
|
48
|
+
"--force",
|
|
49
|
+
"-f",
|
|
50
|
+
help="Force token recreation, even if current token is still valid.",
|
|
51
|
+
),
|
|
52
|
+
verbose: int = typer.Option(
|
|
53
|
+
0, "-v", help="Increase verbosity", count=True
|
|
54
|
+
),
|
|
55
|
+
version: Optional[bool] = typer.Option(
|
|
56
|
+
False,
|
|
57
|
+
"-V",
|
|
58
|
+
"--version",
|
|
59
|
+
help="Show version an exit",
|
|
60
|
+
callback=version_callback,
|
|
61
|
+
),
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Create OAuth2 access and refresh token."""
|
|
64
|
+
logger.set_verbosity(verbose)
|
|
65
|
+
token_data = authenticate(
|
|
66
|
+
host=host,
|
|
67
|
+
username=username,
|
|
68
|
+
refresh_token=refresh_token,
|
|
69
|
+
force=force,
|
|
70
|
+
)
|
|
71
|
+
print(json.dumps(token_data, indent=3))
|
freva_client/cli/cli_app.py
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
"""Freva the Free Evaluation System command line interface."""
|
|
2
2
|
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
3
5
|
import typer
|
|
4
|
-
from freva_client import __version__
|
|
5
6
|
from freva_client.utils import logger
|
|
6
|
-
from rich import print as pprint
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
from .auth_cli import authenticate_cli
|
|
9
|
+
from .cli_utils import APP_NAME, version_callback
|
|
10
|
+
from .databrowser_cli import databrowser_app
|
|
9
11
|
|
|
10
12
|
app = typer.Typer(
|
|
11
13
|
name=APP_NAME,
|
|
12
14
|
help=__doc__,
|
|
13
|
-
add_completion=
|
|
15
|
+
add_completion=True,
|
|
14
16
|
callback=logger.set_cli,
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
@app.callback()
|
|
21
|
+
def main(
|
|
22
|
+
version: Optional[bool] = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--version",
|
|
25
|
+
"-V",
|
|
26
|
+
help="Show version and exit",
|
|
27
|
+
callback=version_callback,
|
|
28
|
+
is_eager=True,
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""The main cli app."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
app.add_typer(databrowser_app, name="databrowser")
|
|
35
|
+
app.command(name="auth", help=authenticate_cli.__doc__)(authenticate_cli)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Command line argument completion definitions."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
|
|
6
|
+
|
|
7
|
+
from freva_client import databrowser
|
|
8
|
+
from freva_client.cli.cli_app import app
|
|
9
|
+
|
|
10
|
+
from .cli_utils import parse_cli_args
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Completer:
|
|
14
|
+
"""Base class for command line argument completers."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
argv: List[str],
|
|
19
|
+
choices: Optional[Dict[str, Tuple[str, str]]] = None,
|
|
20
|
+
shell: str = "bash",
|
|
21
|
+
strip: bool = False,
|
|
22
|
+
flags_only: bool = False,
|
|
23
|
+
):
|
|
24
|
+
self.choices = choices or {}
|
|
25
|
+
self.strip = strip
|
|
26
|
+
self.argv = argv
|
|
27
|
+
self.flags_only = flags_only
|
|
28
|
+
if shell == "zsh":
|
|
29
|
+
self.get_print = self._print_zsh
|
|
30
|
+
elif shell == "fish":
|
|
31
|
+
self.get_print = self._print_fish
|
|
32
|
+
else:
|
|
33
|
+
self.get_print = self._print_default
|
|
34
|
+
|
|
35
|
+
def _print_zsh(self, choices: Dict[str, Tuple[str, str]]) -> List[str]:
|
|
36
|
+
out = []
|
|
37
|
+
for key, _help in choices.items():
|
|
38
|
+
if key.startswith("-"):
|
|
39
|
+
out.append(f"{key}[{_help}]")
|
|
40
|
+
else:
|
|
41
|
+
out.append(f"{key}: {_help}")
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
def _print_fish(self, choices: Dict[str, Tuple[str, str]]) -> List[str]:
|
|
45
|
+
out = []
|
|
46
|
+
for key, _help in choices.items():
|
|
47
|
+
out.append(f"{key}: {_help}")
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
def _print_default(self, choices: Dict[str, Tuple[str, str]]) -> List[str]:
|
|
51
|
+
out = []
|
|
52
|
+
for key, _help in choices.items():
|
|
53
|
+
if not key.startswith("-"):
|
|
54
|
+
out.append(f"{key}: {_help}")
|
|
55
|
+
else:
|
|
56
|
+
out.append(key)
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
def _get_choices(self) -> Dict[str, Tuple[str, str]]:
|
|
60
|
+
"""Get the choices for databrowser command."""
|
|
61
|
+
|
|
62
|
+
facet_args = []
|
|
63
|
+
for arg in self.argv:
|
|
64
|
+
if len(arg.split("=")) == 2:
|
|
65
|
+
facet_args.append(arg)
|
|
66
|
+
search_keys = parse_cli_args(facet_args)
|
|
67
|
+
search = databrowser.metadata_search(
|
|
68
|
+
flavour="freva",
|
|
69
|
+
time=None,
|
|
70
|
+
host=None,
|
|
71
|
+
time_select="flexible",
|
|
72
|
+
multiversion=False,
|
|
73
|
+
extended_search=True,
|
|
74
|
+
fail_on_error=False,
|
|
75
|
+
**search_keys,
|
|
76
|
+
)
|
|
77
|
+
choices = {}
|
|
78
|
+
for att, values in search.items():
|
|
79
|
+
if att not in search_keys:
|
|
80
|
+
keys = ",".join([v for n, v in enumerate(values)])
|
|
81
|
+
choices[att] = (keys, "")
|
|
82
|
+
return choices
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def command_choices(self) -> Dict[str, Tuple[str, str]]:
|
|
86
|
+
"""Get the command line arguments for all sub commands."""
|
|
87
|
+
|
|
88
|
+
if self.flags_only:
|
|
89
|
+
return self.choices
|
|
90
|
+
choices = self._get_choices()
|
|
91
|
+
return {**self.choices, **choices}
|
|
92
|
+
|
|
93
|
+
def formatted_print(self) -> None:
|
|
94
|
+
"""Print all choices to be processed by the shell completion function."""
|
|
95
|
+
|
|
96
|
+
out = self.get_print(self.command_choices)
|
|
97
|
+
for line in out:
|
|
98
|
+
if line.startswith("-") and self.strip and not self.flags_only:
|
|
99
|
+
continue
|
|
100
|
+
if not line.startswith("-") and self.flags_only:
|
|
101
|
+
continue
|
|
102
|
+
print(line)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def parse_choices(cls, argv: list[str]) -> "Completer":
|
|
106
|
+
"""Create the completion choices from given cmd arguments."""
|
|
107
|
+
parser = argparse.ArgumentParser(
|
|
108
|
+
description="Get choices for command line arguments"
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--strip",
|
|
112
|
+
help="Do not print options starting with -",
|
|
113
|
+
default=False,
|
|
114
|
+
action="store_true",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--shell",
|
|
118
|
+
help="Set the target shell type.",
|
|
119
|
+
default=False,
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--flags-only",
|
|
123
|
+
help="Only print options starting with -",
|
|
124
|
+
default=False,
|
|
125
|
+
action="store_true",
|
|
126
|
+
)
|
|
127
|
+
cli_app, args = parser.parse_known_args(argv)
|
|
128
|
+
main_choices = {c.name: (c, c.help) for c in app.registered_commands}
|
|
129
|
+
choices = {}
|
|
130
|
+
if args and args[0] == "freva-databrowser":
|
|
131
|
+
_ = args.pop(0)
|
|
132
|
+
if args[0] in main_choices:
|
|
133
|
+
signature = inspect.signature(
|
|
134
|
+
cast(Callable[..., Any], main_choices[args[0]][0].callback)
|
|
135
|
+
)
|
|
136
|
+
for param, value in signature.parameters.items():
|
|
137
|
+
if param in args or param == "search_keys":
|
|
138
|
+
continue
|
|
139
|
+
if hasattr(value.annotation, "__metadata__"):
|
|
140
|
+
option = value.annotation.__metadata__
|
|
141
|
+
else:
|
|
142
|
+
option = value.default
|
|
143
|
+
choices[option.param_decls[-1]] = option.help
|
|
144
|
+
else:
|
|
145
|
+
choices = {k: v[1] for k, v in main_choices.items()}
|
|
146
|
+
return cls(
|
|
147
|
+
args,
|
|
148
|
+
choices,
|
|
149
|
+
shell=cli_app.shell,
|
|
150
|
+
strip=cli_app.strip,
|
|
151
|
+
flags_only=cli_app.flags_only,
|
|
152
|
+
)
|
freva_client/cli/cli_utils.py
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
"""Utilities for the command line interface."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import inspect
|
|
5
|
-
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
|
|
3
|
+
from typing import Dict, List
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
from freva_client
|
|
5
|
+
import typer
|
|
6
|
+
from freva_client import __version__
|
|
9
7
|
from freva_client.utils import logger
|
|
8
|
+
from rich import print as pprint
|
|
9
|
+
|
|
10
|
+
APP_NAME: str = "freva-client"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def version_callback(version: bool) -> None:
|
|
14
|
+
"""Print the version and exit."""
|
|
15
|
+
if version:
|
|
16
|
+
pprint(f"{APP_NAME}: {__version__}")
|
|
17
|
+
raise typer.Exit()
|
|
10
18
|
|
|
11
19
|
|
|
12
20
|
def parse_cli_args(cli_args: List[str]) -> Dict[str, List[str]]:
|
|
@@ -21,145 +29,3 @@ def parse_cli_args(cli_args: List[str]) -> Dict[str, List[str]]:
|
|
|
21
29
|
kwargs[key].append(value)
|
|
22
30
|
logger.debug(kwargs)
|
|
23
31
|
return kwargs
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class Completer:
|
|
27
|
-
"""Base class for command line argument completers."""
|
|
28
|
-
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
31
|
-
argv: List[str],
|
|
32
|
-
choices: Optional[Dict[str, Tuple[str, str]]] = None,
|
|
33
|
-
shell: str = "bash",
|
|
34
|
-
strip: bool = False,
|
|
35
|
-
flags_only: bool = False,
|
|
36
|
-
):
|
|
37
|
-
self.choices = choices or {}
|
|
38
|
-
self.strip = strip
|
|
39
|
-
self.argv = argv
|
|
40
|
-
self.flags_only = flags_only
|
|
41
|
-
if shell == "zsh":
|
|
42
|
-
self.get_print = self._print_zsh
|
|
43
|
-
elif shell == "fish":
|
|
44
|
-
self.get_print = self._print_fish
|
|
45
|
-
else:
|
|
46
|
-
self.get_print = self._print_default
|
|
47
|
-
|
|
48
|
-
def _print_zsh(self, choices: Dict[str, Tuple[str, str]]) -> List[str]:
|
|
49
|
-
out = []
|
|
50
|
-
for key, _help in choices.items():
|
|
51
|
-
if key.startswith("-"):
|
|
52
|
-
out.append(f"{key}[{_help}]")
|
|
53
|
-
else:
|
|
54
|
-
out.append(f"{key}: {_help}")
|
|
55
|
-
return out
|
|
56
|
-
|
|
57
|
-
def _print_fish(self, choices: Dict[str, Tuple[str, str]]) -> List[str]:
|
|
58
|
-
out = []
|
|
59
|
-
for key, _help in choices.items():
|
|
60
|
-
out.append(f"{key}: {_help}")
|
|
61
|
-
return out
|
|
62
|
-
|
|
63
|
-
def _print_default(self, choices: Dict[str, Tuple[str, str]]) -> List[str]:
|
|
64
|
-
out = []
|
|
65
|
-
for key, _help in choices.items():
|
|
66
|
-
if not key.startswith("-"):
|
|
67
|
-
out.append(f"{key}: {_help}")
|
|
68
|
-
else:
|
|
69
|
-
out.append(key)
|
|
70
|
-
return out
|
|
71
|
-
|
|
72
|
-
def _get_choices(self) -> Dict[str, Tuple[str, str]]:
|
|
73
|
-
"""Get the choices for databrowser command."""
|
|
74
|
-
|
|
75
|
-
facet_args = []
|
|
76
|
-
for arg in self.argv:
|
|
77
|
-
if len(arg.split("=")) == 2:
|
|
78
|
-
facet_args.append(arg)
|
|
79
|
-
search_keys = parse_cli_args(facet_args)
|
|
80
|
-
search = databrowser.metadata_search(
|
|
81
|
-
flavour="freva",
|
|
82
|
-
time=None,
|
|
83
|
-
host=None,
|
|
84
|
-
time_select="flexible",
|
|
85
|
-
multiversion=False,
|
|
86
|
-
extended_search=True,
|
|
87
|
-
fail_on_error=False,
|
|
88
|
-
**search_keys,
|
|
89
|
-
)
|
|
90
|
-
choices = {}
|
|
91
|
-
for att, values in search.items():
|
|
92
|
-
if att not in search_keys:
|
|
93
|
-
keys = ",".join([v for n, v in enumerate(values)])
|
|
94
|
-
choices[att] = (keys, "")
|
|
95
|
-
return choices
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def command_choices(self) -> Dict[str, Tuple[str, str]]:
|
|
99
|
-
"""Get the command line arguments for all sub commands."""
|
|
100
|
-
|
|
101
|
-
if self.flags_only:
|
|
102
|
-
return self.choices
|
|
103
|
-
choices = self._get_choices()
|
|
104
|
-
return {**self.choices, **choices}
|
|
105
|
-
|
|
106
|
-
def formated_print(self) -> None:
|
|
107
|
-
"""Print all choices to be processed by the shell completion function."""
|
|
108
|
-
|
|
109
|
-
out = self.get_print(self.command_choices)
|
|
110
|
-
for line in out:
|
|
111
|
-
if line.startswith("-") and self.strip and not self.flags_only:
|
|
112
|
-
continue
|
|
113
|
-
if not line.startswith("-") and self.flags_only:
|
|
114
|
-
continue
|
|
115
|
-
print(line)
|
|
116
|
-
|
|
117
|
-
@classmethod
|
|
118
|
-
def parse_choices(cls, argv: list[str]) -> "Completer":
|
|
119
|
-
"""Create the completion choices from given cmd arguments."""
|
|
120
|
-
parser = argparse.ArgumentParser(
|
|
121
|
-
description="Get choices for command line arguments"
|
|
122
|
-
)
|
|
123
|
-
parser.add_argument(
|
|
124
|
-
"--strip",
|
|
125
|
-
help="Do not print options starting with -",
|
|
126
|
-
default=False,
|
|
127
|
-
action="store_true",
|
|
128
|
-
)
|
|
129
|
-
parser.add_argument(
|
|
130
|
-
"--shell",
|
|
131
|
-
help="Set the target shell type.",
|
|
132
|
-
default=False,
|
|
133
|
-
)
|
|
134
|
-
parser.add_argument(
|
|
135
|
-
"--flags-only",
|
|
136
|
-
help="Only print options starting with -",
|
|
137
|
-
default=False,
|
|
138
|
-
action="store_true",
|
|
139
|
-
)
|
|
140
|
-
cli_app, args = parser.parse_known_args(argv)
|
|
141
|
-
main_choices = {c.name: (c, c.help) for c in app.registered_commands}
|
|
142
|
-
choices = {}
|
|
143
|
-
if args and args[0] == "freva-databrowser":
|
|
144
|
-
_ = args.pop(0)
|
|
145
|
-
if args[0] in main_choices:
|
|
146
|
-
signature = inspect.signature(
|
|
147
|
-
cast(Callable[..., Any], main_choices[args[0]][0].callback)
|
|
148
|
-
)
|
|
149
|
-
for param, value in signature.parameters.items():
|
|
150
|
-
if param in args or param == "search_keys":
|
|
151
|
-
continue
|
|
152
|
-
if hasattr(value.annotation, "__metadata__"):
|
|
153
|
-
option = value.annotation.__metadata__
|
|
154
|
-
else:
|
|
155
|
-
option = value.default
|
|
156
|
-
choices[option.param_decls[-1]] = option.help
|
|
157
|
-
else:
|
|
158
|
-
choices = {k: v[1] for k, v in main_choices.items()}
|
|
159
|
-
return cls(
|
|
160
|
-
args,
|
|
161
|
-
choices,
|
|
162
|
-
shell=cli_app.shell,
|
|
163
|
-
strip=cli_app.strip,
|
|
164
|
-
flags_only=cli_app.flags_only,
|
|
165
|
-
)
|