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,473 @@
|
|
|
1
|
+
"""Terminal rendering for Jupyter notebooks using Rich."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.columns import Columns
|
|
13
|
+
from rich.rule import Rule
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def format_relative_time(timestamp: str) -> str:
|
|
17
|
+
"""Format a timestamp as relative time (e.g., '2 hours ago')."""
|
|
18
|
+
try:
|
|
19
|
+
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
|
20
|
+
now = datetime.now(dt.tzinfo)
|
|
21
|
+
diff = now - dt
|
|
22
|
+
|
|
23
|
+
seconds = diff.total_seconds()
|
|
24
|
+
if seconds < 60:
|
|
25
|
+
return "just now"
|
|
26
|
+
elif seconds < 3600:
|
|
27
|
+
minutes = int(seconds / 60)
|
|
28
|
+
return f"{minutes}m ago"
|
|
29
|
+
elif seconds < 86400:
|
|
30
|
+
hours = int(seconds / 3600)
|
|
31
|
+
return f"{hours}h ago"
|
|
32
|
+
elif seconds < 604800:
|
|
33
|
+
days = int(seconds / 86400)
|
|
34
|
+
return f"{days}d ago"
|
|
35
|
+
elif seconds < 2592000:
|
|
36
|
+
weeks = int(seconds / 604800)
|
|
37
|
+
return f"{weeks}w ago"
|
|
38
|
+
else:
|
|
39
|
+
return dt.strftime("%Y-%m-%d")
|
|
40
|
+
except Exception:
|
|
41
|
+
return timestamp[:10] if len(timestamp) >= 10 else timestamp
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_visibility_badge(is_public: bool) -> Text:
|
|
45
|
+
"""Create a visibility badge."""
|
|
46
|
+
if is_public:
|
|
47
|
+
return Text("● Public", style="green")
|
|
48
|
+
else:
|
|
49
|
+
return Text("○ Private", style="dim")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format_stat(value: Optional[int], icon: str) -> str:
|
|
53
|
+
"""Format a statistic with icon."""
|
|
54
|
+
return f"{icon} {value or 0}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def display_notebooks_table(notebooks: List[Dict[str, Any]], console: Console) -> None:
|
|
58
|
+
"""Display notebooks in a rich table."""
|
|
59
|
+
table = Table(show_header=True, header_style="bold", expand=True)
|
|
60
|
+
table.add_column("Title", style="cyan", no_wrap=False, ratio=3)
|
|
61
|
+
table.add_column("Organization", style="blue", no_wrap=True, ratio=1)
|
|
62
|
+
table.add_column("Status", justify="center", ratio=1)
|
|
63
|
+
table.add_column("Stats", justify="center", ratio=1)
|
|
64
|
+
table.add_column("Updated", justify="right", style="dim", ratio=1)
|
|
65
|
+
|
|
66
|
+
for notebook in notebooks:
|
|
67
|
+
title = notebook.get("title", "Untitled")
|
|
68
|
+
description = notebook.get("description", "")
|
|
69
|
+
|
|
70
|
+
# Build title cell with description
|
|
71
|
+
title_text = Text()
|
|
72
|
+
title_text.append(title, style="bold")
|
|
73
|
+
if description:
|
|
74
|
+
title_text.append(f"\n{description[:60]}{'...' if len(description) > 60 else ''}", style="dim")
|
|
75
|
+
|
|
76
|
+
# Organization
|
|
77
|
+
org = notebook.get("organizations", {})
|
|
78
|
+
org_name = org.get("name", "") if isinstance(org, dict) else ""
|
|
79
|
+
|
|
80
|
+
# Visibility
|
|
81
|
+
is_public = notebook.get("is_public", False)
|
|
82
|
+
visibility = get_visibility_badge(is_public)
|
|
83
|
+
|
|
84
|
+
# Stats
|
|
85
|
+
likes = notebook.get("like_count", 0) or 0
|
|
86
|
+
bookmarks = notebook.get("bookmark_count", 0) or 0
|
|
87
|
+
views = notebook.get("view_count", 0) or 0
|
|
88
|
+
stats = f"♥ {likes} ★ {bookmarks} 👁 {views}"
|
|
89
|
+
|
|
90
|
+
# Updated time
|
|
91
|
+
updated = format_relative_time(notebook.get("updated_at", ""))
|
|
92
|
+
|
|
93
|
+
table.add_row(title_text, org_name, visibility, stats, updated)
|
|
94
|
+
|
|
95
|
+
console.print(table)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def display_notebook_info(notebook: Dict[str, Any], console: Console) -> None:
|
|
99
|
+
"""Display detailed notebook information in a panel."""
|
|
100
|
+
title = notebook.get("title", "Untitled")
|
|
101
|
+
description = notebook.get("description", "No description")
|
|
102
|
+
slug = notebook.get("slug", "")
|
|
103
|
+
is_public = notebook.get("is_public", False)
|
|
104
|
+
|
|
105
|
+
# Organization info
|
|
106
|
+
org = notebook.get("organizations", {})
|
|
107
|
+
org_name = org.get("name", "Unknown") if isinstance(org, dict) else "Unknown"
|
|
108
|
+
org_slug = org.get("slug", "") if isinstance(org, dict) else ""
|
|
109
|
+
|
|
110
|
+
# Stats
|
|
111
|
+
likes = notebook.get("like_count", 0) or 0
|
|
112
|
+
bookmarks = notebook.get("bookmark_count", 0) or 0
|
|
113
|
+
forks = notebook.get("fork_count", 0) or 0
|
|
114
|
+
views = notebook.get("view_count", 0) or 0
|
|
115
|
+
|
|
116
|
+
# Timestamps
|
|
117
|
+
created = format_relative_time(notebook.get("created_at", ""))
|
|
118
|
+
updated = format_relative_time(notebook.get("updated_at", ""))
|
|
119
|
+
|
|
120
|
+
# Tags
|
|
121
|
+
tags = notebook.get("tags", [])
|
|
122
|
+
tag_names = [t.get("name", "") for t in tags if isinstance(t, dict)]
|
|
123
|
+
|
|
124
|
+
# Build info text
|
|
125
|
+
info_lines = []
|
|
126
|
+
info_lines.append(f"[bold cyan]{title}[/bold cyan]")
|
|
127
|
+
info_lines.append("")
|
|
128
|
+
info_lines.append(f"[dim]{description}[/dim]")
|
|
129
|
+
info_lines.append("")
|
|
130
|
+
info_lines.append(f"[bold]Organization:[/bold] {org_name} [dim]({org_slug})[/dim]")
|
|
131
|
+
info_lines.append(f"[bold]Slug:[/bold] {slug}")
|
|
132
|
+
info_lines.append(f"[bold]Visibility:[/bold] {'Public' if is_public else 'Private'}")
|
|
133
|
+
info_lines.append("")
|
|
134
|
+
|
|
135
|
+
if tag_names:
|
|
136
|
+
tags_str = " ".join(f"[on blue] {t} [/on blue]" for t in tag_names)
|
|
137
|
+
info_lines.append(f"[bold]Tags:[/bold] {tags_str}")
|
|
138
|
+
info_lines.append("")
|
|
139
|
+
|
|
140
|
+
info_lines.append(f"[bold]Stats:[/bold] ♥ {likes} likes ★ {bookmarks} bookmarks 🔀 {forks} forks 👁 {views} views")
|
|
141
|
+
info_lines.append("")
|
|
142
|
+
info_lines.append(f"[bold]Created:[/bold] {created} [bold]Updated:[/bold] {updated}")
|
|
143
|
+
|
|
144
|
+
# User permissions
|
|
145
|
+
if notebook.get("can_edit"):
|
|
146
|
+
info_lines.append("")
|
|
147
|
+
info_lines.append("[green]✓ You can edit this notebook[/green]")
|
|
148
|
+
|
|
149
|
+
panel = Panel(
|
|
150
|
+
"\n".join(info_lines),
|
|
151
|
+
title="Notebook Info",
|
|
152
|
+
border_style="blue"
|
|
153
|
+
)
|
|
154
|
+
console.print(panel)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def display_notebook_preview(notebook: Dict[str, Any], console: Console,
|
|
158
|
+
max_cells: int = 20) -> None:
|
|
159
|
+
"""Display notebook content preview with cells shown in terminal."""
|
|
160
|
+
# First show info
|
|
161
|
+
display_notebook_info(notebook, console)
|
|
162
|
+
|
|
163
|
+
content = notebook.get("content", {})
|
|
164
|
+
cells = content.get("cells", [])
|
|
165
|
+
|
|
166
|
+
if not cells:
|
|
167
|
+
console.print("\n[yellow]Notebook has no cells.[/yellow]")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Summary
|
|
171
|
+
code_cells = sum(1 for c in cells if c.get("cell_type") == "code")
|
|
172
|
+
markdown_cells = sum(1 for c in cells if c.get("cell_type") == "markdown")
|
|
173
|
+
|
|
174
|
+
console.print(f"\n[bold]Cells:[/bold] {len(cells)} total ({code_cells} code, {markdown_cells} markdown)")
|
|
175
|
+
|
|
176
|
+
console.print("")
|
|
177
|
+
console.print(Rule("Cell Contents"))
|
|
178
|
+
console.print("")
|
|
179
|
+
|
|
180
|
+
for i, cell in enumerate(cells[:max_cells]):
|
|
181
|
+
cell_type = cell.get("cell_type", "unknown")
|
|
182
|
+
source = cell.get("source", [])
|
|
183
|
+
|
|
184
|
+
# Handle source as list or string
|
|
185
|
+
if isinstance(source, list):
|
|
186
|
+
source_text = "".join(source)
|
|
187
|
+
else:
|
|
188
|
+
source_text = str(source)
|
|
189
|
+
|
|
190
|
+
# Cell header
|
|
191
|
+
cell_icon = "📝" if cell_type == "markdown" else "💻" if cell_type == "code" else "❓"
|
|
192
|
+
console.print(f"[bold]{cell_icon} Cell {i + 1}[/bold] [dim]({cell_type})[/dim]")
|
|
193
|
+
|
|
194
|
+
if cell_type == "markdown":
|
|
195
|
+
# Render markdown
|
|
196
|
+
try:
|
|
197
|
+
md = Markdown(source_text[:500] + ("..." if len(source_text) > 500 else ""))
|
|
198
|
+
console.print(Panel(md, border_style="dim"))
|
|
199
|
+
except Exception:
|
|
200
|
+
console.print(Panel(source_text[:500], border_style="dim"))
|
|
201
|
+
|
|
202
|
+
elif cell_type == "code":
|
|
203
|
+
# Syntax highlight code
|
|
204
|
+
# Truncate very long cells
|
|
205
|
+
display_source = source_text[:1000]
|
|
206
|
+
if len(source_text) > 1000:
|
|
207
|
+
display_source += "\n... (truncated)"
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
syntax = Syntax(display_source, "python", theme="monokai", line_numbers=True)
|
|
211
|
+
console.print(Panel(syntax, border_style="green"))
|
|
212
|
+
except Exception:
|
|
213
|
+
console.print(Panel(display_source, border_style="green"))
|
|
214
|
+
|
|
215
|
+
# Show outputs if any
|
|
216
|
+
outputs = cell.get("outputs", [])
|
|
217
|
+
if outputs:
|
|
218
|
+
console.print("[dim] Outputs:[/dim]")
|
|
219
|
+
for output in outputs[:3]: # Limit outputs shown
|
|
220
|
+
output_type = output.get("output_type", "")
|
|
221
|
+
|
|
222
|
+
if output_type == "stream":
|
|
223
|
+
text = output.get("text", [])
|
|
224
|
+
if isinstance(text, list):
|
|
225
|
+
text = "".join(text)
|
|
226
|
+
text = str(text)[:200]
|
|
227
|
+
console.print(f" [dim]{text}[/dim]")
|
|
228
|
+
|
|
229
|
+
elif output_type in ["execute_result", "display_data"]:
|
|
230
|
+
data = output.get("data", {})
|
|
231
|
+
if "text/plain" in data:
|
|
232
|
+
plain = data["text/plain"]
|
|
233
|
+
if isinstance(plain, list):
|
|
234
|
+
plain = "".join(plain)
|
|
235
|
+
plain = str(plain)[:200]
|
|
236
|
+
console.print(f" [cyan]{plain}[/cyan]")
|
|
237
|
+
elif "image/png" in data:
|
|
238
|
+
console.print(" [yellow]📊 [Image output][/yellow]")
|
|
239
|
+
elif "application/vnd.plotly.v1+json" in data:
|
|
240
|
+
console.print(" [yellow]📈 [Plotly chart][/yellow]")
|
|
241
|
+
|
|
242
|
+
elif output_type == "error":
|
|
243
|
+
ename = output.get("ename", "Error")
|
|
244
|
+
evalue = output.get("evalue", "")
|
|
245
|
+
console.print(f" [red]❌ {ename}: {evalue[:100]}[/red]")
|
|
246
|
+
|
|
247
|
+
console.print("")
|
|
248
|
+
|
|
249
|
+
if len(cells) > max_cells:
|
|
250
|
+
console.print(f"[dim]... and {len(cells) - max_cells} more cells. Use --max-cells to see more, or download the notebook.[/dim]")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def render_single_cell(cell: Dict[str, Any], index: int, total: int, console: Console) -> None:
|
|
254
|
+
"""Render a single cell with its content and outputs."""
|
|
255
|
+
cell_type = cell.get("cell_type", "unknown")
|
|
256
|
+
source = cell.get("source", [])
|
|
257
|
+
|
|
258
|
+
# Handle source as list or string
|
|
259
|
+
if isinstance(source, list):
|
|
260
|
+
source_text = "".join(source)
|
|
261
|
+
else:
|
|
262
|
+
source_text = str(source)
|
|
263
|
+
|
|
264
|
+
# Clear screen and show header
|
|
265
|
+
console.clear()
|
|
266
|
+
|
|
267
|
+
# Header with navigation info
|
|
268
|
+
cell_icon = "📝" if cell_type == "markdown" else "💻" if cell_type == "code" else "❓"
|
|
269
|
+
header = Text()
|
|
270
|
+
header.append(f"{cell_icon} Cell {index + 1} of {total}", style="bold cyan")
|
|
271
|
+
header.append(f" ({cell_type})", style="dim")
|
|
272
|
+
console.print(header)
|
|
273
|
+
console.print()
|
|
274
|
+
|
|
275
|
+
if cell_type == "markdown":
|
|
276
|
+
# Render markdown
|
|
277
|
+
try:
|
|
278
|
+
md = Markdown(source_text)
|
|
279
|
+
console.print(Panel(md, border_style="blue", title="Markdown", title_align="left"))
|
|
280
|
+
except Exception:
|
|
281
|
+
console.print(Panel(source_text, border_style="blue", title="Markdown", title_align="left"))
|
|
282
|
+
|
|
283
|
+
elif cell_type == "code":
|
|
284
|
+
# Syntax highlight code
|
|
285
|
+
try:
|
|
286
|
+
syntax = Syntax(source_text, "python", theme="monokai", line_numbers=True)
|
|
287
|
+
console.print(Panel(syntax, border_style="green", title="Python", title_align="left"))
|
|
288
|
+
except Exception:
|
|
289
|
+
console.print(Panel(source_text, border_style="green", title="Code", title_align="left"))
|
|
290
|
+
|
|
291
|
+
# Show outputs if any
|
|
292
|
+
outputs = cell.get("outputs", [])
|
|
293
|
+
if outputs:
|
|
294
|
+
console.print()
|
|
295
|
+
console.print("[bold]Output:[/bold]")
|
|
296
|
+
for output in outputs:
|
|
297
|
+
output_type = output.get("output_type", "")
|
|
298
|
+
|
|
299
|
+
if output_type == "stream":
|
|
300
|
+
text = output.get("text", [])
|
|
301
|
+
if isinstance(text, list):
|
|
302
|
+
text = "".join(text)
|
|
303
|
+
console.print(Panel(str(text), border_style="dim", title=f"stdout", title_align="left"))
|
|
304
|
+
|
|
305
|
+
elif output_type in ["execute_result", "display_data"]:
|
|
306
|
+
data = output.get("data", {})
|
|
307
|
+
if "text/plain" in data:
|
|
308
|
+
plain = data["text/plain"]
|
|
309
|
+
if isinstance(plain, list):
|
|
310
|
+
plain = "".join(plain)
|
|
311
|
+
console.print(Panel(str(plain), border_style="cyan", title="Result", title_align="left"))
|
|
312
|
+
if "image/png" in data:
|
|
313
|
+
console.print("[yellow] 📊 [Image output - view in browser][/yellow]")
|
|
314
|
+
if "application/vnd.plotly.v1+json" in data:
|
|
315
|
+
console.print("[yellow] 📈 [Plotly chart - view in browser][/yellow]")
|
|
316
|
+
|
|
317
|
+
elif output_type == "error":
|
|
318
|
+
ename = output.get("ename", "Error")
|
|
319
|
+
evalue = output.get("evalue", "")
|
|
320
|
+
traceback = output.get("traceback", [])
|
|
321
|
+
error_text = f"{ename}: {evalue}"
|
|
322
|
+
if traceback:
|
|
323
|
+
# Clean ANSI codes from traceback
|
|
324
|
+
import re
|
|
325
|
+
clean_tb = "\n".join(re.sub(r'\x1b\[[0-9;]*m', '', line) for line in traceback[:5])
|
|
326
|
+
error_text = clean_tb
|
|
327
|
+
console.print(Panel(error_text, border_style="red", title="Error", title_align="left"))
|
|
328
|
+
else:
|
|
329
|
+
console.print(Panel(source_text[:500], border_style="yellow"))
|
|
330
|
+
|
|
331
|
+
# Footer with navigation hints
|
|
332
|
+
console.print()
|
|
333
|
+
nav_hints = Text()
|
|
334
|
+
if index > 0:
|
|
335
|
+
nav_hints.append("← ", style="bold")
|
|
336
|
+
nav_hints.append("prev ", style="dim")
|
|
337
|
+
if index < total - 1:
|
|
338
|
+
nav_hints.append("→ ", style="bold")
|
|
339
|
+
nav_hints.append("next ", style="dim")
|
|
340
|
+
nav_hints.append("q ", style="bold red")
|
|
341
|
+
nav_hints.append("quit", style="dim")
|
|
342
|
+
console.print(nav_hints)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def interactive_cell_viewer(cells: List[Dict[str, Any]], console: Console) -> None:
|
|
346
|
+
"""Interactive viewer that allows scrolling through cells."""
|
|
347
|
+
if not cells:
|
|
348
|
+
console.print("[yellow]No cells in notebook.[/yellow]")
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
import sys
|
|
352
|
+
|
|
353
|
+
current_index = 0
|
|
354
|
+
total = len(cells)
|
|
355
|
+
|
|
356
|
+
# Platform-specific key reading
|
|
357
|
+
try:
|
|
358
|
+
import tty
|
|
359
|
+
import termios
|
|
360
|
+
|
|
361
|
+
def get_key():
|
|
362
|
+
"""Read a single keypress (Unix)."""
|
|
363
|
+
fd = sys.stdin.fileno()
|
|
364
|
+
old_settings = termios.tcgetattr(fd)
|
|
365
|
+
try:
|
|
366
|
+
tty.setraw(fd)
|
|
367
|
+
ch = sys.stdin.read(1)
|
|
368
|
+
# Handle arrow keys (escape sequences)
|
|
369
|
+
if ch == '\x1b':
|
|
370
|
+
ch2 = sys.stdin.read(1)
|
|
371
|
+
if ch2 == '[':
|
|
372
|
+
ch3 = sys.stdin.read(1)
|
|
373
|
+
if ch3 == 'C': # Right arrow
|
|
374
|
+
return 'right'
|
|
375
|
+
elif ch3 == 'D': # Left arrow
|
|
376
|
+
return 'left'
|
|
377
|
+
elif ch3 == 'A': # Up arrow
|
|
378
|
+
return 'left'
|
|
379
|
+
elif ch3 == 'B': # Down arrow
|
|
380
|
+
return 'right'
|
|
381
|
+
return ch
|
|
382
|
+
finally:
|
|
383
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
384
|
+
except ImportError:
|
|
385
|
+
# Windows fallback
|
|
386
|
+
try:
|
|
387
|
+
import msvcrt
|
|
388
|
+
|
|
389
|
+
def get_key():
|
|
390
|
+
"""Read a single keypress (Windows)."""
|
|
391
|
+
ch = msvcrt.getch()
|
|
392
|
+
if ch in (b'\x00', b'\xe0'): # Special key prefix
|
|
393
|
+
ch2 = msvcrt.getch()
|
|
394
|
+
if ch2 == b'M': # Right arrow
|
|
395
|
+
return 'right'
|
|
396
|
+
elif ch2 == b'K': # Left arrow
|
|
397
|
+
return 'left'
|
|
398
|
+
elif ch2 == b'H': # Up arrow
|
|
399
|
+
return 'left'
|
|
400
|
+
elif ch2 == b'P': # Down arrow
|
|
401
|
+
return 'right'
|
|
402
|
+
return ch.decode('utf-8', errors='ignore')
|
|
403
|
+
except ImportError:
|
|
404
|
+
# Fallback to simple input
|
|
405
|
+
def get_key():
|
|
406
|
+
"""Fallback key reading."""
|
|
407
|
+
return input("Press n/p/q: ").strip().lower()[:1] or 'n'
|
|
408
|
+
|
|
409
|
+
while True:
|
|
410
|
+
render_single_cell(cells[current_index], current_index, total, console)
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
key = get_key()
|
|
414
|
+
except (KeyboardInterrupt, EOFError):
|
|
415
|
+
console.clear()
|
|
416
|
+
break
|
|
417
|
+
|
|
418
|
+
if key in ('q', 'Q', '\x03'): # q, Q, or Ctrl+C
|
|
419
|
+
console.clear()
|
|
420
|
+
break
|
|
421
|
+
elif key in ('right', 'n', 'j', ' ', '\r'): # Next
|
|
422
|
+
if current_index < total - 1:
|
|
423
|
+
current_index += 1
|
|
424
|
+
elif key in ('left', 'p', 'k'): # Previous
|
|
425
|
+
if current_index > 0:
|
|
426
|
+
current_index -= 1
|
|
427
|
+
elif key == 'g': # Go to first
|
|
428
|
+
current_index = 0
|
|
429
|
+
elif key == 'G': # Go to last
|
|
430
|
+
current_index = total - 1
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def display_notebook_cell_summary(cells: List[Dict[str, Any]], console: Console) -> None:
|
|
434
|
+
"""Display a summary of notebook cells."""
|
|
435
|
+
if not cells:
|
|
436
|
+
console.print("[yellow]No cells in notebook.[/yellow]")
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
table = Table(show_header=True, header_style="bold")
|
|
440
|
+
table.add_column("#", justify="right", style="dim")
|
|
441
|
+
table.add_column("Type", justify="center")
|
|
442
|
+
table.add_column("Preview", no_wrap=False)
|
|
443
|
+
table.add_column("Outputs", justify="center")
|
|
444
|
+
|
|
445
|
+
for i, cell in enumerate(cells[:30]):
|
|
446
|
+
cell_type = cell.get("cell_type", "unknown")
|
|
447
|
+
source = cell.get("source", [])
|
|
448
|
+
outputs = cell.get("outputs", [])
|
|
449
|
+
|
|
450
|
+
if isinstance(source, list):
|
|
451
|
+
source_text = "".join(source)
|
|
452
|
+
else:
|
|
453
|
+
source_text = str(source)
|
|
454
|
+
|
|
455
|
+
# Truncate preview
|
|
456
|
+
preview = source_text[:80].replace("\n", " ")
|
|
457
|
+
if len(source_text) > 80:
|
|
458
|
+
preview += "..."
|
|
459
|
+
|
|
460
|
+
type_style = "cyan" if cell_type == "markdown" else "green" if cell_type == "code" else "yellow"
|
|
461
|
+
|
|
462
|
+
table.add_row(
|
|
463
|
+
str(i + 1),
|
|
464
|
+
Text(cell_type, style=type_style),
|
|
465
|
+
preview,
|
|
466
|
+
str(len(outputs)) if outputs else "-"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
console.print(table)
|
|
470
|
+
|
|
471
|
+
if len(cells) > 30:
|
|
472
|
+
console.print(f"\n[dim]Showing first 30 of {len(cells)} cells.[/dim]")
|
|
473
|
+
|
alphai/utils.py
CHANGED
|
@@ -2,10 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
+
import logging
|
|
5
6
|
from typing import Dict, Any, Optional
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
import json
|
|
8
9
|
from datetime import datetime
|
|
10
|
+
from logging.handlers import RotatingFileHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_logging(debug: bool = False) -> logging.Logger:
|
|
14
|
+
"""Setup logging configuration for alphai.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
debug: If True, set log level to DEBUG and output to console
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Configured logger instance
|
|
21
|
+
"""
|
|
22
|
+
from .config import Config
|
|
23
|
+
|
|
24
|
+
log_dir = Config.get_config_dir() / "logs"
|
|
25
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
# Create logger
|
|
28
|
+
logger = logging.getLogger("alphai")
|
|
29
|
+
logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
30
|
+
|
|
31
|
+
# Remove existing handlers to avoid duplicates
|
|
32
|
+
logger.handlers.clear()
|
|
33
|
+
|
|
34
|
+
# File handler with rotation (max 5 files of 10MB each)
|
|
35
|
+
log_file = log_dir / "alphai.log"
|
|
36
|
+
file_handler = RotatingFileHandler(
|
|
37
|
+
log_file,
|
|
38
|
+
maxBytes=10 * 1024 * 1024, # 10MB
|
|
39
|
+
backupCount=5
|
|
40
|
+
)
|
|
41
|
+
file_handler.setLevel(logging.DEBUG)
|
|
42
|
+
file_formatter = logging.Formatter(
|
|
43
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
|
|
44
|
+
)
|
|
45
|
+
file_handler.setFormatter(file_formatter)
|
|
46
|
+
logger.addHandler(file_handler)
|
|
47
|
+
|
|
48
|
+
# Console handler (only in debug mode)
|
|
49
|
+
if debug:
|
|
50
|
+
console_handler = logging.StreamHandler()
|
|
51
|
+
console_handler.setLevel(logging.DEBUG)
|
|
52
|
+
console_formatter = logging.Formatter(
|
|
53
|
+
'%(levelname)s - %(message)s'
|
|
54
|
+
)
|
|
55
|
+
console_handler.setFormatter(console_formatter)
|
|
56
|
+
logger.addHandler(console_handler)
|
|
57
|
+
|
|
58
|
+
# Prevent propagation to root logger
|
|
59
|
+
logger.propagate = False
|
|
60
|
+
|
|
61
|
+
logger.debug(f"Logging initialized. Debug mode: {debug}, Log file: {log_file}")
|
|
62
|
+
|
|
63
|
+
return logger
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_logger(name: str = "alphai") -> logging.Logger:
|
|
67
|
+
"""Get a logger instance.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name: Logger name (default: alphai)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Logger instance
|
|
74
|
+
"""
|
|
75
|
+
return logging.getLogger(name)
|
|
9
76
|
|
|
10
77
|
|
|
11
78
|
def format_datetime(dt_string: Optional[str]) -> str:
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alphai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A CLI tool and Python package for the runalph.ai platform
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Andrew Chang <andrew@runalph.ai>
|
|
6
6
|
Project-URL: Homepage, https://runalph.ai
|
|
7
7
|
Project-URL: Documentation, https://docs.runalph.ai
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
9
|
-
Project-URL: Issues, https://github.com/
|
|
8
|
+
Project-URL: Repository, https://github.com/alph-ai/alphai
|
|
9
|
+
Project-URL: Issues, https://github.com/alph-ai/alphai/issues
|
|
10
10
|
Keywords: cli,api,data-science,alph
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
-
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.10
|
|
17
19
|
Description-Content-Type: text/markdown
|
|
18
20
|
Requires-Dist: click>=8.1.0
|
|
19
21
|
Requires-Dist: rich>=13.0.0
|
|
20
|
-
Requires-Dist: alph-sdk>=0.
|
|
22
|
+
Requires-Dist: alph-sdk>=0.5.0
|
|
21
23
|
Requires-Dist: httpx>=0.25.0
|
|
22
24
|
Requires-Dist: pydantic>=2.0.0
|
|
23
|
-
Requires-Dist: keyring>=24.0.0
|
|
24
|
-
Requires-Dist: typer>=0.9.0
|
|
25
25
|
Requires-Dist: questionary>=2.1.0
|
|
26
|
+
Requires-Dist: jupyterlab>=4.5.1
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
29
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
alphai/__init__.py,sha256=bqujmQ7WrxKzmGVS_QFOJCTFi3u1EFzV4eaxiBv7uSE,1063
|
|
2
|
+
alphai/auth.py,sha256=rmMlikW9M0wVlNb8FXixrvcemPLaKXFO3Ocaiijjge8,15589
|
|
3
|
+
alphai/cleanup.py,sha256=JEgysHuvK0sR1X4M5u0mBdKB8Kz1KIyF7fhW0i4uU0Q,12250
|
|
4
|
+
alphai/cli.py,sha256=HTSqjDOJPAGj4WqsCFavjUEGqx4rzQ_a6CahhfRyU04,4660
|
|
5
|
+
alphai/client.py,sha256=RD6XnpBkHaJY2pqOxIouEHZxUmkSVVfg_43oIrbK8wA,18892
|
|
6
|
+
alphai/config.py,sha256=b6494qVN_lN2Xuy64W7Ceo5fAnP5f97Z2wH5-lIHMQQ,3449
|
|
7
|
+
alphai/docker.py,sha256=t86SnK2_QeQjqn3yisWLod5DBlDOAF05qpEZcFxYZD8,32387
|
|
8
|
+
alphai/exceptions.py,sha256=ShSHxwc7WnsMu2ugRUxNgF4C9bipKu0_PvfoK7591dE,3401
|
|
9
|
+
alphai/jupyter_manager.py,sha256=DFhXGpDsWq3UE7A1TckLjlGDCJL3AYjKnW5-Ru6su1o,23712
|
|
10
|
+
alphai/notebook_renderer.py,sha256=RFaoUnicMYfTUJOd2dUQjab-ZBpomyK5RbpbjiGBBjU,18393
|
|
11
|
+
alphai/utils.py,sha256=-zqDCRBS_D20xGzRCN-BVuwyt1jmF2-A6Y4_arOTvUE,7651
|
|
12
|
+
alphai/commands/__init__.py,sha256=8ZFsPVBcT4KqgVv7xLI_uiv_Sb2FGDyStFF5Gcsjnd4,528
|
|
13
|
+
alphai/commands/config.py,sha256=y_kIxoC7t0TV5pRVYP5pKCDz2xWybWv43PeA1R07L80,1780
|
|
14
|
+
alphai/commands/docker.py,sha256=alDM2ZBhGGHRgayIwPz3gKkX8dLIPzNrBXlHq2_qi7s,23782
|
|
15
|
+
alphai/commands/jupyter.py,sha256=UzKZvloB0J4iwri59PZ_EK_imXjKVDGvG4hyAUJ24Pk,13367
|
|
16
|
+
alphai/commands/notebooks.py,sha256=flYKfVVTvlooB1ijFmSJLlK-j28RiphmCAP7JNbFl80,43264
|
|
17
|
+
alphai/commands/orgs.py,sha256=qhcuXXTCfwxspe3SAgpaITyyqPye2747jPNz91ysHv4,735
|
|
18
|
+
alphai/commands/projects.py,sha256=-udwF_cew5DUR9m_BhHjjm2cFEJb1HBtWeiKdzrNPlQ,988
|
|
19
|
+
alphai-0.2.0.dist-info/METADATA,sha256=6x3WJ2pDWmrQxuxcvlclcJnJY5GtP_OdI1MgO0fV9S8,9634
|
|
20
|
+
alphai-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
alphai-0.2.0.dist-info/entry_points.txt,sha256=ITOwv5erK-gjYw47KsOoENHvIAUW_ynEsEMLZ7GHZwA,43
|
|
22
|
+
alphai-0.2.0.dist-info/top_level.txt,sha256=dFmybyT4Kzcgpsccun8RnBxkm9lK0Y0TPfaVe2FyxNY,7
|
|
23
|
+
alphai-0.2.0.dist-info/RECORD,,
|
alphai-0.1.1.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
alphai/__init__.py,sha256=3qW-BaHhXUKB-PfTfUzHguwN-LuXaRhbWlev3P8YmFE,294
|
|
2
|
-
alphai/auth.py,sha256=fk6Ow7GwL8xbK03bKyiyT9XMUXXJllJe9XqWv-ENXrs,14283
|
|
3
|
-
alphai/cli.py,sha256=nWE0I7bkDRkbfCYnvXu3qsvvoWoQ87ryX98MUG9CttE,38779
|
|
4
|
-
alphai/client.py,sha256=Q5N9WdOB0eGUTZeNQPBMBj7kI13TbjPQfGlEJLQvFKg,16042
|
|
5
|
-
alphai/config.py,sha256=Mp8DJt0CDF_holnHDHzfCs7uRkgSCAQOkLNqGPnKKyo,3143
|
|
6
|
-
alphai/docker.py,sha256=eLTYmO1NFhTOsn7ZTE1DggVIJKrsnzOgRSCHwXpxkp4,30294
|
|
7
|
-
alphai/utils.py,sha256=tXGoaIjM1RAXKsjEQ0PjA9vrM-KFf5DCQ8DB83peVBU,5718
|
|
8
|
-
alphai-0.1.1.dist-info/METADATA,sha256=1Lhg2O70nc61P8G4yF-3IjoL2XULvix7DsXPhf2hCcc,9605
|
|
9
|
-
alphai-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
alphai-0.1.1.dist-info/entry_points.txt,sha256=ITOwv5erK-gjYw47KsOoENHvIAUW_ynEsEMLZ7GHZwA,43
|
|
11
|
-
alphai-0.1.1.dist-info/top_level.txt,sha256=dFmybyT4Kzcgpsccun8RnBxkm9lK0Y0TPfaVe2FyxNY,7
|
|
12
|
-
alphai-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|