dayhoff-tools 1.9.4__py3-none-any.whl → 1.9.6__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.
@@ -693,9 +693,10 @@ def list_engines(
693
693
  @engine_app.command("status")
694
694
  def engine_status(
695
695
  name_or_id: str = typer.Argument(help="Engine name or instance ID"),
696
- show_log: bool = typer.Option(False, "--show-log", help="Show bootstrap log"),
696
+ detailed: bool = typer.Option(False, "--detailed", "-d", help="Show detailed status (slower)"),
697
+ show_log: bool = typer.Option(False, "--show-log", help="Show bootstrap log (requires --detailed)"),
697
698
  ):
698
- """Show detailed engine status and information."""
699
+ """Show engine status and information."""
699
700
  check_aws_sso()
700
701
 
701
702
  # Get all engines to resolve name
@@ -706,8 +707,131 @@ def engine_status(
706
707
 
707
708
  engines = response.json().get("engines", [])
708
709
  engine = resolve_engine(name_or_id, engines)
710
+
711
+ # Fast status display (default)
712
+ if not detailed:
713
+ # Get basic engine info and studios from API
714
+ response = make_api_request("GET", f"/engines/{engine['instance_id']}")
715
+ if response.status_code != 200:
716
+ console.print("[red]❌ Failed to fetch engine details[/red]")
717
+ raise typer.Exit(1)
718
+
719
+ engine_details = response.json()
720
+ engine = engine_details.get("engine", engine)
721
+ attached_studios = engine_details.get("attached_studios", [])
722
+
723
+ # Fetch idle status via SSM with longer timeout
724
+ ssm = boto3.client("ssm", region_name="us-east-1")
725
+ idle_data = {}
726
+
727
+ if engine["state"].lower() == "running":
728
+ try:
729
+ resp = ssm.send_command(
730
+ InstanceIds=[engine["instance_id"]],
731
+ DocumentName="AWS-RunShellScript",
732
+ Parameters={
733
+ "commands": [
734
+ "cat /var/run/idle-detector/last_state.json 2>/dev/null || echo '{}'"
735
+ ],
736
+ "executionTimeout": ["10"],
737
+ },
738
+ )
739
+ cid = resp["Command"]["CommandId"]
740
+
741
+ # Wait up to 3 seconds for result
742
+ for _ in range(6): # 6 * 0.5 = 3 seconds
743
+ time.sleep(0.5)
744
+ inv = ssm.get_command_invocation(
745
+ CommandId=cid, InstanceId=engine["instance_id"]
746
+ )
747
+ if inv["Status"] in ["Success", "Failed"]:
748
+ break
749
+
750
+ if inv["Status"] == "Success":
751
+ content = inv["StandardOutputContent"].strip()
752
+ if content and content != "{}":
753
+ idle_data = json.loads(content)
754
+ except Exception:
755
+ pass # Best effort
756
+
757
+ # Build status display
758
+ hourly_cost = HOURLY_COSTS.get(engine["engine_type"], 0)
759
+
760
+ # Determine running state display
761
+ running_state = engine["state"].lower()
762
+ if running_state == "running":
763
+ run_disp = "[green]Running[/green]"
764
+ elif running_state == "pending":
765
+ run_disp = "[yellow]Starting...[/yellow]"
766
+ elif running_state == "stopping":
767
+ run_disp = "[yellow]Stopping...[/yellow]"
768
+ elif running_state == "stopped":
769
+ run_disp = "[dim]Stopped[/dim]"
770
+ else:
771
+ run_disp = engine["state"].capitalize()
772
+
773
+ # Determine idle/active status
774
+ idle_disp = ""
775
+ if running_state == "running":
776
+ is_idle = idle_data.get("idle", False)
777
+ timeout_sec = idle_data.get("timeout_sec")
778
+ idle_seconds = idle_data.get("idle_seconds", 0) if is_idle else 0
779
+
780
+ if is_idle:
781
+ if isinstance(timeout_sec, int) and isinstance(idle_seconds, int):
782
+ remaining = max(0, timeout_sec - idle_seconds)
783
+ idle_disp = f" [yellow]Idle {idle_seconds//60}m/{timeout_sec//60}m: [red]{remaining//60}m {remaining%60}s[/red] left[/yellow]"
784
+ else:
785
+ idle_disp = " [yellow]Idle ?/?[/yellow]"
786
+ else:
787
+ idle_disp = " [green]Active[/green]"
788
+
789
+ # Build status lines
790
+ status_lines = [
791
+ f"[blue]{engine['name']}[/blue] {run_disp}{idle_disp}\n",
792
+ ]
793
+
794
+ # Add studios if attached
795
+ if attached_studios:
796
+ studio_names = [f"[magenta]{s.get('user', 'studio')}[/magenta]" for s in attached_studios]
797
+ status_lines.append(f"Studios: {', '.join(studio_names)}")
798
+
799
+ status_lines.append("") # blank line
800
+
801
+ # Basic info
802
+ status_lines.extend([
803
+ f"Type: {engine['engine_type']} ({engine['instance_type']})",
804
+ f"User: {engine['user']}",
805
+ f"IP: {engine.get('public_ip', 'N/A')}",
806
+ f"$/hour: ${hourly_cost:.2f}",
807
+ ])
808
+
809
+ # Add activity sensors if we have idle data
810
+ if idle_data.get("reasons"):
811
+ status_lines.append("")
812
+ status_lines.append("[bold]Activity:[/bold]")
813
+
814
+ sensor_map = {
815
+ "CoffeeLockSensor": ("☕", "Coffee"),
816
+ "ActiveLoginSensor": ("🐚", "SSH"),
817
+ "IDEConnectionSensor": ("🖥", "IDE"),
818
+ "DockerWorkloadSensor": ("🐳", "Docker"),
819
+ }
820
+
821
+ for r in idle_data.get("reasons", []):
822
+ sensor = r.get("sensor", "Unknown")
823
+ active = r.get("active", False)
824
+ icon, label = sensor_map.get(sensor, ("?", sensor))
825
+ status_str = "[green]YES[/green]" if active else "[dim]no[/dim]"
826
+ status_lines.append(f" {icon} {label:6} {status_str}")
827
+
828
+ # Display in a nice panel
829
+ console.print(
830
+ Panel("\n".join(status_lines), title="Engine Status", border_style="blue")
831
+ )
832
+ return # Exit early for fast status
709
833
 
710
- # Get detailed engine status including idle detector info
834
+ # Get detailed engine status including idle detector info (for --detailed mode)
711
835
  response = make_api_request("GET", f"/engines/{engine['instance_id']}")
712
836
  if response.status_code != 200:
713
837
  console.print("[red]❌ Failed to fetch engine details[/red]")
@@ -778,15 +902,25 @@ def engine_status(
778
902
  return "[green]Active[/green]"
779
903
  if running_state in ("stopped", "stopping"):
780
904
  return "[dim]N/A[/dim]"
781
- idle_seconds_v = idle_info.get("idle_seconds")
782
- thresh_v = idle_info.get("idle_threshold")
783
- if isinstance(idle_seconds_v, (int, float)) and isinstance(
784
- thresh_v, (int, float)
785
- ):
786
- return (
787
- f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m[/yellow]"
788
- )
789
- return "[yellow]Idle[/yellow]"
905
+
906
+ # If we don't have idle info at all, show N/A
907
+ if not idle_info.get("available"):
908
+ return "[dim]N/A[/dim]"
909
+
910
+ # If idle, show time/threshold with time remaining if available
911
+ if idle_info.get("status") == "idle":
912
+ idle_seconds_v = idle_info.get("idle_seconds")
913
+ thresh_v = idle_info.get("idle_threshold")
914
+ if isinstance(idle_seconds_v, (int, float)) and isinstance(thresh_v, (int, float)):
915
+ remaining = max(0, int(thresh_v) - int(idle_seconds_v))
916
+ return f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m: [red]{remaining//60}m {remaining%60}s[/red] left[/yellow]"
917
+ elif isinstance(thresh_v, (int, float)):
918
+ return f"[yellow]Idle ?/{int(thresh_v)//60}m[/yellow]"
919
+ else:
920
+ return "[yellow]Idle ?/?[/yellow]"
921
+
922
+ # Default to N/A if we can't determine status
923
+ return "[dim]N/A[/dim]"
790
924
 
791
925
  active_disp = _compute_active_disp(idle_detector)
792
926
 
@@ -916,8 +1050,12 @@ def engine_status(
916
1050
  },
917
1051
  )
918
1052
  cid = res["Command"]["CommandId"]
919
- time.sleep(1)
920
- inv = ssm.get_command_invocation(CommandId=cid, InstanceId=instance_id)
1053
+ # Wait up to 2 seconds for SSM command to complete (was 1 second)
1054
+ for _ in range(4): # 4 * 0.5 = 2 seconds
1055
+ time.sleep(0.5)
1056
+ inv = ssm.get_command_invocation(CommandId=cid, InstanceId=instance_id)
1057
+ if inv["Status"] in ["Success", "Failed"]:
1058
+ break
921
1059
  if inv["Status"] != "Success":
922
1060
  return None
923
1061
  content = inv["StandardOutputContent"].strip()
@@ -1037,23 +1175,10 @@ def engine_status(
1037
1175
  status_lines.append(_sensor_line(" IDE ", "IDEConnectionSensor", "🖥"))
1038
1176
  status_lines.append(_sensor_line("Docker", "DockerWorkloadSensor", "🐳"))
1039
1177
 
1040
- # If we have elapsed idle seconds and threshold, reflect that in the header
1041
- try:
1042
- if idle_detector.get("status") == "idle":
1043
- idle_secs = int(idle_detector.get("idle_seconds", 0))
1044
- thresh_secs = int(idle_detector.get("idle_threshold", 0))
1045
- if thresh_secs > 0:
1046
- active_disp = (
1047
- f"[yellow]Idle {idle_secs//60}m/{thresh_secs//60}m[/yellow]"
1048
- )
1049
- # Rewrite top header line (index 0) to include updated display
1050
- all_header = top_lines[0]
1051
- # Replace the portion after two spaces (name and running state fixed)
1052
- top_lines[0] = (
1053
- f"[blue]{engine['name']}[/blue] {run_disp} {active_disp}\n"
1054
- )
1055
- except Exception:
1056
- pass
1178
+ # Recompute display with latest idle detector data
1179
+ active_disp = _compute_active_disp(idle_detector)
1180
+ # Rewrite top header line (index 0) to include updated display
1181
+ top_lines[0] = f"[blue]{engine['name']}[/blue] {run_disp} {active_disp}\n"
1057
1182
 
1058
1183
  # Combine top summary and details
1059
1184
  all_lines = top_lines + status_lines
@@ -1062,6 +1187,9 @@ def engine_status(
1062
1187
  )
1063
1188
 
1064
1189
  if show_log:
1190
+ if not detailed:
1191
+ console.print("[yellow]Note: --show-log requires --detailed flag[/yellow]")
1192
+ return
1065
1193
  console.print("\n[bold]Bootstrap Log:[/bold]")
1066
1194
  try:
1067
1195
  ssm = boto3.client("ssm", region_name="us-east-1")
@@ -2631,6 +2759,7 @@ def idle_timeout_cmd(
2631
2759
  set: Optional[str] = typer.Option(
2632
2760
  None, "--set", "-s", help="New timeout (e.g., 2h30m, 45m)"
2633
2761
  ),
2762
+
2634
2763
  ):
2635
2764
  """Show or set the engine idle-detector timeout."""
2636
2765
  check_aws_sso()
@@ -2647,7 +2776,7 @@ def idle_timeout_cmd(
2647
2776
  ssm = boto3.client("ssm", region_name="us-east-1")
2648
2777
 
2649
2778
  if set is None:
2650
- # Show current
2779
+ # Show current timeout setting
2651
2780
  resp = ssm.send_command(
2652
2781
  InstanceIds=[engine["instance_id"]],
2653
2782
  DocumentName="AWS-RunShellScript",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dayhoff-tools
3
- Version: 1.9.4
3
+ Version: 1.9.6
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
@@ -3,7 +3,7 @@ dayhoff_tools/chemistry/standardizer.py,sha256=uMn7VwHnx02nc404eO6fRuS4rsl4dvSPf
3
3
  dayhoff_tools/chemistry/utils.py,sha256=jt-7JgF-GeeVC421acX-bobKbLU_X94KNOW24p_P-_M,2257
4
4
  dayhoff_tools/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  dayhoff_tools/cli/cloud_commands.py,sha256=33qcWLmq-FwEXMdL3F0OHm-5Stlh2r65CldyEZgQ1no,40904
6
- dayhoff_tools/cli/engine_commands.py,sha256=sJroAFeJqHBMwONTLTFUNfuJf0PvN6xu-OBoVS_b4uU,107111
6
+ dayhoff_tools/cli/engine_commands.py,sha256=A4EZtbU52HI1Xnm44cnko2l2r6eUDZej4zzgiXU3TaE,112781
7
7
  dayhoff_tools/cli/main.py,sha256=LoFs3SI4fdCjP4pdxEAhri-_q0dmNYupmBCRE4KbBac,5933
8
8
  dayhoff_tools/cli/swarm_commands.py,sha256=5EyKj8yietvT5lfoz8Zx0iQvVaNgc3SJX1z2zQR6o6M,5614
9
9
  dayhoff_tools/cli/utility_commands.py,sha256=WQTHOh1MttuxaJjl2c6zMa4x7_JuaKMQgcyotYrU3GA,25883
@@ -27,7 +27,7 @@ dayhoff_tools/intake/uniprot.py,sha256=BZYJQF63OtPcBBnQ7_P9gulxzJtqyorgyuDiPeOJq
27
27
  dayhoff_tools/logs.py,sha256=DKdeP0k0kliRcilwvX0mUB2eipO5BdWUeHwh-VnsICs,838
28
28
  dayhoff_tools/sqlite.py,sha256=jV55ikF8VpTfeQqqlHSbY8OgfyfHj8zgHNpZjBLos_E,18672
29
29
  dayhoff_tools/warehouse.py,sha256=UETBtZD3r7WgvURqfGbyHlT7cxoiVq8isjzMuerKw8I,24475
30
- dayhoff_tools-1.9.4.dist-info/METADATA,sha256=fZi-XFFqeuhdjIIEJKkVN2bpMjyIO6q2G6RcaCURZLk,2914
31
- dayhoff_tools-1.9.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
- dayhoff_tools-1.9.4.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
33
- dayhoff_tools-1.9.4.dist-info/RECORD,,
30
+ dayhoff_tools-1.9.6.dist-info/METADATA,sha256=sDQKrTIcHUWSK3oUbacWbr2MuhAfat7SDrHdIXQXW48,2914
31
+ dayhoff_tools-1.9.6.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
+ dayhoff_tools-1.9.6.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
33
+ dayhoff_tools-1.9.6.dist-info/RECORD,,