adalab-cli 3.0.0__tar.gz → 3.2.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.
Files changed (22) hide show
  1. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/PKG-INFO +3 -2
  2. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/app.py +17 -1
  3. adalab_cli-3.2.0/adalab_cli/cli/completion_tree.py +143 -0
  4. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/databases_commands.py +1 -1
  5. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/gallery_commands.py +60 -8
  6. adalab_cli-3.2.0/adalab_cli/cli/logs_commands.py +337 -0
  7. adalab_cli-3.2.0/adalab_cli/cli/pictures_commands.py +85 -0
  8. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/schedule_commands.py +91 -2
  9. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/user_commands.py +1 -1
  10. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/pyproject.toml +18 -22
  11. adalab_cli-3.0.0/adalab_cli/cli/completion_tree.py +0 -67
  12. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/README.md +0 -0
  13. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/__init__.py +0 -0
  14. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/__init__.py +0 -0
  15. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/authentication_flows.py +0 -0
  16. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/factory.py +0 -0
  17. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/interactive.py +0 -0
  18. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/kernel_commands.py +0 -0
  19. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/logger.py +0 -0
  20. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/login_commands.py +0 -0
  21. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/oci_commands.py +0 -0
  22. {adalab_cli-3.0.0 → adalab_cli-3.2.0}/adalab_cli/cli/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: adalab-cli
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: The CLI app to interact with AdaLab.
5
5
  Home-page: https://adamatics.com
6
6
  Author: Adamatics ApS
@@ -16,8 +16,9 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Classifier: Typing :: Typed
19
- Requires-Dist: adalib (>=5.0.0)
19
+ Requires-Dist: adalib (>=7.2.0)
20
20
  Requires-Dist: adalib-auth (>=1.2.2)
21
+ Requires-Dist: ascii-magic (>=2.3.0)
21
22
  Requires-Dist: loguru (>=0.7.2)
22
23
  Requires-Dist: prompt-toolkit (>=3.0.47)
23
24
  Requires-Dist: sh (>=2.0.6)
@@ -28,13 +28,15 @@ from .gallery_commands import gallery_app
28
28
  from .interactive import interactive_mode
29
29
  from .logger import Logger
30
30
  from .login_commands import load_config
31
+ from .logs_commands import logs_app
32
+ from .pictures_commands import pictures_app
31
33
  from .schedule_commands import schedules_app
32
34
  from .user_commands import user_app
33
35
 
34
36
  app = typer.Typer(no_args_is_help=False)
35
37
 
36
38
 
37
- @app.callback("adalab", invoke_without_command=True, no_args_is_help=True)
39
+ @app.callback(invoke_without_command=True, no_args_is_help=True)
38
40
  def main(
39
41
  token: str = typer.Option(None, help="User token for authentication"),
40
42
  adalab_url: str = typer.Option(None, help="URL of the AdaLab server"),
@@ -189,3 +191,17 @@ app.add_typer(
189
191
  help="Commands for interacting with schedules.",
190
192
  no_args_is_help=True,
191
193
  )
194
+
195
+ app.add_typer(
196
+ logs_app,
197
+ name="logs",
198
+ help="Commands for fetching AdaLab logs.",
199
+ no_args_is_help=True,
200
+ )
201
+
202
+ app.add_typer(
203
+ pictures_app,
204
+ name="pictures",
205
+ help="Commands for handling pictures in AdaLab.",
206
+ no_args_is_help=True,
207
+ )
@@ -0,0 +1,143 @@
1
+ """Map of the CLI commands and subcommands for the CLI completion tree."""
2
+
3
+ completion_tree = {
4
+ "cards": {
5
+ "toggle-review": {"help": None, "exit": None},
6
+ "expose-card": {"help": None, "exit": None},
7
+ "get-card": {"help": None, "--incl-content": None, "exit": None},
8
+ "hide-card": {"help": None, "exit": None},
9
+ "list-cards": {
10
+ "all": {
11
+ "--search-phrase": None,
12
+ "--group-category": None,
13
+ "--only-hidden": None,
14
+ "--only-exposed": None,
15
+ "help": None,
16
+ "exit": None,
17
+ },
18
+ "notebook": {
19
+ "--search-phrase": None,
20
+ "--group-category": None,
21
+ "--only-hidden": None,
22
+ "--only-exposed": None,
23
+ "help": None,
24
+ "exit": None,
25
+ },
26
+ "voila": {
27
+ "--search-phrase": None,
28
+ "--group-category": None,
29
+ "--only-hidden": None,
30
+ "--only-exposed": None,
31
+ "help": None,
32
+ "exit": None,
33
+ },
34
+ "url": {
35
+ "--search-phrase": None,
36
+ "--group-category": None,
37
+ "--url-subtype": None,
38
+ "--only-hidden": None,
39
+ "--only-exposed": None,
40
+ "help": None,
41
+ "exit": None,
42
+ },
43
+ "group": {
44
+ "--search-phrase": None,
45
+ "--group-category": None,
46
+ "--only-hidden": None,
47
+ "--only-exposed": None,
48
+ "help": None,
49
+ "exit": None,
50
+ },
51
+ "help": None,
52
+ "exit": None,
53
+ },
54
+ "help": None,
55
+ "exit": None,
56
+ },
57
+ "databases": {
58
+ "list-databases": {"--no-pretty": None, "help": None},
59
+ "list-datasets": {"--no-pretty": None, "--return-sql": None, "help": None},
60
+ "list-tables": {"--no-pretty": None, "help": None},
61
+ "view-dataset": {"--head": None, "--no-pretty": None, "help": None},
62
+ "help": None,
63
+ "exit": None,
64
+ },
65
+ "logs": {
66
+ "app": {
67
+ "--container": None,
68
+ "--container-index": None,
69
+ "--from": None,
70
+ "--source": None,
71
+ "--to": None,
72
+ "help": None,
73
+ "exit": None,
74
+ },
75
+ "build": {"--source": None, "help": None},
76
+ "lab": {"--from": None, "--source": None, "--to": None, "help": None},
77
+ "publish": {"--source": None, "help": None},
78
+ "run": {"help": None},
79
+ "help": None,
80
+ "exit": None,
81
+ },
82
+ "schedules": {
83
+ "list-schedules": {
84
+ "--only-active": None,
85
+ "--search-phrase": None,
86
+ "help": None,
87
+ "exit": None,
88
+ },
89
+ "get-schedule": {"--incl-content": None, "help": None},
90
+ "create-schedule": {
91
+ "--schedule": None,
92
+ "--pool": None,
93
+ "--active": None,
94
+ "--concurrent": None,
95
+ "--cleanup": None,
96
+ "--timeout": None,
97
+ "help": None,
98
+ "exit": None,
99
+ },
100
+ "delete-run": {"help": None, "exit": None},
101
+ "delete-schedule": {"help": None, "exit": None},
102
+ "start-run": {
103
+ "--cleanup": None,
104
+ "--pool": None,
105
+ "--timeout": None,
106
+ "help": None,
107
+ "exit": None,
108
+ },
109
+ "stop-run": {"help": None, "exit": None},
110
+ "update-schedule": {
111
+ "--schedule": None,
112
+ "--pool": None,
113
+ "--active": None,
114
+ "--concurrent": None,
115
+ "--cleanup": None,
116
+ "--timeout": None,
117
+ "help": None,
118
+ "exit": None,
119
+ },
120
+ "help": None,
121
+ "exit": None,
122
+ },
123
+ "user": {
124
+ "whoami": {
125
+ "--full-information": None,
126
+ "--with-notifications": None,
127
+ "help": None,
128
+ "exit": None,
129
+ },
130
+ "help": None,
131
+ "exit": None,
132
+ },
133
+ "pictures": {
134
+ "show": {"help": None, "exit": None},
135
+ "download": {"--output-path": None, "help": None, "exit": None},
136
+ "create": {"--path": None, "--url": None, "help": None, "exit": None},
137
+ "help": None,
138
+ "exit": None,
139
+ },
140
+ "version": None,
141
+ "help": None,
142
+ "exit": None,
143
+ }
@@ -204,7 +204,7 @@ def list_tables(
204
204
  utils.print_dataframe(dataframe=tables, pretty=pretty, title=f"Tables in database {db_index}")
205
205
 
206
206
 
207
- @db_app.callback("callback", invoke_without_command=True)
207
+ @db_app.callback(invoke_without_command=True)
208
208
  def main(
209
209
  interactive: bool = typer.Option(
210
210
  False, "-i", "--interactive", help="Activate interactive mode"
@@ -38,26 +38,65 @@ def gallery_list_cards(
38
38
  typer.Argument(
39
39
  help="The type of cards to list. " "Choose from [all, notebook, voila, url, group].",
40
40
  ),
41
- ]
41
+ ] = "all",
42
+ head: Annotated[int | None, typer.Option(help="Number of cards to display.")] = None,
43
+ url_subtype: Annotated[
44
+ str,
45
+ typer.Option(
46
+ help="The URL subtype to filter by.",
47
+ ),
48
+ ] = "all",
49
+ search_phrase: Annotated[str, typer.Option(help="Search phrase to filter cards by.")] = None,
50
+ group_category: Annotated[str, typer.Option(help="Group category to filter by.")] = None,
51
+ only_hidden: Annotated[bool, typer.Option(help="Show only hidden cards.")] = False,
52
+ only_exposed: Annotated[bool, typer.Option(help="Show only exposed cards.")] = False,
42
53
  ):
43
54
  """
44
- Prints all cards from the Gallery with the type CARD_TYPE.
55
+ Prints all cards from the Gallery with the type CARD_TYPE. Optionally filter by URL_SUBTYPE
56
+ for URL cards.
45
57
  """
46
58
  try:
47
59
  with Progress(
48
60
  SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True
49
61
  ) as progress:
50
62
  progress.add_task(description="Fetching data...", total=None)
51
- cards = get_cards(card_type=card_type)
63
+ cards = get_cards(
64
+ card_type=card_type,
65
+ url_subtype=url_subtype,
66
+ head=head,
67
+ search_phrase=search_phrase,
68
+ group_category=group_category,
69
+ only_hidden=only_hidden,
70
+ only_exposed=only_exposed,
71
+ )
52
72
  except Exception as e:
53
73
  logger.error(f"Error: {str(e)}")
54
74
  sys.exit()
55
75
 
56
76
  rich_print(f"Card type: {card_type} ({len(cards)})")
57
77
 
58
- table = Table("Card id", "Card type", "Card name", "Card author", title="Cards")
78
+ cols = (
79
+ [("Card id", 0), ("Card type", 1)]
80
+ + ([("URL type", 2)] if card_type == "url" else [])
81
+ + [("Reviewed", 3), ("Exposed", 4), ("Card name", 5)]
82
+ + ([("Cards in Group", 6)] if card_type == "group" else [])
83
+ + [("Card author", 7)]
84
+ )
85
+ table = Table(
86
+ *[x[0] for x in cols],
87
+ title="Cards",
88
+ )
59
89
  for card in cards:
60
- table.add_row(str(card[0]), card[1], card[2], card[3])
90
+ table.add_row(
91
+ *[
92
+ (
93
+ str(card[ind])
94
+ if _cname not in ["Reviewed", "Exposed"]
95
+ else ("✅" if card[ind] else "❌")
96
+ )
97
+ for _cname, ind in cols
98
+ ]
99
+ )
61
100
  console = Console()
62
101
  console.print(table)
63
102
 
@@ -70,6 +109,9 @@ def gallery_get_card(
70
109
  help="The ID of the card you want to retrieve.",
71
110
  ),
72
111
  ],
112
+ incl_content: Annotated[
113
+ bool, typer.Option(help="Include card content in the output.")
114
+ ] = False,
73
115
  ):
74
116
  """
75
117
  Gets the contents of a card identified by CARD_ID.
@@ -81,12 +123,22 @@ def gallery_get_card(
81
123
  transient=True,
82
124
  ) as progress:
83
125
  progress.add_task(description="Fetching data...", total=None)
84
- card = get_card_contents(card_id=card_id)
126
+ card = get_card_contents(card_id=card_id, incl_content=incl_content)
85
127
  except Exception:
86
128
  rich_print(f"Card {card_id} [bold red]failed[/bold red] to retrieve.")
87
129
  sys.exit(1)
88
130
 
89
- rich_print(card)
131
+ console = Console()
132
+
133
+ table = Table(title="Card Details")
134
+ table.add_column("Property", style="cyan", no_wrap=True)
135
+ table.add_column("Value", style="magenta")
136
+ for key, value in card.items():
137
+ if isinstance(value, bool):
138
+ value = "✅" if value else "❌"
139
+ table.add_row(key, str(value) if value != [] else "[dim]Empty List[/dim]")
140
+
141
+ console.print(table)
90
142
 
91
143
 
92
144
  @gallery_app.command("expose-card", no_args_is_help=True)
@@ -149,7 +201,7 @@ def gallery_toggle_card_review(
149
201
  rich_print(f"Card {card_id} review status [bold green]updated[/bold green] successfully.")
150
202
 
151
203
 
152
- @gallery_app.callback("callback", invoke_without_command=True)
204
+ @gallery_app.callback(invoke_without_command=True)
153
205
  def main(
154
206
  interactive: bool = typer.Option(
155
207
  False, "-i", "--interactive", help="Activate interactive mode"
@@ -0,0 +1,337 @@
1
+ """Logs command group for the adalib CLI.
2
+
3
+ This submodule encompasses commands related to AdaLab logs
4
+ in the adalib CLI. It uses Typer for command line interface implementation,
5
+ allowing the user to retrieve different types of logs.
6
+
7
+ Functions:
8
+
9
+ """
10
+
11
+ import sys
12
+
13
+ import typer
14
+ from adalib.apps import get_app, get_app_logs
15
+ from adalib.harbor import get_publish_logs
16
+ from adalib.lab import get_build_logs, get_lab_logs
17
+ from adalib.schedules import get_run_logs
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.progress import Progress, SpinnerColumn, TextColumn
23
+ from rich.table import Table
24
+ from typing_extensions import Annotated
25
+
26
+ from .completion_tree import completion_tree
27
+ from .factory import check_authentication
28
+ from .interactive import interactive_mode
29
+
30
+ logs_app = typer.Typer()
31
+
32
+
33
+ @logs_app.command("app", no_args_is_help=True)
34
+ def logs_get_app_logs(
35
+ app_id: Annotated[
36
+ str,
37
+ typer.Argument(
38
+ help="The ID of the app to fetch logs for.",
39
+ ),
40
+ ],
41
+ container: Annotated[
42
+ str,
43
+ typer.Option(
44
+ "-c",
45
+ "--container",
46
+ help="The name of the container whose logs to fetch.",
47
+ ),
48
+ ] = "",
49
+ container_idx: Annotated[
50
+ int,
51
+ typer.Option(
52
+ "-i",
53
+ "--container-index",
54
+ help="The index of the container whose logs to fetch.",
55
+ ),
56
+ ] = 0,
57
+ from_date: Annotated[
58
+ str,
59
+ typer.Option(
60
+ "-f",
61
+ "--from",
62
+ help="The start date for the logs, in ISO format (YYYY-MM-DDTHH:mm:ss+HH:mm).",
63
+ ),
64
+ ] = "",
65
+ source: Annotated[
66
+ str,
67
+ typer.Option(
68
+ "-s",
69
+ "--source",
70
+ help="The source of the logs. One of 'container' or 'system'.",
71
+ ),
72
+ ] = "container",
73
+ to_date: Annotated[
74
+ str,
75
+ typer.Option(
76
+ "-t",
77
+ "--to",
78
+ help="The end date for the logs, in ISO format (YYYY-MM-DDTHH:mm:ss+HH:mm).",
79
+ ),
80
+ ] = "",
81
+ ):
82
+ """
83
+ Fetches logs for the app with the given ID.
84
+ """
85
+ try:
86
+ with Progress(
87
+ SpinnerColumn(),
88
+ TextColumn("[progress.description]{task.description}"),
89
+ transient=True,
90
+ ) as progress:
91
+ progress.add_task(description="Fetching data...", total=None)
92
+ tmp = get_app(app_id)
93
+ app_name = tmp["name"]
94
+ logs = get_app_logs(
95
+ app_id,
96
+ container=container,
97
+ container_idx=container_idx,
98
+ from_date=from_date,
99
+ source=source,
100
+ to_date=to_date,
101
+ )
102
+ except Exception as e:
103
+ logger.error(f"Error: {str(e)}")
104
+ sys.exit()
105
+
106
+ rich_print(f"{source.capitalize()} logs for app: {app_name} ({len(logs)})")
107
+
108
+ table = Table("Timestamp", "Log message", title="Logs")
109
+ for log in logs:
110
+ table.add_row(log["time"], log["message"])
111
+
112
+ console = Console()
113
+ console.print(table)
114
+
115
+
116
+ @logs_app.command("build", no_args_is_help=True)
117
+ def logs_get_build_logs(
118
+ build_id: Annotated[
119
+ int,
120
+ typer.Argument(
121
+ help="The ID of the build process to fetch logs for.",
122
+ ),
123
+ ],
124
+ source: Annotated[
125
+ str,
126
+ typer.Option(
127
+ "-s",
128
+ "--source",
129
+ help="The source of the logs. One of 'build' or 'system'.",
130
+ ),
131
+ ] = "build",
132
+ ):
133
+ """
134
+ Fetches logs for the build with the given ID.
135
+ """
136
+ try:
137
+ with Progress(
138
+ SpinnerColumn(),
139
+ TextColumn("[progress.description]{task.description}"),
140
+ transient=True,
141
+ ) as progress:
142
+ progress.add_task(description="Fetching data...", total=None)
143
+ logs = get_build_logs(
144
+ build_id=build_id,
145
+ source=source,
146
+ )
147
+ except Exception as e:
148
+ logger.error(f"Error: {str(e)}")
149
+ sys.exit()
150
+
151
+ rich_print(f"{source.capitalize()} logs for build process: {build_id} ({len(logs)})")
152
+
153
+ if len(logs[0].keys()) == 2:
154
+ table = Table("Timestamp", "Log message", title="Logs")
155
+ for log in logs:
156
+ table.add_row(log["time"], log["message"])
157
+ else:
158
+ table = Table("Timestamp", "Input", "Output", title="Logs")
159
+ for log in logs:
160
+ table.add_row(log["time"], log["input"], log["output"])
161
+
162
+ console = Console()
163
+ console.print(table)
164
+
165
+
166
+ @logs_app.command("lab", no_args_is_help=True)
167
+ def logs_get_lab_logs(
168
+ from_date: Annotated[
169
+ str,
170
+ typer.Option(
171
+ "-f",
172
+ "--from",
173
+ help="The start date for the logs, in ISO format (YYYY-MM-DDTHH:mm:ss+HH:mm).",
174
+ ),
175
+ ] = "",
176
+ source: Annotated[
177
+ str,
178
+ typer.Option(
179
+ "-s",
180
+ "--source",
181
+ help="The source of the logs. One of 'user' or 'system'.",
182
+ ),
183
+ ] = "user",
184
+ to_date: Annotated[
185
+ str,
186
+ typer.Option(
187
+ "-t",
188
+ "--to",
189
+ help="The end date for the logs, in ISO format (YYYY-MM-DDTHH:mm:ss+HH:mm).",
190
+ ),
191
+ ] = "",
192
+ ):
193
+ """
194
+ Fetches logs for the user's Lab instance.
195
+ """
196
+ try:
197
+ with Progress(
198
+ SpinnerColumn(),
199
+ TextColumn("[progress.description]{task.description}"),
200
+ transient=True,
201
+ ) as progress:
202
+ progress.add_task(description="Fetching data...", total=None)
203
+ logs = get_lab_logs(
204
+ from_date=from_date,
205
+ source=source,
206
+ to_date=to_date,
207
+ )
208
+ except Exception as e:
209
+ logger.error(f"Error: {str(e)}")
210
+ sys.exit()
211
+
212
+ rich_print(f"{source.capitalize()} Lab logs ({len(logs)})")
213
+
214
+ table = Table("Timestamp", "Log message", title="Logs")
215
+ for log in logs:
216
+ table.add_row(log["time"], log["message"])
217
+
218
+ console = Console()
219
+ console.print(table)
220
+
221
+
222
+ @logs_app.command("publish", no_args_is_help=True)
223
+ def logs_get_publish_logs(
224
+ metadata_id: Annotated[
225
+ int,
226
+ typer.Argument(
227
+ help="The ID of the publish process to fetch logs for.",
228
+ ),
229
+ ],
230
+ source: Annotated[
231
+ str,
232
+ typer.Option(
233
+ "-s",
234
+ "--source",
235
+ help="The source of the logs. One of 'publish' or 'system'.",
236
+ ),
237
+ ] = "publish",
238
+ ):
239
+ """
240
+ Fetches logs for the publish process with the given ID.
241
+ """
242
+ try:
243
+ with Progress(
244
+ SpinnerColumn(),
245
+ TextColumn("[progress.description]{task.description}"),
246
+ transient=True,
247
+ ) as progress:
248
+ progress.add_task(description="Fetching data...", total=None)
249
+ logs = get_publish_logs(
250
+ metadata_id=metadata_id,
251
+ source=source,
252
+ )
253
+ except Exception as e:
254
+ logger.error(f"Error: {str(e)}")
255
+ sys.exit()
256
+
257
+ rich_print(
258
+ f"{source.capitalize()} logs for publish process of metadata {metadata_id} ({len(logs)})"
259
+ )
260
+
261
+ if len(logs[0].keys()) == 2:
262
+ table = Table("Timestamp", "Log message", title="Logs")
263
+ for log in logs:
264
+ table.add_row(log["time"], log["message"])
265
+ else:
266
+ table = Table("Timestamp", "Input", "Output", title="Logs")
267
+ for log in logs:
268
+ table.add_row(log["time"], log["input"], log["output"])
269
+
270
+ console = Console()
271
+ console.print(table)
272
+
273
+
274
+ @logs_app.command("run", no_args_is_help=True)
275
+ def logs_get_run_logs(
276
+ schedule_id: Annotated[
277
+ int,
278
+ typer.Argument(
279
+ help="The ID of the schedule to fetch logs for.",
280
+ ),
281
+ ],
282
+ run_id: Annotated[
283
+ int,
284
+ typer.Argument(
285
+ help="The ID of the run instance to fetch logs for.",
286
+ ),
287
+ ],
288
+ ):
289
+ """
290
+ Fetches logs for the schedule run instance with the given ID.
291
+ """
292
+ try:
293
+ with Progress(
294
+ SpinnerColumn(),
295
+ TextColumn("[progress.description]{task.description}"),
296
+ transient=True,
297
+ ) as progress:
298
+ progress.add_task(description="Fetching data...", total=None)
299
+ logs = get_run_logs(
300
+ schedule_id=schedule_id,
301
+ run_id=run_id,
302
+ )
303
+ except Exception as e:
304
+ logger.error(f"Error: {str(e)}")
305
+ sys.exit()
306
+
307
+ rich_print(f"Logs for run process {run_id} of schedule {schedule_id} ({len(logs)})")
308
+
309
+ table = Table("Timestamp", "Source", "Input", "Output", "Cell number", title="Logs")
310
+ for log in logs:
311
+ table.add_row(
312
+ log["timestamp"],
313
+ log["log_type"],
314
+ log["input"] or "",
315
+ log["output"] or "",
316
+ str(log["cell_number"]) or "",
317
+ )
318
+
319
+ console = Console()
320
+ console.print(table)
321
+
322
+
323
+ @logs_app.callback(invoke_without_command=True)
324
+ def main(
325
+ interactive: bool = typer.Option(
326
+ False, "-i", "--interactive", help="Activate interactive mode"
327
+ )
328
+ ):
329
+ if interactive:
330
+ interactive_mode(
331
+ this_app=logs_app,
332
+ title="logs",
333
+ color="maroon",
334
+ completion=NestedCompleter.from_nested_dict(completion_tree["logs"]),
335
+ )
336
+ else:
337
+ check_authentication()
@@ -0,0 +1,85 @@
1
+ """Pictures-related commands"""
2
+
3
+ import sys
4
+ from io import BytesIO
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from adalib.adaboard import request_adaboard
9
+ from adalib.pictures import get_picture, post_picture, post_picture_url
10
+ from ascii_magic import AsciiArt
11
+ from prompt_toolkit.completion import NestedCompleter
12
+ from rich import print as rich_print
13
+
14
+ from .completion_tree import completion_tree
15
+ from .factory import check_authentication
16
+ from .interactive import interactive_mode
17
+
18
+ pictures_app = typer.Typer()
19
+
20
+
21
+ @pictures_app.command("show", no_args_is_help=True)
22
+ def show_picture(picture_id: Annotated[int, typer.Argument(help="The ID of the picture to show")]):
23
+ """Show the specific picture as an ASCII art"""
24
+ try:
25
+ data = BytesIO(request_adaboard(f"image/{picture_id}").content)
26
+ AsciiArt.from_image(data).to_terminal()
27
+ except Exception:
28
+ rich_print(f"Could not show the picture with ID: {picture_id}")
29
+ sys.exit(1)
30
+
31
+
32
+ @pictures_app.command("download", no_args_is_help=True)
33
+ def download_picture(
34
+ picture_id: Annotated[int, typer.Argument(help="The ID of the picture to download")],
35
+ output_path: Annotated[
36
+ str | None, typer.Option(help="The path to save the picture to")
37
+ ] = None,
38
+ ):
39
+ """Download the specific picture into a file."""
40
+ if output_path is None:
41
+ output_path = f"picture_{picture_id}"
42
+ try:
43
+ get_picture(picture_id=picture_id, output_file_path=output_path)
44
+ except Exception:
45
+ rich_print(f"Could not download the picture with ID: {picture_id}")
46
+ sys.exit(1)
47
+
48
+
49
+ @pictures_app.command("create", no_args_is_help=True)
50
+ def create_new_picture(
51
+ path: Annotated[str | None, typer.Option(help="The path to the picture file")] = None,
52
+ url: Annotated[str | None, typer.Option(help="The URL of the picture")] = None,
53
+ ):
54
+ """Create a new picture from a file or URL."""
55
+ if (path is not None and url is not None) or (path is None and url is None):
56
+ rich_print("Please provide either a path or a URL to create a picture.")
57
+ sys.exit(1)
58
+
59
+ try:
60
+ if path is not None:
61
+ picture_id = post_picture(input_file_path=path)
62
+ else:
63
+ picture_id = post_picture_url(picture_url=url)
64
+
65
+ rich_print(f"Picture created with ID: {picture_id}")
66
+ except Exception:
67
+ rich_print("Could not create the picture.")
68
+ sys.exit(1)
69
+
70
+
71
+ @pictures_app.callback(invoke_without_command=True)
72
+ def main(
73
+ interactive: bool = typer.Option(
74
+ False, "-i", "--interactive", help="Activate interactive mode"
75
+ )
76
+ ):
77
+ if interactive:
78
+ interactive_mode(
79
+ this_app=pictures_app,
80
+ title="pictures",
81
+ color="maroon",
82
+ completion=NestedCompleter.from_nested_dict(completion_tree["pictures"]),
83
+ )
84
+ else:
85
+ check_authentication()
@@ -10,10 +10,11 @@ Functions:
10
10
 
11
11
  """
12
12
 
13
+ import base64
13
14
  import sys
14
15
 
15
16
  import typer
16
- from adalib.adaboard import get_user
17
+ from adalib.adaboard import get_all_pages, get_user, request_adaboard
17
18
  from adalib.schedules import (
18
19
  create_schedule,
19
20
  delete_run,
@@ -24,6 +25,7 @@ from adalib.schedules import (
24
25
  )
25
26
  from prompt_toolkit.completion import NestedCompleter
26
27
  from rich import print as rich_print
28
+ from rich.table import Table
27
29
  from typing_extensions import Annotated
28
30
 
29
31
  from .completion_tree import completion_tree
@@ -33,6 +35,93 @@ from .interactive import interactive_mode
33
35
  schedules_app = typer.Typer()
34
36
 
35
37
 
38
+ @schedules_app.command("list-schedules", no_args_is_help=False)
39
+ def list_all_schedules(
40
+ only_active: Annotated[bool, typer.Option(help="Include only active schedules")] = False,
41
+ search_phrase: Annotated[str, typer.Option(help="Search phrase to filter schedules")] = None,
42
+ ):
43
+ """Lists all available schedules."""
44
+ params = dict()
45
+ if only_active:
46
+ params = {"only_active": True}
47
+
48
+ if search_phrase is not None:
49
+ params["search"] = search_phrase
50
+
51
+ try:
52
+ res = get_all_pages("schedules/v2", params=params)
53
+ except Exception:
54
+ rich_print("Failed to list schedules.")
55
+ sys.exit(1)
56
+
57
+ cols = ["ID", "Name", "Owner", "Cron", "Pool", "Active", "Last Run", "Next Run"]
58
+ table = Table(
59
+ *[x for x in cols],
60
+ title="Schedules",
61
+ )
62
+ for row in res:
63
+ table.add_row(
64
+ str(row["schedule_id"]),
65
+ row["name"],
66
+ row["owner"]["name"],
67
+ row["cron"],
68
+ row["pool"],
69
+ "✅" if row["active"] else "❌",
70
+ row["last_scheduled_ts"] or "N/A",
71
+ row["next_scheduled_ts"] or "N/A",
72
+ )
73
+ rich_print(table)
74
+
75
+
76
+ @schedules_app.command("get-schedule", no_args_is_help=True)
77
+ def get_single_schedule(
78
+ schedule_id: Annotated[int, typer.Argument(help="ID of the schedule.")],
79
+ incl_content: Annotated[bool, typer.Option(help="Include the schedule content.")] = False,
80
+ ):
81
+ """Retrieves the details of a single schedule."""
82
+ params = {"with_content": incl_content}
83
+ try:
84
+ res = request_adaboard(f"schedules/v2/{schedule_id}", params=params).json()
85
+ except Exception:
86
+ rich_print(f"Failed to get schedule {schedule_id}.")
87
+ sys.exit(1)
88
+
89
+ if res["content"]:
90
+ for _content in res["content"]:
91
+ _content["content"] = base64.b64decode(_content["content"]).decode("utf-8")
92
+
93
+ schedule = {
94
+ "ID": schedule_id,
95
+ "Name": res["name"],
96
+ "Owner": res["owner"]["name"],
97
+ "Runner": res["runner"]["name"],
98
+ "Timeout": res["timeout"],
99
+ "Pool": res["pool"],
100
+ "Active": "✅" if res["active"] else "❌",
101
+ "Timezone": res["timezone"],
102
+ "Cron": res["cron"],
103
+ "Last Run": res["last_scheduled_ts"] or "N/A",
104
+ "Next Run": res["next_scheduled_ts"] or "N/A",
105
+ "Concurrent": "✅" if res["concurrent"] else "❌",
106
+ "Cleanup": "✅" if res["cleanup"] else "❌",
107
+ "Contents": res["content"],
108
+ "Inputs": res["run_config"]["inputs"],
109
+ "Pre-run script": res["run_config"]["scripts"]["pre_run_script"] or "N/A",
110
+ "Post-run script": res["run_config"]["scripts"]["post_run_script"] or "N/A",
111
+ "Kernel ID": res["kernel_metadata_id"],
112
+ }
113
+
114
+ table = Table(title="Schedule Details")
115
+ table.add_column("Property", style="cyan", no_wrap=True)
116
+ table.add_column("Value", style="magenta")
117
+ for key, value in schedule.items():
118
+ if isinstance(value, bool):
119
+ value = "✅" if value else "❌"
120
+ table.add_row(key, str(value) if value != [] else "[dim]Empty List[/dim]")
121
+
122
+ rich_print(table)
123
+
124
+
36
125
  @schedules_app.command("start-run", no_args_is_help=True)
37
126
  def notebooks_start_run(
38
127
  schedule_id: Annotated[int, typer.Argument(help="ID of the schedule to start run.")]
@@ -172,7 +261,7 @@ def notebooks_delete_schedule(
172
261
  sys.exit(1)
173
262
 
174
263
 
175
- @schedules_app.callback("interactive", invoke_without_command=True)
264
+ @schedules_app.callback(invoke_without_command=True)
176
265
  def main(
177
266
  interactive: bool = typer.Option(
178
267
  False, "-i", "--interactive", help="Activate interactive mode"
@@ -58,7 +58,7 @@ def user_who_am_i_in_adalab(
58
58
  console.print(table)
59
59
 
60
60
 
61
- @user_app.callback("interactive", invoke_without_command=True)
61
+ @user_app.callback(invoke_without_command=True)
62
62
  def main(
63
63
  interactive: bool = typer.Option(
64
64
  False, "-i", "--interactive", help="Activate interactive mode"
@@ -1,8 +1,8 @@
1
1
  [project]
2
2
  name = "adalab-cli"
3
- version = "3.0.0"
3
+ version = "3.2.0"
4
4
  description = "The CLI app to interact with AdaLab."
5
- keywords = [ "cli",]
5
+ keywords = ["cli"]
6
6
 
7
7
  [[project.authors]]
8
8
  name = "Adamatics ApS"
@@ -10,35 +10,34 @@ email = "info@adamatics.com"
10
10
 
11
11
  [tool.poetry]
12
12
  name = "adalab-cli"
13
- version = "3.0.0"
13
+ version = "3.2.0"
14
14
  description = "The CLI app to interact with AdaLab."
15
15
  authors = ["Adamatics ApS <info@adamatics.com>"]
16
16
  readme = "README.md"
17
17
  homepage = "https://adamatics.com"
18
18
  repository = "https://gitlab.com/adamatics/python/adalab-cli"
19
19
  classifiers = [
20
- "Development Status :: 4 - Beta",
21
- "Intended Audience :: Developers",
22
- "Operating System :: OS Independent",
23
- "Programming Language :: Python",
24
- "Programming Language :: Python :: 3",
25
- "Programming Language :: Python :: 3.10",
26
- "Topic :: Software Development :: Libraries :: Python Modules",
27
- "Typing :: Typed",
28
- ]
29
- packages = [
30
- { include = "adalab_cli" }
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Topic :: Software Development :: Libraries :: Python Modules",
27
+ "Typing :: Typed",
31
28
  ]
29
+ packages = [{ include = "adalab_cli" }]
32
30
 
33
31
  [tool.poetry.dependencies]
34
32
  python = "^3.10.0"
35
33
  tabulate = ">=0.9.0"
36
34
  typer = ">=0.9.0"
37
35
  sh = ">=2.0.6"
38
- adalib = {version = ">=5.0.0", source = "adalib-dev"}
39
- adalib-auth = {version = ">=1.2.2", source = "adalib-dev"}
36
+ adalib = { version = ">=7.2.0", source = "adalib-dev" }
37
+ adalib-auth = { version = ">=1.2.2", source = "adalib-dev" }
40
38
  loguru = ">=0.7.2"
41
39
  prompt-toolkit = ">=3.0.47"
40
+ ascii-magic = ">=2.3.0"
42
41
 
43
42
  [tool.poetry.group.dev.dependencies]
44
43
  black = "^23.9.1"
@@ -87,8 +86,8 @@ line-length = 99
87
86
  [tool.flake8]
88
87
  ignore = ["E203", "E266", "E501", "W503"]
89
88
  max-complexity = 18
90
- select = ["B","C","E","F","W","T4"]
91
- exclude = [".git",".venv",".tox"]
89
+ select = ["B", "C", "E", "F", "W", "T4"]
90
+ exclude = [".git", ".venv", ".tox"]
92
91
  max-line-length = 99
93
92
 
94
93
  [tool.pytest]
@@ -104,10 +103,7 @@ addopts = """\
104
103
 
105
104
  [tool.coverage.report]
106
105
  fail_under = 100
107
- exclude_lines = [
108
- 'if TYPE_CHECKING:',
109
- 'pragma: no cover'
110
- ]
106
+ exclude_lines = ['if TYPE_CHECKING:', 'pragma: no cover']
111
107
 
112
108
  [tool.mypy]
113
109
  disallow_any_unimported = true
@@ -1,67 +0,0 @@
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
- }
File without changes