adalab-cli 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
adalab_cli/__init__.py ADDED
@@ -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"
adalab_cli/cli/app.py ADDED
@@ -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
+ }
@@ -0,0 +1,221 @@
1
+ """ docstrings"""
2
+
3
+ import sys
4
+
5
+ import typer
6
+ from loguru import logger
7
+ from prompt_toolkit.completion import NestedCompleter
8
+ from rich import print as rich_print
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
+
11
+ # from adalib.superset import databases, datasets
12
+ from typing_extensions import Annotated
13
+
14
+ from .completion_tree import completion_tree
15
+ from .factory import check_authentication
16
+
17
+ # from . import utils
18
+ from .interactive import interactive_mode
19
+
20
+ db_app = typer.Typer()
21
+
22
+
23
+ @db_app.command("list-databases")
24
+ def database_list_databases(
25
+ pretty: Annotated[
26
+ bool,
27
+ typer.Option(help="Set this flag to get the output in a nice tabular view."),
28
+ ] = True
29
+ ):
30
+ """Prints all databases available."""
31
+ from adalib.superset import databases # noqa: E402
32
+
33
+ from . import utils
34
+
35
+ try:
36
+ with Progress(
37
+ SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True
38
+ ) as progress:
39
+ progress.add_task(description="Fetching data...", total=None)
40
+ my_databases = databases.all().as_df()
41
+ except Exception as e:
42
+ logger.error(f"Error: {str(e)}")
43
+ sys.exit(1)
44
+ # my_databases = databases.all().as_df()
45
+ utils.print_dataframe(dataframe=my_databases, pretty=pretty, title="Databases")
46
+
47
+
48
+ @db_app.command("list-datasets")
49
+ def database_list_datasets(
50
+ database_id: Annotated[
51
+ int,
52
+ typer.Argument(help="Specific database from which datasets are listed."),
53
+ ] = None,
54
+ return_sql: Annotated[
55
+ bool,
56
+ typer.Option(help="Set this flag to get the SQL query in the output."),
57
+ ] = False,
58
+ pretty: Annotated[
59
+ bool,
60
+ typer.Option(help="Set this flag to get the output in a nice tabular view."),
61
+ ] = True,
62
+ ):
63
+ """
64
+ Prints all datasets available in the databases DATABASE_ID.
65
+ If DATABASE_ID is not specified then datasets in all available databases are returned.
66
+
67
+ Example usage:
68
+
69
+ adalab databases list-datasets --return-sql --pretty 1
70
+
71
+ will print the available datasets in the database identified with DATABASE_ID=1, including
72
+ the SQL query (if available) in a nice tabular view.
73
+ """
74
+ from . import utils
75
+
76
+ try:
77
+ with Progress(
78
+ SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True
79
+ ) as progress:
80
+ progress.add_task(description="Fetching data...", total=None)
81
+ my_datasets = utils.get_datasets()
82
+
83
+ except Exception as e:
84
+ logger.error(f"Error: {str(e)}")
85
+ sys.exit(1)
86
+
87
+ filtered_datasets = utils.filter_datasets_by_database_id(
88
+ dataset_list=my_datasets, database_id=database_id
89
+ )
90
+
91
+ if filtered_datasets is False:
92
+ rich_print(f"The database with index {database_id} is not available.")
93
+
94
+ else:
95
+ format_datasets = utils.check_sql(dataset_list=filtered_datasets, include_sql=return_sql)
96
+ utils.print_dataframe(
97
+ dataframe=format_datasets,
98
+ pretty=pretty,
99
+ title=(
100
+ f"Datasets in database {database_id}"
101
+ if (database_id is not None)
102
+ else "All Datasets"
103
+ ),
104
+ )
105
+
106
+
107
+ @db_app.command("view-dataset", no_args_is_help=True)
108
+ def database_view_dataset_by_index(
109
+ ds_index: Annotated[int, typer.Argument(help="Index of the dataset to print.")] = None,
110
+ head: Annotated[
111
+ int,
112
+ typer.Option(help="Set --head N for printing the first N elements in the dataframe."),
113
+ ] = 10,
114
+ pretty: Annotated[
115
+ bool,
116
+ typer.Option(help="Set this flag to get the output in a nice tabular view."),
117
+ ] = True,
118
+ ):
119
+ """
120
+ Prints the first elements of the dataset indexed by DS_INDEX, by
121
+ default it prints the first 10 elements.
122
+
123
+ Example usage:
124
+
125
+ adalab databases view-dataset 15 --head 11 --pretty
126
+
127
+ will print the first 11 elements of the dataset with index 15
128
+ in a nice tabular view.
129
+ """
130
+ from adalib.superset import datasets
131
+
132
+ from . import utils
133
+
134
+ if ds_index not in datasets.all().as_df().index:
135
+ return rich_print(
136
+ f"Dataset with index {ds_index} [bold red]not available[/bold red].\n"
137
+ + "Run [bold blue]list-datasets[/bold blue] for available options"
138
+ )
139
+
140
+ try:
141
+ with Progress(
142
+ SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True
143
+ ) as progress:
144
+ progress.add_task(description="Fetching data...", total=None)
145
+ my_dataset = datasets.get(id=ds_index)
146
+
147
+ if my_dataset.kind == "virtual":
148
+ my_df = utils.retrieve_virtual_dataset(my_dataset=my_dataset, head=head)
149
+ elif my_dataset.kind == "physical":
150
+ my_df = utils.retrieve_physical_dataset(my_dataset=my_dataset, head=head)
151
+
152
+ except Exception as e:
153
+ logger.error(f"Error: {str(e)}")
154
+ sys.exit(1)
155
+
156
+ utils.print_dataframe(dataframe=my_df, pretty=pretty, title=f"Dataset {ds_index}")
157
+
158
+
159
+ @db_app.command("list-tables", no_args_is_help=True)
160
+ def list_tables(
161
+ db_index: Annotated[int, typer.Argument(help="Database index.")] = None,
162
+ pretty: Annotated[
163
+ bool,
164
+ typer.Option(help="Set this flag to get the output in a nice tabular view."),
165
+ ] = True,
166
+ ):
167
+ """
168
+ List tables in the database with the specified index.
169
+
170
+ Example usage:
171
+
172
+ adalab databases list-tables 1 --pretty
173
+
174
+ will print the tables in the databes with database index 1 in a nice tabular view.
175
+ """
176
+ from adalib.superset import databases
177
+
178
+ from . import utils
179
+
180
+ try:
181
+ available_index = databases.all().as_df().index
182
+ assert db_index in available_index, (
183
+ f"Database with index {db_index} not available."
184
+ + " Run list-databases for available options"
185
+ )
186
+ except AssertionError as ae:
187
+ logger.error(f"Error: {str(ae)}")
188
+ sys.exit(1)
189
+
190
+ try:
191
+ with Progress(
192
+ SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True
193
+ ) as progress:
194
+ progress.add_task(description="Fetching data...", total=None)
195
+ db = databases.get(id=db_index)
196
+ tables = db.run(
197
+ query="SELECT table_name "
198
+ "FROM information_schema.tables "
199
+ "WHERE table_schema = 'public';"
200
+ ).as_df()
201
+ except Exception as e:
202
+ logger.error(f"Error: {str(e)}")
203
+ sys.exit(1)
204
+ utils.print_dataframe(dataframe=tables, pretty=pretty, title=f"Tables in database {db_index}")
205
+
206
+
207
+ @db_app.callback("callback", invoke_without_command=True)
208
+ def main(
209
+ interactive: bool = typer.Option(
210
+ False, "-i", "--interactive", help="Activate interactive mode"
211
+ )
212
+ ):
213
+ if interactive:
214
+ interactive_mode(
215
+ this_app=db_app,
216
+ title="databases",
217
+ color="maroon",
218
+ completion=NestedCompleter.from_nested_dict(completion_tree["databases"]),
219
+ )
220
+ else:
221
+ check_authentication()