dayhoff-tools 1.10.1__tar.gz → 1.10.3__tar.gz
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.
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/PKG-INFO +3 -2
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine_studio_commands.py → dayhoff_tools-1.10.3/dayhoff_tools/cli/engine/__init__.py +22 -22
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/status.py → dayhoff_tools-1.10.3/dayhoff_tools/cli/engine/engine_core.py +201 -6
- dayhoff_tools-1.10.3/dayhoff_tools/cli/engine/engine_maintenance.py +431 -0
- dayhoff_tools-1.10.3/dayhoff_tools/cli/engine/engine_management.py +505 -0
- dayhoff_tools-1.10.3/dayhoff_tools/cli/engine/shared.py +501 -0
- dayhoff_tools-1.10.3/dayhoff_tools/cli/engine/studio_commands.py +825 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/cli/main.py +2 -1
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/deployment/base.py +10 -2
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/pyproject.toml +1 -1
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/__init__.py +0 -1
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/coffee.py +0 -110
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/config_ssh.py +0 -113
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/debug.py +0 -79
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/gami.py +0 -160
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/idle.py +0 -148
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/launch.py +0 -101
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/list.py +0 -116
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/repair.py +0 -128
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/resize.py +0 -195
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/ssh.py +0 -62
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine_studio_utils/__init__.py +0 -1
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine_studio_utils/api_utils.py +0 -47
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine_studio_utils/aws_utils.py +0 -102
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine_studio_utils/constants.py +0 -21
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine_studio_utils/formatting.py +0 -210
- dayhoff_tools-1.10.1/dayhoff_tools/cli/engine_studio_utils/ssh_utils.py +0 -141
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/__init__.py +0 -1
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/attach.py +0 -314
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/create.py +0 -48
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/delete.py +0 -71
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/detach.py +0 -56
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/list.py +0 -81
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/reset.py +0 -90
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/resize.py +0 -134
- dayhoff_tools-1.10.1/dayhoff_tools/cli/studio/status.py +0 -78
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/README.md +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/__init__.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/chemistry/standardizer.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/chemistry/utils.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/cli/__init__.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/cli/cloud_commands.py +0 -0
- /dayhoff_tools-1.10.1/dayhoff_tools/cli/engine/lifecycle.py → /dayhoff_tools-1.10.3/dayhoff_tools/cli/engine/engine_lifecycle.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/cli/swarm_commands.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/cli/utility_commands.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/deployment/deploy_aws.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/deployment/deploy_gcp.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/deployment/deploy_utils.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/deployment/job_runner.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/deployment/processors.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/deployment/swarm.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/embedders.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/fasta.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/file_ops.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/h5.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/intake/gcp.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/intake/gtdb.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/intake/kegg.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/intake/mmseqs.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/intake/structure.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/intake/uniprot.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/logs.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/sqlite.py +0 -0
- {dayhoff_tools-1.10.1 → dayhoff_tools-1.10.3}/dayhoff_tools/warehouse.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: dayhoff-tools
|
3
|
-
Version: 1.10.
|
3
|
+
Version: 1.10.3
|
4
4
|
Summary: Common tools for all the repos at Dayhoff Labs
|
5
5
|
Author: Daniel Martin-Alarcon
|
6
6
|
Author-email: dma@dayhofflabs.com
|
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
13
14
|
Provides-Extra: embedders
|
14
15
|
Provides-Extra: full
|
15
16
|
Requires-Dist: biopython (>=1.84) ; extra == "full"
|
@@ -36,7 +36,7 @@ def launch_engine_cmd(
|
|
36
36
|
),
|
37
37
|
):
|
38
38
|
"""Launch a new engine instance."""
|
39
|
-
from .
|
39
|
+
from .engine_core import launch_engine
|
40
40
|
|
41
41
|
return launch_engine(name, engine_type, user, boot_disk_size, availability_zone)
|
42
42
|
|
@@ -55,7 +55,7 @@ def list_engines_cmd(
|
|
55
55
|
),
|
56
56
|
):
|
57
57
|
"""List engines (shows all engines by default)."""
|
58
|
-
from .
|
58
|
+
from .engine_core import list_engines
|
59
59
|
|
60
60
|
return list_engines(user, running_only, stopped_only, detailed)
|
61
61
|
|
@@ -71,7 +71,7 @@ def engine_status_cmd(
|
|
71
71
|
),
|
72
72
|
):
|
73
73
|
"""Show engine status and information."""
|
74
|
-
from .
|
74
|
+
from .engine_core import engine_status
|
75
75
|
|
76
76
|
return engine_status(name_or_id, detailed, show_log)
|
77
77
|
|
@@ -81,7 +81,7 @@ def start_engine_cmd(
|
|
81
81
|
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
82
82
|
):
|
83
83
|
"""Start a stopped engine."""
|
84
|
-
from .
|
84
|
+
from .engine_lifecycle import start_engine
|
85
85
|
|
86
86
|
return start_engine(name_or_id)
|
87
87
|
|
@@ -94,7 +94,7 @@ def stop_engine_cmd(
|
|
94
94
|
),
|
95
95
|
):
|
96
96
|
"""Stop an engine."""
|
97
|
-
from .
|
97
|
+
from .engine_lifecycle import stop_engine
|
98
98
|
|
99
99
|
return stop_engine(name_or_id, force)
|
100
100
|
|
@@ -104,7 +104,7 @@ def terminate_engine_cmd(
|
|
104
104
|
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
105
105
|
):
|
106
106
|
"""Permanently terminate an engine."""
|
107
|
-
from .
|
107
|
+
from .engine_lifecycle import terminate_engine
|
108
108
|
|
109
109
|
return terminate_engine(name_or_id)
|
110
110
|
|
@@ -122,7 +122,7 @@ def ssh_engine_cmd(
|
|
122
122
|
),
|
123
123
|
):
|
124
124
|
"""Connect to an engine via SSH."""
|
125
|
-
from .
|
125
|
+
from .engine_management import ssh_engine
|
126
126
|
|
127
127
|
return ssh_engine(name_or_id, admin, idle_timeout)
|
128
128
|
|
@@ -140,7 +140,7 @@ def config_ssh_cmd(
|
|
140
140
|
),
|
141
141
|
):
|
142
142
|
"""Update SSH config with available engines."""
|
143
|
-
from .
|
143
|
+
from .engine_management import config_ssh
|
144
144
|
|
145
145
|
return config_ssh(clean, all_engines, admin)
|
146
146
|
|
@@ -159,7 +159,7 @@ def resize_engine_cmd(
|
|
159
159
|
),
|
160
160
|
):
|
161
161
|
"""Resize an engine's boot disk."""
|
162
|
-
from .
|
162
|
+
from .engine_management import resize_engine
|
163
163
|
|
164
164
|
return resize_engine(name_or_id, size, online, force)
|
165
165
|
|
@@ -171,7 +171,7 @@ def create_ami_cmd(
|
|
171
171
|
),
|
172
172
|
):
|
173
173
|
"""Create a 'Golden AMI' from a running engine."""
|
174
|
-
from .
|
174
|
+
from .engine_management import create_ami
|
175
175
|
|
176
176
|
return create_ami(name_or_id)
|
177
177
|
|
@@ -185,7 +185,7 @@ def coffee_cmd(
|
|
185
185
|
),
|
186
186
|
):
|
187
187
|
"""Pour ☕ for an engine: keeps it awake for the given duration (or cancel)."""
|
188
|
-
from .
|
188
|
+
from .engine_maintenance import coffee
|
189
189
|
|
190
190
|
return coffee(name_or_id, duration, cancel)
|
191
191
|
|
@@ -201,7 +201,7 @@ def idle_timeout_cmd_wrapper(
|
|
201
201
|
),
|
202
202
|
):
|
203
203
|
"""Show or set engine idle-detector settings."""
|
204
|
-
from .
|
204
|
+
from .engine_maintenance import idle_timeout_cmd
|
205
205
|
|
206
206
|
return idle_timeout_cmd(name_or_id=name_or_id, set=set, slack=slack)
|
207
207
|
|
@@ -211,7 +211,7 @@ def debug_engine_cmd(
|
|
211
211
|
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
212
212
|
):
|
213
213
|
"""Debug engine bootstrap status and files."""
|
214
|
-
from .
|
214
|
+
from .engine_maintenance import debug_engine
|
215
215
|
|
216
216
|
return debug_engine(name_or_id)
|
217
217
|
|
@@ -221,7 +221,7 @@ def repair_engine_cmd(
|
|
221
221
|
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
222
222
|
):
|
223
223
|
"""Repair an engine that's stuck in a bad state (e.g., after GAMI creation)."""
|
224
|
-
from .
|
224
|
+
from .engine_maintenance import repair_engine
|
225
225
|
|
226
226
|
return repair_engine(name_or_id)
|
227
227
|
|
@@ -232,7 +232,7 @@ def create_studio_cmd(
|
|
232
232
|
size_gb: int = typer.Option(50, "--size", "-s", help="Studio size in GB"),
|
233
233
|
):
|
234
234
|
"""Create a new studio for the current user."""
|
235
|
-
from .
|
235
|
+
from .studio_commands import create_studio
|
236
236
|
|
237
237
|
return create_studio(size_gb)
|
238
238
|
|
@@ -244,7 +244,7 @@ def studio_status_cmd(
|
|
244
244
|
),
|
245
245
|
):
|
246
246
|
"""Show status of your studio."""
|
247
|
-
from .
|
247
|
+
from .studio_commands import studio_status
|
248
248
|
|
249
249
|
return studio_status(user)
|
250
250
|
|
@@ -257,7 +257,7 @@ def attach_studio_cmd(
|
|
257
257
|
),
|
258
258
|
):
|
259
259
|
"""Attach your studio to an engine."""
|
260
|
-
from .
|
260
|
+
from .studio_commands import attach_studio
|
261
261
|
|
262
262
|
return attach_studio(engine_name_or_id, user)
|
263
263
|
|
@@ -269,7 +269,7 @@ def detach_studio_cmd(
|
|
269
269
|
),
|
270
270
|
):
|
271
271
|
"""Detach your studio from its current engine."""
|
272
|
-
from .
|
272
|
+
from .studio_commands import detach_studio
|
273
273
|
|
274
274
|
return detach_studio(user)
|
275
275
|
|
@@ -281,7 +281,7 @@ def delete_studio_cmd(
|
|
281
281
|
),
|
282
282
|
):
|
283
283
|
"""Delete your studio permanently."""
|
284
|
-
from .
|
284
|
+
from .studio_commands import delete_studio
|
285
285
|
|
286
286
|
return delete_studio(user)
|
287
287
|
|
@@ -293,7 +293,7 @@ def list_studios_cmd(
|
|
293
293
|
),
|
294
294
|
):
|
295
295
|
"""List studios."""
|
296
|
-
from .
|
296
|
+
from .studio_commands import list_studios
|
297
297
|
|
298
298
|
return list_studios(all_users)
|
299
299
|
|
@@ -305,7 +305,7 @@ def reset_studio_cmd(
|
|
305
305
|
),
|
306
306
|
):
|
307
307
|
"""Reset a stuck studio (admin operation)."""
|
308
|
-
from .
|
308
|
+
from .studio_commands import reset_studio
|
309
309
|
|
310
310
|
return reset_studio(user)
|
311
311
|
|
@@ -318,6 +318,6 @@ def resize_studio_cmd(
|
|
318
318
|
),
|
319
319
|
):
|
320
320
|
"""Resize your studio volume (requires detachment)."""
|
321
|
-
from .
|
321
|
+
from .studio_commands import resize_studio
|
322
322
|
|
323
323
|
return resize_studio(size, user)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
"""
|
1
|
+
"""Core engine commands: launch, list, and status."""
|
2
2
|
|
3
3
|
import json
|
4
4
|
import time
|
@@ -7,19 +7,214 @@ from typing import Any, Dict, Optional
|
|
7
7
|
|
8
8
|
import boto3
|
9
9
|
import typer
|
10
|
+
from rich import box
|
10
11
|
from rich.panel import Panel
|
11
|
-
|
12
|
-
from
|
13
|
-
|
14
|
-
from
|
15
|
-
|
12
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
13
|
+
from rich.table import Table
|
14
|
+
|
15
|
+
from .shared import (
|
16
|
+
HOURLY_COSTS,
|
17
|
+
_fetch_init_stages,
|
18
|
+
check_aws_sso,
|
19
|
+
console,
|
16
20
|
format_duration,
|
21
|
+
format_status,
|
17
22
|
get_disk_usage_via_ssm,
|
23
|
+
make_api_request,
|
18
24
|
parse_launch_time,
|
19
25
|
resolve_engine,
|
20
26
|
)
|
21
27
|
|
22
28
|
|
29
|
+
def launch_engine(
|
30
|
+
name: str = typer.Argument(help="Name for the new engine"),
|
31
|
+
engine_type: str = typer.Option(
|
32
|
+
"cpu",
|
33
|
+
"--type",
|
34
|
+
"-t",
|
35
|
+
help="Engine type: cpu, cpumax, t4, a10g, a100, 4_t4, 8_t4, 4_a10g, 8_a10g",
|
36
|
+
),
|
37
|
+
user: Optional[str] = typer.Option(None, "--user", "-u", help="Override username"),
|
38
|
+
boot_disk_size: Optional[int] = typer.Option(
|
39
|
+
None,
|
40
|
+
"--size",
|
41
|
+
"-s",
|
42
|
+
help="Boot disk size in GB (default: 50GB, min: 20GB, max: 1000GB)",
|
43
|
+
),
|
44
|
+
availability_zone: Optional[str] = typer.Option(
|
45
|
+
None,
|
46
|
+
"--az",
|
47
|
+
help="Prefer a specific Availability Zone (e.g., us-east-1b). If omitted the service will try all public subnets.",
|
48
|
+
),
|
49
|
+
):
|
50
|
+
"""Launch a new engine instance."""
|
51
|
+
username = check_aws_sso()
|
52
|
+
if user:
|
53
|
+
username = user
|
54
|
+
|
55
|
+
# Validate engine type
|
56
|
+
valid_types = [
|
57
|
+
"cpu",
|
58
|
+
"cpumax",
|
59
|
+
"t4",
|
60
|
+
"a10g",
|
61
|
+
"a100",
|
62
|
+
"4_t4",
|
63
|
+
"8_t4",
|
64
|
+
"4_a10g",
|
65
|
+
"8_a10g",
|
66
|
+
]
|
67
|
+
if engine_type not in valid_types:
|
68
|
+
console.print(f"[red]❌ Invalid engine type: {engine_type}[/red]")
|
69
|
+
console.print(f"Valid types: {', '.join(valid_types)}")
|
70
|
+
raise typer.Exit(1)
|
71
|
+
|
72
|
+
# Validate boot disk size
|
73
|
+
if boot_disk_size is not None:
|
74
|
+
if boot_disk_size < 20:
|
75
|
+
console.print("[red]❌ Boot disk size must be at least 20GB[/red]")
|
76
|
+
raise typer.Exit(1)
|
77
|
+
if boot_disk_size > 1000:
|
78
|
+
console.print("[red]❌ Boot disk size cannot exceed 1000GB[/red]")
|
79
|
+
raise typer.Exit(1)
|
80
|
+
|
81
|
+
cost = HOURLY_COSTS.get(engine_type, 0)
|
82
|
+
disk_info = f" with {boot_disk_size}GB boot disk" if boot_disk_size else ""
|
83
|
+
console.print(
|
84
|
+
f"Launching [cyan]{name}[/cyan] ({engine_type}){disk_info} for ${cost:.2f}/hour..."
|
85
|
+
)
|
86
|
+
|
87
|
+
with Progress(
|
88
|
+
SpinnerColumn(),
|
89
|
+
TextColumn("[progress.description]{task.description}"),
|
90
|
+
transient=True,
|
91
|
+
) as progress:
|
92
|
+
progress.add_task("Creating engine...", total=None)
|
93
|
+
|
94
|
+
request_data: Dict[str, Any] = {
|
95
|
+
"name": name,
|
96
|
+
"user": username,
|
97
|
+
"engine_type": engine_type,
|
98
|
+
}
|
99
|
+
if boot_disk_size is not None:
|
100
|
+
request_data["boot_disk_size"] = boot_disk_size
|
101
|
+
if availability_zone:
|
102
|
+
request_data["availability_zone"] = availability_zone
|
103
|
+
|
104
|
+
response = make_api_request("POST", "/engines", json_data=request_data)
|
105
|
+
|
106
|
+
if response.status_code == 201:
|
107
|
+
data = response.json()
|
108
|
+
console.print(f"[green]✓ Engine launched successfully![/green]")
|
109
|
+
console.print(f"Instance ID: [cyan]{data['instance_id']}[/cyan]")
|
110
|
+
console.print(f"Type: {data['instance_type']} (${cost:.2f}/hour)")
|
111
|
+
if boot_disk_size:
|
112
|
+
console.print(f"Boot disk: {boot_disk_size}GB")
|
113
|
+
console.print("\nThe engine is initializing. This may take a few minutes.")
|
114
|
+
console.print(f"Check status with: [cyan]dh engine status {name}[/cyan]")
|
115
|
+
else:
|
116
|
+
error = response.json().get("error", "Unknown error")
|
117
|
+
console.print(f"[red]❌ Failed to launch engine: {error}[/red]")
|
118
|
+
|
119
|
+
|
120
|
+
def list_engines(
|
121
|
+
user: Optional[str] = typer.Option(None, "--user", "-u", help="Filter by user"),
|
122
|
+
running_only: bool = typer.Option(
|
123
|
+
False, "--running", help="Show only running engines"
|
124
|
+
),
|
125
|
+
stopped_only: bool = typer.Option(
|
126
|
+
False, "--stopped", help="Show only stopped engines"
|
127
|
+
),
|
128
|
+
detailed: bool = typer.Option(
|
129
|
+
False, "--detailed", "-d", help="Show detailed status (slower)"
|
130
|
+
),
|
131
|
+
):
|
132
|
+
"""List engines (shows all engines by default)."""
|
133
|
+
current_user = check_aws_sso()
|
134
|
+
|
135
|
+
params = {}
|
136
|
+
if user:
|
137
|
+
params["user"] = user
|
138
|
+
if detailed:
|
139
|
+
params["check_ready"] = "true"
|
140
|
+
|
141
|
+
response = make_api_request("GET", "/engines", params=params)
|
142
|
+
|
143
|
+
if response.status_code == 200:
|
144
|
+
data = response.json()
|
145
|
+
engines = data.get("engines", [])
|
146
|
+
|
147
|
+
# Filter by state if requested
|
148
|
+
if running_only:
|
149
|
+
engines = [e for e in engines if e["state"].lower() == "running"]
|
150
|
+
elif stopped_only:
|
151
|
+
engines = [e for e in engines if e["state"].lower() == "stopped"]
|
152
|
+
|
153
|
+
if not engines:
|
154
|
+
console.print("No engines found.")
|
155
|
+
return
|
156
|
+
|
157
|
+
# Only fetch detailed info if requested (slow)
|
158
|
+
stages_map = {}
|
159
|
+
if detailed:
|
160
|
+
stages_map = _fetch_init_stages([e["instance_id"] for e in engines])
|
161
|
+
|
162
|
+
# Create table
|
163
|
+
table = Table(title="Engines", box=box.ROUNDED)
|
164
|
+
table.add_column("Name", style="cyan")
|
165
|
+
table.add_column("Instance ID", style="dim")
|
166
|
+
table.add_column("Type")
|
167
|
+
table.add_column("User")
|
168
|
+
table.add_column("Status")
|
169
|
+
if detailed:
|
170
|
+
table.add_column("Disk Usage")
|
171
|
+
table.add_column("Uptime/Since")
|
172
|
+
table.add_column("$/hour", justify="right")
|
173
|
+
|
174
|
+
for engine in engines:
|
175
|
+
launch_time = parse_launch_time(engine["launch_time"])
|
176
|
+
uptime = datetime.now(timezone.utc) - launch_time
|
177
|
+
hourly_cost = HOURLY_COSTS.get(engine["engine_type"], 0)
|
178
|
+
|
179
|
+
if engine["state"].lower() == "running":
|
180
|
+
time_str = format_duration(uptime)
|
181
|
+
# Only get disk usage if detailed mode
|
182
|
+
if detailed:
|
183
|
+
disk_usage = get_disk_usage_via_ssm(engine["instance_id"]) or "-"
|
184
|
+
else:
|
185
|
+
disk_usage = None
|
186
|
+
else:
|
187
|
+
time_str = launch_time.strftime("%Y-%m-%d %H:%M")
|
188
|
+
disk_usage = "-" if detailed else None
|
189
|
+
|
190
|
+
row_data = [
|
191
|
+
engine["name"],
|
192
|
+
engine["instance_id"],
|
193
|
+
engine["engine_type"],
|
194
|
+
engine["user"],
|
195
|
+
format_status(engine["state"], engine.get("ready")),
|
196
|
+
]
|
197
|
+
if detailed:
|
198
|
+
row_data.append(disk_usage)
|
199
|
+
row_data.extend(
|
200
|
+
[
|
201
|
+
time_str,
|
202
|
+
f"${hourly_cost:.2f}",
|
203
|
+
]
|
204
|
+
)
|
205
|
+
|
206
|
+
table.add_row(*row_data)
|
207
|
+
|
208
|
+
console.print(table)
|
209
|
+
if not detailed and any(e["state"].lower() == "running" for e in engines):
|
210
|
+
console.print(
|
211
|
+
"\n[dim]Tip: Use --detailed to see disk usage and bootstrap status (slower)[/dim]"
|
212
|
+
)
|
213
|
+
else:
|
214
|
+
error = response.json().get("error", "Unknown error")
|
215
|
+
console.print(f"[red]❌ Failed to list engines: {error}[/red]")
|
216
|
+
|
217
|
+
|
23
218
|
def engine_status(
|
24
219
|
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
25
220
|
detailed: bool = typer.Option(
|