alphai 0.1.1__py3-none-any.whl → 0.2.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.
- alphai/__init__.py +40 -2
- alphai/auth.py +31 -11
- alphai/cleanup.py +351 -0
- alphai/cli.py +45 -910
- alphai/client.py +115 -70
- alphai/commands/__init__.py +24 -0
- alphai/commands/config.py +67 -0
- alphai/commands/docker.py +615 -0
- alphai/commands/jupyter.py +350 -0
- alphai/commands/notebooks.py +1173 -0
- alphai/commands/orgs.py +27 -0
- alphai/commands/projects.py +35 -0
- alphai/config.py +15 -5
- alphai/docker.py +80 -45
- alphai/exceptions.py +122 -0
- alphai/jupyter_manager.py +577 -0
- alphai/notebook_renderer.py +473 -0
- alphai/utils.py +67 -0
- {alphai-0.1.1.dist-info → alphai-0.2.0.dist-info}/METADATA +9 -8
- alphai-0.2.0.dist-info/RECORD +23 -0
- alphai-0.1.1.dist-info/RECORD +0 -12
- {alphai-0.1.1.dist-info → alphai-0.2.0.dist-info}/WHEEL +0 -0
- {alphai-0.1.1.dist-info → alphai-0.2.0.dist-info}/entry_points.txt +0 -0
- {alphai-0.1.1.dist-info → alphai-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
"""Notebook commands for alphai CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import webbrowser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import httpx
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
13
|
+
from rich.prompt import Confirm
|
|
14
|
+
|
|
15
|
+
from ..client import AlphAIClient
|
|
16
|
+
from ..config import Config
|
|
17
|
+
from ..utils import get_logger
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_api_client(config: Config) -> httpx.Client:
|
|
24
|
+
"""Create an HTTP client for API calls."""
|
|
25
|
+
return httpx.Client(
|
|
26
|
+
base_url=config.api_url.rstrip('/api') if config.api_url.endswith('/api') else config.api_url,
|
|
27
|
+
headers={
|
|
28
|
+
"Authorization": f"Bearer {config.bearer_token}",
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
},
|
|
31
|
+
timeout=30.0,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _select_organization(client: AlphAIClient) -> Optional[str]:
|
|
36
|
+
"""Interactively select an organization."""
|
|
37
|
+
import questionary
|
|
38
|
+
|
|
39
|
+
console.print("[yellow]Select an organization:[/yellow]")
|
|
40
|
+
|
|
41
|
+
with Progress(
|
|
42
|
+
SpinnerColumn(),
|
|
43
|
+
TextColumn("[progress.description]{task.description}"),
|
|
44
|
+
console=console
|
|
45
|
+
) as progress:
|
|
46
|
+
task = progress.add_task("Fetching organizations...", total=None)
|
|
47
|
+
try:
|
|
48
|
+
orgs_data = client.get_organizations()
|
|
49
|
+
except Exception as e:
|
|
50
|
+
progress.update(task, completed=1)
|
|
51
|
+
console.print(f"[red]Error fetching organizations: {e}[/red]")
|
|
52
|
+
return None
|
|
53
|
+
progress.update(task, completed=1)
|
|
54
|
+
|
|
55
|
+
if not orgs_data:
|
|
56
|
+
console.print("[red]No organizations found. Please create one first.[/red]")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
org_choices = []
|
|
60
|
+
for org in orgs_data:
|
|
61
|
+
display_name = f"{org.name} ({org.slug})"
|
|
62
|
+
org_choices.append(questionary.Choice(title=display_name, value=org.slug))
|
|
63
|
+
|
|
64
|
+
selected = questionary.select(
|
|
65
|
+
"Organization:",
|
|
66
|
+
choices=org_choices,
|
|
67
|
+
style=questionary.Style([
|
|
68
|
+
('question', 'bold'),
|
|
69
|
+
('pointer', 'fg:#673ab7 bold'),
|
|
70
|
+
('highlighted', 'fg:#673ab7 bold'),
|
|
71
|
+
('selected', 'fg:#cc5454'),
|
|
72
|
+
])
|
|
73
|
+
).ask()
|
|
74
|
+
|
|
75
|
+
if selected:
|
|
76
|
+
org_name = next((o.name for o in orgs_data if o.slug == selected), selected)
|
|
77
|
+
console.print(f"[green]✓ {org_name}[/green]\n")
|
|
78
|
+
|
|
79
|
+
return selected
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _fetch_notebooks(config: Config, org_slug: str) -> List[Dict[str, Any]]:
|
|
83
|
+
"""Fetch notebooks for an organization."""
|
|
84
|
+
with Progress(
|
|
85
|
+
SpinnerColumn(),
|
|
86
|
+
TextColumn("[progress.description]{task.description}"),
|
|
87
|
+
console=console
|
|
88
|
+
) as progress:
|
|
89
|
+
task = progress.add_task("Fetching notebooks...", total=None)
|
|
90
|
+
try:
|
|
91
|
+
with get_api_client(config) as client:
|
|
92
|
+
response = client.get("/api/notebooks", params={"org_slug": org_slug, "limit": 50})
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
data = response.json()
|
|
95
|
+
except Exception as e:
|
|
96
|
+
progress.update(task, completed=1)
|
|
97
|
+
console.print(f"[red]Error fetching notebooks: {e}[/red]")
|
|
98
|
+
return []
|
|
99
|
+
progress.update(task, completed=1)
|
|
100
|
+
|
|
101
|
+
return data.get("notebooks", [])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _select_notebook(notebooks: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
105
|
+
"""Interactively select a notebook."""
|
|
106
|
+
import questionary
|
|
107
|
+
|
|
108
|
+
if not notebooks:
|
|
109
|
+
console.print("[yellow]No notebooks found in this organization.[/yellow]")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
# Build a lookup dict by ID
|
|
113
|
+
nb_by_id = {nb.get("id"): nb for nb in notebooks}
|
|
114
|
+
|
|
115
|
+
choices = []
|
|
116
|
+
for nb in notebooks:
|
|
117
|
+
visibility = "🌍" if nb.get("is_public") else "🔒"
|
|
118
|
+
title = nb.get("title", "Untitled")
|
|
119
|
+
slug = nb.get("slug", "")
|
|
120
|
+
nb_id = nb.get("id", "")
|
|
121
|
+
choices.append(questionary.Choice(
|
|
122
|
+
title=f"{visibility} {title} ({slug})",
|
|
123
|
+
value=nb_id # Use ID as value
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
choices.append(questionary.Choice(title="← Back", value="__back__"))
|
|
127
|
+
|
|
128
|
+
selected_id = questionary.select(
|
|
129
|
+
"Select a notebook:",
|
|
130
|
+
choices=choices,
|
|
131
|
+
style=questionary.Style([
|
|
132
|
+
('question', 'bold'),
|
|
133
|
+
('pointer', 'fg:#673ab7 bold'),
|
|
134
|
+
('highlighted', 'fg:#673ab7 bold'),
|
|
135
|
+
])
|
|
136
|
+
).ask()
|
|
137
|
+
|
|
138
|
+
if not selected_id or selected_id == "__back__":
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
return nb_by_id.get(selected_id)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _select_action(notebook: Dict[str, Any]) -> Optional[str]:
|
|
145
|
+
"""Interactively select an action for the notebook."""
|
|
146
|
+
import questionary
|
|
147
|
+
|
|
148
|
+
is_public = notebook.get("is_public", False)
|
|
149
|
+
|
|
150
|
+
actions = [
|
|
151
|
+
questionary.Choice(title="👁 View content", value="view"),
|
|
152
|
+
questionary.Choice(title="ℹ️ Show info", value="info"),
|
|
153
|
+
questionary.Choice(title="🌐 Open in browser", value="browser"),
|
|
154
|
+
questionary.Choice(title="⬇️ Download", value="download"),
|
|
155
|
+
questionary.Choice(title="🏷 Manage tags", value="tags"),
|
|
156
|
+
questionary.Separator(),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
# Show only the relevant visibility toggle
|
|
160
|
+
if is_public:
|
|
161
|
+
actions.append(questionary.Choice(title="🔒 Make private", value="unpublish"))
|
|
162
|
+
else:
|
|
163
|
+
actions.append(questionary.Choice(title="🌍 Make public", value="publish"))
|
|
164
|
+
|
|
165
|
+
actions.extend([
|
|
166
|
+
questionary.Separator(),
|
|
167
|
+
questionary.Choice(title="🗑 Delete", value="delete"),
|
|
168
|
+
questionary.Separator(),
|
|
169
|
+
questionary.Choice(title="← Back", value="back"),
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
return questionary.select(
|
|
173
|
+
"What would you like to do?",
|
|
174
|
+
choices=actions,
|
|
175
|
+
style=questionary.Style([
|
|
176
|
+
('question', 'bold'),
|
|
177
|
+
('pointer', 'fg:#673ab7 bold'),
|
|
178
|
+
('highlighted', 'fg:#673ab7 bold'),
|
|
179
|
+
])
|
|
180
|
+
).ask()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _execute_action(ctx: click.Context, notebook: Dict[str, Any], action: str) -> bool:
|
|
184
|
+
"""Execute an action on a notebook. Returns True to continue, False to go back."""
|
|
185
|
+
config: Config = ctx.obj['config']
|
|
186
|
+
nb_id = notebook.get("id", "")
|
|
187
|
+
slug = notebook.get("slug", "")
|
|
188
|
+
org_slug = notebook.get("organizations", {}).get("slug", "")
|
|
189
|
+
|
|
190
|
+
if action == "view":
|
|
191
|
+
# Fetch full content
|
|
192
|
+
with Progress(
|
|
193
|
+
SpinnerColumn(),
|
|
194
|
+
TextColumn("[progress.description]{task.description}"),
|
|
195
|
+
console=console
|
|
196
|
+
) as progress:
|
|
197
|
+
task = progress.add_task("Fetching notebook content...", total=None)
|
|
198
|
+
try:
|
|
199
|
+
with get_api_client(config) as client:
|
|
200
|
+
response = client.get(f"/api/notebooks/{nb_id}", params={"include_content": "true"})
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
data = response.json()
|
|
203
|
+
except Exception as e:
|
|
204
|
+
progress.update(task, completed=1)
|
|
205
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
206
|
+
return True
|
|
207
|
+
progress.update(task, completed=1)
|
|
208
|
+
|
|
209
|
+
nb_data = data.get("notebook", {})
|
|
210
|
+
cells = nb_data.get("content", {}).get("cells", [])
|
|
211
|
+
|
|
212
|
+
if not cells:
|
|
213
|
+
console.print("[yellow]Notebook has no cells.[/yellow]")
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
from ..notebook_renderer import interactive_cell_viewer
|
|
217
|
+
interactive_cell_viewer(cells, console)
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
elif action == "info":
|
|
221
|
+
from ..notebook_renderer import display_notebook_info
|
|
222
|
+
display_notebook_info(notebook, console)
|
|
223
|
+
console.print()
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
elif action == "browser":
|
|
227
|
+
if org_slug and slug:
|
|
228
|
+
url = f"{config.api_url.replace('/api', '')}/{org_slug}/~/notebooks/{slug}"
|
|
229
|
+
console.print(f"[cyan]Opening: {url}[/cyan]")
|
|
230
|
+
webbrowser.open(url)
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
elif action == "download":
|
|
234
|
+
import questionary
|
|
235
|
+
default_name = f"{slug or 'notebook'}.ipynb"
|
|
236
|
+
filename = questionary.text(
|
|
237
|
+
"Save as:",
|
|
238
|
+
default=default_name
|
|
239
|
+
).ask()
|
|
240
|
+
|
|
241
|
+
if filename:
|
|
242
|
+
try:
|
|
243
|
+
with get_api_client(config) as client:
|
|
244
|
+
response = client.get(f"/api/notebooks/{nb_id}/download")
|
|
245
|
+
response.raise_for_status()
|
|
246
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
247
|
+
f.write(response.text)
|
|
248
|
+
console.print(f"[green]✓ Downloaded to {filename}[/green]\n")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
console.print(f"[red]Error downloading: {e}[/red]\n")
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
elif action == "tags":
|
|
254
|
+
import questionary
|
|
255
|
+
current_tags = [t.get("name", "") for t in notebook.get("tags", [])]
|
|
256
|
+
console.print(f"[dim]Current tags: {', '.join(current_tags) if current_tags else 'none'}[/dim]")
|
|
257
|
+
|
|
258
|
+
new_tags = questionary.text(
|
|
259
|
+
"Enter tags (comma-separated):",
|
|
260
|
+
default=", ".join(current_tags)
|
|
261
|
+
).ask()
|
|
262
|
+
|
|
263
|
+
if new_tags is not None:
|
|
264
|
+
tag_list = [t.strip() for t in new_tags.split(',') if t.strip()]
|
|
265
|
+
try:
|
|
266
|
+
with get_api_client(config) as client:
|
|
267
|
+
response = client.patch(f"/api/notebooks/{nb_id}", json={"tags": tag_list})
|
|
268
|
+
response.raise_for_status()
|
|
269
|
+
console.print(f"[green]✓ Tags updated[/green]\n")
|
|
270
|
+
except Exception as e:
|
|
271
|
+
console.print(f"[red]Error updating tags: {e}[/red]\n")
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
elif action == "publish":
|
|
275
|
+
try:
|
|
276
|
+
with get_api_client(config) as client:
|
|
277
|
+
response = client.patch(f"/api/notebooks/{nb_id}", json={"is_public": True})
|
|
278
|
+
response.raise_for_status()
|
|
279
|
+
console.print(f"[green]✓ Notebook published![/green]\n")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
console.print(f"[red]Error: {e}[/red]\n")
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
elif action == "unpublish":
|
|
285
|
+
try:
|
|
286
|
+
with get_api_client(config) as client:
|
|
287
|
+
response = client.patch(f"/api/notebooks/{nb_id}", json={"is_public": False})
|
|
288
|
+
response.raise_for_status()
|
|
289
|
+
console.print(f"[green]✓ Notebook unpublished![/green]\n")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
console.print(f"[red]Error: {e}[/red]\n")
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
elif action == "delete":
|
|
295
|
+
if Confirm.ask(f"[red]Delete '{notebook.get('title')}'? This cannot be undone.[/red]"):
|
|
296
|
+
try:
|
|
297
|
+
with get_api_client(config) as client:
|
|
298
|
+
response = client.delete(f"/api/notebooks/{nb_id}")
|
|
299
|
+
response.raise_for_status()
|
|
300
|
+
console.print(f"[green]✓ Notebook deleted[/green]\n")
|
|
301
|
+
return False # Go back to notebook list
|
|
302
|
+
except Exception as e:
|
|
303
|
+
console.print(f"[red]Error: {e}[/red]\n")
|
|
304
|
+
return True
|
|
305
|
+
|
|
306
|
+
elif action == "back":
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _select_org_action() -> Optional[str]:
|
|
313
|
+
"""Select what to do in an organization."""
|
|
314
|
+
import questionary
|
|
315
|
+
|
|
316
|
+
actions = [
|
|
317
|
+
questionary.Choice(title="📂 Browse notebooks", value="browse"),
|
|
318
|
+
questionary.Choice(title="⬆️ Upload notebook", value="upload"),
|
|
319
|
+
questionary.Separator(),
|
|
320
|
+
questionary.Choice(title="← Back", value="back"),
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
return questionary.select(
|
|
324
|
+
"What would you like to do?",
|
|
325
|
+
choices=actions,
|
|
326
|
+
style=questionary.Style([
|
|
327
|
+
('question', 'bold'),
|
|
328
|
+
('pointer', 'fg:#673ab7 bold'),
|
|
329
|
+
('highlighted', 'fg:#673ab7 bold'),
|
|
330
|
+
])
|
|
331
|
+
).ask()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _interactive_upload(ctx: click.Context, org_slug: str) -> None:
|
|
335
|
+
"""Interactive notebook upload flow."""
|
|
336
|
+
import questionary
|
|
337
|
+
|
|
338
|
+
config: Config = ctx.obj['config']
|
|
339
|
+
|
|
340
|
+
# Get file path
|
|
341
|
+
file_path = questionary.path(
|
|
342
|
+
"Select notebook file:",
|
|
343
|
+
only_directories=False,
|
|
344
|
+
validate=lambda p: p.endswith('.ipynb') or "Must be a .ipynb file"
|
|
345
|
+
).ask()
|
|
346
|
+
|
|
347
|
+
if not file_path:
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
path = Path(file_path)
|
|
351
|
+
if not path.exists():
|
|
352
|
+
console.print(f"[red]File not found: {file_path}[/red]")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
# Read and validate notebook
|
|
356
|
+
try:
|
|
357
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
358
|
+
notebook_content = json.load(f)
|
|
359
|
+
if 'cells' not in notebook_content:
|
|
360
|
+
console.print("[red]Invalid notebook format.[/red]")
|
|
361
|
+
return
|
|
362
|
+
except json.JSONDecodeError:
|
|
363
|
+
console.print("[red]Invalid JSON in notebook file.[/red]")
|
|
364
|
+
return
|
|
365
|
+
except Exception as e:
|
|
366
|
+
console.print(f"[red]Error reading file: {e}[/red]")
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
# Get title
|
|
370
|
+
default_title = path.stem.replace('_', ' ').replace('-', ' ').title()
|
|
371
|
+
title = questionary.text(
|
|
372
|
+
"Title:",
|
|
373
|
+
default=default_title
|
|
374
|
+
).ask()
|
|
375
|
+
|
|
376
|
+
if not title:
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
# Optional description
|
|
380
|
+
description = questionary.text(
|
|
381
|
+
"Description (optional):",
|
|
382
|
+
default=""
|
|
383
|
+
).ask()
|
|
384
|
+
|
|
385
|
+
# Visibility
|
|
386
|
+
is_public = questionary.confirm(
|
|
387
|
+
"Make public?",
|
|
388
|
+
default=False
|
|
389
|
+
).ask()
|
|
390
|
+
|
|
391
|
+
# Tags
|
|
392
|
+
tags_input = questionary.text(
|
|
393
|
+
"Tags (comma-separated, optional):",
|
|
394
|
+
default=""
|
|
395
|
+
).ask()
|
|
396
|
+
|
|
397
|
+
tag_list = [t.strip() for t in tags_input.split(',') if t.strip()] if tags_input else []
|
|
398
|
+
|
|
399
|
+
# Upload
|
|
400
|
+
with Progress(
|
|
401
|
+
SpinnerColumn(),
|
|
402
|
+
TextColumn("[progress.description]{task.description}"),
|
|
403
|
+
console=console
|
|
404
|
+
) as progress:
|
|
405
|
+
task = progress.add_task("Uploading notebook...", total=None)
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
with get_api_client(config) as api_client:
|
|
409
|
+
payload = {
|
|
410
|
+
"org_slug": org_slug,
|
|
411
|
+
"title": title,
|
|
412
|
+
"content": notebook_content,
|
|
413
|
+
"is_public": is_public,
|
|
414
|
+
}
|
|
415
|
+
if description:
|
|
416
|
+
payload["description"] = description
|
|
417
|
+
if tag_list:
|
|
418
|
+
payload["tags"] = tag_list
|
|
419
|
+
|
|
420
|
+
response = api_client.post("/api/notebooks", json=payload)
|
|
421
|
+
response.raise_for_status()
|
|
422
|
+
data = response.json()
|
|
423
|
+
except httpx.HTTPStatusError as e:
|
|
424
|
+
progress.update(task, completed=1)
|
|
425
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
426
|
+
return
|
|
427
|
+
except Exception as e:
|
|
428
|
+
progress.update(task, completed=1)
|
|
429
|
+
console.print(f"[red]Error uploading: {e}[/red]")
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
progress.update(task, completed=1)
|
|
433
|
+
|
|
434
|
+
notebook = data.get("notebook", {})
|
|
435
|
+
console.print(f"\n[green]✓ Uploaded successfully![/green]")
|
|
436
|
+
console.print(f" Title: {notebook.get('title')}")
|
|
437
|
+
console.print(f" Slug: {notebook.get('slug')}")
|
|
438
|
+
visibility = "🌍 Public" if notebook.get('is_public') else "🔒 Private"
|
|
439
|
+
console.print(f" Visibility: {visibility}")
|
|
440
|
+
|
|
441
|
+
slug = notebook.get("slug", "")
|
|
442
|
+
if org_slug and slug:
|
|
443
|
+
url = f"{config.api_url.replace('/api', '')}/{org_slug}/~/notebooks/{slug}"
|
|
444
|
+
console.print(f" URL: [cyan]{url}[/cyan]")
|
|
445
|
+
console.print()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _interactive_mode(ctx: click.Context) -> None:
|
|
449
|
+
"""Run the interactive notebook browser."""
|
|
450
|
+
config: Config = ctx.obj['config']
|
|
451
|
+
client: AlphAIClient = ctx.obj['client']
|
|
452
|
+
|
|
453
|
+
if not config.bearer_token:
|
|
454
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
455
|
+
sys.exit(1)
|
|
456
|
+
|
|
457
|
+
console.print("[bold cyan]📓 Notebook Browser[/bold cyan]\n")
|
|
458
|
+
|
|
459
|
+
while True:
|
|
460
|
+
# Select organization
|
|
461
|
+
org_slug = _select_organization(client)
|
|
462
|
+
if not org_slug:
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
while True:
|
|
466
|
+
# Ask what to do in this org
|
|
467
|
+
org_action = _select_org_action()
|
|
468
|
+
|
|
469
|
+
if not org_action or org_action == "back":
|
|
470
|
+
console.print()
|
|
471
|
+
break
|
|
472
|
+
|
|
473
|
+
if org_action == "upload":
|
|
474
|
+
_interactive_upload(ctx, org_slug)
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
# Browse notebooks
|
|
478
|
+
notebooks_list = _fetch_notebooks(config, org_slug)
|
|
479
|
+
notebook = _select_notebook(notebooks_list)
|
|
480
|
+
|
|
481
|
+
if not notebook:
|
|
482
|
+
continue # Back to org action menu
|
|
483
|
+
|
|
484
|
+
console.print(f"\n[bold]{notebook.get('title')}[/bold]")
|
|
485
|
+
console.print(f"[dim]{notebook.get('description', 'No description')}[/dim]\n")
|
|
486
|
+
|
|
487
|
+
while True:
|
|
488
|
+
action = _select_action(notebook)
|
|
489
|
+
if not action or action == "back":
|
|
490
|
+
console.print()
|
|
491
|
+
break
|
|
492
|
+
|
|
493
|
+
if not _execute_action(ctx, notebook, action):
|
|
494
|
+
break # Action requests going back
|
|
495
|
+
|
|
496
|
+
# Refresh notebook state after visibility changes
|
|
497
|
+
if action in ("publish", "unpublish"):
|
|
498
|
+
notebook["is_public"] = (action == "publish")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@click.group(invoke_without_command=True)
|
|
502
|
+
@click.pass_context
|
|
503
|
+
def notebooks(ctx: click.Context) -> None:
|
|
504
|
+
"""Manage Jupyter notebooks.
|
|
505
|
+
|
|
506
|
+
Run without arguments for interactive mode, or use subcommands:
|
|
507
|
+
|
|
508
|
+
\b
|
|
509
|
+
Examples:
|
|
510
|
+
alphai nb # Interactive browser
|
|
511
|
+
alphai nb list
|
|
512
|
+
alphai nb view my-notebook
|
|
513
|
+
alphai nb upload analysis.ipynb --org my-org
|
|
514
|
+
"""
|
|
515
|
+
if ctx.invoked_subcommand is None:
|
|
516
|
+
_interactive_mode(ctx)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@notebooks.command(name="list")
|
|
520
|
+
@click.option('--org', help='Organization slug')
|
|
521
|
+
@click.option('--search', help='Search in title and description')
|
|
522
|
+
@click.option('--tag', help='Filter by tag')
|
|
523
|
+
@click.option('--public', 'visibility', flag_value='public', help='Show only public notebooks')
|
|
524
|
+
@click.option('--private', 'visibility', flag_value='private', help='Show only private notebooks')
|
|
525
|
+
@click.option('--limit', default=20, type=int, help='Maximum results')
|
|
526
|
+
@click.pass_context
|
|
527
|
+
def list_notebooks(ctx: click.Context, org: Optional[str], search: Optional[str],
|
|
528
|
+
tag: Optional[str], visibility: Optional[str], limit: int) -> None:
|
|
529
|
+
"""List notebooks in your organizations."""
|
|
530
|
+
config: Config = ctx.obj['config']
|
|
531
|
+
|
|
532
|
+
if not config.bearer_token:
|
|
533
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
# Use current org if not specified
|
|
537
|
+
org_slug = org or config.current_org
|
|
538
|
+
|
|
539
|
+
with Progress(
|
|
540
|
+
SpinnerColumn(),
|
|
541
|
+
TextColumn("[progress.description]{task.description}"),
|
|
542
|
+
console=console
|
|
543
|
+
) as progress:
|
|
544
|
+
task = progress.add_task("Fetching notebooks...", total=None)
|
|
545
|
+
|
|
546
|
+
try:
|
|
547
|
+
with get_api_client(config) as client:
|
|
548
|
+
params = {"limit": limit}
|
|
549
|
+
if org_slug:
|
|
550
|
+
params["org_slug"] = org_slug
|
|
551
|
+
if search:
|
|
552
|
+
params["search"] = search
|
|
553
|
+
if tag:
|
|
554
|
+
params["tag"] = tag
|
|
555
|
+
if visibility:
|
|
556
|
+
params["visibility"] = visibility
|
|
557
|
+
|
|
558
|
+
response = client.get("/api/notebooks", params=params)
|
|
559
|
+
response.raise_for_status()
|
|
560
|
+
data = response.json()
|
|
561
|
+
except httpx.HTTPStatusError as e:
|
|
562
|
+
progress.update(task, completed=1)
|
|
563
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
564
|
+
return
|
|
565
|
+
except Exception as e:
|
|
566
|
+
progress.update(task, completed=1)
|
|
567
|
+
console.print(f"[red]Error fetching notebooks: {e}[/red]")
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
progress.update(task, completed=1)
|
|
571
|
+
|
|
572
|
+
notebooks_data = data.get("notebooks", [])
|
|
573
|
+
|
|
574
|
+
if not notebooks_data:
|
|
575
|
+
console.print("[yellow]No notebooks found.[/yellow]")
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
# Import here to avoid circular imports
|
|
579
|
+
from ..notebook_renderer import display_notebooks_table
|
|
580
|
+
display_notebooks_table(notebooks_data, console)
|
|
581
|
+
|
|
582
|
+
total = data.get("total", len(notebooks_data))
|
|
583
|
+
if total > limit:
|
|
584
|
+
console.print(f"\n[dim]Showing {len(notebooks_data)} of {total} notebooks. Use --limit to see more.[/dim]")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@notebooks.command()
|
|
588
|
+
@click.argument('identifier')
|
|
589
|
+
@click.option('--browser', '-b', is_flag=True, help='Open in web browser')
|
|
590
|
+
@click.pass_context
|
|
591
|
+
def info(ctx: click.Context, identifier: str, browser: bool) -> None:
|
|
592
|
+
"""Show notebook information."""
|
|
593
|
+
config: Config = ctx.obj['config']
|
|
594
|
+
|
|
595
|
+
if not config.bearer_token:
|
|
596
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
with Progress(
|
|
600
|
+
SpinnerColumn(),
|
|
601
|
+
TextColumn("[progress.description]{task.description}"),
|
|
602
|
+
console=console
|
|
603
|
+
) as progress:
|
|
604
|
+
task = progress.add_task("Fetching notebook...", total=None)
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
with get_api_client(config) as client:
|
|
608
|
+
response = client.get(f"/api/notebooks/{identifier}", params={"include_content": "false"})
|
|
609
|
+
response.raise_for_status()
|
|
610
|
+
data = response.json()
|
|
611
|
+
except httpx.HTTPStatusError as e:
|
|
612
|
+
progress.update(task, completed=1)
|
|
613
|
+
if e.response.status_code == 404:
|
|
614
|
+
console.print(f"[red]Notebook '{identifier}' not found.[/red]")
|
|
615
|
+
else:
|
|
616
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
617
|
+
return
|
|
618
|
+
except Exception as e:
|
|
619
|
+
progress.update(task, completed=1)
|
|
620
|
+
console.print(f"[red]Error fetching notebook: {e}[/red]")
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
progress.update(task, completed=1)
|
|
624
|
+
|
|
625
|
+
notebook = data.get("notebook", {})
|
|
626
|
+
|
|
627
|
+
from ..notebook_renderer import display_notebook_info
|
|
628
|
+
display_notebook_info(notebook, console)
|
|
629
|
+
|
|
630
|
+
if browser:
|
|
631
|
+
org_slug = notebook.get("organizations", {}).get("slug", "")
|
|
632
|
+
slug = notebook.get("slug", "")
|
|
633
|
+
if org_slug and slug:
|
|
634
|
+
url = f"{config.api_url.replace('/api', '')}/{org_slug}/~/notebooks/{slug}"
|
|
635
|
+
console.print(f"\n[cyan]Opening in browser: {url}[/cyan]")
|
|
636
|
+
webbrowser.open(url)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@notebooks.command()
|
|
640
|
+
@click.argument('identifier')
|
|
641
|
+
@click.option('--browser', '-b', is_flag=True, help='Open in web browser instead')
|
|
642
|
+
@click.option('--static', '-s', is_flag=True, help='Show all cells at once (non-interactive)')
|
|
643
|
+
@click.pass_context
|
|
644
|
+
def view(ctx: click.Context, identifier: str, browser: bool, static: bool) -> None:
|
|
645
|
+
"""View notebook content in the terminal.
|
|
646
|
+
|
|
647
|
+
By default, opens an interactive viewer where you can scroll through cells
|
|
648
|
+
using arrow keys. Press 'q' to quit.
|
|
649
|
+
|
|
650
|
+
Use --static to display all cells at once without interaction.
|
|
651
|
+
"""
|
|
652
|
+
config: Config = ctx.obj['config']
|
|
653
|
+
|
|
654
|
+
if not config.bearer_token:
|
|
655
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
if browser:
|
|
659
|
+
# Just open in browser, fetch minimal info
|
|
660
|
+
with Progress(
|
|
661
|
+
SpinnerColumn(),
|
|
662
|
+
TextColumn("[progress.description]{task.description}"),
|
|
663
|
+
console=console
|
|
664
|
+
) as progress:
|
|
665
|
+
task = progress.add_task("Fetching notebook...", total=None)
|
|
666
|
+
try:
|
|
667
|
+
with get_api_client(config) as client:
|
|
668
|
+
response = client.get(f"/api/notebooks/{identifier}", params={"include_content": "false"})
|
|
669
|
+
response.raise_for_status()
|
|
670
|
+
data = response.json()
|
|
671
|
+
except httpx.HTTPStatusError as e:
|
|
672
|
+
progress.update(task, completed=1)
|
|
673
|
+
if e.response.status_code == 404:
|
|
674
|
+
console.print(f"[red]Notebook '{identifier}' not found.[/red]")
|
|
675
|
+
else:
|
|
676
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
677
|
+
return
|
|
678
|
+
except Exception as e:
|
|
679
|
+
progress.update(task, completed=1)
|
|
680
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
681
|
+
return
|
|
682
|
+
progress.update(task, completed=1)
|
|
683
|
+
|
|
684
|
+
notebook = data.get("notebook", {})
|
|
685
|
+
org_slug = notebook.get("organizations", {}).get("slug", "")
|
|
686
|
+
slug = notebook.get("slug", "")
|
|
687
|
+
if org_slug and slug:
|
|
688
|
+
url = f"{config.api_url.replace('/api', '')}/{org_slug}/~/notebooks/{slug}"
|
|
689
|
+
console.print(f"[cyan]Opening: {url}[/cyan]")
|
|
690
|
+
webbrowser.open(url)
|
|
691
|
+
return
|
|
692
|
+
|
|
693
|
+
with Progress(
|
|
694
|
+
SpinnerColumn(),
|
|
695
|
+
TextColumn("[progress.description]{task.description}"),
|
|
696
|
+
console=console
|
|
697
|
+
) as progress:
|
|
698
|
+
task = progress.add_task("Fetching notebook...", total=None)
|
|
699
|
+
|
|
700
|
+
try:
|
|
701
|
+
with get_api_client(config) as client:
|
|
702
|
+
response = client.get(f"/api/notebooks/{identifier}", params={"include_content": "true"})
|
|
703
|
+
response.raise_for_status()
|
|
704
|
+
data = response.json()
|
|
705
|
+
except httpx.HTTPStatusError as e:
|
|
706
|
+
progress.update(task, completed=1)
|
|
707
|
+
if e.response.status_code == 404:
|
|
708
|
+
console.print(f"[red]Notebook '{identifier}' not found.[/red]")
|
|
709
|
+
else:
|
|
710
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
711
|
+
return
|
|
712
|
+
except Exception as e:
|
|
713
|
+
progress.update(task, completed=1)
|
|
714
|
+
console.print(f"[red]Error fetching notebook: {e}[/red]")
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
progress.update(task, completed=1)
|
|
718
|
+
|
|
719
|
+
notebook = data.get("notebook", {})
|
|
720
|
+
cells = notebook.get("content", {}).get("cells", [])
|
|
721
|
+
|
|
722
|
+
if not cells:
|
|
723
|
+
console.print("[yellow]Notebook has no cells.[/yellow]")
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
if static:
|
|
727
|
+
from ..notebook_renderer import display_notebook_preview
|
|
728
|
+
display_notebook_preview(notebook, console)
|
|
729
|
+
else:
|
|
730
|
+
from ..notebook_renderer import interactive_cell_viewer
|
|
731
|
+
interactive_cell_viewer(cells, console)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
@notebooks.command()
|
|
735
|
+
@click.argument('file_path', type=click.Path(exists=True))
|
|
736
|
+
@click.option('--org', required=True, help='Organization slug')
|
|
737
|
+
@click.option('--title', help='Notebook title (default: filename)')
|
|
738
|
+
@click.option('--description', help='Notebook description')
|
|
739
|
+
@click.option('--public', is_flag=True, help='Make notebook public')
|
|
740
|
+
@click.option('--tags', help='Comma-separated tags')
|
|
741
|
+
@click.pass_context
|
|
742
|
+
def upload(ctx: click.Context, file_path: str, org: str, title: Optional[str],
|
|
743
|
+
description: Optional[str], public: bool, tags: Optional[str]) -> None:
|
|
744
|
+
"""Upload a local .ipynb file."""
|
|
745
|
+
config: Config = ctx.obj['config']
|
|
746
|
+
|
|
747
|
+
if not config.bearer_token:
|
|
748
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
path = Path(file_path)
|
|
752
|
+
if not path.suffix == '.ipynb':
|
|
753
|
+
console.print("[red]Error: File must be a .ipynb file.[/red]")
|
|
754
|
+
return
|
|
755
|
+
|
|
756
|
+
# Read and validate the file
|
|
757
|
+
try:
|
|
758
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
759
|
+
notebook_content = json.load(f)
|
|
760
|
+
|
|
761
|
+
if 'cells' not in notebook_content:
|
|
762
|
+
console.print("[red]Error: Invalid notebook format.[/red]")
|
|
763
|
+
return
|
|
764
|
+
except json.JSONDecodeError:
|
|
765
|
+
console.print("[red]Error: Invalid JSON in notebook file.[/red]")
|
|
766
|
+
return
|
|
767
|
+
except Exception as e:
|
|
768
|
+
console.print(f"[red]Error reading file: {e}[/red]")
|
|
769
|
+
return
|
|
770
|
+
|
|
771
|
+
notebook_title = title or path.stem
|
|
772
|
+
tag_list = [t.strip() for t in tags.split(',')] if tags else []
|
|
773
|
+
|
|
774
|
+
with Progress(
|
|
775
|
+
SpinnerColumn(),
|
|
776
|
+
TextColumn("[progress.description]{task.description}"),
|
|
777
|
+
console=console
|
|
778
|
+
) as progress:
|
|
779
|
+
task = progress.add_task("Uploading notebook...", total=None)
|
|
780
|
+
|
|
781
|
+
try:
|
|
782
|
+
with get_api_client(config) as client:
|
|
783
|
+
payload = {
|
|
784
|
+
"org_slug": org,
|
|
785
|
+
"title": notebook_title,
|
|
786
|
+
"content": notebook_content,
|
|
787
|
+
"is_public": public,
|
|
788
|
+
}
|
|
789
|
+
if description:
|
|
790
|
+
payload["description"] = description
|
|
791
|
+
if tag_list:
|
|
792
|
+
payload["tags"] = tag_list
|
|
793
|
+
|
|
794
|
+
response = client.post("/api/notebooks", json=payload)
|
|
795
|
+
response.raise_for_status()
|
|
796
|
+
data = response.json()
|
|
797
|
+
except httpx.HTTPStatusError as e:
|
|
798
|
+
progress.update(task, completed=1)
|
|
799
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
800
|
+
return
|
|
801
|
+
except Exception as e:
|
|
802
|
+
progress.update(task, completed=1)
|
|
803
|
+
console.print(f"[red]Error uploading notebook: {e}[/red]")
|
|
804
|
+
return
|
|
805
|
+
|
|
806
|
+
progress.update(task, completed=1)
|
|
807
|
+
|
|
808
|
+
notebook = data.get("notebook", {})
|
|
809
|
+
console.print(f"[green]✓ Notebook uploaded successfully![/green]")
|
|
810
|
+
console.print(f" Title: {notebook.get('title')}")
|
|
811
|
+
console.print(f" Slug: {notebook.get('slug')}")
|
|
812
|
+
console.print(f" Visibility: {'Public' if notebook.get('is_public') else 'Private'}")
|
|
813
|
+
|
|
814
|
+
org_slug = notebook.get("organization", {}).get("slug", org)
|
|
815
|
+
slug = notebook.get("slug", "")
|
|
816
|
+
if org_slug and slug:
|
|
817
|
+
url = f"{config.api_url.replace('/api', '')}/{org_slug}/~/notebooks/{slug}"
|
|
818
|
+
console.print(f" URL: [cyan]{url}[/cyan]")
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
@notebooks.command()
|
|
822
|
+
@click.argument('identifier')
|
|
823
|
+
@click.option('--output', '-o', type=click.Path(), help='Output file path')
|
|
824
|
+
@click.pass_context
|
|
825
|
+
def download(ctx: click.Context, identifier: str, output: Optional[str]) -> None:
|
|
826
|
+
"""Download a notebook as .ipynb file."""
|
|
827
|
+
config: Config = ctx.obj['config']
|
|
828
|
+
|
|
829
|
+
if not config.bearer_token:
|
|
830
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
with Progress(
|
|
834
|
+
SpinnerColumn(),
|
|
835
|
+
TextColumn("[progress.description]{task.description}"),
|
|
836
|
+
console=console
|
|
837
|
+
) as progress:
|
|
838
|
+
task = progress.add_task("Downloading notebook...", total=None)
|
|
839
|
+
|
|
840
|
+
try:
|
|
841
|
+
with get_api_client(config) as client:
|
|
842
|
+
response = client.get(f"/api/notebooks/{identifier}/download")
|
|
843
|
+
response.raise_for_status()
|
|
844
|
+
|
|
845
|
+
# Get filename from content-disposition or use identifier
|
|
846
|
+
content_disp = response.headers.get("content-disposition", "")
|
|
847
|
+
if "filename=" in content_disp:
|
|
848
|
+
filename = content_disp.split("filename=")[1].strip('"')
|
|
849
|
+
else:
|
|
850
|
+
filename = f"{identifier}.ipynb"
|
|
851
|
+
|
|
852
|
+
output_path = output or filename
|
|
853
|
+
|
|
854
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
855
|
+
f.write(response.text)
|
|
856
|
+
|
|
857
|
+
except httpx.HTTPStatusError as e:
|
|
858
|
+
progress.update(task, completed=1)
|
|
859
|
+
if e.response.status_code == 404:
|
|
860
|
+
console.print(f"[red]Notebook '{identifier}' not found.[/red]")
|
|
861
|
+
else:
|
|
862
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
863
|
+
return
|
|
864
|
+
except Exception as e:
|
|
865
|
+
progress.update(task, completed=1)
|
|
866
|
+
console.print(f"[red]Error downloading notebook: {e}[/red]")
|
|
867
|
+
return
|
|
868
|
+
|
|
869
|
+
progress.update(task, completed=1)
|
|
870
|
+
|
|
871
|
+
console.print(f"[green]✓ Notebook downloaded to: {output_path}[/green]")
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
@notebooks.command()
|
|
875
|
+
@click.argument('identifier')
|
|
876
|
+
@click.option('--force', '-f', is_flag=True, help='Skip confirmation')
|
|
877
|
+
@click.pass_context
|
|
878
|
+
def delete(ctx: click.Context, identifier: str, force: bool) -> None:
|
|
879
|
+
"""Delete a notebook."""
|
|
880
|
+
config: Config = ctx.obj['config']
|
|
881
|
+
|
|
882
|
+
if not config.bearer_token:
|
|
883
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
884
|
+
return
|
|
885
|
+
|
|
886
|
+
if not force:
|
|
887
|
+
if not Confirm.ask(f"Are you sure you want to delete notebook '{identifier}'?"):
|
|
888
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
889
|
+
return
|
|
890
|
+
|
|
891
|
+
with Progress(
|
|
892
|
+
SpinnerColumn(),
|
|
893
|
+
TextColumn("[progress.description]{task.description}"),
|
|
894
|
+
console=console
|
|
895
|
+
) as progress:
|
|
896
|
+
task = progress.add_task("Deleting notebook...", total=None)
|
|
897
|
+
|
|
898
|
+
try:
|
|
899
|
+
with get_api_client(config) as client:
|
|
900
|
+
response = client.delete(f"/api/notebooks/{identifier}")
|
|
901
|
+
response.raise_for_status()
|
|
902
|
+
except httpx.HTTPStatusError as e:
|
|
903
|
+
progress.update(task, completed=1)
|
|
904
|
+
if e.response.status_code == 404:
|
|
905
|
+
console.print(f"[red]Notebook '{identifier}' not found.[/red]")
|
|
906
|
+
else:
|
|
907
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
908
|
+
return
|
|
909
|
+
except Exception as e:
|
|
910
|
+
progress.update(task, completed=1)
|
|
911
|
+
console.print(f"[red]Error deleting notebook: {e}[/red]")
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
progress.update(task, completed=1)
|
|
915
|
+
|
|
916
|
+
console.print(f"[green]✓ Notebook deleted successfully.[/green]")
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
@notebooks.command()
|
|
920
|
+
@click.argument('identifier')
|
|
921
|
+
@click.pass_context
|
|
922
|
+
def publish(ctx: click.Context, identifier: str) -> None:
|
|
923
|
+
"""Publish a notebook (make it public)."""
|
|
924
|
+
config: Config = ctx.obj['config']
|
|
925
|
+
|
|
926
|
+
if not config.bearer_token:
|
|
927
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
928
|
+
return
|
|
929
|
+
|
|
930
|
+
with Progress(
|
|
931
|
+
SpinnerColumn(),
|
|
932
|
+
TextColumn("[progress.description]{task.description}"),
|
|
933
|
+
console=console
|
|
934
|
+
) as progress:
|
|
935
|
+
task = progress.add_task("Publishing notebook...", total=None)
|
|
936
|
+
|
|
937
|
+
try:
|
|
938
|
+
with get_api_client(config) as client:
|
|
939
|
+
response = client.patch(f"/api/notebooks/{identifier}", json={"is_public": True})
|
|
940
|
+
response.raise_for_status()
|
|
941
|
+
except httpx.HTTPStatusError as e:
|
|
942
|
+
progress.update(task, completed=1)
|
|
943
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
944
|
+
return
|
|
945
|
+
except Exception as e:
|
|
946
|
+
progress.update(task, completed=1)
|
|
947
|
+
console.print(f"[red]Error publishing notebook: {e}[/red]")
|
|
948
|
+
return
|
|
949
|
+
|
|
950
|
+
progress.update(task, completed=1)
|
|
951
|
+
|
|
952
|
+
console.print(f"[green]✓ Notebook published! It is now publicly visible.[/green]")
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
@notebooks.command()
|
|
956
|
+
@click.argument('identifier')
|
|
957
|
+
@click.pass_context
|
|
958
|
+
def unpublish(ctx: click.Context, identifier: str) -> None:
|
|
959
|
+
"""Unpublish a notebook (make it private)."""
|
|
960
|
+
config: Config = ctx.obj['config']
|
|
961
|
+
|
|
962
|
+
if not config.bearer_token:
|
|
963
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
964
|
+
return
|
|
965
|
+
|
|
966
|
+
with Progress(
|
|
967
|
+
SpinnerColumn(),
|
|
968
|
+
TextColumn("[progress.description]{task.description}"),
|
|
969
|
+
console=console
|
|
970
|
+
) as progress:
|
|
971
|
+
task = progress.add_task("Unpublishing notebook...", total=None)
|
|
972
|
+
|
|
973
|
+
try:
|
|
974
|
+
with get_api_client(config) as client:
|
|
975
|
+
response = client.patch(f"/api/notebooks/{identifier}", json={"is_public": False})
|
|
976
|
+
response.raise_for_status()
|
|
977
|
+
except httpx.HTTPStatusError as e:
|
|
978
|
+
progress.update(task, completed=1)
|
|
979
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
980
|
+
return
|
|
981
|
+
except Exception as e:
|
|
982
|
+
progress.update(task, completed=1)
|
|
983
|
+
console.print(f"[red]Error unpublishing notebook: {e}[/red]")
|
|
984
|
+
return
|
|
985
|
+
|
|
986
|
+
progress.update(task, completed=1)
|
|
987
|
+
|
|
988
|
+
console.print(f"[green]✓ Notebook unpublished. It is now private.[/green]")
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
@notebooks.command(name="tags")
|
|
992
|
+
@click.argument('identifier')
|
|
993
|
+
@click.option('--add', 'add_tags', help='Tags to add (comma-separated)')
|
|
994
|
+
@click.option('--remove', 'remove_tags', help='Tags to remove (comma-separated)')
|
|
995
|
+
@click.option('--set', 'set_tags', help='Set tags (replaces existing, comma-separated)')
|
|
996
|
+
@click.pass_context
|
|
997
|
+
def manage_tags(ctx: click.Context, identifier: str, add_tags: Optional[str],
|
|
998
|
+
remove_tags: Optional[str], set_tags: Optional[str]) -> None:
|
|
999
|
+
"""Manage notebook tags."""
|
|
1000
|
+
config: Config = ctx.obj['config']
|
|
1001
|
+
|
|
1002
|
+
if not config.bearer_token:
|
|
1003
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
1004
|
+
return
|
|
1005
|
+
|
|
1006
|
+
if set_tags:
|
|
1007
|
+
# Replace all tags
|
|
1008
|
+
new_tags = [t.strip() for t in set_tags.split(',') if t.strip()]
|
|
1009
|
+
else:
|
|
1010
|
+
# Get current tags first
|
|
1011
|
+
try:
|
|
1012
|
+
with get_api_client(config) as client:
|
|
1013
|
+
response = client.get(f"/api/notebooks/{identifier}", params={"include_content": "false"})
|
|
1014
|
+
response.raise_for_status()
|
|
1015
|
+
data = response.json()
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
console.print(f"[red]Error fetching notebook: {e}[/red]")
|
|
1018
|
+
return
|
|
1019
|
+
|
|
1020
|
+
current_tags = set(t.get("name", "") for t in data.get("notebook", {}).get("tags", []))
|
|
1021
|
+
|
|
1022
|
+
if add_tags:
|
|
1023
|
+
for tag in add_tags.split(','):
|
|
1024
|
+
current_tags.add(tag.strip())
|
|
1025
|
+
|
|
1026
|
+
if remove_tags:
|
|
1027
|
+
for tag in remove_tags.split(','):
|
|
1028
|
+
current_tags.discard(tag.strip())
|
|
1029
|
+
|
|
1030
|
+
new_tags = list(current_tags)
|
|
1031
|
+
|
|
1032
|
+
with Progress(
|
|
1033
|
+
SpinnerColumn(),
|
|
1034
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1035
|
+
console=console
|
|
1036
|
+
) as progress:
|
|
1037
|
+
task = progress.add_task("Updating tags...", total=None)
|
|
1038
|
+
|
|
1039
|
+
try:
|
|
1040
|
+
with get_api_client(config) as client:
|
|
1041
|
+
response = client.patch(f"/api/notebooks/{identifier}", json={"tags": new_tags})
|
|
1042
|
+
response.raise_for_status()
|
|
1043
|
+
except httpx.HTTPStatusError as e:
|
|
1044
|
+
progress.update(task, completed=1)
|
|
1045
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
1046
|
+
return
|
|
1047
|
+
except Exception as e:
|
|
1048
|
+
progress.update(task, completed=1)
|
|
1049
|
+
console.print(f"[red]Error updating tags: {e}[/red]")
|
|
1050
|
+
return
|
|
1051
|
+
|
|
1052
|
+
progress.update(task, completed=1)
|
|
1053
|
+
|
|
1054
|
+
if new_tags:
|
|
1055
|
+
console.print(f"[green]✓ Tags updated: {', '.join(new_tags)}[/green]")
|
|
1056
|
+
else:
|
|
1057
|
+
console.print(f"[green]✓ All tags removed.[/green]")
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
@notebooks.command()
|
|
1061
|
+
@click.argument('identifier')
|
|
1062
|
+
@click.option('--org', required=True, help='Target organization slug')
|
|
1063
|
+
@click.option('--title', help='Custom title for the fork')
|
|
1064
|
+
@click.pass_context
|
|
1065
|
+
def fork(ctx: click.Context, identifier: str, org: str, title: Optional[str]) -> None:
|
|
1066
|
+
"""Fork a public notebook to your organization."""
|
|
1067
|
+
config: Config = ctx.obj['config']
|
|
1068
|
+
|
|
1069
|
+
if not config.bearer_token:
|
|
1070
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
1071
|
+
return
|
|
1072
|
+
|
|
1073
|
+
with Progress(
|
|
1074
|
+
SpinnerColumn(),
|
|
1075
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1076
|
+
console=console
|
|
1077
|
+
) as progress:
|
|
1078
|
+
task = progress.add_task("Forking notebook...", total=None)
|
|
1079
|
+
|
|
1080
|
+
try:
|
|
1081
|
+
with get_api_client(config) as client:
|
|
1082
|
+
payload = {"org_slug": org}
|
|
1083
|
+
if title:
|
|
1084
|
+
payload["title"] = title
|
|
1085
|
+
|
|
1086
|
+
response = client.post(f"/api/notebooks/{identifier}/fork", json=payload)
|
|
1087
|
+
response.raise_for_status()
|
|
1088
|
+
data = response.json()
|
|
1089
|
+
except httpx.HTTPStatusError as e:
|
|
1090
|
+
progress.update(task, completed=1)
|
|
1091
|
+
if e.response.status_code == 404:
|
|
1092
|
+
console.print(f"[red]Notebook '{identifier}' not found.[/red]")
|
|
1093
|
+
elif e.response.status_code == 403:
|
|
1094
|
+
console.print(f"[red]Cannot fork: notebook is not public or you don't have access.[/red]")
|
|
1095
|
+
else:
|
|
1096
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
1097
|
+
return
|
|
1098
|
+
except Exception as e:
|
|
1099
|
+
progress.update(task, completed=1)
|
|
1100
|
+
console.print(f"[red]Error forking notebook: {e}[/red]")
|
|
1101
|
+
return
|
|
1102
|
+
|
|
1103
|
+
progress.update(task, completed=1)
|
|
1104
|
+
|
|
1105
|
+
notebook = data.get("notebook", {})
|
|
1106
|
+
console.print(f"[green]✓ Notebook forked successfully![/green]")
|
|
1107
|
+
console.print(f" Title: {notebook.get('title')}")
|
|
1108
|
+
console.print(f" Slug: {notebook.get('slug')}")
|
|
1109
|
+
|
|
1110
|
+
org_slug = notebook.get("organization", {}).get("slug", org)
|
|
1111
|
+
slug = notebook.get("slug", "")
|
|
1112
|
+
if org_slug and slug:
|
|
1113
|
+
url = f"{config.api_url.replace('/api', '')}/{org_slug}/~/notebooks/{slug}"
|
|
1114
|
+
console.print(f" URL: [cyan]{url}[/cyan]")
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
@notebooks.command()
|
|
1118
|
+
@click.argument('query')
|
|
1119
|
+
@click.option('--org', help='Search within organization')
|
|
1120
|
+
@click.option('--public', is_flag=True, help='Search only public notebooks')
|
|
1121
|
+
@click.option('--limit', default=20, type=int, help='Maximum results')
|
|
1122
|
+
@click.pass_context
|
|
1123
|
+
def search(ctx: click.Context, query: str, org: Optional[str], public: bool, limit: int) -> None:
|
|
1124
|
+
"""Search notebooks by title and description."""
|
|
1125
|
+
config: Config = ctx.obj['config']
|
|
1126
|
+
|
|
1127
|
+
if not config.bearer_token:
|
|
1128
|
+
console.print("[red]Error: Not logged in. Run 'alphai login' first.[/red]")
|
|
1129
|
+
return
|
|
1130
|
+
|
|
1131
|
+
with Progress(
|
|
1132
|
+
SpinnerColumn(),
|
|
1133
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1134
|
+
console=console
|
|
1135
|
+
) as progress:
|
|
1136
|
+
task = progress.add_task(f"Searching for '{query}'...", total=None)
|
|
1137
|
+
|
|
1138
|
+
try:
|
|
1139
|
+
with get_api_client(config) as client:
|
|
1140
|
+
params = {"q": query, "limit": limit}
|
|
1141
|
+
if org:
|
|
1142
|
+
params["org_slug"] = org
|
|
1143
|
+
if public:
|
|
1144
|
+
params["public_only"] = "true"
|
|
1145
|
+
|
|
1146
|
+
response = client.get("/api/notebooks/search", params=params)
|
|
1147
|
+
response.raise_for_status()
|
|
1148
|
+
data = response.json()
|
|
1149
|
+
except httpx.HTTPStatusError as e:
|
|
1150
|
+
progress.update(task, completed=1)
|
|
1151
|
+
console.print(f"[red]Error: {e.response.status_code} - {e.response.text}[/red]")
|
|
1152
|
+
return
|
|
1153
|
+
except Exception as e:
|
|
1154
|
+
progress.update(task, completed=1)
|
|
1155
|
+
console.print(f"[red]Error searching notebooks: {e}[/red]")
|
|
1156
|
+
return
|
|
1157
|
+
|
|
1158
|
+
progress.update(task, completed=1)
|
|
1159
|
+
|
|
1160
|
+
notebooks_data = data.get("notebooks", [])
|
|
1161
|
+
|
|
1162
|
+
if not notebooks_data:
|
|
1163
|
+
console.print(f"[yellow]No notebooks found matching '{query}'.[/yellow]")
|
|
1164
|
+
return
|
|
1165
|
+
|
|
1166
|
+
console.print(f"\n[bold]Search results for '{query}':[/bold]\n")
|
|
1167
|
+
|
|
1168
|
+
from ..notebook_renderer import display_notebooks_table
|
|
1169
|
+
display_notebooks_table(notebooks_data, console)
|
|
1170
|
+
|
|
1171
|
+
total = data.get("total", len(notebooks_data))
|
|
1172
|
+
if total > limit:
|
|
1173
|
+
console.print(f"\n[dim]Showing {len(notebooks_data)} of {total} results.[/dim]")
|