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,27 @@
1
+ """Organization commands for alphai CLI."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+
7
+ from ..client import AlphAIClient
8
+
9
+ console = Console()
10
+
11
+
12
+ @click.command()
13
+ @click.pass_context
14
+ def orgs(ctx: click.Context) -> None:
15
+ """List your organizations."""
16
+ client: AlphAIClient = ctx.obj['client']
17
+
18
+ with Progress(
19
+ SpinnerColumn(),
20
+ TextColumn("[progress.description]{task.description}"),
21
+ console=console
22
+ ) as progress:
23
+ task = progress.add_task("Fetching organizations...", total=None)
24
+ orgs_data = client.get_organizations()
25
+ progress.update(task, completed=1)
26
+
27
+ client.display_organizations(orgs_data)
@@ -0,0 +1,35 @@
1
+ """Project commands for alphai CLI."""
2
+
3
+ from typing import Optional
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+
9
+ from ..client import AlphAIClient
10
+ from ..config import Config
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.command()
16
+ @click.option('--org', help='Organization slug to filter by')
17
+ @click.pass_context
18
+ def projects(ctx: click.Context, org: Optional[str]) -> None:
19
+ """List your projects."""
20
+ client: AlphAIClient = ctx.obj['client']
21
+ config: Config = ctx.obj['config']
22
+
23
+ # Use provided org or current org
24
+ org_id = org or config.current_org
25
+
26
+ with Progress(
27
+ SpinnerColumn(),
28
+ TextColumn("[progress.description]{task.description}"),
29
+ console=console
30
+ ) as progress:
31
+ task = progress.add_task("Fetching projects...", total=None)
32
+ projects_data = client.get_projects(org_id)
33
+ progress.update(task, completed=1)
34
+
35
+ client.display_projects(projects_data)
alphai/config.py CHANGED
@@ -11,10 +11,9 @@ from pydantic import BaseModel, Field
11
11
  class Config(BaseModel):
12
12
  """Configuration model for alphai CLI."""
13
13
 
14
- api_url: str = Field(default="https://www.runalph.ai", description="API base URL")
14
+ api_url: str = Field(default="https://www.runalph.ai/api", description="API base URL")
15
15
  bearer_token: Optional[str] = Field(default=None, description="Bearer token for API authentication")
16
- current_org: Optional[str] = Field(default=None, description="Current organization ID")
17
- current_project: Optional[str] = Field(default=None, description="Current project ID")
16
+ current_org: Optional[str] = Field(default=None, description="Current organization slug")
18
17
  debug: bool = Field(default=False, description="Enable debug mode")
19
18
 
20
19
  @classmethod
@@ -80,9 +79,20 @@ class Config(BaseModel):
80
79
  self.bearer_token = None
81
80
  self.save()
82
81
 
82
+ @property
83
+ def base_url(self) -> str:
84
+ """Get the base URL (without /api suffix) for auth and frontend URLs."""
85
+ if self.api_url.endswith("/api"):
86
+ return self.api_url[:-4]
87
+ return self.api_url.rstrip("/")
88
+
83
89
  def to_sdk_config(self) -> Dict[str, Any]:
84
- """Convert to SDK configuration format."""
90
+ """Convert to SDK configuration format.
91
+
92
+ Note: The SDK expects the base URL without /api suffix,
93
+ as it constructs paths like /api/orgs internally.
94
+ """
85
95
  return {
86
96
  "bearer_auth": self.bearer_token,
87
- "server_url": self.api_url,
97
+ "server_url": self.base_url,
88
98
  }
alphai/docker.py CHANGED
@@ -8,6 +8,11 @@ from rich.console import Console
8
8
  from rich.progress import Progress, SpinnerColumn, TextColumn
9
9
  from rich.panel import Panel
10
10
 
11
+ from .utils import get_logger
12
+ from . import exceptions
13
+
14
+ logger = get_logger(__name__)
15
+
11
16
 
12
17
  class DockerManager:
13
18
  """Manage Docker operations for the alphai CLI."""
@@ -22,8 +27,11 @@ class DockerManager:
22
27
  if self._docker_available is not None:
23
28
  return self._docker_available
24
29
 
30
+ logger.debug("Checking Docker availability")
31
+
25
32
  # Check if docker command exists
26
33
  if not shutil.which("docker"):
34
+ logger.warning("Docker command not found in PATH")
27
35
  self._docker_available = False
28
36
  return False
29
37
 
@@ -36,8 +44,17 @@ class DockerManager:
36
44
  timeout=10
37
45
  )
38
46
  self._docker_available = result.returncode == 0
47
+ if self._docker_available:
48
+ logger.info("Docker is available and running")
49
+ else:
50
+ logger.warning("Docker daemon is not running")
39
51
  return self._docker_available
40
- except (subprocess.TimeoutExpired, subprocess.SubprocessError):
52
+ except subprocess.TimeoutExpired:
53
+ logger.error("Docker info command timed out")
54
+ self._docker_available = False
55
+ return False
56
+ except subprocess.SubprocessError as e:
57
+ logger.error(f"Error checking Docker availability: {e}")
41
58
  self._docker_available = False
42
59
  return False
43
60
 
@@ -89,10 +106,12 @@ class DockerManager:
89
106
  command: Optional[str] = None
90
107
  ) -> Optional[Any]:
91
108
  """Run a Docker container with the specified configuration."""
109
+ logger.info(f"Starting container from image: {image}")
92
110
  if not self.is_docker_available():
111
+ logger.error("Cannot run container: Docker is not available")
93
112
  self.console.print("[red]Error: Docker is not available or not running[/red]")
94
113
  self.console.print("[yellow]Please install Docker and ensure it's running[/yellow]")
95
- return None
114
+ raise exceptions.DockerNotAvailableError()
96
115
 
97
116
  # Build docker run command
98
117
  cmd = ["docker", "run"]
@@ -142,9 +161,11 @@ class DockerManager:
142
161
  )
143
162
 
144
163
  if not check_result.stdout.strip():
164
+ logger.info(f"Image {image} not found locally, pulling...")
145
165
  self.console.print(f"[yellow]Image {image} not found locally, pulling...[/yellow]")
146
166
  if not self.pull_image(image):
147
- return None
167
+ logger.error(f"Failed to pull image: {image}")
168
+ raise exceptions.DockerError(f"Failed to pull image: {image}")
148
169
 
149
170
  # Run the container
150
171
  if detach:
@@ -166,10 +187,12 @@ class DockerManager:
166
187
 
167
188
  if result.returncode == 0:
168
189
  container_id = result.stdout.strip()
169
- return MockContainer(container_id)
190
+ logger.info(f"Container started successfully: {container_id[:12]}")
191
+ return ContainerHandle(container_id)
170
192
  else:
193
+ logger.error(f"Failed to start container: {result.stderr}")
171
194
  self.console.print(f"[red]Error starting container: {result.stderr}[/red]")
172
- return None
195
+ raise exceptions.ContainerError(f"Failed to start container: {result.stderr}")
173
196
  else:
174
197
  # Interactive mode
175
198
  self.console.print(f"[green]Starting interactive container from {image}...[/green]")
@@ -178,17 +201,21 @@ class DockerManager:
178
201
  try:
179
202
  # Run interactively without capturing output
180
203
  result = subprocess.run(cmd)
181
- return MockContainer("interactive")
204
+ return ContainerHandle("interactive")
182
205
  except KeyboardInterrupt:
183
206
  self.console.print("\n[yellow]Container stopped by user[/yellow]")
184
- return MockContainer("interactive")
207
+ return ContainerHandle("interactive")
185
208
 
186
209
  except subprocess.TimeoutExpired:
210
+ logger.error("Timeout starting container")
187
211
  self.console.print("[red]Timeout starting container[/red]")
188
- return None
212
+ raise exceptions.TimeoutError("start container", 30)
213
+ except exceptions.DockerError:
214
+ raise
189
215
  except Exception as e:
216
+ logger.error(f"Unexpected error running container: {e}", exc_info=True)
190
217
  self.console.print(f"[red]Error running container: {e}[/red]")
191
- return None
218
+ raise exceptions.ContainerError(f"Error running container: {e}")
192
219
 
193
220
  def list_containers(self, all_containers: bool = False) -> list:
194
221
  """List Docker containers."""
@@ -262,8 +289,9 @@ class DockerManager:
262
289
 
263
290
  def install_cloudflared_in_container(self, container_id: str) -> bool:
264
291
  """Install cloudflared in a running container."""
292
+ logger.info(f"Installing cloudflared in container {container_id[:12]}")
265
293
  if not self.is_docker_available():
266
- return False
294
+ raise exceptions.DockerNotAvailableError()
267
295
 
268
296
  try:
269
297
  # Detect the container's package manager and architecture
@@ -271,8 +299,9 @@ class DockerManager:
271
299
  architecture = self._detect_architecture(container_id)
272
300
 
273
301
  if not package_manager:
302
+ logger.error(f"No compatible package manager found in container {container_id[:12]}")
274
303
  self.console.print("[red]Unsupported container: No compatible package manager found[/red]")
275
- return False
304
+ raise exceptions.CloudflaredError("No compatible package manager found")
276
305
 
277
306
  # Commands to install cloudflared based on package manager
278
307
  install_commands = self._get_install_commands(package_manager, architecture)
@@ -298,15 +327,20 @@ class DockerManager:
298
327
 
299
328
  progress.update(task, advance=1)
300
329
 
301
- self.console.print("[green]✓ cloudflared installed successfully[/green]")
330
+ logger.info(f"Connector installed successfully in container {container_id[:12]}")
331
+ self.console.print("[green]✓ Connector installed[/green]")
302
332
  return True
303
333
 
304
334
  except subprocess.TimeoutExpired:
305
- self.console.print("[red]Timeout installing cloudflared[/red]")
306
- return False
335
+ logger.error(f"Timeout installing connector in container {container_id[:12]}")
336
+ self.console.print("[red]Timeout installing connector[/red]")
337
+ raise exceptions.TimeoutError("install connector", 60)
338
+ except exceptions.CloudflaredError:
339
+ raise
307
340
  except Exception as e:
308
- self.console.print(f"[red]Error installing cloudflared: {e}[/red]")
309
- return False
341
+ logger.error(f"Error installing connector: {e}", exc_info=True)
342
+ self.console.print(f"[red]Error installing connector: {e}[/red]")
343
+ raise exceptions.CloudflaredError(f"Failed to install connector: {e}")
310
344
 
311
345
  def _detect_package_manager(self, container_id: str) -> Optional[str]:
312
346
  """Detect the package manager available in the container."""
@@ -329,7 +363,7 @@ class DockerManager:
329
363
  )
330
364
  if result.returncode == 0:
331
365
  return pm_name
332
- except:
366
+ except Exception:
333
367
  continue
334
368
 
335
369
  return None
@@ -353,7 +387,7 @@ class DockerManager:
353
387
  'i386': '386'
354
388
  }
355
389
  return arch_map.get(arch, 'amd64')
356
- except:
390
+ except Exception:
357
391
  pass
358
392
 
359
393
  return 'amd64' # Default fallback
@@ -403,7 +437,7 @@ class DockerManager:
403
437
  ]
404
438
 
405
439
  def setup_tunnel_in_container(self, container_id: str, tunnel_token: str) -> bool:
406
- """Set up cloudflared tunnel service in a running container."""
440
+ """Start cloudflared connector in container as a background process."""
407
441
  if not self.is_docker_available():
408
442
  return False
409
443
 
@@ -413,30 +447,30 @@ class DockerManager:
413
447
  TextColumn("[progress.description]{task.description}"),
414
448
  console=self.console
415
449
  ) as progress:
416
- task = progress.add_task("Setting up tunnel service...", total=None)
450
+ task = progress.add_task("Establishing connection...", total=None)
417
451
 
418
- # Install the tunnel service with the token
452
+ # Run cloudflared in background (no service installation needed)
419
453
  result = subprocess.run(
420
- ["docker", "exec", "--user", "root", container_id, "cloudflared", "service", "install", tunnel_token],
454
+ ["docker", "exec", "-d", container_id, "cloudflared", "tunnel", "run", "--token", tunnel_token],
421
455
  capture_output=True,
422
456
  text=True,
423
- timeout=30
457
+ timeout=10
424
458
  )
425
459
 
426
460
  progress.update(task, completed=1)
427
461
 
428
462
  if result.returncode == 0:
429
- self.console.print("[green]✓ Tunnel service installed successfully[/green]")
463
+ self.console.print("[green]✓ Connected[/green]")
430
464
  return True
431
465
  else:
432
- self.console.print(f"[red]Error setting up tunnel service: {result.stderr}[/red]")
466
+ self.console.print(f"[red]Error connecting: {result.stderr}[/red]")
433
467
  return False
434
468
 
435
469
  except subprocess.TimeoutExpired:
436
- self.console.print("[red]Timeout setting up tunnel service[/red]")
470
+ self.console.print("[red]Connection timed out[/red]")
437
471
  return False
438
472
  except Exception as e:
439
- self.console.print(f"[red]Error setting up tunnel service: {e}[/red]")
473
+ self.console.print(f"[red]Error connecting: {e}[/red]")
440
474
  return False
441
475
 
442
476
  def exec_command(self, container_id: str, command: str) -> Optional[str]:
@@ -643,8 +677,8 @@ class DockerManager:
643
677
  import secrets
644
678
  return secrets.token_hex(32) # 64-character hex token
645
679
 
646
- def uninstall_cloudflared_service(self, container_id: str) -> bool:
647
- """Uninstall cloudflared service from a running container."""
680
+ def stop_cloudflared_in_container(self, container_id: str) -> bool:
681
+ """Stop cloudflared processes in a running container."""
648
682
  if not self.is_docker_available():
649
683
  return False
650
684
 
@@ -654,31 +688,32 @@ class DockerManager:
654
688
  TextColumn("[progress.description]{task.description}"),
655
689
  console=self.console
656
690
  ) as progress:
657
- task = progress.add_task("Uninstalling cloudflared service...", total=None)
691
+ task = progress.add_task("Stopping cloudflared...", total=None)
658
692
 
659
- # Uninstall the cloudflared service
693
+ # Kill all cloudflared processes in the container
660
694
  result = subprocess.run(
661
- ["docker", "exec", "--user", "root", container_id, "cloudflared", "service", "uninstall"],
695
+ ["docker", "exec", container_id, "pkill", "-f", "cloudflared"],
662
696
  capture_output=True,
663
697
  text=True,
664
- timeout=30
698
+ timeout=10
665
699
  )
666
700
 
667
701
  progress.update(task, completed=1)
668
702
 
669
- if result.returncode == 0:
670
- self.console.print("[green]✓ Cloudflared service uninstalled successfully[/green]")
703
+ # pkill returns 0 if processes were found and killed, 1 if none found
704
+ if result.returncode in [0, 1]:
705
+ self.console.print("[green]✓ Connection closed[/green]")
671
706
  return True
672
707
  else:
673
- # Don't treat this as a hard error since the service might not be installed
674
- self.console.print(f"[yellow]Warning: Could not uninstall cloudflared service: {result.stderr}[/yellow]")
708
+ # Don't treat this as a hard error
709
+ self.console.print(f"[yellow]Warning: Could not close connection: {result.stderr}[/yellow]")
675
710
  return True
676
711
 
677
712
  except subprocess.TimeoutExpired:
678
- self.console.print("[yellow]Warning: Timeout uninstalling cloudflared service[/yellow]")
713
+ self.console.print("[yellow]Warning: Timeout closing connection[/yellow]")
679
714
  return True
680
715
  except Exception as e:
681
- self.console.print(f"[yellow]Warning: Error uninstalling cloudflared service: {e}[/yellow]")
716
+ self.console.print(f"[yellow]Warning: Error closing connection: {e}[/yellow]")
682
717
  return True
683
718
 
684
719
  def stop_and_remove_container(self, container_id: str, force: bool = False) -> bool:
@@ -729,9 +764,9 @@ class DockerManager:
729
764
  try:
730
765
  # Check if container is running
731
766
  if self.is_container_running(container_id):
732
- # Step 1: Uninstall cloudflared service if container is running
733
- self.console.print("[yellow]Cleaning up cloudflared service...[/yellow]")
734
- if not self.uninstall_cloudflared_service(container_id):
767
+ # Step 1: Stop connection service if container is running
768
+ self.console.print("[yellow]Closing connection...[/yellow]")
769
+ if not self.stop_cloudflared_in_container(container_id):
735
770
  success = False
736
771
 
737
772
  # Give it a moment for the service to stop
@@ -755,10 +790,10 @@ class DockerManager:
755
790
  return False
756
791
 
757
792
 
758
- class MockContainer:
759
- """Mock container object to simulate Docker container."""
793
+ class ContainerHandle:
794
+ """Lightweight container reference returned from Docker operations."""
760
795
 
761
796
  def __init__(self, container_id: str):
762
- """Initialize mock container."""
797
+ """Initialize container handle."""
763
798
  self.id = container_id
764
799
  self.short_id = container_id[:12] if len(container_id) > 12 else container_id
alphai/exceptions.py ADDED
@@ -0,0 +1,122 @@
1
+ """Custom exceptions for alphai CLI."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class AlphAIException(Exception):
7
+ """Base exception for all alphai errors."""
8
+
9
+ def __init__(self, message: str, details: Optional[str] = None):
10
+ """Initialize exception with message and optional details."""
11
+ self.message = message
12
+ self.details = details
13
+ super().__init__(self.message)
14
+
15
+ def __str__(self) -> str:
16
+ """String representation of the exception."""
17
+ if self.details:
18
+ return f"{self.message}\nDetails: {self.details}"
19
+ return self.message
20
+
21
+
22
+ class AuthenticationError(AlphAIException):
23
+ """Raised when authentication fails."""
24
+ pass
25
+
26
+
27
+ class AuthorizationError(AlphAIException):
28
+ """Raised when user lacks permissions for an operation."""
29
+ pass
30
+
31
+
32
+ class APIError(AlphAIException):
33
+ """Raised when API request fails."""
34
+
35
+ def __init__(self, message: str, status_code: Optional[int] = None, details: Optional[str] = None):
36
+ """Initialize API error with status code."""
37
+ self.status_code = status_code
38
+ super().__init__(message, details)
39
+
40
+ def __str__(self) -> str:
41
+ """String representation including status code."""
42
+ base = super().__str__()
43
+ if self.status_code:
44
+ return f"{base} (Status: {self.status_code})"
45
+ return base
46
+
47
+
48
+ class DockerError(AlphAIException):
49
+ """Raised when Docker operations fail."""
50
+ pass
51
+
52
+
53
+ class DockerNotAvailableError(DockerError):
54
+ """Raised when Docker is not installed or not running."""
55
+
56
+ def __init__(self):
57
+ super().__init__(
58
+ "Docker is not available or not running",
59
+ "Please install Docker and ensure it's running. Visit https://docs.docker.com/get-docker/"
60
+ )
61
+
62
+
63
+ class ContainerError(DockerError):
64
+ """Raised when container operations fail."""
65
+ pass
66
+
67
+
68
+ class TunnelError(AlphAIException):
69
+ """Raised when tunnel operations fail."""
70
+ pass
71
+
72
+
73
+ class CloudflaredError(TunnelError):
74
+ """Raised when cloudflared operations fail."""
75
+ pass
76
+
77
+
78
+ class ConfigurationError(AlphAIException):
79
+ """Raised when configuration is invalid."""
80
+ pass
81
+
82
+
83
+ class ValidationError(AlphAIException):
84
+ """Raised when input validation fails."""
85
+ pass
86
+
87
+
88
+ class NetworkError(AlphAIException):
89
+ """Raised when network operations fail."""
90
+ pass
91
+
92
+
93
+ class TimeoutError(AlphAIException):
94
+ """Raised when an operation times out."""
95
+
96
+ def __init__(self, operation: str, timeout_seconds: int):
97
+ """Initialize timeout error."""
98
+ super().__init__(
99
+ f"Operation '{operation}' timed out after {timeout_seconds} seconds",
100
+ "Try increasing the timeout or check your network connection"
101
+ )
102
+ self.operation = operation
103
+ self.timeout_seconds = timeout_seconds
104
+
105
+
106
+ class ResourceNotFoundError(AlphAIException):
107
+ """Raised when a requested resource is not found."""
108
+
109
+ def __init__(self, resource_type: str, resource_id: str):
110
+ """Initialize resource not found error."""
111
+ super().__init__(
112
+ f"{resource_type} '{resource_id}' not found",
113
+ f"Verify the {resource_type.lower()} ID is correct"
114
+ )
115
+ self.resource_type = resource_type
116
+ self.resource_id = resource_id
117
+
118
+
119
+ class JupyterError(AlphAIException):
120
+ """Raised when Jupyter operations fail."""
121
+ pass
122
+