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.
Files changed (32) hide show
  1. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/PKG-INFO +1 -1
  2. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/engine_commands.py +153 -64
  3. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/pyproject.toml +1 -1
  4. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/README.md +0 -0
  5. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/__init__.py +0 -0
  6. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/chemistry/standardizer.py +0 -0
  7. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/chemistry/utils.py +0 -0
  8. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/__init__.py +0 -0
  9. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/cloud_commands.py +0 -0
  10. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/main.py +0 -0
  11. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/swarm_commands.py +0 -0
  12. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/cli/utility_commands.py +0 -0
  13. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/base.py +0 -0
  14. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/deploy_aws.py +0 -0
  15. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/deploy_gcp.py +0 -0
  16. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/deploy_utils.py +0 -0
  17. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/job_runner.py +0 -0
  18. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/processors.py +0 -0
  19. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/deployment/swarm.py +0 -0
  20. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/embedders.py +0 -0
  21. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/fasta.py +0 -0
  22. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/file_ops.py +0 -0
  23. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/h5.py +0 -0
  24. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/gcp.py +0 -0
  25. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/gtdb.py +0 -0
  26. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/kegg.py +0 -0
  27. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/mmseqs.py +0 -0
  28. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/structure.py +0 -0
  29. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/intake/uniprot.py +0 -0
  30. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/logs.py +0 -0
  31. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/sqlite.py +0 -0
  32. {dayhoff_tools-1.3.6 → dayhoff_tools-1.3.8}/dayhoff_tools/warehouse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dayhoff-tools
3
- Version: 1.3.6
3
+ Version: 1.3.8
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
@@ -27,10 +27,10 @@ console = Console()
27
27
  # Cost information
28
28
  HOURLY_COSTS = {
29
29
  "cpu": 0.50, # r6i.2xlarge
30
- "cpumax": 1.00, # r7i.8xlarge
31
- "t4": 1.00, # g4dn.2xlarge
32
- "a10g": 2.00, # g5.2xlarge
33
- "a100": 5.00, # p4d.24xlarge
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, endpoint: str, json_data: Optional[Dict] = None, params: Optional[Dict] = None
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('Z', '+00:00'))
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 ⚠ (Initializing...)[/yellow]"
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", default=1, choices=[str(i) for i in range(1, len(matches) + 1)]
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(f"Launching [cyan]{name}[/cyan] ({engine_type}) for ${cost:.2f}/hour...")
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(False, "--running", help="Show only running engines"),
342
- stopped_only: bool = typer.Option(False, "--stopped", help="Show only stopped engines"),
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("GET", f"/engines/{engine['instance_id']}/attached-studios")
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(False, "--force", "-f", help="Force stop and detach all studios"),
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(f"\n[yellow]⚠️ This will permanently terminate engine '{engine['name']}'[/yellow]")
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(False, "--all", "-a", help="Include all engines from all users"),
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(f"Updating SSH config with running engines for [cyan]{username}[/cyan] and [cyan]shared[/cyan]...")
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' ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters \'portNumber=%p\'"',
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 = f"[dim]({engine['user']})[/dim]" if engine['user'] != username else ""
700
- console.print(f"{engine['name']} {engine['instance_id']} {user_display}")
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(f"[red]❌ Failed to set keep-awake: {result.get('StatusDetails', 'Unknown error')}[/red]")
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("[green]✓ Keep-awake cancelled, auto-shutdown re-enabled[/green]")
852
+ console.print(
853
+ "[green]✓ Keep-awake cancelled, auto-shutdown re-enabled[/green]"
854
+ )
825
855
  else:
826
- console.print(f"[red]❌ Failed to cancel keep-awake: {result.get('StatusDetails', 'Unknown error')}[/red]")
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(help="Engine name or instance ID to create AMI from"),
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("\n[yellow]⚠️ Important: This will reboot the engine to ensure a clean snapshot.[/yellow]")
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("[yellow]⚠️ Warning: Cleanup command may have failed[/yellow]")
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({"DeviceName": device_name, "NoDevice": ""})
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("Creating AMI (this will take several minutes)...", total=None)
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
- ami_id = response["ImageId"]
952
- console.print(f"[green]✓ AMI creation initiated![/green]")
953
- console.print(f"AMI ID: [cyan]{ami_id}[/cyan]")
954
- console.print("\n[dim]The AMI creation process will continue in the background.[/dim]")
955
- console.print("[dim]You can monitor progress in the EC2 Console under 'AMIs'.[/dim]")
956
- console.print(
957
- f"\nOnce complete, run [cyan]terraform apply[/cyan] in "
958
- f"terraform/environments/dev to use the new AMI."
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(f"[yellow]You already have a studio: {existing['studio_id']}[/yellow]")
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['status']
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(f"[bold]Engine Name:[/bold] {attached_engine['name']}")
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("Start the engine first?"):
1122
- response = make_api_request("POST", f"/engines/{engine['instance_id']}/start")
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("[red]⚠️ WARNING: This will permanently delete your studio and all data![/red]")
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(False, "--all", "-a", help="Show all users' studios"),
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 'T' in created:
1291
- created_dt = datetime.fromisoformat(created.replace('Z', '+00:00'))
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('T')[0] if 'T' in created else created[:16]
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.6"
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