freva-client 2404.0.1__tar.gz → 2408.0.0.dev1__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-2404.0.1 → freva_client-2408.0.0.dev1}/PKG-INFO +3 -1
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/pyproject.toml +2 -0
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/__init__.py +3 -2
- freva_client-2408.0.0.dev1/src/freva_client/auth.py +197 -0
- freva_client-2408.0.0.dev1/src/freva_client/cli/auth_cli.py +71 -0
- freva_client-2408.0.0.dev1/src/freva_client/cli/cli_app.py +35 -0
- freva_client-2404.0.1/src/freva_client/cli/cli_utils.py → freva_client-2408.0.0.dev1/src/freva_client/cli/cli_parser.py +3 -16
- freva_client-2408.0.0.dev1/src/freva_client/cli/cli_utils.py +31 -0
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/cli/databrowser_cli.py +177 -9
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/query.py +166 -23
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/utils/__init__.py +1 -1
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/utils/databrowser_utils.py +26 -9
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/utils/logger.py +2 -2
- freva_client-2404.0.1/src/freva_client/cli/cli_app.py +0 -22
- freva_client-2404.0.1/src/tests/__init__.py +0 -0
- freva_client-2404.0.1/src/tests/conftest.py +0 -90
- freva_client-2404.0.1/src/tests/test_cli.py +0 -136
- freva_client-2404.0.1/src/tests/test_databrowser.py +0 -116
- freva_client-2404.0.1/src/tests/test_url.py +0 -63
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/MANIFEST.in +0 -0
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/README.md +0 -0
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/assets/share/freva/freva.toml +0 -0
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/__main__.py +0 -0
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/cli/__init__.py +0 -0
- {freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: freva-client
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2408.0.0.dev1
|
|
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
|
|
@@ -18,7 +18,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Requires-Dist: appdirs
|
|
20
20
|
Requires-Dist: pyyaml
|
|
21
|
+
Requires-Dist: authlib
|
|
21
22
|
Requires-Dist: requests
|
|
23
|
+
Requires-Dist: intake_esm
|
|
22
24
|
Requires-Dist: rich
|
|
23
25
|
Requires-Dist: tomli
|
|
24
26
|
Requires-Dist: typer
|
|
@@ -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.dev1"
|
|
21
|
+
__all__ = ["authenticate", "databrowser", "__version__"]
|
|
@@ -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))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Freva the Free Evaluation System command line interface."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from freva_client.utils import logger
|
|
7
|
+
|
|
8
|
+
from .auth_cli import authenticate_cli
|
|
9
|
+
from .cli_utils import APP_NAME, version_callback
|
|
10
|
+
from .databrowser_cli import databrowser_app
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name=APP_NAME,
|
|
14
|
+
help=__doc__,
|
|
15
|
+
add_completion=True,
|
|
16
|
+
callback=logger.set_cli,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
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)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Command line argument completion definitions."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import inspect
|
|
@@ -6,21 +6,8 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, cast
|
|
|
6
6
|
|
|
7
7
|
from freva_client import databrowser
|
|
8
8
|
from freva_client.cli.cli_app import app
|
|
9
|
-
from freva_client.utils import logger
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
def parse_cli_args(cli_args: List[str]) -> Dict[str, List[str]]:
|
|
13
|
-
"""Convert the cli arguments to a dictionary."""
|
|
14
|
-
logger.debug("parsing command line arguments.")
|
|
15
|
-
kwargs = {}
|
|
16
|
-
for entry in cli_args:
|
|
17
|
-
key, _, value = entry.partition("=")
|
|
18
|
-
if value and key not in kwargs:
|
|
19
|
-
kwargs[key] = [value]
|
|
20
|
-
elif value:
|
|
21
|
-
kwargs[key].append(value)
|
|
22
|
-
logger.debug(kwargs)
|
|
23
|
-
return kwargs
|
|
10
|
+
from .cli_utils import parse_cli_args
|
|
24
11
|
|
|
25
12
|
|
|
26
13
|
class Completer:
|
|
@@ -103,7 +90,7 @@ class Completer:
|
|
|
103
90
|
choices = self._get_choices()
|
|
104
91
|
return {**self.choices, **choices}
|
|
105
92
|
|
|
106
|
-
def
|
|
93
|
+
def formatted_print(self) -> None:
|
|
107
94
|
"""Print all choices to be processed by the shell completion function."""
|
|
108
95
|
|
|
109
96
|
out = self.get_print(self.command_choices)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Utilities for the command line interface."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from freva_client import __version__
|
|
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()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_cli_args(cli_args: List[str]) -> Dict[str, List[str]]:
|
|
21
|
+
"""Convert the cli arguments to a dictionary."""
|
|
22
|
+
logger.debug("parsing command line arguments.")
|
|
23
|
+
kwargs = {}
|
|
24
|
+
for entry in cli_args:
|
|
25
|
+
key, _, value = entry.partition("=")
|
|
26
|
+
if value and key not in kwargs:
|
|
27
|
+
kwargs[key] = [value]
|
|
28
|
+
elif value:
|
|
29
|
+
kwargs[key].append(value)
|
|
30
|
+
logger.debug(kwargs)
|
|
31
|
+
return kwargs
|
{freva_client-2404.0.1 → freva_client-2408.0.0.dev1}/src/freva_client/cli/databrowser_cli.py
RENAMED
|
@@ -5,14 +5,26 @@ Search quickly and intuitively for many different climate datasets.
|
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from tempfile import NamedTemporaryFile
|
|
8
10
|
from typing import Dict, List, Literal, Optional, Union, cast
|
|
9
11
|
|
|
10
12
|
import typer
|
|
11
13
|
from freva_client import databrowser
|
|
14
|
+
from freva_client.auth import Auth
|
|
12
15
|
from freva_client.utils import exception_handler, logger
|
|
13
16
|
|
|
14
|
-
from .
|
|
15
|
-
|
|
17
|
+
from .cli_utils import parse_cli_args, version_callback
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _auth(url: str, token: Optional[str]) -> None:
|
|
21
|
+
if token:
|
|
22
|
+
auth = Auth()
|
|
23
|
+
auth.set_token(
|
|
24
|
+
access_token=token, expires=auth.token_expiration_time.timestamp()
|
|
25
|
+
)
|
|
26
|
+
else:
|
|
27
|
+
raise ValueError("`--access-token` is required for authentication.")
|
|
16
28
|
|
|
17
29
|
|
|
18
30
|
class UniqKeys(str, Enum):
|
|
@@ -55,7 +67,12 @@ class TimeSelect(str, Enum):
|
|
|
55
67
|
)
|
|
56
68
|
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
databrowser_app = typer.Typer(
|
|
71
|
+
help="Data search related commands", callback=logger.set_cli
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@databrowser_app.command(
|
|
59
76
|
name="data-overview",
|
|
60
77
|
help="Get an overview over what is available in the databrowser.",
|
|
61
78
|
)
|
|
@@ -74,7 +91,7 @@ def overview(
|
|
|
74
91
|
print(databrowser.overview(host=host))
|
|
75
92
|
|
|
76
93
|
|
|
77
|
-
@
|
|
94
|
+
@databrowser_app.command(
|
|
78
95
|
name="metadata-search", help="Search databrowser for metadata (facets)."
|
|
79
96
|
)
|
|
80
97
|
@exception_handler
|
|
@@ -190,7 +207,9 @@ def metadata_search(
|
|
|
190
207
|
print(f"{key}: {', '.join(values)}")
|
|
191
208
|
|
|
192
209
|
|
|
193
|
-
@
|
|
210
|
+
@databrowser_app.command(
|
|
211
|
+
name="data-search", help="Search the databrowser for datasets."
|
|
212
|
+
)
|
|
194
213
|
@exception_handler
|
|
195
214
|
def data_search(
|
|
196
215
|
search_keys: Optional[List[str]] = typer.Argument(
|
|
@@ -234,6 +253,17 @@ def data_search(
|
|
|
234
253
|
"--time-select",
|
|
235
254
|
help=TimeSelect.get_help(),
|
|
236
255
|
),
|
|
256
|
+
zarr: bool = typer.Option(
|
|
257
|
+
False, "--zarr", help="Create zarr stream files."
|
|
258
|
+
),
|
|
259
|
+
access_token: Optional[str] = typer.Option(
|
|
260
|
+
None,
|
|
261
|
+
"--access-token",
|
|
262
|
+
help=(
|
|
263
|
+
"Use this access token for authentication"
|
|
264
|
+
" when creating a zarr stream files."
|
|
265
|
+
),
|
|
266
|
+
),
|
|
237
267
|
time: Optional[str] = typer.Option(
|
|
238
268
|
None,
|
|
239
269
|
"-t",
|
|
@@ -264,14 +294,14 @@ def data_search(
|
|
|
264
294
|
),
|
|
265
295
|
multiversion: bool = typer.Option(
|
|
266
296
|
False,
|
|
267
|
-
"--
|
|
297
|
+
"--multi-version",
|
|
268
298
|
help="Select all versions and not just the latest version (default).",
|
|
269
299
|
),
|
|
270
300
|
version: Optional[bool] = typer.Option(
|
|
271
301
|
False,
|
|
272
302
|
"-V",
|
|
273
303
|
"--version",
|
|
274
|
-
help="Show
|
|
304
|
+
help="Show version an exit",
|
|
275
305
|
callback=version_callback,
|
|
276
306
|
),
|
|
277
307
|
) -> None:
|
|
@@ -295,8 +325,11 @@ def data_search(
|
|
|
295
325
|
host=host,
|
|
296
326
|
fail_on_error=False,
|
|
297
327
|
multiversion=multiversion,
|
|
328
|
+
stream_zarr=zarr,
|
|
298
329
|
**(parse_cli_args(search_keys or [])),
|
|
299
330
|
)
|
|
331
|
+
if zarr:
|
|
332
|
+
_auth(result._cfg.auth_url, access_token)
|
|
300
333
|
if parse_json:
|
|
301
334
|
print(json.dumps(sorted(result)))
|
|
302
335
|
else:
|
|
@@ -304,7 +337,141 @@ def data_search(
|
|
|
304
337
|
print(res)
|
|
305
338
|
|
|
306
339
|
|
|
307
|
-
@
|
|
340
|
+
@databrowser_app.command(
|
|
341
|
+
name="intake-catalogue", help="Create an intake catalogue from the search."
|
|
342
|
+
)
|
|
343
|
+
@exception_handler
|
|
344
|
+
def intake_catalogue(
|
|
345
|
+
search_keys: Optional[List[str]] = typer.Argument(
|
|
346
|
+
default=None,
|
|
347
|
+
help="Refine your data search with this `key=value` pair search "
|
|
348
|
+
"parameters. The parameters could be, depending on the DRS standard, "
|
|
349
|
+
"flavour product, project model etc.",
|
|
350
|
+
),
|
|
351
|
+
facets: Optional[List[str]] = typer.Option(
|
|
352
|
+
None,
|
|
353
|
+
"--facet",
|
|
354
|
+
help=(
|
|
355
|
+
"If you are not sure about the correct search key's you can use"
|
|
356
|
+
" the ``--facet`` flag to search of any matching entries. For "
|
|
357
|
+
"example --facet 'era5' would allow you to search for any entries"
|
|
358
|
+
" containing era5, regardless of project, product etc."
|
|
359
|
+
),
|
|
360
|
+
),
|
|
361
|
+
uniq_key: UniqKeys = typer.Option(
|
|
362
|
+
"file",
|
|
363
|
+
"--uniq-key",
|
|
364
|
+
"-u",
|
|
365
|
+
help=(
|
|
366
|
+
"The type of search result, which can be either “file” "
|
|
367
|
+
"or “uri”. This parameter determines whether the search will be "
|
|
368
|
+
"based on file paths or Uniform Resource Identifiers"
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
flavour: Flavours = typer.Option(
|
|
372
|
+
"freva",
|
|
373
|
+
"--flavour",
|
|
374
|
+
"-f",
|
|
375
|
+
help=(
|
|
376
|
+
"The Data Reference Syntax (DRS) standard specifying the type "
|
|
377
|
+
"of climate datasets to query."
|
|
378
|
+
),
|
|
379
|
+
),
|
|
380
|
+
time_select: TimeSelect = typer.Option(
|
|
381
|
+
"flexible",
|
|
382
|
+
"-ts",
|
|
383
|
+
"--time-select",
|
|
384
|
+
help=TimeSelect.get_help(),
|
|
385
|
+
),
|
|
386
|
+
time: Optional[str] = typer.Option(
|
|
387
|
+
None,
|
|
388
|
+
"-t",
|
|
389
|
+
"--time",
|
|
390
|
+
help=(
|
|
391
|
+
"Special search facet to refine/subset search results by time. "
|
|
392
|
+
"This can be a string representation of a time range or a single "
|
|
393
|
+
"time step. The time steps have to follow ISO-8601. Valid strings "
|
|
394
|
+
"are ``%Y-%m-%dT%H:%M`` to ``%Y-%m-%dT%H:%M`` for time ranges and "
|
|
395
|
+
"``%Y-%m-%dT%H:%M``. **Note**: You don't have to give the full "
|
|
396
|
+
"string format to subset time steps ``%Y``, ``%Y-%m`` etc are also"
|
|
397
|
+
" valid."
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
zarr: bool = typer.Option(
|
|
401
|
+
False, "--zarr", help="Create zarr stream files, as catalogue targets."
|
|
402
|
+
),
|
|
403
|
+
access_token: Optional[str] = typer.Option(
|
|
404
|
+
None,
|
|
405
|
+
"--access-token",
|
|
406
|
+
help=(
|
|
407
|
+
"Use this access token for authentication"
|
|
408
|
+
" when creating a zarr based intake catalogue."
|
|
409
|
+
),
|
|
410
|
+
),
|
|
411
|
+
filename: Optional[Path] = typer.Option(
|
|
412
|
+
None,
|
|
413
|
+
"-f",
|
|
414
|
+
"--filename",
|
|
415
|
+
help=(
|
|
416
|
+
"Path to the file where the catalogue, should be written to. "
|
|
417
|
+
"if None given (default) the catalogue is parsed to stdout."
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
host: Optional[str] = typer.Option(
|
|
421
|
+
None,
|
|
422
|
+
"--host",
|
|
423
|
+
help=(
|
|
424
|
+
"Set the hostname of the databrowser, if not set (default) "
|
|
425
|
+
"the hostname is read from a config file"
|
|
426
|
+
),
|
|
427
|
+
),
|
|
428
|
+
verbose: int = typer.Option(
|
|
429
|
+
0, "-v", help="Increase verbosity", count=True
|
|
430
|
+
),
|
|
431
|
+
multiversion: bool = typer.Option(
|
|
432
|
+
False,
|
|
433
|
+
"--multi-version",
|
|
434
|
+
help="Select all versions and not just the latest version (default).",
|
|
435
|
+
),
|
|
436
|
+
version: Optional[bool] = typer.Option(
|
|
437
|
+
False,
|
|
438
|
+
"-V",
|
|
439
|
+
"--version",
|
|
440
|
+
help="Show version an exit",
|
|
441
|
+
callback=version_callback,
|
|
442
|
+
),
|
|
443
|
+
) -> None:
|
|
444
|
+
"""Create an intake catalogue for climate datasets based on the specified "
|
|
445
|
+
"Data Reference Syntax (DRS) standard (flavour) and the type of search "
|
|
446
|
+
result (uniq_key), which can be either “file” or “uri”."""
|
|
447
|
+
logger.set_verbosity(verbose)
|
|
448
|
+
logger.debug("Search the databrowser")
|
|
449
|
+
result = databrowser(
|
|
450
|
+
*(facets or []),
|
|
451
|
+
time=time or "",
|
|
452
|
+
time_select=cast(Literal["file", "flexible", "strict"], time_select),
|
|
453
|
+
flavour=cast(
|
|
454
|
+
Literal["freva", "cmip6", "cmip5", "cordex", "nextgems"],
|
|
455
|
+
flavour.value,
|
|
456
|
+
),
|
|
457
|
+
uniq_key=cast(Literal["uri", "file"], uniq_key.value),
|
|
458
|
+
host=host,
|
|
459
|
+
fail_on_error=False,
|
|
460
|
+
multiversion=multiversion,
|
|
461
|
+
stream_zarr=zarr,
|
|
462
|
+
**(parse_cli_args(search_keys or [])),
|
|
463
|
+
)
|
|
464
|
+
if zarr:
|
|
465
|
+
_auth(result._cfg.auth_url, access_token)
|
|
466
|
+
with NamedTemporaryFile(suffix=".json") as temp_f:
|
|
467
|
+
result._create_intake_catalogue_file(str(filename or temp_f.name))
|
|
468
|
+
if not filename:
|
|
469
|
+
print(Path(temp_f.name).read_text())
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@databrowser_app.command(
|
|
473
|
+
name="data-count", help="Count the databrowser search results"
|
|
474
|
+
)
|
|
308
475
|
@exception_handler
|
|
309
476
|
def count_values(
|
|
310
477
|
search_keys: Optional[List[str]] = typer.Argument(
|
|
@@ -387,7 +554,7 @@ def count_values(
|
|
|
387
554
|
False,
|
|
388
555
|
"-V",
|
|
389
556
|
"--version",
|
|
390
|
-
help="Show
|
|
557
|
+
help="Show version an exit",
|
|
391
558
|
callback=version_callback,
|
|
392
559
|
),
|
|
393
560
|
) -> None:
|
|
@@ -438,6 +605,7 @@ def count_values(
|
|
|
438
605
|
multiversion=multiversion,
|
|
439
606
|
fail_on_error=False,
|
|
440
607
|
uniq_key="file",
|
|
608
|
+
stream_zarr=False,
|
|
441
609
|
**search_kws,
|
|
442
610
|
)
|
|
443
611
|
)
|