dayhoff-tools 1.10.0__py3-none-any.whl → 1.10.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.
- dayhoff_tools/cli/engine/__init__.py +1 -323
- dayhoff_tools/cli/engine/coffee.py +110 -0
- dayhoff_tools/cli/engine/config_ssh.py +113 -0
- dayhoff_tools/cli/engine/debug.py +79 -0
- dayhoff_tools/cli/engine/gami.py +160 -0
- dayhoff_tools/cli/engine/idle.py +148 -0
- dayhoff_tools/cli/engine/launch.py +101 -0
- dayhoff_tools/cli/engine/list.py +116 -0
- dayhoff_tools/cli/engine/repair.py +128 -0
- dayhoff_tools/cli/engine/resize.py +195 -0
- dayhoff_tools/cli/engine/ssh.py +62 -0
- dayhoff_tools/cli/engine/{engine_core.py → status.py} +6 -201
- dayhoff_tools/cli/engine_studio_commands.py +323 -0
- dayhoff_tools/cli/engine_studio_utils/__init__.py +1 -0
- dayhoff_tools/cli/engine_studio_utils/api_utils.py +47 -0
- dayhoff_tools/cli/engine_studio_utils/aws_utils.py +102 -0
- dayhoff_tools/cli/engine_studio_utils/constants.py +21 -0
- dayhoff_tools/cli/engine_studio_utils/formatting.py +210 -0
- dayhoff_tools/cli/engine_studio_utils/ssh_utils.py +141 -0
- dayhoff_tools/cli/main.py +1 -2
- dayhoff_tools/cli/studio/__init__.py +1 -0
- dayhoff_tools/cli/studio/attach.py +314 -0
- dayhoff_tools/cli/studio/create.py +48 -0
- dayhoff_tools/cli/studio/delete.py +71 -0
- dayhoff_tools/cli/studio/detach.py +56 -0
- dayhoff_tools/cli/studio/list.py +81 -0
- dayhoff_tools/cli/studio/reset.py +90 -0
- dayhoff_tools/cli/studio/resize.py +134 -0
- dayhoff_tools/cli/studio/status.py +78 -0
- {dayhoff_tools-1.10.0.dist-info → dayhoff_tools-1.10.1.dist-info}/METADATA +1 -1
- dayhoff_tools-1.10.1.dist-info/RECORD +61 -0
- dayhoff_tools/cli/engine/engine_maintenance.py +0 -431
- dayhoff_tools/cli/engine/engine_management.py +0 -505
- dayhoff_tools/cli/engine/shared.py +0 -501
- dayhoff_tools/cli/engine/studio_commands.py +0 -825
- dayhoff_tools-1.10.0.dist-info/RECORD +0 -39
- /dayhoff_tools/cli/engine/{engine_lifecycle.py → lifecycle.py} +0 -0
- {dayhoff_tools-1.10.0.dist-info → dayhoff_tools-1.10.1.dist-info}/WHEEL +0 -0
- {dayhoff_tools-1.10.0.dist-info → dayhoff_tools-1.10.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
"""Studio create command."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
5
|
+
|
6
|
+
from ..engine_studio_utils.api_utils import get_user_studio, make_api_request
|
7
|
+
from ..engine_studio_utils.aws_utils import check_aws_sso
|
8
|
+
from ..engine_studio_utils.constants import console
|
9
|
+
|
10
|
+
|
11
|
+
def create_studio(
|
12
|
+
size_gb: int = typer.Option(50, "--size", "-s", help="Studio size in GB"),
|
13
|
+
):
|
14
|
+
"""Create a new studio for the current user."""
|
15
|
+
username = check_aws_sso()
|
16
|
+
|
17
|
+
# Check if user already has a studio
|
18
|
+
existing = get_user_studio(username)
|
19
|
+
if existing:
|
20
|
+
console.print(
|
21
|
+
f"[yellow]You already have a studio: {existing['studio_id']}[/yellow]"
|
22
|
+
)
|
23
|
+
return
|
24
|
+
|
25
|
+
console.print(f"Creating {size_gb}GB studio for user [cyan]{username}[/cyan]...")
|
26
|
+
|
27
|
+
with Progress(
|
28
|
+
SpinnerColumn(),
|
29
|
+
TextColumn("[progress.description]{task.description}"),
|
30
|
+
transient=True,
|
31
|
+
) as progress:
|
32
|
+
progress.add_task("Creating studio volume...", total=None)
|
33
|
+
|
34
|
+
response = make_api_request(
|
35
|
+
"POST",
|
36
|
+
"/studios",
|
37
|
+
json_data={"user": username, "size_gb": size_gb},
|
38
|
+
)
|
39
|
+
|
40
|
+
if response.status_code == 201:
|
41
|
+
data = response.json()
|
42
|
+
console.print(f"[green]✓ Studio created successfully![/green]")
|
43
|
+
console.print(f"Studio ID: [cyan]{data['studio_id']}[/cyan]")
|
44
|
+
console.print(f"Size: {data['size_gb']}GB")
|
45
|
+
console.print(f"\nNext step: [cyan]dh studio attach <engine-name>[/cyan]")
|
46
|
+
else:
|
47
|
+
error = response.json().get("error", "Unknown error")
|
48
|
+
console.print(f"[red]❌ Failed to create studio: {error}[/red]")
|
@@ -0,0 +1,71 @@
|
|
1
|
+
"""Studio delete command."""
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
import typer
|
6
|
+
from rich.prompt import Confirm, Prompt
|
7
|
+
|
8
|
+
from ..engine_studio_utils.api_utils import get_user_studio, make_api_request
|
9
|
+
from ..engine_studio_utils.aws_utils import check_aws_sso
|
10
|
+
from ..engine_studio_utils.constants import console
|
11
|
+
|
12
|
+
|
13
|
+
def delete_studio(
|
14
|
+
user: Optional[str] = typer.Option(
|
15
|
+
None, "--user", "-u", help="Delete a different user's studio (admin only)"
|
16
|
+
),
|
17
|
+
):
|
18
|
+
"""Delete your studio permanently."""
|
19
|
+
username = check_aws_sso()
|
20
|
+
|
21
|
+
# Use specified user if provided, otherwise use current user
|
22
|
+
target_user = user if user else username
|
23
|
+
|
24
|
+
# Extra warning when deleting another user's studio
|
25
|
+
if target_user != username:
|
26
|
+
console.print(
|
27
|
+
f"[red]⚠️ ADMIN ACTION: Deleting studio for user: {target_user}[/red]"
|
28
|
+
)
|
29
|
+
|
30
|
+
studio = get_user_studio(target_user)
|
31
|
+
if not studio:
|
32
|
+
if target_user == username:
|
33
|
+
console.print("[yellow]You don't have a studio to delete.[/yellow]")
|
34
|
+
else:
|
35
|
+
console.print(
|
36
|
+
f"[yellow]User {target_user} doesn't have a studio to delete.[/yellow]"
|
37
|
+
)
|
38
|
+
return
|
39
|
+
|
40
|
+
console.print(
|
41
|
+
"[red]⚠️ WARNING: This will permanently delete the studio and all data![/red]"
|
42
|
+
)
|
43
|
+
console.print(f"Studio ID: {studio['studio_id']}")
|
44
|
+
console.print(f"User: {target_user}")
|
45
|
+
console.print(f"Size: {studio['size_gb']}GB")
|
46
|
+
|
47
|
+
# Multiple confirmations
|
48
|
+
if not Confirm.ask(
|
49
|
+
f"\nAre you sure you want to delete {target_user}'s studio?"
|
50
|
+
if target_user != username
|
51
|
+
else "\nAre you sure you want to delete your studio?"
|
52
|
+
):
|
53
|
+
console.print("Deletion cancelled.")
|
54
|
+
return
|
55
|
+
|
56
|
+
if not Confirm.ask("[red]This action cannot be undone. Continue?[/red]"):
|
57
|
+
console.print("Deletion cancelled.")
|
58
|
+
return
|
59
|
+
|
60
|
+
typed_confirm = Prompt.ask('Type "DELETE" to confirm permanent deletion')
|
61
|
+
if typed_confirm != "DELETE":
|
62
|
+
console.print("Deletion cancelled.")
|
63
|
+
return
|
64
|
+
|
65
|
+
response = make_api_request("DELETE", f"/studios/{studio['studio_id']}")
|
66
|
+
|
67
|
+
if response.status_code == 200:
|
68
|
+
console.print(f"[green]✓ Studio deleted successfully![/green]")
|
69
|
+
else:
|
70
|
+
error = response.json().get("error", "Unknown error")
|
71
|
+
console.print(f"[red]❌ Failed to delete studio: {error}[/red]")
|
@@ -0,0 +1,56 @@
|
|
1
|
+
"""Studio detach command."""
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
import typer
|
6
|
+
from rich.prompt import Confirm
|
7
|
+
|
8
|
+
from ..engine_studio_utils.api_utils import get_user_studio, make_api_request
|
9
|
+
from ..engine_studio_utils.aws_utils import check_aws_sso
|
10
|
+
from ..engine_studio_utils.constants import console
|
11
|
+
|
12
|
+
|
13
|
+
def detach_studio(
|
14
|
+
user: Optional[str] = typer.Option(
|
15
|
+
None, "--user", "-u", help="Detach a different user's studio (admin only)"
|
16
|
+
),
|
17
|
+
):
|
18
|
+
"""Detach your studio from its current engine."""
|
19
|
+
username = check_aws_sso()
|
20
|
+
|
21
|
+
# Use specified user if provided, otherwise use current user
|
22
|
+
target_user = user if user else username
|
23
|
+
|
24
|
+
# Add confirmation when detaching another user's studio
|
25
|
+
if target_user != username:
|
26
|
+
console.print(f"[yellow]⚠️ Managing studio for user: {target_user}[/yellow]")
|
27
|
+
if not Confirm.ask(f"Are you sure you want to detach {target_user}'s studio?"):
|
28
|
+
console.print("Operation cancelled.")
|
29
|
+
return
|
30
|
+
|
31
|
+
studio = get_user_studio(target_user)
|
32
|
+
if not studio:
|
33
|
+
if target_user == username:
|
34
|
+
console.print("[yellow]You don't have a studio.[/yellow]")
|
35
|
+
else:
|
36
|
+
console.print(f"[yellow]User {target_user} doesn't have a studio.[/yellow]")
|
37
|
+
return
|
38
|
+
|
39
|
+
if studio.get("status") != "in-use":
|
40
|
+
if target_user == username:
|
41
|
+
console.print("[yellow]Your studio is not attached to any engine.[/yellow]")
|
42
|
+
else:
|
43
|
+
console.print(
|
44
|
+
f"[yellow]{target_user}'s studio is not attached to any engine.[/yellow]"
|
45
|
+
)
|
46
|
+
return
|
47
|
+
|
48
|
+
console.print(f"Detaching studio from {studio.get('attached_vm_id')}...")
|
49
|
+
|
50
|
+
response = make_api_request("POST", f"/studios/{studio['studio_id']}/detach")
|
51
|
+
|
52
|
+
if response.status_code == 200:
|
53
|
+
console.print(f"[green]✓ Studio detached successfully![/green]")
|
54
|
+
else:
|
55
|
+
error = response.json().get("error", "Unknown error")
|
56
|
+
console.print(f"[red]❌ Failed to detach studio: {error}[/red]")
|
@@ -0,0 +1,81 @@
|
|
1
|
+
"""Studio list command."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from rich import box
|
5
|
+
from rich.table import Table
|
6
|
+
|
7
|
+
from ..engine_studio_utils.api_utils import make_api_request
|
8
|
+
from ..engine_studio_utils.aws_utils import check_aws_sso
|
9
|
+
from ..engine_studio_utils.constants import console
|
10
|
+
from ..engine_studio_utils.formatting import get_studio_disk_usage_via_ssm
|
11
|
+
|
12
|
+
|
13
|
+
def list_studios(
|
14
|
+
all_users: bool = typer.Option(
|
15
|
+
False, "--all", "-a", help="Show all users' studios"
|
16
|
+
),
|
17
|
+
):
|
18
|
+
"""List studios."""
|
19
|
+
username = check_aws_sso()
|
20
|
+
|
21
|
+
response = make_api_request("GET", "/studios")
|
22
|
+
|
23
|
+
if response.status_code == 200:
|
24
|
+
studios = response.json().get("studios", [])
|
25
|
+
|
26
|
+
if not studios:
|
27
|
+
console.print("No studios found.")
|
28
|
+
return
|
29
|
+
|
30
|
+
# Get all engines to map instance IDs to names
|
31
|
+
engines_response = make_api_request("GET", "/engines")
|
32
|
+
engines = {}
|
33
|
+
if engines_response.status_code == 200:
|
34
|
+
for engine in engines_response.json().get("engines", []):
|
35
|
+
engines[engine["instance_id"]] = engine["name"]
|
36
|
+
|
37
|
+
# Create table
|
38
|
+
table = Table(title="Studios", box=box.ROUNDED)
|
39
|
+
table.add_column("Studio ID", style="cyan")
|
40
|
+
table.add_column("User")
|
41
|
+
table.add_column("Status")
|
42
|
+
table.add_column("Size", justify="right")
|
43
|
+
table.add_column("Disk Usage", justify="right")
|
44
|
+
table.add_column("Attached To")
|
45
|
+
|
46
|
+
for studio in studios:
|
47
|
+
# Change status display
|
48
|
+
if studio["status"] == "in-use":
|
49
|
+
status_display = "[bright_blue]attached[/bright_blue]"
|
50
|
+
elif studio["status"] in ["attaching", "detaching"]:
|
51
|
+
status_display = "[yellow]" + studio["status"] + "[/yellow]"
|
52
|
+
else:
|
53
|
+
status_display = "[green]available[/green]"
|
54
|
+
|
55
|
+
# Format attached engine info
|
56
|
+
attached_to = "-"
|
57
|
+
disk_usage = "?/?"
|
58
|
+
if studio.get("attached_vm_id"):
|
59
|
+
vm_id = studio["attached_vm_id"]
|
60
|
+
engine_name = engines.get(vm_id, "unknown")
|
61
|
+
attached_to = f"{engine_name} ({vm_id})"
|
62
|
+
|
63
|
+
# Try to get disk usage if attached
|
64
|
+
if studio["status"] == "in-use":
|
65
|
+
usage = get_studio_disk_usage_via_ssm(vm_id, studio["user"])
|
66
|
+
if usage:
|
67
|
+
disk_usage = usage
|
68
|
+
|
69
|
+
table.add_row(
|
70
|
+
studio["studio_id"],
|
71
|
+
studio["user"],
|
72
|
+
status_display,
|
73
|
+
f"{studio['size_gb']}GB",
|
74
|
+
disk_usage,
|
75
|
+
attached_to,
|
76
|
+
)
|
77
|
+
|
78
|
+
console.print(table)
|
79
|
+
else:
|
80
|
+
error = response.json().get("error", "Unknown error")
|
81
|
+
console.print(f"[red]❌ Failed to list studios: {error}[/red]")
|
@@ -0,0 +1,90 @@
|
|
1
|
+
"""Studio reset command."""
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
import boto3
|
6
|
+
import typer
|
7
|
+
from botocore.exceptions import ClientError
|
8
|
+
from rich.prompt import Confirm
|
9
|
+
|
10
|
+
from ..engine_studio_utils.api_utils import get_user_studio
|
11
|
+
from ..engine_studio_utils.aws_utils import check_aws_sso
|
12
|
+
from ..engine_studio_utils.constants import console
|
13
|
+
|
14
|
+
|
15
|
+
def reset_studio(
|
16
|
+
user: Optional[str] = typer.Option(
|
17
|
+
None, "--user", "-u", help="Reset a different user's studio"
|
18
|
+
),
|
19
|
+
):
|
20
|
+
"""Reset a stuck studio (admin operation)."""
|
21
|
+
username = check_aws_sso()
|
22
|
+
|
23
|
+
# Use specified user if provided, otherwise use current user
|
24
|
+
target_user = user if user else username
|
25
|
+
|
26
|
+
# Add warning when resetting another user's studio
|
27
|
+
if target_user != username:
|
28
|
+
console.print(f"[yellow]⚠️ Resetting studio for user: {target_user}[/yellow]")
|
29
|
+
|
30
|
+
studio = get_user_studio(target_user)
|
31
|
+
if not studio:
|
32
|
+
if target_user == username:
|
33
|
+
console.print("[yellow]You don't have a studio.[/yellow]")
|
34
|
+
else:
|
35
|
+
console.print(f"[yellow]User {target_user} doesn't have a studio.[/yellow]")
|
36
|
+
return
|
37
|
+
|
38
|
+
console.print(f"[yellow]⚠️ This will force-reset the studio state[/yellow]")
|
39
|
+
console.print(f"Current status: {studio['status']}")
|
40
|
+
if studio.get("attached_vm_id"):
|
41
|
+
console.print(f"Listed as attached to: {studio['attached_vm_id']}")
|
42
|
+
|
43
|
+
if not Confirm.ask("\nReset studio state?"):
|
44
|
+
console.print("Reset cancelled.")
|
45
|
+
return
|
46
|
+
|
47
|
+
# Direct DynamoDB update
|
48
|
+
console.print("Resetting studio state...")
|
49
|
+
|
50
|
+
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
|
51
|
+
table = dynamodb.Table("dev-studios")
|
52
|
+
|
53
|
+
try:
|
54
|
+
# Check if volume is actually attached
|
55
|
+
ec2 = boto3.client("ec2", region_name="us-east-1")
|
56
|
+
volumes = ec2.describe_volumes(VolumeIds=[studio["studio_id"]])
|
57
|
+
|
58
|
+
if volumes["Volumes"]:
|
59
|
+
volume = volumes["Volumes"][0]
|
60
|
+
attachments = volume.get("Attachments", [])
|
61
|
+
if attachments:
|
62
|
+
console.print(
|
63
|
+
f"[red]Volume is still attached to {attachments[0]['InstanceId']}![/red]"
|
64
|
+
)
|
65
|
+
if Confirm.ask("Force-detach the volume?"):
|
66
|
+
ec2.detach_volume(
|
67
|
+
VolumeId=studio["studio_id"],
|
68
|
+
InstanceId=attachments[0]["InstanceId"],
|
69
|
+
Force=True,
|
70
|
+
)
|
71
|
+
console.print("Waiting for volume to detach...")
|
72
|
+
waiter = ec2.get_waiter("volume_available")
|
73
|
+
waiter.wait(VolumeIds=[studio["studio_id"]])
|
74
|
+
|
75
|
+
# Reset in DynamoDB – align attribute names with Studio Manager backend
|
76
|
+
table.update_item(
|
77
|
+
Key={"StudioID": studio["studio_id"]},
|
78
|
+
UpdateExpression="SET #st = :status, AttachedVMID = :vm_id, AttachedDevice = :device",
|
79
|
+
ExpressionAttributeNames={"#st": "Status"},
|
80
|
+
ExpressionAttributeValues={
|
81
|
+
":status": "available",
|
82
|
+
":vm_id": None,
|
83
|
+
":device": None,
|
84
|
+
},
|
85
|
+
)
|
86
|
+
|
87
|
+
console.print(f"[green]✓ Studio reset to available state![/green]")
|
88
|
+
|
89
|
+
except ClientError as e:
|
90
|
+
console.print(f"[red]❌ Failed to reset studio: {e}[/red]")
|
@@ -0,0 +1,134 @@
|
|
1
|
+
"""Studio resize command."""
|
2
|
+
|
3
|
+
import time
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
import boto3
|
7
|
+
import typer
|
8
|
+
from botocore.exceptions import ClientError
|
9
|
+
from rich.prompt import Confirm
|
10
|
+
|
11
|
+
from ..engine_studio_utils.api_utils import get_user_studio, make_api_request
|
12
|
+
from ..engine_studio_utils.aws_utils import check_aws_sso
|
13
|
+
from ..engine_studio_utils.constants import console
|
14
|
+
|
15
|
+
|
16
|
+
def resize_studio(
|
17
|
+
size: int = typer.Option(..., "--size", "-s", help="New size in GB"),
|
18
|
+
user: Optional[str] = typer.Option(
|
19
|
+
None, "--user", "-u", help="Resize a different user's studio (admin only)"
|
20
|
+
),
|
21
|
+
):
|
22
|
+
"""Resize your studio volume (requires detachment)."""
|
23
|
+
username = check_aws_sso()
|
24
|
+
|
25
|
+
# Use specified user if provided, otherwise use current user
|
26
|
+
target_user = user if user else username
|
27
|
+
|
28
|
+
# Add warning when resizing another user's studio
|
29
|
+
if target_user != username:
|
30
|
+
console.print(f"[yellow]⚠️ Resizing studio for user: {target_user}[/yellow]")
|
31
|
+
|
32
|
+
studio = get_user_studio(target_user)
|
33
|
+
if not studio:
|
34
|
+
if target_user == username:
|
35
|
+
console.print("[yellow]You don't have a studio yet.[/yellow]")
|
36
|
+
else:
|
37
|
+
console.print(f"[yellow]User {target_user} doesn't have a studio.[/yellow]")
|
38
|
+
return
|
39
|
+
|
40
|
+
current_size = studio["size_gb"]
|
41
|
+
|
42
|
+
if size <= current_size:
|
43
|
+
console.print(
|
44
|
+
f"[red]❌ New size ({size}GB) must be larger than current size ({current_size}GB)[/red]"
|
45
|
+
)
|
46
|
+
raise typer.Exit(1)
|
47
|
+
|
48
|
+
# Check if studio is attached
|
49
|
+
if studio["status"] == "in-use":
|
50
|
+
console.print("[yellow]⚠️ Studio must be detached before resizing[/yellow]")
|
51
|
+
console.print(f"Currently attached to: {studio.get('attached_vm_id')}")
|
52
|
+
|
53
|
+
if not Confirm.ask("\nDetach studio and proceed with resize?"):
|
54
|
+
console.print("Resize cancelled.")
|
55
|
+
return
|
56
|
+
|
57
|
+
# Detach the studio
|
58
|
+
console.print("Detaching studio...")
|
59
|
+
response = make_api_request("POST", f"/studios/{studio['studio_id']}/detach")
|
60
|
+
if response.status_code != 200:
|
61
|
+
console.print("[red]❌ Failed to detach studio[/red]")
|
62
|
+
raise typer.Exit(1)
|
63
|
+
|
64
|
+
console.print("[green]✓ Studio detached[/green]")
|
65
|
+
|
66
|
+
# Wait a moment for detachment to complete
|
67
|
+
time.sleep(5)
|
68
|
+
|
69
|
+
console.print(f"[yellow]Resizing studio from {current_size}GB to {size}GB[/yellow]")
|
70
|
+
|
71
|
+
# Call the resize API
|
72
|
+
resize_response = make_api_request(
|
73
|
+
"POST", f"/studios/{studio['studio_id']}/resize", json_data={"size": size}
|
74
|
+
)
|
75
|
+
|
76
|
+
if resize_response.status_code != 200:
|
77
|
+
error = resize_response.json().get("error", "Unknown error")
|
78
|
+
console.print(f"[red]❌ Failed to resize studio: {error}[/red]")
|
79
|
+
raise typer.Exit(1)
|
80
|
+
|
81
|
+
# Wait for volume modification to complete
|
82
|
+
ec2 = boto3.client("ec2", region_name="us-east-1")
|
83
|
+
console.print("Resizing volume...")
|
84
|
+
|
85
|
+
# Track progress
|
86
|
+
last_progress = 0
|
87
|
+
|
88
|
+
while True:
|
89
|
+
try:
|
90
|
+
mod_state = ec2.describe_volumes_modifications(
|
91
|
+
VolumeIds=[studio["studio_id"]]
|
92
|
+
)
|
93
|
+
if not mod_state["VolumesModifications"]:
|
94
|
+
break # Modification complete
|
95
|
+
|
96
|
+
modification = mod_state["VolumesModifications"][0]
|
97
|
+
state = modification["ModificationState"]
|
98
|
+
progress = modification.get("Progress", 0)
|
99
|
+
|
100
|
+
# Show progress updates only for the resize phase
|
101
|
+
if state == "modifying" and progress > last_progress:
|
102
|
+
console.print(f"[yellow]Progress: {progress}%[/yellow]")
|
103
|
+
last_progress = progress
|
104
|
+
|
105
|
+
# Exit as soon as optimization starts (resize is complete)
|
106
|
+
if state == "optimizing":
|
107
|
+
console.print(
|
108
|
+
f"[green]✓ Studio resized successfully to {size}GB![/green]"
|
109
|
+
)
|
110
|
+
console.print(
|
111
|
+
"[dim]AWS is optimizing the volume in the background (no action needed).[/dim]"
|
112
|
+
)
|
113
|
+
break
|
114
|
+
|
115
|
+
if state == "completed":
|
116
|
+
console.print(
|
117
|
+
f"[green]✓ Studio resized successfully to {size}GB![/green]"
|
118
|
+
)
|
119
|
+
break
|
120
|
+
elif state == "failed":
|
121
|
+
console.print("[red]❌ Volume modification failed[/red]")
|
122
|
+
raise typer.Exit(1)
|
123
|
+
|
124
|
+
time.sleep(2) # Check more frequently for better UX
|
125
|
+
|
126
|
+
except ClientError:
|
127
|
+
# Modification might be complete
|
128
|
+
console.print(f"[green]✓ Studio resized successfully to {size}GB![/green]")
|
129
|
+
break
|
130
|
+
|
131
|
+
console.print(
|
132
|
+
"\n[dim]The filesystem will be automatically expanded when you next attach the studio.[/dim]"
|
133
|
+
)
|
134
|
+
console.print(f"To attach: [cyan]dh studio attach <engine-name>[/cyan]")
|
@@ -0,0 +1,78 @@
|
|
1
|
+
"""Studio status command."""
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
import typer
|
6
|
+
from rich.panel import Panel
|
7
|
+
|
8
|
+
from ..engine_studio_utils.api_utils import get_user_studio, make_api_request
|
9
|
+
from ..engine_studio_utils.aws_utils import check_aws_sso
|
10
|
+
from ..engine_studio_utils.constants import console
|
11
|
+
|
12
|
+
|
13
|
+
def studio_status(
|
14
|
+
user: Optional[str] = typer.Option(
|
15
|
+
None, "--user", "-u", help="Check status for a different user (admin only)"
|
16
|
+
),
|
17
|
+
):
|
18
|
+
"""Show status of your studio."""
|
19
|
+
username = check_aws_sso()
|
20
|
+
|
21
|
+
# Use specified user if provided, otherwise use current user
|
22
|
+
target_user = user if user else username
|
23
|
+
|
24
|
+
# Add warning when checking another user's studio
|
25
|
+
if target_user != username:
|
26
|
+
console.print(
|
27
|
+
f"[yellow]⚠️ Checking studio status for user: {target_user}[/yellow]"
|
28
|
+
)
|
29
|
+
|
30
|
+
studio = get_user_studio(target_user)
|
31
|
+
if not studio:
|
32
|
+
if target_user == username:
|
33
|
+
console.print("[yellow]You don't have a studio yet.[/yellow]")
|
34
|
+
console.print("Create one with: [cyan]dh studio create[/cyan]")
|
35
|
+
else:
|
36
|
+
console.print(f"[yellow]User {target_user} doesn't have a studio.[/yellow]")
|
37
|
+
return
|
38
|
+
|
39
|
+
# Create status panel
|
40
|
+
# Format status with colors
|
41
|
+
status = studio["status"]
|
42
|
+
if status == "in-use":
|
43
|
+
status_display = "[bright_blue]attached[/bright_blue]"
|
44
|
+
elif status in ["attaching", "detaching"]:
|
45
|
+
status_display = f"[yellow]{status}[/yellow]"
|
46
|
+
else:
|
47
|
+
status_display = f"[green]{status}[/green]"
|
48
|
+
|
49
|
+
status_lines = [
|
50
|
+
f"[bold]Studio ID:[/bold] {studio['studio_id']}",
|
51
|
+
f"[bold]User:[/bold] {studio['user']}",
|
52
|
+
f"[bold]Status:[/bold] {status_display}",
|
53
|
+
f"[bold]Size:[/bold] {studio['size_gb']}GB",
|
54
|
+
f"[bold]Created:[/bold] {studio['creation_date']}",
|
55
|
+
]
|
56
|
+
|
57
|
+
if studio.get("attached_vm_id"):
|
58
|
+
status_lines.append(f"[bold]Attached to:[/bold] {studio['attached_vm_id']}")
|
59
|
+
|
60
|
+
# Try to get engine details
|
61
|
+
response = make_api_request("GET", "/engines")
|
62
|
+
if response.status_code == 200:
|
63
|
+
engines = response.json().get("engines", [])
|
64
|
+
attached_engine = next(
|
65
|
+
(e for e in engines if e["instance_id"] == studio["attached_vm_id"]),
|
66
|
+
None,
|
67
|
+
)
|
68
|
+
if attached_engine:
|
69
|
+
status_lines.append(
|
70
|
+
f"[bold]Engine Name:[/bold] {attached_engine['name']}"
|
71
|
+
)
|
72
|
+
|
73
|
+
panel = Panel(
|
74
|
+
"\n".join(status_lines),
|
75
|
+
title="Studio Details",
|
76
|
+
border_style="blue",
|
77
|
+
)
|
78
|
+
console.print(panel)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
dayhoff_tools/__init__.py,sha256=M5zThPyEBRYa5CfwlzKhcqTevWn3OKu62cjV6Zqie2A,469
|
2
|
+
dayhoff_tools/chemistry/standardizer.py,sha256=uMn7VwHnx02nc404eO6fRuS4rsl4dvSPf2ElfZDXEpY,11188
|
3
|
+
dayhoff_tools/chemistry/utils.py,sha256=jt-7JgF-GeeVC421acX-bobKbLU_X94KNOW24p_P-_M,2257
|
4
|
+
dayhoff_tools/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
dayhoff_tools/cli/cloud_commands.py,sha256=33qcWLmq-FwEXMdL3F0OHm-5Stlh2r65CldyEZgQ1no,40904
|
6
|
+
dayhoff_tools/cli/engine/__init__.py,sha256=WV7aojsipkqQRXQ8U6KHGdPr0C1spX348sHp6dqz63Y,34
|
7
|
+
dayhoff_tools/cli/engine/coffee.py,sha256=2IKKc6skpDv7kBJz7l5Tvu0FM8hjUoYA-k6LAxdVzKE,3935
|
8
|
+
dayhoff_tools/cli/engine/config_ssh.py,sha256=qh5bJH-ELOZnFa9UdcGGXKV8wH5DzybhXJ4b2XSaTyI,3974
|
9
|
+
dayhoff_tools/cli/engine/debug.py,sha256=29Cmzj3dyzkAjO3HcJzp4jkB4eCgRhyF2MjtHE4WarE,2714
|
10
|
+
dayhoff_tools/cli/engine/gami.py,sha256=OwZKb87oq3zIQ2a9-n_oJ4UK84XOUP3InHYRSAERvaQ,6219
|
11
|
+
dayhoff_tools/cli/engine/idle.py,sha256=YGAzII7Db0w1R4_3ebQZVSPWKRmwc_WR3ZafItFs35E,5607
|
12
|
+
dayhoff_tools/cli/engine/launch.py,sha256=G6-vUbpFLC2DnMfD5HXQqWgCuCqZ5E42ZuOl45g6N3c,3519
|
13
|
+
dayhoff_tools/cli/engine/lifecycle.py,sha256=_Dk-EZs_qbm8APdOuGOuxhlbK6RgkkoLk2nrwKoo1-A,4519
|
14
|
+
dayhoff_tools/cli/engine/list.py,sha256=UsghtBxLgXIlvPuKu4_jqe9yrCHgh8uFEbulCA3Cfmg,3896
|
15
|
+
dayhoff_tools/cli/engine/repair.py,sha256=lfe5vSfdgRX3uY2sPA4bLtl0Oj0myqSQZZNV_lYD7_g,5037
|
16
|
+
dayhoff_tools/cli/engine/resize.py,sha256=BzbvmT5Hf-FbfiCFwS8Sjj6dfMZ8CYihUwpOLAUNpOI,7818
|
17
|
+
dayhoff_tools/cli/engine/ssh.py,sha256=m8h-SdfXsok5PgkcvfAoai0-AKI78oE9sJeXWQnkn3g,2095
|
18
|
+
dayhoff_tools/cli/engine/status.py,sha256=zlxDpKXCbWpfY7fnsKqxlvakq5txOmHTSeNA4VIX3cg,20461
|
19
|
+
dayhoff_tools/cli/engine_studio_commands.py,sha256=tePPfiBtAUmhJ4ttjbtbkse9roqoTAi36nUAbKijFJM,9485
|
20
|
+
dayhoff_tools/cli/engine_studio_utils/__init__.py,sha256=G9uYoIG79B9u8BmcWyy5Y_UN4U4CVLZe7yHhcQPJZNk,54
|
21
|
+
dayhoff_tools/cli/engine_studio_utils/api_utils.py,sha256=3c7hwwD18b5v6aaDH0O_RZe8V0i5lz8UjgpR-2gl8xA,1347
|
22
|
+
dayhoff_tools/cli/engine_studio_utils/aws_utils.py,sha256=T71HXI2RjZQB3QVlcGAB7Qpkn71dHf0Tzxt1QXjSW6Y,3656
|
23
|
+
dayhoff_tools/cli/engine_studio_utils/constants.py,sha256=9usS7R2jS4qtieOi4EKZeB372clQpSIsa_NdOIGcrcc,526
|
24
|
+
dayhoff_tools/cli/engine_studio_utils/formatting.py,sha256=0n_nHKjejJJCUF8HZOEbjtLOgDVNSImaPRH31Ag0eGs,6800
|
25
|
+
dayhoff_tools/cli/engine_studio_utils/ssh_utils.py,sha256=1L5t-ZE3V5-eVznrEViJ8j1HouPwFq6CfoICQkySfCU,4867
|
26
|
+
dayhoff_tools/cli/main.py,sha256=IIwgJBuCCSFg1JFnUOoo-o3g7E7eLLXgRvISWUy_HdQ,5939
|
27
|
+
dayhoff_tools/cli/studio/__init__.py,sha256=SIg-34_DZT_w6OQGFEoHcf4NLWY5XyHlwcP585WwVWs,34
|
28
|
+
dayhoff_tools/cli/studio/attach.py,sha256=NOT9jxCE6tGRXxxYF4_o4cCbnJ2xIh7B0B0HGD3YxaU,11810
|
29
|
+
dayhoff_tools/cli/studio/create.py,sha256=x2UuSgj5V8S_P6derLdCsBmX3KTU4KJBuA2O17WxrOs,1664
|
30
|
+
dayhoff_tools/cli/studio/delete.py,sha256=Fvt581Uve-IMazfHo_5vXws9smqWxXTdtCaxUCDKESA,2385
|
31
|
+
dayhoff_tools/cli/studio/detach.py,sha256=NRvAAmOtJCkYPPnT04msL8Z2LfYA2zYo71WYekYreIA,2018
|
32
|
+
dayhoff_tools/cli/studio/list.py,sha256=e-7psFx3A2fxxSbxafI0RqHtvZxaa8qWjK_4F3iO5RI,2813
|
33
|
+
dayhoff_tools/cli/studio/reset.py,sha256=gENkUOsTzLqV4oyeVsMEF_EN5c6qAAf0pZtvGhk8Bnc,3308
|
34
|
+
dayhoff_tools/cli/studio/resize.py,sha256=LadryhJoo-vo4xrl88wsLgiTEiaq1x5XXQNiWrGgQcU,4830
|
35
|
+
dayhoff_tools/cli/studio/status.py,sha256=bua30619ue8tnmDc8KvY8X-udUHw8vSoXV9HLmiBKB4,2647
|
36
|
+
dayhoff_tools/cli/swarm_commands.py,sha256=5EyKj8yietvT5lfoz8Zx0iQvVaNgc3SJX1z2zQR6o6M,5614
|
37
|
+
dayhoff_tools/cli/utility_commands.py,sha256=WQTHOh1MttuxaJjl2c6zMa4x7_JuaKMQgcyotYrU3GA,25883
|
38
|
+
dayhoff_tools/deployment/base.py,sha256=mYp560l6hSDFtyY2H42VoM8k9VUzfwuiyh9Knqpgc28,17441
|
39
|
+
dayhoff_tools/deployment/deploy_aws.py,sha256=GvZpE2YIFA5Dl9rkAljFjtUypmPDNbWgw8NicHYTP24,18265
|
40
|
+
dayhoff_tools/deployment/deploy_gcp.py,sha256=xgaOVsUDmP6wSEMYNkm1yRNcVskfdz80qJtCulkBIAM,8860
|
41
|
+
dayhoff_tools/deployment/deploy_utils.py,sha256=StFwbqnr2_FWiKVg3xnJF4kagTHzndqqDkpaIOaAn_4,26027
|
42
|
+
dayhoff_tools/deployment/job_runner.py,sha256=hljvFpH2Bw96uYyUup5Ths72PZRL_X27KxlYzBMgguo,5086
|
43
|
+
dayhoff_tools/deployment/processors.py,sha256=LM0CQbr4XCb3AtLbrcuDQm4tYPXsoNqgVJ4WQYDjzJc,12406
|
44
|
+
dayhoff_tools/deployment/swarm.py,sha256=YJfvVOcAS8cYcIj2fiN4qwC2leh0I9w5A4px8ZWSF6g,22833
|
45
|
+
dayhoff_tools/embedders.py,sha256=1THnmio4FYkBswy_xkIiwT-ZOEMn6ZLbTAa-Uz0-kyE,36615
|
46
|
+
dayhoff_tools/fasta.py,sha256=USdemH4c_dNhWXOTAhldvlDi8eLHogsy0YSrOnODB5I,50773
|
47
|
+
dayhoff_tools/file_ops.py,sha256=JlGowvr-CUJFidV-4g_JmhUTN9bsYuaxtqKmnKomm-Q,8506
|
48
|
+
dayhoff_tools/h5.py,sha256=j1nxxaiHsMidVX_XwB33P1Pz9d7K8ZKiDZwJWQUUQSY,21158
|
49
|
+
dayhoff_tools/intake/gcp.py,sha256=uCeEskhbEwJIYpN6ne6siT1dbpTizCjjel-hRe0kReE,3030
|
50
|
+
dayhoff_tools/intake/gtdb.py,sha256=58JNLWpr3LkXiIq-ubAcEFeR5DyN9YrA-YEoQI_FzlA,10608
|
51
|
+
dayhoff_tools/intake/kegg.py,sha256=SaVbumB4leNTSevamT29yIqHurejw1wmcCC32D5Qyco,965
|
52
|
+
dayhoff_tools/intake/mmseqs.py,sha256=uEYzRsthJAlUeRYNCfFtJFE73SbuhfUIS1ygYFkhmtw,6435
|
53
|
+
dayhoff_tools/intake/structure.py,sha256=ufN3gAodQxhnt7psK1VTQeu9rKERmo_PhoxIbB4QKMw,27660
|
54
|
+
dayhoff_tools/intake/uniprot.py,sha256=BZYJQF63OtPcBBnQ7_P9gulxzJtqyorgyuDiPeOJqE4,16456
|
55
|
+
dayhoff_tools/logs.py,sha256=DKdeP0k0kliRcilwvX0mUB2eipO5BdWUeHwh-VnsICs,838
|
56
|
+
dayhoff_tools/sqlite.py,sha256=jV55ikF8VpTfeQqqlHSbY8OgfyfHj8zgHNpZjBLos_E,18672
|
57
|
+
dayhoff_tools/warehouse.py,sha256=UETBtZD3r7WgvURqfGbyHlT7cxoiVq8isjzMuerKw8I,24475
|
58
|
+
dayhoff_tools-1.10.1.dist-info/METADATA,sha256=gQ7_S_-Xe_eFvVIi2VncpKssTfB3EhnODS0i47YTo0E,2915
|
59
|
+
dayhoff_tools-1.10.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
60
|
+
dayhoff_tools-1.10.1.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
|
61
|
+
dayhoff_tools-1.10.1.dist-info/RECORD,,
|