dayhoff-tools 1.4.6__py3-none-any.whl → 1.4.12__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.
@@ -616,7 +616,6 @@ def engine_status(
616
616
  f"[bold]Instance:[/bold] {engine['instance_id']}",
617
617
  f"[bold]Type:[/bold] {engine['engine_type']} ({engine['instance_type']})",
618
618
  f"[bold]Status:[/bold] {format_status(engine['state'], engine.get('ready'))}",
619
- f"[bold]Bootstrap:[/bold] {_colour_stage(stage_val)}",
620
619
  f"[bold]User:[/bold] {engine['user']}",
621
620
  f"[bold]IP:[/bold] {engine.get('public_ip', 'N/A')}",
622
621
  f"[bold]Launched:[/bold] {launch_time.strftime('%Y-%m-%d %H:%M:%S')} ({format_duration(uptime)} ago)",
@@ -1607,6 +1606,9 @@ def attach_studio(
1607
1606
  engines = response.json().get("engines", [])
1608
1607
  engine = resolve_engine(engine_name_or_id, engines)
1609
1608
 
1609
+ # Flag to track if we started the engine in this command (affects retry length)
1610
+ engine_started_now: bool = False
1611
+
1610
1612
  if engine["state"].lower() != "running":
1611
1613
  console.print(f"[yellow]⚠️ Engine is {engine['state']}[/yellow]")
1612
1614
  if engine["state"].lower() == "stopped" and Confirm.ask(
@@ -1619,33 +1621,14 @@ def attach_studio(
1619
1621
  console.print("[red]❌ Failed to start engine[/red]")
1620
1622
  raise typer.Exit(1)
1621
1623
  console.print("[green]✓ Engine started[/green]")
1622
- console.print("Waiting for engine to be ready...")
1623
- # Wait until the engine is fully running & marked ready (max 5 min)
1624
- console.print("Waiting for engine to be ready (this can take a couple of minutes)…")
1625
- # Progress classes already imported at module level – do not re-import to avoid UnboundLocalError
1626
-
1627
- with Progress(SpinnerColumn(), TimeElapsedColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as prog:
1628
- prog.add_task("Awaiting engine readiness…", total=None)
1629
- max_attempts = 30 # 30 × 10 s ≈ 5 min
1630
- for _ in range(max_attempts):
1631
- time.sleep(10)
1632
-
1633
- status_resp = make_api_request("GET", f"/engines/{engine['instance_id']}")
1634
- if status_resp.status_code != 200:
1635
- continue
1636
-
1637
- detailed = status_resp.json().get("engine", {})
1638
- state_ok = detailed.get("state", "").lower() == "running"
1639
- ready_ok = detailed.get("ready") is True
1640
-
1641
- if state_ok and ready_ok:
1642
- break
1643
- else:
1644
- console.print("[yellow]Engine is still starting up. If attachment fails, wait a little longer and retry.[/yellow]")
1624
+ # Mark that we booted the engine so attach loop gets extended retries
1625
+ engine_started_now = True
1626
+ # No further waiting here attachment attempts below handle retry logic while the
1627
+ # engine finishes booting.
1645
1628
  else:
1646
1629
  raise typer.Exit(1)
1647
1630
 
1648
- # Get SSH key
1631
+ # Retrieve SSH public key (required for authorised_keys provisioning)
1649
1632
  try:
1650
1633
  public_key = get_ssh_public_key()
1651
1634
  except FileNotFoundError as e:
@@ -1654,36 +1637,113 @@ def attach_studio(
1654
1637
 
1655
1638
  console.print(f"Attaching studio to engine [cyan]{engine['name']}[/cyan]...")
1656
1639
 
1657
- with Progress(
1658
- SpinnerColumn(),
1659
- TextColumn("[progress.description]{task.description}"),
1660
- transient=True,
1661
- ) as progress:
1662
- task = progress.add_task("Attaching studio...", total=100)
1640
+ # Determine retry strategy
1641
+ max_attempts = 40 if engine_started_now else 3
1642
+ retry_delay = 10 if engine_started_now else 3
1663
1643
 
1664
- response = make_api_request(
1665
- "POST",
1666
- f"/studios/{studio['studio_id']}/attach",
1667
- json_data={
1668
- "vm_id": engine["instance_id"],
1669
- "user": target_user, # Use target_user instead of username
1670
- "public_key": public_key,
1671
- },
1672
- )
1644
+ if engine_started_now:
1645
+ # Long spinner-based loop while the freshly started engine finishes booting
1646
+ with Progress(
1647
+ SpinnerColumn(),
1648
+ TimeElapsedColumn(),
1649
+ TextColumn("[progress.description]{task.description}"),
1650
+ transient=True,
1651
+ ) as prog:
1652
+ task = prog.add_task("Attaching studio (engine is still booting)…", total=None)
1653
+
1654
+ for attempt in range(max_attempts):
1655
+ success, error_msg = _attempt_studio_attach(studio, engine, target_user, public_key)
1656
+
1657
+ if success:
1658
+ break # success!
1659
+
1660
+ # Update spinner every 3rd try to avoid log spam
1661
+ if attempt % 3 == 0:
1662
+ prog.update(task, description=f"Attaching studio (engine is still booting)… {attempt+1}/{max_attempts}")
1663
+
1664
+ if error_msg:
1665
+ console.print(f"[red]❌ Failed to attach studio: {error_msg}[/red]")
1666
+ return
1667
+
1668
+ time.sleep(retry_delay)
1673
1669
 
1674
- progress.update(task, completed=100)
1670
+ else:
1671
+ console.print("[yellow]Engine is still starting up – please retry in a minute.[/yellow]")
1672
+ return
1673
+ else:
1674
+ # Quick path: engine is already running – try a couple of times then fail fast
1675
+ for attempt in range(max_attempts):
1676
+ success, error_msg = _attempt_studio_attach(studio, engine, target_user, public_key)
1677
+
1678
+ if success:
1679
+ break # good to go
1680
+
1681
+ if error_msg:
1682
+ console.print(f"[red]❌ Failed to attach studio: {error_msg}[/red]")
1683
+ return
1684
+
1685
+ time.sleep(retry_delay)
1686
+
1687
+ else:
1688
+ console.print("[yellow]Could not attach studio – please retry in a moment.[/yellow]")
1689
+ return
1690
+
1691
+ # Successful attach path
1692
+ console.print(f"[green]✓ Studio attached successfully![/green]")
1693
+
1694
+ # Update SSH config - use target_user for the connection
1695
+ update_ssh_config_entry(engine["name"], engine["instance_id"], target_user)
1696
+ console.print(f"[green]✓ SSH config updated[/green]")
1697
+ console.print(f"\nConnect with: [cyan]ssh {engine['name']}[/cyan]")
1698
+ console.print(f"Files are at: [cyan]/studios/{target_user}[/cyan]")
1699
+
1700
+
1701
+ def _attempt_studio_attach(studio, engine, target_user, public_key):
1702
+ response = make_api_request(
1703
+ "POST",
1704
+ f"/studios/{studio['studio_id']}/attach",
1705
+ json_data={
1706
+ "vm_id": engine["instance_id"],
1707
+ "user": target_user,
1708
+ "public_key": public_key,
1709
+ },
1710
+ )
1675
1711
 
1676
1712
  if response.status_code == 200:
1677
- console.print(f"[green]✓ Studio attached successfully![/green]")
1713
+ return True, None
1678
1714
 
1679
- # Update SSH config - use target_user for the connection
1680
- update_ssh_config_entry(engine["name"], engine["instance_id"], target_user)
1681
- console.print(f"[green]✓ SSH config updated[/green]")
1682
- console.print(f"\nConnect with: [cyan]ssh {engine['name']}[/cyan]")
1683
- console.print(f"Files are at: [cyan]/studios/{target_user}[/cyan]")
1715
+ # --- determine if we should retry ---
1716
+ recoverable = False
1717
+ if response.status_code in (409, 503):
1718
+ recoverable = True
1684
1719
  else:
1720
+ err_msg = response.json().get("error", "").lower()
1721
+ RECOVERABLE_PATTERNS = [
1722
+ "not ready",
1723
+ "still starting",
1724
+ "initializing",
1725
+ "failed to mount",
1726
+ "device busy",
1727
+ "not available",
1728
+ "pending", # VM state pending
1729
+ ]
1730
+ FATAL_PATTERNS = [
1731
+ "in-use",
1732
+ "already attached",
1733
+ "permission",
1734
+ ]
1735
+ if any(p in err_msg for p in FATAL_PATTERNS):
1736
+ recoverable = False
1737
+ elif any(p in err_msg for p in RECOVERABLE_PATTERNS):
1738
+ recoverable = True
1739
+
1740
+ if not recoverable:
1741
+ # fatal – abort immediately and show message
1685
1742
  error = response.json().get("error", "Unknown error")
1686
- console.print(f"[red]❌ Failed to attach studio: {error}[/red]")
1743
+ return False, error
1744
+
1745
+ # otherwise wait and retry
1746
+ return False, None
1687
1747
 
1688
1748
 
1689
1749
  @studio_app.command("detach")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dayhoff-tools
3
- Version: 1.4.6
3
+ Version: 1.4.12
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=A1fs8_8eClQ24hRQSeVALZt-_ABvv_XNLx7KA0UtRSU,83443
6
+ dayhoff_tools/cli/engine_commands.py,sha256=enbpFSLCsmPACL1EOarxCcF8L-TkpiNqq4DB4vhv6X0,85005
7
7
  dayhoff_tools/cli/main.py,sha256=rgeEHD9lJ8SBCR34BTLb7gVInHUUdmEBNXAJnq5yEU4,4795
8
8
  dayhoff_tools/cli/swarm_commands.py,sha256=5EyKj8yietvT5lfoz8Zx0iQvVaNgc3SJX1z2zQR6o6M,5614
9
9
  dayhoff_tools/cli/utility_commands.py,sha256=qs8vH9TBFHsOPC3X8cU3qZigM3dDn-2Ytq4o_F2WubU,27874
@@ -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=8YbnQ--usrEgDQGfvpV4MrMji55A0rq2hZaOgFGh6ag,15896
30
- dayhoff_tools-1.4.6.dist-info/METADATA,sha256=rAMwQNz7gAwDPqfHieIQ4-EiOaCzho3kIlnUVXq7Pdg,2824
31
- dayhoff_tools-1.4.6.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
- dayhoff_tools-1.4.6.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
33
- dayhoff_tools-1.4.6.dist-info/RECORD,,
30
+ dayhoff_tools-1.4.12.dist-info/METADATA,sha256=GjDgKBeFXN1cwgetohVhR8Ix8SD_bTxHAQqd7HrqX_4,2825
31
+ dayhoff_tools-1.4.12.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
+ dayhoff_tools-1.4.12.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
33
+ dayhoff_tools-1.4.12.dist-info/RECORD,,