dayhoff-tools 1.3.6__tar.gz → 1.3.8__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.3.6 → dayhoff_tools-1.3.8}/PKG-INFO +1 -1
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/engine_commands.py +153 -64
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/pyproject.toml +1 -1
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/README.md +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/__init__.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/chemistry/standardizer.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/chemistry/utils.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/__init__.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/cloud_commands.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/main.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/swarm_commands.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/utility_commands.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/base.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/deploy_aws.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/deploy_gcp.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/deploy_utils.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/job_runner.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/processors.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/swarm.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/embedders.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/fasta.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/file_ops.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/h5.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/gcp.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/gtdb.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/kegg.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/mmseqs.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/structure.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/uniprot.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/logs.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/sqlite.py +0 -0
- {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/warehouse.py +0 -0
@@ -27,10 +27,10 @@ console = Console()
|
|
27
27
|
# Cost information
|
28
28
|
HOURLY_COSTS = {
|
29
29
|
"cpu": 0.50, # r6i.2xlarge
|
30
|
-
"cpumax":
|
31
|
-
"t4":
|
32
|
-
"a10g":
|
33
|
-
"a100":
|
30
|
+
"cpumax": 2.02, # r7i.8xlarge
|
31
|
+
"t4": 0.75, # g4dn.2xlarge
|
32
|
+
"a10g": 1.50, # g5.2xlarge
|
33
|
+
"a100": 21.96, # p4d.24xlarge
|
34
34
|
}
|
35
35
|
|
36
36
|
# SSH config management
|
@@ -90,7 +90,10 @@ def get_api_url() -> str:
|
|
90
90
|
|
91
91
|
|
92
92
|
def make_api_request(
|
93
|
-
method: str,
|
93
|
+
method: str,
|
94
|
+
endpoint: str,
|
95
|
+
json_data: Optional[Dict] = None,
|
96
|
+
params: Optional[Dict] = None,
|
94
97
|
) -> requests.Response:
|
95
98
|
"""Make an API request with error handling."""
|
96
99
|
api_url = get_api_url()
|
@@ -134,14 +137,14 @@ def parse_launch_time(launch_time_str: str) -> datetime:
|
|
134
137
|
"%Y-%m-%dT%H:%M:%S+00:00", # Explicit UTC offset
|
135
138
|
"%Y-%m-%d %H:%M:%S",
|
136
139
|
]
|
137
|
-
|
140
|
+
|
138
141
|
# First try parsing with fromisoformat for better timezone handling
|
139
142
|
try:
|
140
143
|
# Handle the ISO format properly
|
141
|
-
return datetime.fromisoformat(launch_time_str.replace(
|
144
|
+
return datetime.fromisoformat(launch_time_str.replace("Z", "+00:00"))
|
142
145
|
except (ValueError, AttributeError):
|
143
146
|
pass
|
144
|
-
|
147
|
+
|
145
148
|
# Fallback to manual format parsing
|
146
149
|
for fmt in formats:
|
147
150
|
try:
|
@@ -152,7 +155,7 @@ def parse_launch_time(launch_time_str: str) -> datetime:
|
|
152
155
|
return parsed
|
153
156
|
except ValueError:
|
154
157
|
continue
|
155
|
-
|
158
|
+
|
156
159
|
# Fallback: assume it's recent
|
157
160
|
return datetime.now(timezone.utc)
|
158
161
|
|
@@ -163,7 +166,7 @@ def format_status(state: str, ready: Optional[bool]) -> str:
|
|
163
166
|
if ready is True:
|
164
167
|
return "[green]Running ✓[/green]"
|
165
168
|
elif ready is False:
|
166
|
-
return "[yellow]Running ⚠ (
|
169
|
+
return "[yellow]Running ⚠ (Bootstrapping...)[/yellow]"
|
167
170
|
else:
|
168
171
|
return "[green]Running[/green]"
|
169
172
|
elif state.lower() == "stopped":
|
@@ -213,7 +216,9 @@ def resolve_engine(name_or_id: str, engines: List[Dict]) -> Dict:
|
|
213
216
|
while True:
|
214
217
|
try:
|
215
218
|
choice = IntPrompt.ask(
|
216
|
-
"Select engine",
|
219
|
+
"Select engine",
|
220
|
+
default=1,
|
221
|
+
choices=[str(i) for i in range(1, len(matches) + 1)],
|
217
222
|
)
|
218
223
|
return matches[choice - 1]
|
219
224
|
except (ValueError, IndexError):
|
@@ -307,7 +312,9 @@ def launch_engine(
|
|
307
312
|
raise typer.Exit(1)
|
308
313
|
|
309
314
|
cost = HOURLY_COSTS.get(engine_type, 0)
|
310
|
-
console.print(
|
315
|
+
console.print(
|
316
|
+
f"Launching [cyan]{name}[/cyan] ({engine_type}) for ${cost:.2f}/hour..."
|
317
|
+
)
|
311
318
|
|
312
319
|
with Progress(
|
313
320
|
SpinnerColumn(),
|
@@ -332,14 +339,17 @@ def launch_engine(
|
|
332
339
|
else:
|
333
340
|
error = response.json().get("error", "Unknown error")
|
334
341
|
console.print(f"[red]❌ Failed to launch engine: {error}[/red]")
|
335
|
-
raise typer.Exit(1)
|
336
342
|
|
337
343
|
|
338
344
|
@engine_app.command("list")
|
339
345
|
def list_engines(
|
340
346
|
user: Optional[str] = typer.Option(None, "--user", "-u", help="Filter by user"),
|
341
|
-
running_only: bool = typer.Option(
|
342
|
-
|
347
|
+
running_only: bool = typer.Option(
|
348
|
+
False, "--running", help="Show only running engines"
|
349
|
+
),
|
350
|
+
stopped_only: bool = typer.Option(
|
351
|
+
False, "--stopped", help="Show only stopped engines"
|
352
|
+
),
|
343
353
|
):
|
344
354
|
"""List engines (shows all engines by default)."""
|
345
355
|
current_user = check_aws_sso()
|
@@ -426,7 +436,9 @@ def engine_status(
|
|
426
436
|
engine = resolve_engine(name_or_id, engines)
|
427
437
|
|
428
438
|
# Get attached studios info
|
429
|
-
response = make_api_request(
|
439
|
+
response = make_api_request(
|
440
|
+
"GET", f"/engines/{engine['instance_id']}/attached-studios"
|
441
|
+
)
|
430
442
|
attached_studios = []
|
431
443
|
if response.status_code == 200:
|
432
444
|
attached_studios = response.json().get("studios", [])
|
@@ -469,7 +481,9 @@ def engine_status(
|
|
469
481
|
@engine_app.command("stop")
|
470
482
|
def stop_engine(
|
471
483
|
name_or_id: str = typer.Argument(help="Engine name or instance ID"),
|
472
|
-
force: bool = typer.Option(
|
484
|
+
force: bool = typer.Option(
|
485
|
+
False, "--force", "-f", help="Force stop and detach all studios"
|
486
|
+
),
|
473
487
|
):
|
474
488
|
"""Stop an engine."""
|
475
489
|
check_aws_sso()
|
@@ -569,7 +583,9 @@ def terminate_engine(
|
|
569
583
|
hourly_cost = HOURLY_COSTS.get(engine["engine_type"], 0)
|
570
584
|
total_cost = hourly_cost * (uptime.total_seconds() / 3600)
|
571
585
|
|
572
|
-
console.print(
|
586
|
+
console.print(
|
587
|
+
f"\n[yellow]⚠️ This will permanently terminate engine '{engine['name']}'[/yellow]"
|
588
|
+
)
|
573
589
|
console.print(f"Total cost for this session: ${total_cost:.2f}")
|
574
590
|
|
575
591
|
if not Confirm.ask("\nAre you sure you want to terminate this engine?"):
|
@@ -617,7 +633,9 @@ def ssh_engine(
|
|
617
633
|
@engine_app.command("config-ssh")
|
618
634
|
def config_ssh(
|
619
635
|
clean: bool = typer.Option(False, "--clean", help="Remove all managed entries"),
|
620
|
-
all_engines: bool = typer.Option(
|
636
|
+
all_engines: bool = typer.Option(
|
637
|
+
False, "--all", "-a", help="Include all engines from all users"
|
638
|
+
),
|
621
639
|
):
|
622
640
|
"""Update SSH config with available engines."""
|
623
641
|
username = check_aws_sso()
|
@@ -628,7 +646,9 @@ def config_ssh(
|
|
628
646
|
if all_engines:
|
629
647
|
console.print("Updating SSH config with all running engines...")
|
630
648
|
else:
|
631
|
-
console.print(
|
649
|
+
console.print(
|
650
|
+
f"Updating SSH config with running engines for [cyan]{username}[/cyan] and [cyan]shared[/cyan]..."
|
651
|
+
)
|
632
652
|
|
633
653
|
# Get all engines
|
634
654
|
response = make_api_request("GET", "/engines")
|
@@ -638,13 +658,12 @@ def config_ssh(
|
|
638
658
|
|
639
659
|
engines = response.json().get("engines", [])
|
640
660
|
running_engines = [e for e in engines if e["state"].lower() == "running"]
|
641
|
-
|
661
|
+
|
642
662
|
# Filter engines based on options
|
643
663
|
if not all_engines:
|
644
664
|
# Show only current user's engines and shared engines
|
645
665
|
running_engines = [
|
646
|
-
e for e in running_engines
|
647
|
-
if e["user"] == username or e["user"] == "shared"
|
666
|
+
e for e in running_engines if e["user"] == username or e["user"] == "shared"
|
648
667
|
]
|
649
668
|
|
650
669
|
# Read existing config
|
@@ -681,7 +700,7 @@ def config_ssh(
|
|
681
700
|
f"Host {engine['name']} {SSH_MANAGED_COMMENT}",
|
682
701
|
f" HostName {engine['instance_id']}",
|
683
702
|
f" User {username}",
|
684
|
-
f
|
703
|
+
f" ProxyCommand sh -c \"aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'\"",
|
685
704
|
]
|
686
705
|
)
|
687
706
|
|
@@ -696,8 +715,12 @@ def config_ssh(
|
|
696
715
|
f"[green]✓ Updated SSH config with {len(running_engines)} engines[/green]"
|
697
716
|
)
|
698
717
|
for engine in running_engines:
|
699
|
-
user_display =
|
700
|
-
|
718
|
+
user_display = (
|
719
|
+
f"[dim]({engine['user']})[/dim]" if engine["user"] != username else ""
|
720
|
+
)
|
721
|
+
console.print(
|
722
|
+
f" • {engine['name']} → {engine['instance_id']} {user_display}"
|
723
|
+
)
|
701
724
|
|
702
725
|
|
703
726
|
@engine_app.command("keep-awake")
|
@@ -710,6 +733,7 @@ def keep_awake(
|
|
710
733
|
|
711
734
|
# Parse duration
|
712
735
|
import re
|
736
|
+
|
713
737
|
match = re.match(r"(?:(\d+)h)?(?:(\d+)m)?", duration)
|
714
738
|
if not match or (not match.group(1) and not match.group(2)):
|
715
739
|
console.print(f"[red]❌ Invalid duration format: {duration}[/red]")
|
@@ -753,6 +777,7 @@ def keep_awake(
|
|
753
777
|
|
754
778
|
# Wait for command to complete
|
755
779
|
import time
|
780
|
+
|
756
781
|
for _ in range(10):
|
757
782
|
time.sleep(1)
|
758
783
|
result = ssm.get_command_invocation(
|
@@ -771,7 +796,9 @@ def keep_awake(
|
|
771
796
|
"[dim]Use keep-awake for nohup operations or other background tasks.[/dim]"
|
772
797
|
)
|
773
798
|
else:
|
774
|
-
console.print(
|
799
|
+
console.print(
|
800
|
+
f"[red]❌ Failed to set keep-awake: {result.get('StatusDetails', 'Unknown error')}[/red]"
|
801
|
+
)
|
775
802
|
|
776
803
|
except ClientError as e:
|
777
804
|
console.print(f"[red]❌ Failed to set keep-awake: {e}[/red]")
|
@@ -811,6 +838,7 @@ def cancel_keep_awake(
|
|
811
838
|
|
812
839
|
# Wait for command to complete
|
813
840
|
import time
|
841
|
+
|
814
842
|
for _ in range(10):
|
815
843
|
time.sleep(1)
|
816
844
|
result = ssm.get_command_invocation(
|
@@ -821,9 +849,13 @@ def cancel_keep_awake(
|
|
821
849
|
break
|
822
850
|
|
823
851
|
if result["Status"] == "Success":
|
824
|
-
console.print(
|
852
|
+
console.print(
|
853
|
+
"[green]✓ Keep-awake cancelled, auto-shutdown re-enabled[/green]"
|
854
|
+
)
|
825
855
|
else:
|
826
|
-
console.print(
|
856
|
+
console.print(
|
857
|
+
f"[red]❌ Failed to cancel keep-awake: {result.get('StatusDetails', 'Unknown error')}[/red]"
|
858
|
+
)
|
827
859
|
|
828
860
|
except ClientError as e:
|
829
861
|
console.print(f"[red]❌ Failed to cancel keep-awake: {e}[/red]")
|
@@ -831,7 +863,9 @@ def cancel_keep_awake(
|
|
831
863
|
|
832
864
|
@engine_app.command("create-ami")
|
833
865
|
def create_ami(
|
834
|
-
name_or_id: str = typer.Argument(
|
866
|
+
name_or_id: str = typer.Argument(
|
867
|
+
help="Engine name or instance ID to create AMI from"
|
868
|
+
),
|
835
869
|
):
|
836
870
|
"""Create a Golden AMI from an engine."""
|
837
871
|
check_aws_sso()
|
@@ -860,7 +894,9 @@ def create_ami(
|
|
860
894
|
|
861
895
|
console.print(f"AMI Name: [cyan]{ami_name}[/cyan]")
|
862
896
|
console.print(f"Description: {ami_description}")
|
863
|
-
console.print(
|
897
|
+
console.print(
|
898
|
+
"\n[yellow]⚠️ Important: This will reboot the engine to ensure a clean snapshot.[/yellow]"
|
899
|
+
)
|
864
900
|
|
865
901
|
if not Confirm.ask("\nContinue with AMI creation?"):
|
866
902
|
console.print("AMI creation cancelled.")
|
@@ -873,7 +909,7 @@ def create_ami(
|
|
873
909
|
# First, we need to clean up the sentinel file via SSM
|
874
910
|
console.print("Cleaning up bootstrap sentinel file...")
|
875
911
|
ssm = boto3.client("ssm", region_name="us-east-1")
|
876
|
-
|
912
|
+
|
877
913
|
cleanup_response = ssm.send_command(
|
878
914
|
InstanceIds=[engine["instance_id"]],
|
879
915
|
DocumentName="AWS-RunShellScript",
|
@@ -889,6 +925,7 @@ def create_ami(
|
|
889
925
|
|
890
926
|
# Wait for cleanup to complete
|
891
927
|
import time
|
928
|
+
|
892
929
|
command_id = cleanup_response["Command"]["CommandId"]
|
893
930
|
for _ in range(10):
|
894
931
|
time.sleep(1)
|
@@ -900,21 +937,25 @@ def create_ami(
|
|
900
937
|
break
|
901
938
|
|
902
939
|
if result["Status"] != "Success":
|
903
|
-
console.print(
|
940
|
+
console.print(
|
941
|
+
"[yellow]⚠️ Warning: Cleanup command may have failed[/yellow]"
|
942
|
+
)
|
904
943
|
|
905
944
|
# Get instance details to find volumes to exclude
|
906
945
|
instances = ec2.describe_instances(InstanceIds=[engine["instance_id"]])
|
907
946
|
instance = instances["Reservations"][0]["Instances"][0]
|
908
|
-
|
947
|
+
|
909
948
|
root_device = instance.get("RootDeviceName", "/dev/xvda")
|
910
949
|
block_mappings = instance.get("BlockDeviceMappings", [])
|
911
|
-
|
950
|
+
|
912
951
|
# Build exclusion list for non-root volumes
|
913
952
|
block_device_mappings = []
|
914
953
|
for mapping in block_mappings:
|
915
954
|
device_name = mapping.get("DeviceName", "")
|
916
955
|
if device_name != root_device:
|
917
|
-
block_device_mappings.append(
|
956
|
+
block_device_mappings.append(
|
957
|
+
{"DeviceName": device_name, "NoDevice": ""}
|
958
|
+
)
|
918
959
|
console.print(f" Excluding volume at {device_name}")
|
919
960
|
|
920
961
|
# Create the AMI
|
@@ -923,7 +964,9 @@ def create_ami(
|
|
923
964
|
TextColumn("[progress.description]{task.description}"),
|
924
965
|
transient=True,
|
925
966
|
) as progress:
|
926
|
-
progress.add_task(
|
967
|
+
progress.add_task(
|
968
|
+
"Creating AMI (this will take several minutes)...", total=None
|
969
|
+
)
|
927
970
|
|
928
971
|
create_params = {
|
929
972
|
"InstanceId": engine["instance_id"],
|
@@ -948,15 +991,50 @@ def create_ami(
|
|
948
991
|
|
949
992
|
response = ec2.create_image(**create_params)
|
950
993
|
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
994
|
+
ami_id = response["ImageId"]
|
995
|
+
console.print(f"[green]✓ AMI creation initiated![/green]")
|
996
|
+
console.print(f"AMI ID: [cyan]{ami_id}[/cyan]")
|
997
|
+
|
998
|
+
# Restore the source engine to a normal state
|
999
|
+
console.print("Restoring source engine state...")
|
1000
|
+
restore_response = ssm.send_command(
|
1001
|
+
InstanceIds=[engine["instance_id"]],
|
1002
|
+
DocumentName="AWS-RunShellScript",
|
1003
|
+
Parameters={
|
1004
|
+
"commands": [
|
1005
|
+
"sudo touch /opt/dayhoff/first_boot_complete.sentinel",
|
1006
|
+
"sudo systemctl restart engine-idle-detector.timer",
|
1007
|
+
],
|
1008
|
+
"executionTimeout": ["60"],
|
1009
|
+
},
|
1010
|
+
)
|
1011
|
+
|
1012
|
+
# Quick wait to see if it failed immediately
|
1013
|
+
time.sleep(1)
|
1014
|
+
restore_command_id = restore_response["Command"]["CommandId"]
|
1015
|
+
result = ssm.get_command_invocation(
|
1016
|
+
CommandId=restore_command_id,
|
1017
|
+
InstanceId=engine["instance_id"],
|
1018
|
+
)
|
1019
|
+
if result["Status"] not in ["Pending", "InProgress", "Success"]:
|
1020
|
+
console.print(
|
1021
|
+
"[yellow]⚠️ Warning: Failed to restore source engine state.[/yellow]"
|
1022
|
+
)
|
1023
|
+
else:
|
1024
|
+
console.print(
|
1025
|
+
"[green]✓ Source engine restored to normal operation.[/green]"
|
1026
|
+
)
|
1027
|
+
|
1028
|
+
console.print(
|
1029
|
+
"\n[dim]The AMI creation process will continue in the background.[/dim]"
|
1030
|
+
)
|
1031
|
+
console.print(
|
1032
|
+
"[dim]You can monitor progress in the EC2 Console under 'AMIs'.[/dim]"
|
1033
|
+
)
|
1034
|
+
console.print(
|
1035
|
+
f"\nOnce complete, run [cyan]terraform apply[/cyan] in "
|
1036
|
+
f"terraform/environments/dev to use the new AMI."
|
1037
|
+
)
|
960
1038
|
|
961
1039
|
except ClientError as e:
|
962
1040
|
console.print(f"[red]❌ Failed to create AMI: {e}[/red]")
|
@@ -988,7 +1066,9 @@ def create_studio(
|
|
988
1066
|
# Check if user already has a studio
|
989
1067
|
existing = get_user_studio(username)
|
990
1068
|
if existing:
|
991
|
-
console.print(
|
1069
|
+
console.print(
|
1070
|
+
f"[yellow]You already have a studio: {existing['studio_id']}[/yellow]"
|
1071
|
+
)
|
992
1072
|
return
|
993
1073
|
|
994
1074
|
console.print(f"Creating {size_gb}GB studio for user [cyan]{username}[/cyan]...")
|
@@ -1030,14 +1110,14 @@ def studio_status():
|
|
1030
1110
|
|
1031
1111
|
# Create status panel
|
1032
1112
|
# Format status with colors
|
1033
|
-
status = studio[
|
1113
|
+
status = studio["status"]
|
1034
1114
|
if status == "in-use":
|
1035
1115
|
status_display = "[bright_blue]attached[/bright_blue]"
|
1036
1116
|
elif status in ["attaching", "detaching"]:
|
1037
1117
|
status_display = f"[yellow]{status}[/yellow]"
|
1038
1118
|
else:
|
1039
1119
|
status_display = f"[green]{status}[/green]"
|
1040
|
-
|
1120
|
+
|
1041
1121
|
status_lines = [
|
1042
1122
|
f"[bold]Studio ID:[/bold] {studio['studio_id']}",
|
1043
1123
|
f"[bold]User:[/bold] {studio['user']}",
|
@@ -1048,17 +1128,19 @@ def studio_status():
|
|
1048
1128
|
|
1049
1129
|
if studio.get("attached_vm_id"):
|
1050
1130
|
status_lines.append(f"[bold]Attached to:[/bold] {studio['attached_vm_id']}")
|
1051
|
-
|
1131
|
+
|
1052
1132
|
# Try to get engine details
|
1053
1133
|
response = make_api_request("GET", "/engines")
|
1054
1134
|
if response.status_code == 200:
|
1055
1135
|
engines = response.json().get("engines", [])
|
1056
1136
|
attached_engine = next(
|
1057
1137
|
(e for e in engines if e["instance_id"] == studio["attached_vm_id"]),
|
1058
|
-
None
|
1138
|
+
None,
|
1059
1139
|
)
|
1060
1140
|
if attached_engine:
|
1061
|
-
status_lines.append(
|
1141
|
+
status_lines.append(
|
1142
|
+
f"[bold]Engine Name:[/bold] {attached_engine['name']}"
|
1143
|
+
)
|
1062
1144
|
|
1063
1145
|
panel = Panel(
|
1064
1146
|
"\n".join(status_lines),
|
@@ -1118,14 +1200,19 @@ def attach_studio(
|
|
1118
1200
|
|
1119
1201
|
if engine["state"].lower() != "running":
|
1120
1202
|
console.print(f"[yellow]⚠️ Engine is {engine['state']}[/yellow]")
|
1121
|
-
if engine["state"].lower() == "stopped" and Confirm.ask(
|
1122
|
-
|
1203
|
+
if engine["state"].lower() == "stopped" and Confirm.ask(
|
1204
|
+
"Start the engine first?"
|
1205
|
+
):
|
1206
|
+
response = make_api_request(
|
1207
|
+
"POST", f"/engines/{engine['instance_id']}/start"
|
1208
|
+
)
|
1123
1209
|
if response.status_code != 200:
|
1124
1210
|
console.print("[red]❌ Failed to start engine[/red]")
|
1125
1211
|
raise typer.Exit(1)
|
1126
1212
|
console.print("[green]✓ Engine started[/green]")
|
1127
1213
|
console.print("Waiting for engine to be ready...")
|
1128
1214
|
import time
|
1215
|
+
|
1129
1216
|
time.sleep(10)
|
1130
1217
|
else:
|
1131
1218
|
raise typer.Exit(1)
|
@@ -1206,7 +1293,9 @@ def delete_studio():
|
|
1206
1293
|
console.print("[yellow]You don't have a studio to delete.[/yellow]")
|
1207
1294
|
return
|
1208
1295
|
|
1209
|
-
console.print(
|
1296
|
+
console.print(
|
1297
|
+
"[red]⚠️ WARNING: This will permanently delete your studio and all data![/red]"
|
1298
|
+
)
|
1210
1299
|
console.print(f"Studio ID: {studio['studio_id']}")
|
1211
1300
|
console.print(f"Size: {studio['size_gb']}GB")
|
1212
1301
|
|
@@ -1219,9 +1308,7 @@ def delete_studio():
|
|
1219
1308
|
console.print("Deletion cancelled.")
|
1220
1309
|
return
|
1221
1310
|
|
1222
|
-
typed_confirm = Prompt.ask(
|
1223
|
-
'Type "DELETE" to confirm permanent deletion'
|
1224
|
-
)
|
1311
|
+
typed_confirm = Prompt.ask('Type "DELETE" to confirm permanent deletion')
|
1225
1312
|
if typed_confirm != "DELETE":
|
1226
1313
|
console.print("Deletion cancelled.")
|
1227
1314
|
return
|
@@ -1237,7 +1324,9 @@ def delete_studio():
|
|
1237
1324
|
|
1238
1325
|
@studio_app.command("list")
|
1239
1326
|
def list_studios(
|
1240
|
-
all_users: bool = typer.Option(
|
1327
|
+
all_users: bool = typer.Option(
|
1328
|
+
False, "--all", "-a", help="Show all users' studios"
|
1329
|
+
),
|
1241
1330
|
):
|
1242
1331
|
"""List studios."""
|
1243
1332
|
username = check_aws_sso()
|
@@ -1275,24 +1364,24 @@ def list_studios(
|
|
1275
1364
|
status_display = "[yellow]" + studio["status"] + "[/yellow]"
|
1276
1365
|
else:
|
1277
1366
|
status_display = "[green]available[/green]"
|
1278
|
-
|
1367
|
+
|
1279
1368
|
# Format attached engine info
|
1280
1369
|
attached_to = "-"
|
1281
1370
|
if studio.get("attached_vm_id"):
|
1282
1371
|
vm_id = studio["attached_vm_id"]
|
1283
1372
|
engine_name = engines.get(vm_id, "unknown")
|
1284
1373
|
attached_to = f"{engine_name} ({vm_id})"
|
1285
|
-
|
1374
|
+
|
1286
1375
|
# Format creation date (remove microseconds and timezone info)
|
1287
1376
|
created = studio["creation_date"]
|
1288
1377
|
try:
|
1289
1378
|
# Parse and reformat to just show date and time
|
1290
|
-
if
|
1291
|
-
created_dt = datetime.fromisoformat(created.replace(
|
1379
|
+
if "T" in created:
|
1380
|
+
created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
|
1292
1381
|
created = created_dt.strftime("%Y-%m-%d %H:%M")
|
1293
1382
|
except:
|
1294
1383
|
# If parsing fails, just truncate
|
1295
|
-
created = created.split(
|
1384
|
+
created = created.split("T")[0] if "T" in created else created[:16]
|
1296
1385
|
|
1297
1386
|
table.add_row(
|
1298
1387
|
studio["studio_id"],
|
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
|
|
5
5
|
|
6
6
|
[project]
|
7
7
|
name = "dayhoff-tools"
|
8
|
-
version = "1.3.
|
8
|
+
version = "1.3.8"
|
9
9
|
description = "Common tools for all the repos at Dayhoff Labs"
|
10
10
|
authors = [
|
11
11
|
{name = "Daniel Martin-Alarcon", email = "dma@dayhofflabs.com"}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|