adalab-cli 1.4.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.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.1
2
+ Name: adalab-cli
3
+ Version: 1.4.0
4
+ Summary: The CLI app to interact with AdaLab.
5
+ Home-page: https://adamatics.com
6
+ Author: Adamatics ApS
7
+ Author-email: info@adamatics.com
8
+ Requires-Python: >=3.10.0,<4.0.0
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: adalib (>=2.6.0)
19
+ Requires-Dist: adalib-auth (>=1.1.0)
20
+ Requires-Dist: loguru (>=0.7.2)
21
+ Requires-Dist: prompt-toolkit (>=3.0.47)
22
+ Requires-Dist: sh (>=2.0.6)
23
+ Requires-Dist: tabulate (>=0.9.0)
24
+ Requires-Dist: typer (>=0.9.0)
25
+ Project-URL: Repository, https://gitlab.com/adamatics/python/adalab-cli
26
+ Description-Content-Type: text/markdown
27
+
28
+ # adalab-cli
29
+
30
+ This repository contains the source code of `adalab-cli`, the CLI app to interact with the AdaLab platform.
31
+
32
+ ## Installation
33
+
34
+ `adalab-cli` can be installed from PyPI or a `devpi` index:
35
+
36
+ ```sh
37
+ # PyPI
38
+ pip install adalab-cli
39
+ # devpi
40
+ pip install --extra-index-url <devpi_index_url> adalab-cli
41
+ ```
42
+
43
+ In order to add it to the dependencies of a Python project using `poetry` use:
44
+
45
+ ```sh
46
+ poetry source add --priority=supplemental <repo_name> <devpi_index_url>
47
+ poetry source add --priority=primary PyPI
48
+ poetry add --source <repo_name> adalab-cli
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ See the corresponding documentation pages.
54
+
55
+ ## Contributing
56
+
57
+ See the [contributor's guide](CONTRIBUTING.md).
58
+
@@ -0,0 +1,30 @@
1
+ # adalab-cli
2
+
3
+ This repository contains the source code of `adalab-cli`, the CLI app to interact with the AdaLab platform.
4
+
5
+ ## Installation
6
+
7
+ `adalab-cli` can be installed from PyPI or a `devpi` index:
8
+
9
+ ```sh
10
+ # PyPI
11
+ pip install adalab-cli
12
+ # devpi
13
+ pip install --extra-index-url <devpi_index_url> adalab-cli
14
+ ```
15
+
16
+ In order to add it to the dependencies of a Python project using `poetry` use:
17
+
18
+ ```sh
19
+ poetry source add --priority=supplemental <repo_name> <devpi_index_url>
20
+ poetry source add --priority=primary PyPI
21
+ poetry add --source <repo_name> adalab-cli
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ See the corresponding documentation pages.
27
+
28
+ ## Contributing
29
+
30
+ See the [contributor's guide](CONTRIBUTING.md).
@@ -0,0 +1,11 @@
1
+ """The adalab-cli base module exposes the CLI application.
2
+ """
3
+
4
+ import importlib.metadata
5
+
6
+ _DISTRIBUTION_METADATA = importlib.metadata.metadata("adalab-cli")
7
+ __project__ = _DISTRIBUTION_METADATA["name"]
8
+ __version__ = _DISTRIBUTION_METADATA["version"]
9
+ __description__ = _DISTRIBUTION_METADATA["description"]
10
+
11
+ __all__ = ["cli"]
@@ -0,0 +1,5 @@
1
+ from .app import app
2
+
3
+ __all__ = ["app"]
4
+
5
+ __title__ = "adalab-cli"
@@ -0,0 +1,191 @@
1
+ """Main module for the adalib CLI.
2
+
3
+ This module initializes the Typer application and integrates various
4
+ submodules that handle different command groups for the adalib CLI.
5
+
6
+ Submodules:
7
+ - oci_commands: Handles commands related to Open Container Initiative (OCI).
8
+ - gallery_commands: Manages commands for gallery operations.
9
+ - kernel_commands: Contains commands for kernel-related operations.
10
+ - user_commands: Deals with user-related commands.
11
+ """
12
+
13
+ import importlib.metadata
14
+ import os
15
+
16
+ import requests
17
+ import typer
18
+ from loguru import logger
19
+ from prompt_toolkit.completion import NestedCompleter
20
+ from rich import print as rich_print
21
+ from rich.console import Console
22
+ from rich.table import Table
23
+
24
+ from . import authentication_flows as auth
25
+ from .completion_tree import completion_tree
26
+ from .databases_commands import db_app
27
+ from .gallery_commands import gallery_app
28
+ from .interactive import interactive_mode
29
+ from .logger import Logger
30
+ from .login_commands import load_config
31
+ from .schedule_commands import schedules_app
32
+ from .user_commands import user_app
33
+
34
+ app = typer.Typer(no_args_is_help=False)
35
+
36
+
37
+ @app.callback("adalab", invoke_without_command=True, no_args_is_help=True)
38
+ def main(
39
+ token: str = typer.Option(None, help="User token for authentication"),
40
+ adalab_url: str = typer.Option(None, help="URL of the AdaLab server"),
41
+ adalab_secret: str = typer.Option(None, help="Client secret for AdaLab"),
42
+ interactive: bool = typer.Option(
43
+ False, "-i", "--interactive", help="Activate interactive mode"
44
+ ),
45
+ verbose: int = typer.Option(
46
+ 0,
47
+ "-v",
48
+ count=True,
49
+ help="Verbosity level of the logs: -v (default): INFO | -vv: DEBUG | -vvv: TRACE",
50
+ min=0,
51
+ max=3,
52
+ ),
53
+ no_output: bool = typer.Option(False, "--no-output", help="Disable output from the CLI."),
54
+ ):
55
+ if no_output:
56
+ logger.remove()
57
+ else:
58
+ Logger(verbosity=verbose)
59
+ logger.info("Starting adalab CLI.")
60
+ logger.debug("Debugging mode activated.")
61
+ logger.trace("Tracing mode activated.")
62
+
63
+ # Attempt to configure adalib with user token.
64
+ # This works when token is either provided or stored in environment.
65
+ try:
66
+ auth.in_line_authentication(adalab_url, adalab_secret, token)
67
+ except AssertionError as e:
68
+ logger.info(f"{str(e)}")
69
+
70
+ # Start the interactive mode
71
+ if interactive:
72
+ rich_print("🏡 Welcome to AdaLab CLI interactive mode.")
73
+ rich_print("👋 Type 'exit' to quit.")
74
+ rich_print("ℹ️ Type 'help' for help.")
75
+ interactive_mode(
76
+ this_app=app,
77
+ title="adalab",
78
+ nested=True,
79
+ color="seagreen",
80
+ completion=NestedCompleter.from_nested_dict(completion_tree),
81
+ )
82
+
83
+
84
+ @app.command("login")
85
+ def login_commands(
86
+ auth_flow: str = typer.Option(
87
+ default="browser",
88
+ help="Authentication flow. Options: browser, device-code, user-token",
89
+ ),
90
+ token: str = typer.Option(None, help="Token for authentication"),
91
+ ):
92
+ """
93
+ Login to adalab
94
+ """
95
+ stored_configuration = load_config()
96
+ adalab_server_url = stored_configuration.get(
97
+ "adalab_url"
98
+ ) # Look for the URL in the config file
99
+ if adalab_server_url is None:
100
+ adalab_server_url = os.getenv("ADALAB_URL") # Look for the URL in env vars
101
+ if adalab_server_url is None:
102
+ adalab_server_url = typer.prompt("What is the AdaLab url?") # Ask for the URL
103
+ logger.info(f"AdaLab server URL: {adalab_server_url}")
104
+
105
+ adalab_client_secret = stored_configuration.get(
106
+ "adalab_secret"
107
+ ) # Look for the client secret in the config file
108
+ if adalab_client_secret is None:
109
+ adalab_client_secret = os.getenv("ADALAB_CLIENT_SECRET")
110
+ if "ADALAB_CLIENT_SECRET" not in os.environ:
111
+ adalab_client_secret = typer.prompt(
112
+ "What is the AdaLab client secret?", hide_input=True
113
+ )
114
+ logger.info("AdaLab client secret: ********")
115
+ # Check validity of URL
116
+ response = requests.get(adalab_server_url + "/adaboard/api", timeout=10)
117
+ if response.status_code != 200:
118
+ typer.echo("Invalid URL. Please try again.")
119
+ raise typer.Exit()
120
+
121
+ # Check which of the three kinds of authentication flows was set
122
+ if auth_flow == "browser":
123
+ auth.start_browser_auth(
124
+ adalab_server_url=adalab_server_url, adalab_client_secret=adalab_client_secret
125
+ )
126
+ elif auth_flow == "user-token":
127
+ auth.start_user_token_auth(
128
+ adalab_server_url=adalab_server_url,
129
+ token=token,
130
+ adalab_client_secret=adalab_client_secret,
131
+ )
132
+ elif auth_flow == "device-code":
133
+ auth.start_device_code_auth(
134
+ adalab_server_url=adalab_server_url, adalab_client_secret=adalab_client_secret
135
+ )
136
+
137
+ else:
138
+ typer.echo(
139
+ "Invalid authentication flow. Please try again."
140
+ "Options: browser, device-code, user-token"
141
+ )
142
+ raise typer.Exit()
143
+
144
+
145
+ @app.command("version")
146
+ def get_version():
147
+ """
148
+ Prints the version of adalib.
149
+ """
150
+
151
+ version_string_adalib = importlib.metadata.version("adalib")
152
+ version_string_adalib_auth = importlib.metadata.version("adalib-auth")
153
+ version_string_adalab_cli = importlib.metadata.version("adalab-cli")
154
+
155
+ table = Table("Package", "Version")
156
+ table.add_row("adalib", version_string_adalib)
157
+ table.add_row("adalib-auth", version_string_adalib_auth)
158
+ table.add_row("adalab-cli", version_string_adalab_cli)
159
+
160
+ console = Console()
161
+
162
+ console.print(table)
163
+
164
+
165
+ app.add_typer(
166
+ db_app,
167
+ name="databases",
168
+ help="Commands for interacting with databases.",
169
+ no_args_is_help=True,
170
+ )
171
+
172
+ app.add_typer(
173
+ gallery_app,
174
+ name="cards",
175
+ help="Commands for interacting with the gallery.",
176
+ no_args_is_help=True,
177
+ )
178
+
179
+ app.add_typer(
180
+ user_app,
181
+ name="user",
182
+ help="Commands for interacting with user information.",
183
+ no_args_is_help=True,
184
+ )
185
+
186
+ app.add_typer(
187
+ schedules_app,
188
+ name="schedules",
189
+ help="Commands for interacting with schedules.",
190
+ no_args_is_help=True,
191
+ )
@@ -0,0 +1,147 @@
1
+ import os
2
+ from time import sleep
3
+
4
+ import requests
5
+ import typer
6
+ from adalib_auth.config import get_config
7
+ from keycloak import exceptions
8
+ from loguru import logger
9
+ from rich import print as rich_print
10
+
11
+ from .login_commands import CLIENT_ID, KeycloakAuth, save_credentials
12
+
13
+
14
+ def start_browser_auth(adalab_server_url: str = None, adalab_client_secret: str = None):
15
+ """Starts the browser-based authentication flow.
16
+
17
+ :param adalab_server_url: The URL of the Adalab server, defaults to None
18
+ :type adalab_server_url: str, optional
19
+ """
20
+ auth = KeycloakAuth(
21
+ adalab_server_url=adalab_server_url, adalab_client_secret=adalab_client_secret
22
+ )
23
+ auth.login()
24
+ logger.success("Authentication successful. Credentials saved.")
25
+ rich_print(
26
+ "🎉 [bold green]Authentication successful[/bold green]. You can now enjoy the AdaLab CLI."
27
+ )
28
+
29
+
30
+ def start_user_token_auth(
31
+ token: str = None,
32
+ adalab_server_url: str = None,
33
+ adalab_client_secret: str = None,
34
+ store_credentials: bool = True,
35
+ ):
36
+ """Starts the user token authentication flow.
37
+
38
+ This function exchanges the user token for an access and refresh token. The token can be given
39
+ directly in the prompt or as an environment variable.
40
+ The access token and refresh token are saved to a file for future use.
41
+
42
+ :param token: The user token, it will be retrieve from ADALAB_USER_TOKEN environment variable
43
+ or prompted from the user.
44
+ :type token: str, optional
45
+ :param adalab_server_url: The URL of the ADALab server. If not provided, it will be retrieved
46
+ from the adalab_server_url argument.
47
+ :type adalab_server_url: str, optional
48
+ """
49
+
50
+ if token is None:
51
+ token = os.getenv("ADALAB_USER_TOKEN") or typer.prompt(
52
+ "What is your user token", hide_input=True
53
+ )
54
+
55
+ resp = requests.get(
56
+ adalab_server_url + "/adaboard/api/adalib/token",
57
+ headers={"Authorization": f"Token {token}"},
58
+ )
59
+ if resp.status_code != 200:
60
+ raise AssertionError(f"Failed to authenticate with user token. {resp.text}")
61
+
62
+ jh_token = resp.json()["access_token"]
63
+ new_token = KeycloakAuth(
64
+ adalab_server_url=adalab_server_url, adalab_client_secret=adalab_client_secret
65
+ ).client.exchange_token(token=jh_token, audience=CLIENT_ID)
66
+ if store_credentials:
67
+ save_credentials(
68
+ my_access_token=new_token["access_token"],
69
+ my_refresh_token=new_token["refresh_token"],
70
+ )
71
+ logger.success("Authentication successful. Credentials saved.")
72
+ rich_print(
73
+ "🎉 [bold green]Authentication successful[/bold green]. You can now enjoy the AdaLab CLI."
74
+ )
75
+
76
+
77
+ def start_device_code_auth(adalab_server_url: str = None, adalab_client_secret: str = None):
78
+ """
79
+ Starts the device code authentication flow.
80
+
81
+ This function initiates the device code authentication flow using the AdaLab server URL.
82
+ It retrieves the device code information from the Keycloak authentication client,
83
+ prompts the user to visit the verification URI and enter the user code,
84
+ and waits for the user authentication to complete.
85
+ Once the authentication is successful, the access token and refresh token are saved.
86
+
87
+ :param adalab_server_url: The AdaLab server URL, defaults to None
88
+ :type adalab_server_url: str, optional
89
+ """
90
+ auth = KeycloakAuth(
91
+ adalab_server_url=adalab_server_url, adalab_client_secret=adalab_client_secret
92
+ )
93
+ device_code_info = auth.client.device()
94
+ device_code = device_code_info["device_code"]
95
+ user_code = device_code_info["user_code"]
96
+ verification_uri = device_code_info["verification_uri"]
97
+ rich_print("Please visit %s and enter the code: %s", verification_uri, user_code)
98
+ rich_print("This code expires in %s seconds", device_code_info["expires_in"])
99
+ token = None
100
+ while token is None:
101
+ try:
102
+ token = auth.client.token(
103
+ grant_type="urn:ietf:params:oauth:grant-type:device_code",
104
+ device_code=device_code,
105
+ )
106
+ except exceptions.KeycloakPostError as e:
107
+ if "authorization_pending" in str(e):
108
+ logger.info("Waiting for user authentication...")
109
+ sleep(device_code_info["interval"])
110
+ else:
111
+ logger.error("Error during device polling: %s", str(e))
112
+ exit(1)
113
+ save_credentials(
114
+ my_access_token=token["access_token"],
115
+ my_refresh_token=token["refresh_token"],
116
+ )
117
+ logger.success("Authentication successful. Credentials saved.")
118
+ rich_print(
119
+ "🎉 [bold green]Authentication successful[/bold green]. You can now enjoy the AdaLab CLI."
120
+ )
121
+
122
+
123
+ def in_line_authentication(
124
+ adalab_server_url: str = None, adalab_secret: str = None, user_token: str = None
125
+ ) -> None:
126
+ """Auxiliary function for in-line authentication. This will not store credentials in the
127
+ configuration file. Instead it will be used for one-time authentication. Only valid for the
128
+ user token authentication flow.
129
+
130
+ :param adalab_server_url: The AdaLab server URL, defaults to None
131
+ :type adalab_server_url: str, optional
132
+ :param adalab_secret: The AdaLab JupyterHub client secret, defaults to None
133
+ :type adalab_secret: str, optional
134
+ :param user_token: The user token retrieved from AdaLab, defaults to None
135
+ :type user_token: str, optional
136
+ """
137
+ if adalab_server_url is None:
138
+ adalab_server_url = os.getenv("ADALAB_URL") or None
139
+ if adalab_secret is None:
140
+ adalab_secret = os.getenv("ADALAB_CLIENT_SECRET") or None
141
+ if user_token is None:
142
+ user_token = os.getenv("ADALAB_USER_TOKEN") or None
143
+
144
+ if None in (adalab_server_url, adalab_secret, user_token):
145
+ raise AssertionError("Missing required variables for in-line authentication.")
146
+
147
+ get_config(adaboard_api_url=adalab_server_url + "/adaboard/api", token=user_token)
@@ -0,0 +1,67 @@
1
+ """Map of the CLI commands and subcommands for the CLI completion tree."""
2
+
3
+ completion_tree = {
4
+ "cards": {
5
+ "approve-card": {"--help": None},
6
+ "expose-card": {"--help": None},
7
+ "get-card": {"--help": None},
8
+ "hide-card": {"--help": None},
9
+ "list-cards": {
10
+ "all": None,
11
+ "notebook": None,
12
+ "voila": None,
13
+ "url": None,
14
+ "group": None,
15
+ "--help": None,
16
+ },
17
+ "help": None,
18
+ "exit": None,
19
+ },
20
+ "databases": {
21
+ "list-databases": {"--no-pretty": None, "--help": None},
22
+ "list-datasets": {"--no-pretty": None, "--return-sql": None, "--help": None},
23
+ "list-tables": {"--no-pretty": None, "--help": None},
24
+ "view-dataset": {"--head": None, "--no-pretty": None, "--help": None},
25
+ "help": None,
26
+ "exit": None,
27
+ },
28
+ "schedules": {
29
+ "create-schedule": {
30
+ "--schedule": None,
31
+ "--pool": None,
32
+ "--active": None,
33
+ "--concurrent": None,
34
+ "--cleanup": None,
35
+ "--timeout": None,
36
+ "--help": None,
37
+ },
38
+ "delete-run": {"--help": None},
39
+ "delete-schedule": {"--help": None},
40
+ "start-run": {
41
+ "--cleanup": None,
42
+ "--pool": None,
43
+ "--timeout": None,
44
+ "--help": None,
45
+ },
46
+ "stop-run": {"--help": None},
47
+ "update-schedule": {
48
+ "--schedule": None,
49
+ "--pool": None,
50
+ "--active": None,
51
+ "--concurrent": None,
52
+ "--cleanup": None,
53
+ "--timeout": None,
54
+ "--help": None,
55
+ },
56
+ "help": None,
57
+ "exit": None,
58
+ },
59
+ "user": {
60
+ "whoami": {"--full-information": None, "--with-notifications": None, "--help": None},
61
+ "help": None,
62
+ "exit": None,
63
+ },
64
+ "version": None,
65
+ "help": None,
66
+ "exit": None,
67
+ }