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
alphai/__init__.py
CHANGED
|
@@ -1,10 +1,48 @@
|
|
|
1
1
|
"""alphai - A CLI tool and Python package for the runalph.ai platform."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
4
|
__author__ = "American Data Science"
|
|
5
5
|
__email__ = "support@americandatascience.com"
|
|
6
6
|
|
|
7
7
|
from .client import AlphAIClient
|
|
8
8
|
from .config import Config
|
|
9
|
+
from .jupyter_manager import JupyterManager
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
AlphAIException,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
AuthorizationError,
|
|
14
|
+
APIError,
|
|
15
|
+
DockerError,
|
|
16
|
+
DockerNotAvailableError,
|
|
17
|
+
ContainerError,
|
|
18
|
+
TunnelError,
|
|
19
|
+
CloudflaredError,
|
|
20
|
+
ConfigurationError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
NetworkError,
|
|
23
|
+
TimeoutError,
|
|
24
|
+
ResourceNotFoundError,
|
|
25
|
+
JupyterError,
|
|
26
|
+
)
|
|
9
27
|
|
|
10
|
-
__all__ = [
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AlphAIClient",
|
|
30
|
+
"Config",
|
|
31
|
+
"JupyterManager",
|
|
32
|
+
"__version__",
|
|
33
|
+
"AlphAIException",
|
|
34
|
+
"AuthenticationError",
|
|
35
|
+
"AuthorizationError",
|
|
36
|
+
"APIError",
|
|
37
|
+
"DockerError",
|
|
38
|
+
"DockerNotAvailableError",
|
|
39
|
+
"ContainerError",
|
|
40
|
+
"TunnelError",
|
|
41
|
+
"CloudflaredError",
|
|
42
|
+
"ConfigurationError",
|
|
43
|
+
"ValidationError",
|
|
44
|
+
"NetworkError",
|
|
45
|
+
"TimeoutError",
|
|
46
|
+
"ResourceNotFoundError",
|
|
47
|
+
"JupyterError",
|
|
48
|
+
]
|
alphai/auth.py
CHANGED
|
@@ -16,6 +16,10 @@ from rich.prompt import Prompt
|
|
|
16
16
|
from rich.panel import Panel
|
|
17
17
|
|
|
18
18
|
from .config import Config
|
|
19
|
+
from .utils import get_logger
|
|
20
|
+
from . import exceptions
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
class CallbackHandler(BaseHTTPRequestHandler):
|
|
@@ -47,7 +51,7 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
47
51
|
else:
|
|
48
52
|
# No success page, redirect directly to dashboard
|
|
49
53
|
redirect_url = api_url
|
|
50
|
-
except:
|
|
54
|
+
except Exception:
|
|
51
55
|
# Network error or timeout, redirect directly to dashboard
|
|
52
56
|
redirect_url = api_url
|
|
53
57
|
|
|
@@ -99,15 +103,19 @@ class AuthManager:
|
|
|
99
103
|
|
|
100
104
|
def login_with_token(self, token: str) -> bool:
|
|
101
105
|
"""Login with a provided bearer token."""
|
|
106
|
+
logger.info("Attempting login with provided token")
|
|
102
107
|
if not token.strip():
|
|
108
|
+
logger.error("Empty token provided")
|
|
103
109
|
self.console.print("[red]Error: Empty token provided[/red]")
|
|
104
|
-
|
|
110
|
+
raise exceptions.ValidationError("Token cannot be empty")
|
|
105
111
|
|
|
106
112
|
# Validate the token
|
|
107
113
|
if self.validate_token(token):
|
|
108
114
|
self.config.set_bearer_token(token)
|
|
115
|
+
logger.info("Login successful with token")
|
|
109
116
|
return True
|
|
110
117
|
else:
|
|
118
|
+
logger.error("Invalid token provided")
|
|
111
119
|
self.console.print("[red]Error: Invalid token[/red]")
|
|
112
120
|
return False
|
|
113
121
|
|
|
@@ -171,6 +179,7 @@ class AuthManager:
|
|
|
171
179
|
|
|
172
180
|
def browser_login(self, port: int = 8080) -> bool:
|
|
173
181
|
"""Perform browser-based login using OAuth flow."""
|
|
182
|
+
logger.info("Starting browser-based authentication")
|
|
174
183
|
# Try to find an available port
|
|
175
184
|
for attempt_port in range(port, port + 10):
|
|
176
185
|
try:
|
|
@@ -179,18 +188,21 @@ class AuthManager:
|
|
|
179
188
|
httpd.timeout = 60 # 1 minute timeout
|
|
180
189
|
httpd.token = None
|
|
181
190
|
httpd.error = None
|
|
182
|
-
httpd.api_url = self.config.
|
|
191
|
+
httpd.api_url = self.config.base_url # Pass base_url for redirects (not /api)
|
|
192
|
+
logger.debug(f"Callback server started on port {attempt_port}")
|
|
183
193
|
break
|
|
184
194
|
except OSError:
|
|
195
|
+
logger.debug(f"Port {attempt_port} not available, trying next")
|
|
185
196
|
continue
|
|
186
197
|
else:
|
|
198
|
+
logger.error("Could not find available port for OAuth callback")
|
|
187
199
|
self.console.print("[red]Error: Could not find an available port for callback[/red]")
|
|
188
200
|
return False
|
|
189
201
|
|
|
190
|
-
redirect_uri = f"http://
|
|
202
|
+
redirect_uri = f"http://127.0.0.1:{attempt_port}"
|
|
191
203
|
|
|
192
|
-
# Construct the authentication URL
|
|
193
|
-
auth_url = f"{self.config.
|
|
204
|
+
# Construct the authentication URL (auth is at base URL, not /api)
|
|
205
|
+
auth_url = f"{self.config.base_url}/auth/cli"
|
|
194
206
|
auth_params = {
|
|
195
207
|
"redirect_uri": redirect_uri,
|
|
196
208
|
"response_type": "token",
|
|
@@ -242,6 +254,7 @@ class AuthManager:
|
|
|
242
254
|
|
|
243
255
|
def validate_token(self, token: str) -> bool:
|
|
244
256
|
"""Validate a bearer token by making a test API call."""
|
|
257
|
+
logger.debug("Validating bearer token")
|
|
245
258
|
try:
|
|
246
259
|
with httpx.Client() as client:
|
|
247
260
|
headers = {
|
|
@@ -251,31 +264,38 @@ class AuthManager:
|
|
|
251
264
|
|
|
252
265
|
# Test the token by trying to get organizations
|
|
253
266
|
response = client.get(
|
|
254
|
-
f"{self.config.api_url}/
|
|
267
|
+
f"{self.config.api_url}/orgs",
|
|
255
268
|
headers=headers,
|
|
256
269
|
timeout=10.0
|
|
257
270
|
)
|
|
258
271
|
|
|
259
272
|
# Check if the response is successful
|
|
260
273
|
if response.status_code in (200, 201):
|
|
274
|
+
logger.info("Token validation successful")
|
|
261
275
|
return True
|
|
262
276
|
elif response.status_code == 401:
|
|
277
|
+
logger.error("Token validation failed: Invalid or expired token")
|
|
263
278
|
self.console.print("[red]Error: Invalid or expired token[/red]")
|
|
264
279
|
return False
|
|
265
280
|
elif response.status_code == 403:
|
|
281
|
+
logger.error("Token validation failed: Access forbidden")
|
|
266
282
|
self.console.print("[red]Error: Access forbidden - check your permissions[/red]")
|
|
267
283
|
return False
|
|
268
284
|
else:
|
|
285
|
+
logger.error(f"Token validation failed: API returned status {response.status_code}")
|
|
269
286
|
self.console.print(f"[red]Error: API returned status {response.status_code}[/red]")
|
|
270
287
|
return False
|
|
271
288
|
|
|
272
|
-
except httpx.ConnectError:
|
|
289
|
+
except httpx.ConnectError as e:
|
|
290
|
+
logger.error(f"Connection error during token validation: {e}")
|
|
273
291
|
self.console.print(f"[red]Error: Could not connect to API at {self.config.api_url}[/red]")
|
|
274
292
|
return False
|
|
275
|
-
except httpx.TimeoutException:
|
|
293
|
+
except httpx.TimeoutException as e:
|
|
294
|
+
logger.error(f"Timeout during token validation: {e}")
|
|
276
295
|
self.console.print("[red]Error: Request timed out[/red]")
|
|
277
296
|
return False
|
|
278
297
|
except Exception as e:
|
|
298
|
+
logger.error(f"Unexpected error validating token: {e}", exc_info=True)
|
|
279
299
|
self.console.print(f"[red]Error validating token: {e}[/red]")
|
|
280
300
|
return False
|
|
281
301
|
|
|
@@ -301,7 +321,7 @@ class AuthManager:
|
|
|
301
321
|
|
|
302
322
|
# Try to get user info (this endpoint may not exist in the actual API)
|
|
303
323
|
response = client.get(
|
|
304
|
-
f"{self.config.api_url}/
|
|
324
|
+
f"{self.config.api_url}/user",
|
|
305
325
|
headers=headers,
|
|
306
326
|
timeout=10.0
|
|
307
327
|
)
|
|
@@ -328,7 +348,7 @@ class AuthManager:
|
|
|
328
348
|
}
|
|
329
349
|
|
|
330
350
|
response = client.get(
|
|
331
|
-
f"{self.config.api_url}/
|
|
351
|
+
f"{self.config.api_url}/orgs",
|
|
332
352
|
headers=headers,
|
|
333
353
|
timeout=10.0
|
|
334
354
|
)
|
alphai/cleanup.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Cleanup management for alphai CLI.
|
|
2
|
+
|
|
3
|
+
Provides a class-based approach to resource cleanup that replaces
|
|
4
|
+
global mutable state and handles signal registration safely.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Optional, Callable, Dict, Any
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from .utils import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CleanupManager:
|
|
18
|
+
"""Manages cleanup of resources with proper signal handling.
|
|
19
|
+
|
|
20
|
+
This class replaces the global _cleanup_state and _jupyter_cleanup_state
|
|
21
|
+
dictionaries with a proper encapsulated approach that:
|
|
22
|
+
- Avoids global mutable state
|
|
23
|
+
- Allows safe signal handler installation/restoration
|
|
24
|
+
- Can be tested in isolation
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, console: Optional[Console] = None):
|
|
28
|
+
"""Initialize the cleanup manager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
console: Rich console for output. Creates one if not provided.
|
|
32
|
+
"""
|
|
33
|
+
self.console = console or Console()
|
|
34
|
+
self._resources: Dict[str, Dict[str, Any]] = {}
|
|
35
|
+
self._cleanup_done = False
|
|
36
|
+
self._original_handlers: Dict[int, Any] = {}
|
|
37
|
+
self._cleanup_callbacks: list[Callable[[], None]] = []
|
|
38
|
+
|
|
39
|
+
def register_resource(
|
|
40
|
+
self,
|
|
41
|
+
resource_type: str,
|
|
42
|
+
resource_id: str,
|
|
43
|
+
cleanup_fn: Optional[Callable[[], bool]] = None,
|
|
44
|
+
**metadata
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Register a resource for cleanup.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
resource_type: Type of resource (e.g., 'container', 'tunnel', 'process')
|
|
50
|
+
resource_id: Unique identifier for the resource
|
|
51
|
+
cleanup_fn: Optional function to call for cleanup (returns success bool)
|
|
52
|
+
**metadata: Additional metadata to store with the resource
|
|
53
|
+
"""
|
|
54
|
+
key = f"{resource_type}:{resource_id}"
|
|
55
|
+
self._resources[key] = {
|
|
56
|
+
'type': resource_type,
|
|
57
|
+
'id': resource_id,
|
|
58
|
+
'cleanup_fn': cleanup_fn,
|
|
59
|
+
**metadata
|
|
60
|
+
}
|
|
61
|
+
logger.debug(f"Registered resource for cleanup: {key}")
|
|
62
|
+
|
|
63
|
+
def unregister_resource(self, resource_type: str, resource_id: str) -> None:
|
|
64
|
+
"""Unregister a resource (e.g., after successful manual cleanup).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
resource_type: Type of resource
|
|
68
|
+
resource_id: Unique identifier for the resource
|
|
69
|
+
"""
|
|
70
|
+
key = f"{resource_type}:{resource_id}"
|
|
71
|
+
if key in self._resources:
|
|
72
|
+
del self._resources[key]
|
|
73
|
+
logger.debug(f"Unregistered resource: {key}")
|
|
74
|
+
|
|
75
|
+
def add_cleanup_callback(self, callback: Callable[[], None]) -> None:
|
|
76
|
+
"""Add a cleanup callback to be called during cleanup.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
callback: Function to call during cleanup (no arguments, no return)
|
|
80
|
+
"""
|
|
81
|
+
self._cleanup_callbacks.append(callback)
|
|
82
|
+
|
|
83
|
+
def get_resource(self, resource_type: str, resource_id: str) -> Optional[Dict[str, Any]]:
|
|
84
|
+
"""Get a registered resource by type and ID.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
resource_type: Type of resource
|
|
88
|
+
resource_id: Unique identifier for the resource
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Resource metadata dict or None if not found
|
|
92
|
+
"""
|
|
93
|
+
key = f"{resource_type}:{resource_id}"
|
|
94
|
+
return self._resources.get(key)
|
|
95
|
+
|
|
96
|
+
def has_resources(self) -> bool:
|
|
97
|
+
"""Check if there are any resources registered for cleanup."""
|
|
98
|
+
return len(self._resources) > 0 or len(self._cleanup_callbacks) > 0
|
|
99
|
+
|
|
100
|
+
def install_signal_handlers(self) -> None:
|
|
101
|
+
"""Install signal handlers for cleanup on interrupt.
|
|
102
|
+
|
|
103
|
+
Stores original handlers so they can be restored later.
|
|
104
|
+
"""
|
|
105
|
+
if self._original_handlers:
|
|
106
|
+
logger.debug("Signal handlers already installed")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Store original handlers
|
|
110
|
+
self._original_handlers[signal.SIGINT] = signal.signal(
|
|
111
|
+
signal.SIGINT, self._signal_handler
|
|
112
|
+
)
|
|
113
|
+
self._original_handlers[signal.SIGTERM] = signal.signal(
|
|
114
|
+
signal.SIGTERM, self._signal_handler
|
|
115
|
+
)
|
|
116
|
+
logger.debug("Signal handlers installed")
|
|
117
|
+
|
|
118
|
+
def restore_signal_handlers(self) -> None:
|
|
119
|
+
"""Restore original signal handlers."""
|
|
120
|
+
for sig, handler in self._original_handlers.items():
|
|
121
|
+
signal.signal(sig, handler)
|
|
122
|
+
self._original_handlers.clear()
|
|
123
|
+
logger.debug("Signal handlers restored")
|
|
124
|
+
|
|
125
|
+
def _signal_handler(self, signum: int, frame) -> None:
|
|
126
|
+
"""Handle interrupt signals by running cleanup.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
signum: Signal number received
|
|
130
|
+
frame: Current stack frame
|
|
131
|
+
"""
|
|
132
|
+
if self._cleanup_done:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if not self.has_resources():
|
|
136
|
+
sys.exit(0)
|
|
137
|
+
|
|
138
|
+
self.cleanup()
|
|
139
|
+
sys.exit(0)
|
|
140
|
+
|
|
141
|
+
def cleanup(self) -> bool:
|
|
142
|
+
"""Run cleanup for all registered resources.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if all cleanup succeeded, False if any failed
|
|
146
|
+
"""
|
|
147
|
+
if self._cleanup_done:
|
|
148
|
+
logger.debug("Cleanup already done, skipping")
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
if not self.has_resources():
|
|
152
|
+
logger.debug("No resources to clean up")
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
self.console.print("\n[yellow]🔄 Cleaning up resources...[/yellow]")
|
|
156
|
+
success = True
|
|
157
|
+
|
|
158
|
+
# Run cleanup callbacks first
|
|
159
|
+
for callback in self._cleanup_callbacks:
|
|
160
|
+
try:
|
|
161
|
+
callback()
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"Cleanup callback failed: {e}", exc_info=True)
|
|
164
|
+
self.console.print(f"[red]Error in cleanup callback: {e}[/red]")
|
|
165
|
+
success = False
|
|
166
|
+
|
|
167
|
+
# Clean up registered resources
|
|
168
|
+
for key, resource in list(self._resources.items()):
|
|
169
|
+
cleanup_fn = resource.get('cleanup_fn')
|
|
170
|
+
if cleanup_fn:
|
|
171
|
+
try:
|
|
172
|
+
logger.debug(f"Running cleanup for {key}")
|
|
173
|
+
if not cleanup_fn():
|
|
174
|
+
success = False
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Cleanup failed for {key}: {e}", exc_info=True)
|
|
177
|
+
self.console.print(f"[red]Error cleaning up {resource['type']}: {e}[/red]")
|
|
178
|
+
success = False
|
|
179
|
+
|
|
180
|
+
if success:
|
|
181
|
+
self.console.print("[green]✓ Cleanup completed[/green]")
|
|
182
|
+
else:
|
|
183
|
+
self.console.print("[yellow]⚠ Cleanup completed with warnings[/yellow]")
|
|
184
|
+
|
|
185
|
+
self._cleanup_done = True
|
|
186
|
+
self._resources.clear()
|
|
187
|
+
self._cleanup_callbacks.clear()
|
|
188
|
+
|
|
189
|
+
return success
|
|
190
|
+
|
|
191
|
+
def reset(self) -> None:
|
|
192
|
+
"""Reset the cleanup manager state (useful for testing)."""
|
|
193
|
+
self._resources.clear()
|
|
194
|
+
self._cleanup_callbacks.clear()
|
|
195
|
+
self._cleanup_done = False
|
|
196
|
+
self.restore_signal_handlers()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class DockerCleanupManager(CleanupManager):
|
|
200
|
+
"""Cleanup manager specialized for Docker container cleanup."""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
console: Optional[Console] = None,
|
|
205
|
+
docker_manager: Any = None,
|
|
206
|
+
client: Any = None
|
|
207
|
+
):
|
|
208
|
+
"""Initialize Docker cleanup manager.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
console: Rich console for output
|
|
212
|
+
docker_manager: DockerManager instance for container operations
|
|
213
|
+
client: AlphAIClient instance for API operations
|
|
214
|
+
"""
|
|
215
|
+
super().__init__(console)
|
|
216
|
+
self.docker_manager = docker_manager
|
|
217
|
+
self.client = client
|
|
218
|
+
self._container_id: Optional[str] = None
|
|
219
|
+
self._tunnel_id: Optional[str] = None
|
|
220
|
+
self._project_id: Optional[str] = None
|
|
221
|
+
|
|
222
|
+
def set_container(self, container_id: str) -> None:
|
|
223
|
+
"""Set the container ID for cleanup."""
|
|
224
|
+
self._container_id = container_id
|
|
225
|
+
|
|
226
|
+
def set_tunnel(self, tunnel_id: str) -> None:
|
|
227
|
+
"""Set the tunnel ID for cleanup."""
|
|
228
|
+
self._tunnel_id = tunnel_id
|
|
229
|
+
|
|
230
|
+
def set_project(self, project_id: str) -> None:
|
|
231
|
+
"""Set the project ID for cleanup."""
|
|
232
|
+
self._project_id = project_id
|
|
233
|
+
|
|
234
|
+
def cleanup(self) -> bool:
|
|
235
|
+
"""Clean up Docker resources (container, tunnel, project)."""
|
|
236
|
+
if self._cleanup_done:
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
if not any([self._container_id, self._tunnel_id, self._project_id]):
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
self.console.print("\n[yellow]🔄 Cleaning up resources...[/yellow]")
|
|
243
|
+
success = True
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
# Clean up container and cloudflared service
|
|
247
|
+
if self._container_id and self.docker_manager:
|
|
248
|
+
if not self.docker_manager.cleanup_container_and_tunnel(
|
|
249
|
+
container_id=self._container_id,
|
|
250
|
+
tunnel_id=self._tunnel_id,
|
|
251
|
+
project_id=self._project_id,
|
|
252
|
+
force=True
|
|
253
|
+
):
|
|
254
|
+
success = False
|
|
255
|
+
|
|
256
|
+
# Clean up tunnel and project via API
|
|
257
|
+
if self.client and (self._tunnel_id or self._project_id):
|
|
258
|
+
if not self.client.cleanup_tunnel_and_project(
|
|
259
|
+
tunnel_id=self._tunnel_id,
|
|
260
|
+
project_id=self._project_id,
|
|
261
|
+
force=True
|
|
262
|
+
):
|
|
263
|
+
success = False
|
|
264
|
+
|
|
265
|
+
if success:
|
|
266
|
+
self.console.print("[green]✓ Cleanup completed[/green]")
|
|
267
|
+
else:
|
|
268
|
+
self.console.print("[yellow]⚠ Cleanup completed with warnings[/yellow]")
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"Error during cleanup: {e}", exc_info=True)
|
|
272
|
+
self.console.print(f"[red]Error during cleanup: {e}[/red]")
|
|
273
|
+
success = False
|
|
274
|
+
|
|
275
|
+
# Reset state
|
|
276
|
+
self._container_id = None
|
|
277
|
+
self._tunnel_id = None
|
|
278
|
+
self._project_id = None
|
|
279
|
+
self._cleanup_done = True
|
|
280
|
+
|
|
281
|
+
return success
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class JupyterCleanupManager(CleanupManager):
|
|
285
|
+
"""Cleanup manager specialized for Jupyter process cleanup."""
|
|
286
|
+
|
|
287
|
+
def __init__(
|
|
288
|
+
self,
|
|
289
|
+
console: Optional[Console] = None,
|
|
290
|
+
jupyter_manager: Any = None,
|
|
291
|
+
client: Any = None
|
|
292
|
+
):
|
|
293
|
+
"""Initialize Jupyter cleanup manager.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
console: Rich console for output
|
|
297
|
+
jupyter_manager: JupyterManager instance for process operations
|
|
298
|
+
client: AlphAIClient instance for API operations
|
|
299
|
+
"""
|
|
300
|
+
super().__init__(console)
|
|
301
|
+
self.jupyter_manager = jupyter_manager
|
|
302
|
+
self.client = client
|
|
303
|
+
self._tunnel_id: Optional[str] = None
|
|
304
|
+
self._project_id: Optional[str] = None
|
|
305
|
+
|
|
306
|
+
def set_tunnel(self, tunnel_id: str) -> None:
|
|
307
|
+
"""Set the tunnel ID for cleanup."""
|
|
308
|
+
self._tunnel_id = tunnel_id
|
|
309
|
+
|
|
310
|
+
def set_project(self, project_id: str) -> None:
|
|
311
|
+
"""Set the project ID for cleanup."""
|
|
312
|
+
self._project_id = project_id
|
|
313
|
+
|
|
314
|
+
def cleanup(self) -> bool:
|
|
315
|
+
"""Clean up Jupyter resources (process, tunnel, project)."""
|
|
316
|
+
if self._cleanup_done:
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
if not self.jupyter_manager:
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
self.console.print("\n[yellow]🔄 Cleaning up Jupyter resources...[/yellow]")
|
|
323
|
+
success = True
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Comprehensive cleanup via JupyterManager
|
|
327
|
+
if not self.jupyter_manager.cleanup(
|
|
328
|
+
client=self.client,
|
|
329
|
+
tunnel_id=self._tunnel_id,
|
|
330
|
+
project_id=self._project_id,
|
|
331
|
+
force=True
|
|
332
|
+
):
|
|
333
|
+
success = False
|
|
334
|
+
|
|
335
|
+
if success:
|
|
336
|
+
self.console.print("[green]✓ Cleanup completed[/green]")
|
|
337
|
+
else:
|
|
338
|
+
self.console.print("[yellow]⚠ Cleanup completed with warnings[/yellow]")
|
|
339
|
+
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f"Error during Jupyter cleanup: {e}", exc_info=True)
|
|
342
|
+
self.console.print(f"[red]Error during cleanup: {e}[/red]")
|
|
343
|
+
success = False
|
|
344
|
+
|
|
345
|
+
# Reset state
|
|
346
|
+
self._tunnel_id = None
|
|
347
|
+
self._project_id = None
|
|
348
|
+
self._cleanup_done = True
|
|
349
|
+
|
|
350
|
+
return success
|
|
351
|
+
|