alphai 0.1.2__py3-none-any.whl → 0.2.1__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.
@@ -0,0 +1,350 @@
1
+ """Jupyter commands for alphai CLI.
2
+
3
+ Contains `jupyter lab` and `jupyter notebook` commands with shared implementation.
4
+ """
5
+
6
+ import sys
7
+ import signal
8
+ from typing import Optional, List
9
+
10
+ import click
11
+ from rich.console import Console
12
+
13
+ from ..client import AlphAIClient
14
+ from ..config import Config
15
+ from ..jupyter_manager import JupyterManager
16
+ from ..cleanup import JupyterCleanupManager
17
+ from ..utils import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+ console = Console()
21
+
22
+
23
+ def _get_frontend_url(api_url: str) -> str:
24
+ """Convert API URL to frontend URL for browser opening."""
25
+ if api_url.startswith("http://localhost") or api_url.startswith("https://localhost"):
26
+ return api_url.replace("/api", "").rstrip("/")
27
+ elif "runalph.ai" in api_url:
28
+ if "/api" in api_url:
29
+ return api_url.replace("runalph.ai/api", "runalph.ai").rstrip("/")
30
+ else:
31
+ return api_url.replace("runalph.ai", "runalph.ai").rstrip("/")
32
+ else:
33
+ return api_url.replace("/api", "").rstrip("/")
34
+
35
+
36
+ def _select_organization(client: AlphAIClient) -> str:
37
+ """Interactively select an organization."""
38
+ import questionary
39
+ from rich.progress import Progress, SpinnerColumn, TextColumn
40
+
41
+ logger.debug("Prompting user to select organization")
42
+ console.print("[yellow]No organization specified. Please select one:[/yellow]")
43
+
44
+ with Progress(
45
+ SpinnerColumn(),
46
+ TextColumn("[progress.description]{task.description}"),
47
+ console=console
48
+ ) as progress:
49
+ task = progress.add_task("Fetching organizations...", total=None)
50
+ orgs_data = client.get_organizations()
51
+ progress.update(task, completed=1)
52
+
53
+ if not orgs_data or len(orgs_data) == 0:
54
+ logger.error("No organizations found for user")
55
+ console.print("[red]No organizations found. Please create one first.[/red]")
56
+ sys.exit(1)
57
+
58
+ org_choices = []
59
+ for org_data in orgs_data:
60
+ display_name = f"{org_data.name} ({org_data.slug})"
61
+ org_choices.append(questionary.Choice(title=display_name, value=org_data.slug))
62
+
63
+ selected_org_slug = questionary.select(
64
+ "Select organization (use ↑↓ arrows and press Enter):",
65
+ choices=org_choices,
66
+ style=questionary.Style([
67
+ ('question', 'bold'),
68
+ ('pointer', 'fg:#673ab7 bold'),
69
+ ('highlighted', 'fg:#673ab7 bold'),
70
+ ('selected', 'fg:#cc5454'),
71
+ ('instruction', 'fg:#888888 italic')
72
+ ])
73
+ ).ask()
74
+
75
+ if not selected_org_slug:
76
+ logger.warning("User cancelled organization selection")
77
+ console.print("[red]No organization selected. Exiting.[/red]")
78
+ sys.exit(1)
79
+
80
+ selected_org_name = next((o.name for o in orgs_data if o.slug == selected_org_slug), selected_org_slug)
81
+ console.print(f"[green]✓ Selected organization: {selected_org_name} ({selected_org_slug})[/green]")
82
+ logger.info(f"Organization selected: {selected_org_slug}")
83
+
84
+ return selected_org_slug
85
+
86
+
87
+ def _get_project_name() -> str:
88
+ """Interactively get project name from user."""
89
+ from rich.prompt import Prompt
90
+
91
+ logger.debug("Prompting user to enter project name")
92
+ console.print("[yellow]No project specified. Please enter a project name:[/yellow]")
93
+
94
+ while True:
95
+ project = Prompt.ask("Enter project name")
96
+ if project and project.strip():
97
+ project = project.strip()
98
+ console.print(f"[green]✓ Will create project: {project}[/green]")
99
+ logger.info(f"Project name entered: {project}")
100
+ return project
101
+ else:
102
+ console.print("[red]Project name cannot be empty[/red]")
103
+
104
+
105
+ def _run_jupyter_session(
106
+ ctx: click.Context,
107
+ jupyter_command: List[str],
108
+ port: int,
109
+ app_port: Optional[int],
110
+ org: Optional[str],
111
+ project: Optional[str],
112
+ local_only: bool,
113
+ quiet: bool,
114
+ extra_args: List[str]
115
+ ) -> None:
116
+ """Shared implementation for jupyter lab and jupyter notebook commands.
117
+
118
+ Args:
119
+ ctx: Click context
120
+ jupyter_command: Base command (e.g., ['jupyter', 'lab'] or ['jupyter', 'notebook'])
121
+ port: Jupyter port
122
+ app_port: Optional app port for cloud connection
123
+ org: Organization slug (interactive if not provided)
124
+ project: Project name (interactive if not provided)
125
+ local_only: Skip cloud connection
126
+ quiet: Suppress Jupyter log output
127
+ extra_args: Additional arguments to pass to Jupyter
128
+ """
129
+ command_name = ' '.join(jupyter_command)
130
+ logger.info(f"Starting {command_name} (jupyter_port={port}, app_port={app_port}, local_only={local_only}, quiet={quiet})")
131
+
132
+ config: Config = ctx.obj['config']
133
+ client: AlphAIClient = ctx.obj['client']
134
+ jupyter_manager = JupyterManager(console)
135
+
136
+ # Set up cleanup manager
137
+ cleanup_mgr = JupyterCleanupManager(
138
+ console=console,
139
+ jupyter_manager=jupyter_manager,
140
+ client=client
141
+ )
142
+ cleanup_mgr.install_signal_handlers()
143
+
144
+ try:
145
+ # Check Jupyter is installed
146
+ jupyter_manager.check_jupyter_or_exit("jupyter")
147
+
148
+ # Find an available port (auto-increment if specified port is in use)
149
+ actual_port = jupyter_manager.find_available_port(port)
150
+
151
+ # Generate token
152
+ jupyter_token = jupyter_manager.generate_jupyter_token()
153
+ console.print(f"[cyan]Generated Jupyter token: {jupyter_token[:12]}...[/cyan]")
154
+
155
+ # Start Jupyter (allow remote access if connecting to cloud)
156
+ jupyter_process = jupyter_manager.start_jupyter(
157
+ command=jupyter_command,
158
+ port=actual_port,
159
+ token=jupyter_token,
160
+ extra_args=extra_args,
161
+ allow_remote=not local_only
162
+ )
163
+
164
+ # Wait for Jupyter to be ready
165
+ if not jupyter_manager.wait_for_jupyter_ready(actual_port, timeout=30):
166
+ console.print("[red]Failed to start Jupyter[/red]")
167
+ jupyter_process.kill()
168
+ sys.exit(1)
169
+
170
+ console.print("[green]✓ Jupyter started successfully[/green]")
171
+
172
+ # Connect to cloud if not local-only
173
+ connection_data = None
174
+ if not local_only:
175
+ if not config.bearer_token:
176
+ logger.info("Not authenticated, skipping cloud connection")
177
+ console.print("[yellow]⚠ Not authenticated - running locally only[/yellow]")
178
+ console.print("[dim]Run 'alphai login' to enable cloud access[/dim]")
179
+ else:
180
+ # Interactive org/project selection
181
+ if not org:
182
+ org = _select_organization(client)
183
+ if not project:
184
+ project = _get_project_name()
185
+
186
+ # Ensure connector is available
187
+ if not jupyter_manager.ensure_cloudflared():
188
+ logger.info("Connector not available, running locally only")
189
+ console.print("[yellow]⚠ Connector not available - running locally only[/yellow]")
190
+ else:
191
+ # Connect to cloud
192
+ logger.info(f"Connecting {org}/{project} to cloud")
193
+ console.print("[yellow]Connecting to cloud...[/yellow]")
194
+
195
+ try:
196
+ connect_kwargs = {
197
+ 'org_slug': org,
198
+ 'project_name': project,
199
+ 'jupyter_port': actual_port,
200
+ 'jupyter_token': jupyter_token
201
+ }
202
+ if app_port is not None:
203
+ connect_kwargs['app_port'] = app_port
204
+
205
+ connection_data = client.create_tunnel_with_project(**connect_kwargs)
206
+
207
+ if connection_data:
208
+ # Setup connector
209
+ if jupyter_manager.setup_cloudflared_tunnel(connection_data.cloudflared_token):
210
+ console.print("[green]✓ Connected to cloud[/green]")
211
+ jupyter_manager.set_tunnel_data(connection_data)
212
+
213
+ # Store IDs for cleanup
214
+ cleanup_mgr.set_tunnel(connection_data.id)
215
+ if connection_data.project_data and hasattr(connection_data.project_data, 'id'):
216
+ cleanup_mgr.set_project(connection_data.project_data.id)
217
+ else:
218
+ console.print("[yellow]⚠ Connection setup failed - running locally only[/yellow]")
219
+ connection_data = None
220
+
221
+ except Exception as e:
222
+ logger.error(f"Failed to connect to cloud: {e}", exc_info=True)
223
+ console.print("[yellow]⚠ Cloud connection failed - running locally only[/yellow]")
224
+ connection_data = None
225
+
226
+ # Display access information
227
+ jupyter_manager.display_jupyter_info(
228
+ jupyter_port=actual_port,
229
+ token=jupyter_token,
230
+ tunnel_data=connection_data,
231
+ org=org,
232
+ project=project,
233
+ api_url=config.api_url,
234
+ app_port=app_port
235
+ )
236
+
237
+ # Monitor and wait for interrupt
238
+ try:
239
+ jupyter_manager.monitor_jupyter(show_logs=not quiet)
240
+ except KeyboardInterrupt:
241
+ pass
242
+
243
+ finally:
244
+ # Cleanup
245
+ cleanup_mgr.cleanup()
246
+ cleanup_mgr.restore_signal_handlers()
247
+
248
+
249
+ @click.group()
250
+ @click.pass_context
251
+ def jupyter(ctx: click.Context) -> None:
252
+ """Run Jupyter with automatic cloud sync to runalph.ai."""
253
+ pass
254
+
255
+
256
+ @jupyter.command(
257
+ name='lab',
258
+ context_settings=dict(ignore_unknown_options=True, allow_extra_args=True)
259
+ )
260
+ @click.option('--port', default=8888, help='Port for Jupyter Lab (default: 8888)')
261
+ @click.option('--app-port', default=None, type=int, help='Additional port for your app (optional)')
262
+ @click.option('--org', help='Organization slug (interactive if not provided)')
263
+ @click.option('--project', help='Project name (interactive if not provided)')
264
+ @click.option('--local-only', is_flag=True, help='Run locally only, skip cloud connection')
265
+ @click.option('--quiet', is_flag=True, help='Suppress Jupyter log output')
266
+ @click.pass_context
267
+ def jupyter_lab(
268
+ ctx: click.Context,
269
+ port: int,
270
+ app_port: Optional[int],
271
+ org: Optional[str],
272
+ project: Optional[str],
273
+ local_only: bool,
274
+ quiet: bool
275
+ ) -> None:
276
+ """Start Jupyter Lab with automatic cloud sync.
277
+
278
+ This command starts Jupyter Lab locally and connects it to your
279
+ cloud workspace, making it accessible from anywhere.
280
+
281
+ All standard Jupyter Lab arguments are supported and passed through.
282
+
283
+ Examples:
284
+ alphai jupyter lab
285
+ alphai jupyter lab --port 9999
286
+ alphai jupyter lab --port 8888 --app-port 5000
287
+ alphai jupyter lab --org my-org --project my-project
288
+ alphai jupyter lab --local-only
289
+ alphai jupyter lab --quiet # Suppress Jupyter logs
290
+ alphai jupyter lab --ServerApp.root_dir=/path/to/notebooks
291
+ """
292
+ _run_jupyter_session(
293
+ ctx=ctx,
294
+ jupyter_command=['jupyter', 'lab'],
295
+ port=port,
296
+ app_port=app_port,
297
+ org=org,
298
+ project=project,
299
+ local_only=local_only,
300
+ quiet=quiet,
301
+ extra_args=ctx.args
302
+ )
303
+
304
+
305
+ @jupyter.command(
306
+ name='notebook',
307
+ context_settings=dict(ignore_unknown_options=True, allow_extra_args=True)
308
+ )
309
+ @click.option('--port', default=8888, help='Port for Jupyter Notebook (default: 8888)')
310
+ @click.option('--app-port', default=None, type=int, help='Additional port for your app (optional)')
311
+ @click.option('--org', help='Organization slug (interactive if not provided)')
312
+ @click.option('--project', help='Project name (interactive if not provided)')
313
+ @click.option('--local-only', is_flag=True, help='Run locally only, skip cloud connection')
314
+ @click.option('--quiet', is_flag=True, help='Suppress Jupyter log output')
315
+ @click.pass_context
316
+ def jupyter_notebook(
317
+ ctx: click.Context,
318
+ port: int,
319
+ app_port: Optional[int],
320
+ org: Optional[str],
321
+ project: Optional[str],
322
+ local_only: bool,
323
+ quiet: bool
324
+ ) -> None:
325
+ """Start Jupyter Notebook with automatic cloud sync.
326
+
327
+ This command starts Jupyter Notebook locally and connects it to your
328
+ cloud workspace, making it accessible from anywhere.
329
+
330
+ All standard Jupyter Notebook arguments are supported and passed through.
331
+
332
+ Examples:
333
+ alphai jupyter notebook
334
+ alphai jupyter notebook --port 9999
335
+ alphai jupyter notebook --port 8888 --app-port 5000
336
+ alphai jupyter notebook --org my-org --project my-project
337
+ alphai jupyter notebook --local-only
338
+ alphai jupyter notebook --quiet # Suppress Jupyter logs
339
+ """
340
+ _run_jupyter_session(
341
+ ctx=ctx,
342
+ jupyter_command=['jupyter', 'notebook'],
343
+ port=port,
344
+ app_port=app_port,
345
+ org=org,
346
+ project=project,
347
+ local_only=local_only,
348
+ quiet=quiet,
349
+ extra_args=ctx.args
350
+ )