dayhoff-tools 1.5.7__py3-none-any.whl → 1.5.9__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.

Potentially problematic release.


This version of dayhoff-tools might be problematic. Click here for more details.

@@ -409,37 +409,35 @@ def update_ssh_config_entry(engine_name: str, instance_id: str, ssh_user: str, i
409
409
 
410
410
  # Read existing config
411
411
  content = config_path.read_text()
412
+ lines = content.splitlines() if content else []
412
413
 
413
- # Create new entry
414
- new_entry = f"""
415
- Host {engine_name} {SSH_MANAGED_COMMENT}
416
- HostName {instance_id}
417
- User {ssh_user}
418
- ProxyCommand sh -c \"AWS_SSM_IDLE_TIMEOUT={idle_timeout} aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'\"\n"""
419
-
420
- # Check if entry already exists
421
- host_line = f"Host {engine_name} {SSH_MANAGED_COMMENT}"
422
- if host_line in content:
423
- # Update existing entry
424
- lines = content.splitlines()
425
- new_lines = []
426
- skip_count = 0
427
- for line in lines:
428
- if line.strip() == host_line.strip():
429
- new_lines.extend(new_entry.strip().splitlines())
430
- skip_count = 4 # Skip the next 4 lines (old entry)
431
- elif skip_count > 0:
432
- skip_count -= 1
433
- continue
434
- else:
435
- new_lines.append(line)
436
- content = "\n".join(new_lines)
437
- else:
438
- # Append new entry
439
- content = content.rstrip() + "\n" + new_entry
414
+ # Remove any existing entry for this engine
415
+ new_lines = []
416
+ skip_until_next_host = False
417
+ for line in lines:
418
+ # Check if this is our managed host
419
+ if line.strip().startswith(f"Host {engine_name}") and SSH_MANAGED_COMMENT in line:
420
+ skip_until_next_host = True
421
+ elif line.strip().startswith("Host ") and skip_until_next_host:
422
+ skip_until_next_host = False
423
+ # This is a different host entry, keep it
424
+ new_lines.append(line)
425
+ elif not skip_until_next_host:
426
+ new_lines.append(line)
427
+
428
+ # Add the new entry
429
+ if new_lines and new_lines[-1].strip(): # Add blank line if needed
430
+ new_lines.append("")
431
+
432
+ new_lines.extend([
433
+ f"Host {engine_name} {SSH_MANAGED_COMMENT}",
434
+ f" HostName {instance_id}",
435
+ f" User {ssh_user}",
436
+ f" ProxyCommand sh -c \"AWS_SSM_IDLE_TIMEOUT={idle_timeout} aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'\"",
437
+ ])
440
438
 
441
439
  # Write back
442
- config_path.write_text(content)
440
+ config_path.write_text("\n".join(new_lines))
443
441
  config_path.chmod(0o600)
444
442
 
445
443
 
@@ -547,6 +545,9 @@ def list_engines(
547
545
  stopped_only: bool = typer.Option(
548
546
  False, "--stopped", help="Show only stopped engines"
549
547
  ),
548
+ detailed: bool = typer.Option(
549
+ False, "--detailed", "-d", help="Show detailed status (slower)"
550
+ ),
550
551
  ):
551
552
  """List engines (shows all engines by default)."""
552
553
  current_user = check_aws_sso()
@@ -554,6 +555,8 @@ def list_engines(
554
555
  params = {}
555
556
  if user:
556
557
  params["user"] = user
558
+ if detailed:
559
+ params["check_ready"] = "true"
557
560
 
558
561
  response = make_api_request("GET", "/engines", params=params)
559
562
 
@@ -571,8 +574,10 @@ def list_engines(
571
574
  console.print("No engines found.")
572
575
  return
573
576
 
574
- # Fetch bootstrap stages once
575
- stages_map = _fetch_init_stages([e["instance_id"] for e in engines])
577
+ # Only fetch detailed info if requested (slow)
578
+ stages_map = {}
579
+ if detailed:
580
+ stages_map = _fetch_init_stages([e["instance_id"] for e in engines])
576
581
 
577
582
  # Create table
578
583
  table = Table(title="Engines", box=box.ROUNDED)
@@ -581,7 +586,8 @@ def list_engines(
581
586
  table.add_column("Type")
582
587
  table.add_column("User")
583
588
  table.add_column("Status")
584
- table.add_column("Disk Usage")
589
+ if detailed:
590
+ table.add_column("Disk Usage")
585
591
  table.add_column("Uptime/Since")
586
592
  table.add_column("$/hour", justify="right")
587
593
 
@@ -592,24 +598,34 @@ def list_engines(
592
598
 
593
599
  if engine["state"].lower() == "running":
594
600
  time_str = format_duration(uptime)
595
- # Get disk usage for running engines
596
- disk_usage = get_disk_usage_via_ssm(engine["instance_id"]) or "-"
601
+ # Only get disk usage if detailed mode
602
+ if detailed:
603
+ disk_usage = get_disk_usage_via_ssm(engine["instance_id"]) or "-"
604
+ else:
605
+ disk_usage = None
597
606
  else:
598
607
  time_str = launch_time.strftime("%Y-%m-%d %H:%M")
599
- disk_usage = "-"
608
+ disk_usage = "-" if detailed else None
600
609
 
601
- table.add_row(
610
+ row_data = [
602
611
  engine["name"],
603
612
  engine["instance_id"],
604
613
  engine["engine_type"],
605
614
  engine["user"],
606
615
  format_status(engine["state"], engine.get("ready")),
607
- disk_usage,
616
+ ]
617
+ if detailed:
618
+ row_data.append(disk_usage)
619
+ row_data.extend([
608
620
  time_str,
609
621
  f"${hourly_cost:.2f}",
610
- )
622
+ ])
623
+
624
+ table.add_row(*row_data)
611
625
 
612
626
  console.print(table)
627
+ if not detailed and any(e["state"].lower() == "running" for e in engines):
628
+ console.print("\n[dim]Tip: Use --detailed to see disk usage and bootstrap status (slower)[/dim]")
613
629
  else:
614
630
  error = response.json().get("error", "Unknown error")
615
631
  console.print(f"[red]❌ Failed to list engines: {error}[/red]")
@@ -1523,32 +1539,60 @@ def create_ami(
1523
1539
 
1524
1540
  # Restore the source engine to a normal state
1525
1541
  console.print("Restoring source engine state...")
1542
+
1543
+ # Wait for instance to come back after reboot (AMI creation reboots by default)
1544
+ console.print("[dim]Waiting for engine to reboot after snapshot...[/dim]")
1545
+ ec2_waiter = ec2.get_waiter('instance_status_ok')
1546
+ try:
1547
+ ec2_waiter.wait(
1548
+ InstanceIds=[engine["instance_id"]],
1549
+ WaiterConfig={'Delay': 10, 'MaxAttempts': 30} # Wait up to 5 minutes
1550
+ )
1551
+ except Exception as e:
1552
+ console.print(f"[yellow]⚠️ Warning: Engine may still be rebooting: {e}[/yellow]")
1553
+
1554
+ # Now restore the sentinel and restart services
1526
1555
  restore_response = ssm.send_command(
1527
1556
  InstanceIds=[engine["instance_id"]],
1528
1557
  DocumentName="AWS-RunShellScript",
1529
1558
  Parameters={
1530
1559
  "commands": [
1560
+ # Ensure the directories exist
1561
+ "sudo mkdir -p /opt/dayhoff /opt/dayhoff/state",
1562
+ # Recreate the sentinel file
1531
1563
  "sudo touch /opt/dayhoff/first_boot_complete.sentinel",
1532
- "sudo systemctl restart engine-idle-detector.timer",
1564
+ # Mark bootstrap as finished
1565
+ "echo 'finished' | sudo tee /opt/dayhoff/state/engine-init.stage > /dev/null",
1566
+ # Restart idle detector if it exists
1567
+ "sudo systemctl restart engine-idle-detector.timer 2>/dev/null || true",
1568
+ # Ensure SSM agent is running
1569
+ "sudo systemctl start amazon-ssm-agent 2>/dev/null || true",
1533
1570
  ],
1534
1571
  "executionTimeout": ["60"],
1535
1572
  },
1536
1573
  )
1537
1574
 
1538
- # Quick wait to see if it failed immediately
1539
- time.sleep(1)
1575
+ # Wait for restore command to complete
1540
1576
  restore_command_id = restore_response["Command"]["CommandId"]
1541
- result = ssm.get_command_invocation(
1542
- CommandId=restore_command_id,
1543
- InstanceId=engine["instance_id"],
1544
- )
1545
- if result["Status"] not in ["Pending", "InProgress", "Success"]:
1577
+ for _ in range(10):
1578
+ time.sleep(2)
1579
+ result = ssm.get_command_invocation(
1580
+ CommandId=restore_command_id,
1581
+ InstanceId=engine["instance_id"],
1582
+ )
1583
+ if result["Status"] in ["Success", "Failed"]:
1584
+ break
1585
+
1586
+ if result["Status"] == "Success":
1546
1587
  console.print(
1547
- "[yellow]⚠️ Warning: Failed to restore source engine state.[/yellow]"
1588
+ "[green] Source engine restored to normal operation.[/green]"
1548
1589
  )
1549
1590
  else:
1550
1591
  console.print(
1551
- "[green] Source engine restored to normal operation.[/green]"
1592
+ "[yellow]⚠️ Warning: Engine state restoration incomplete. You may need to run:[/yellow]"
1593
+ )
1594
+ console.print(
1595
+ f"[dim] dh engine repair {engine['name']}[/dim]"
1552
1596
  )
1553
1597
 
1554
1598
  console.print(
@@ -1792,52 +1836,28 @@ def attach_studio(
1792
1836
 
1793
1837
  console.print(f"Attaching studio to engine [cyan]{engine['name']}[/cyan]...")
1794
1838
 
1795
- # Determine retry strategy
1796
- max_attempts = 40 if engine_started_now else 3
1797
- retry_delay = 10 if engine_started_now else 3
1798
-
1839
+ # Determine retry strategy based on whether we just started the engine
1799
1840
  if engine_started_now:
1800
- # Long spinner-based loop while the freshly started engine finishes booting
1801
- with Progress(
1802
- SpinnerColumn(),
1803
- TimeElapsedColumn(),
1804
- TextColumn("[progress.description]{task.description}"),
1805
- transient=True,
1806
- ) as prog:
1807
- task = prog.add_task(
1808
- "Attaching studio (engine is still booting)…", total=None
1809
- )
1810
-
1811
- for attempt in range(max_attempts):
1812
- success, error_msg = _attempt_studio_attach(
1813
- studio, engine, target_user, public_key
1814
- )
1815
-
1816
- if success:
1817
- break # success!
1818
-
1819
- # Update spinner every 3rd try to avoid log spam
1820
- if attempt % 3 == 0:
1821
- prog.update(
1822
- task,
1823
- description=f"Attaching studio (engine is still booting)… {attempt+1}/{max_attempts}",
1824
- )
1825
-
1826
- if error_msg:
1827
- console.print(f"[red]❌ Failed to attach studio: {error_msg}[/red]")
1828
- return
1829
-
1830
- time.sleep(retry_delay)
1831
-
1832
- else:
1833
- console.print(
1834
- "[yellow]Engine is still starting up – please retry in a minute.[/yellow]"
1835
- )
1836
- return
1841
+ max_attempts = 40 # About 7 minutes total with exponential backoff
1842
+ base_delay = 8
1843
+ max_delay = 20
1837
1844
  else:
1838
- # Give the (already-running) engine a little breathing room – e.g. it may still be mounting EFS
1839
- max_attempts = 10 # ~1 min total
1840
- retry_delay = 6
1845
+ max_attempts = 15 # About 2 minutes total with exponential backoff
1846
+ base_delay = 5
1847
+ max_delay = 10
1848
+
1849
+ # Unified retry loop with exponential backoff
1850
+ with Progress(
1851
+ SpinnerColumn(),
1852
+ TimeElapsedColumn(),
1853
+ TextColumn("[progress.description]{task.description}"),
1854
+ transient=True,
1855
+ ) as prog:
1856
+ desc = "Attaching studio (engine is still booting)…" if engine_started_now else "Attaching studio…"
1857
+ task = prog.add_task(desc, total=None)
1858
+
1859
+ consecutive_not_ready = 0
1860
+ last_error = None
1841
1861
 
1842
1862
  for attempt in range(max_attempts):
1843
1863
  success, error_msg = _attempt_studio_attach(
@@ -1845,22 +1865,54 @@ def attach_studio(
1845
1865
  )
1846
1866
 
1847
1867
  if success:
1848
- break # attached!
1868
+ break # success!
1849
1869
 
1850
1870
  if error_msg:
1851
- # Fatal – bubble up immediately
1871
+ # Fatal error – bubble up immediately
1852
1872
  console.print(f"[red]❌ Failed to attach studio: {error_msg}[/red]")
1873
+
1874
+ # Suggest repair command if engine seems broken
1875
+ if "not ready" in error_msg.lower() and attempt > 5:
1876
+ console.print(f"\n[yellow]Engine may be in a bad state. Try:[/yellow]")
1877
+ console.print(f"[dim] dh engine repair {engine['name']}[/dim]")
1853
1878
  return
1854
1879
 
1855
- # Recoverable and still not ready – short wait + optional info
1856
- if attempt < max_attempts - 1:
1857
- console.print("[dim]Engine not ready yet – retrying …[/dim]")
1858
- time.sleep(retry_delay)
1880
+ # Track consecutive "not ready" responses
1881
+ consecutive_not_ready += 1
1882
+ last_error = "Engine not ready"
1883
+
1884
+ # Update progress display
1885
+ if attempt % 3 == 0:
1886
+ prog.update(
1887
+ task,
1888
+ description=f"{desc} attempt {attempt+1}/{max_attempts}",
1889
+ )
1890
+
1891
+ # If engine seems stuck after many attempts, show a hint
1892
+ if consecutive_not_ready > 10 and attempt == 10:
1893
+ console.print(
1894
+ "[yellow]Engine is taking longer than expected to become ready.[/yellow]"
1895
+ )
1896
+ console.print(
1897
+ "[dim]This can happen after GAMI creation or if the engine is still bootstrapping.[/dim]"
1898
+ )
1899
+
1900
+ # Exponential backoff with jitter
1901
+ delay = min(base_delay * (1.5 ** min(attempt, 5)), max_delay)
1902
+ delay += time.time() % 2 # Add 0-2 seconds of jitter
1903
+ time.sleep(delay)
1859
1904
 
1860
1905
  else:
1906
+ # All attempts exhausted
1861
1907
  console.print(
1862
- "[yellow]Engine is busy or still initialising please retry in about a minute.[/yellow]"
1908
+ f"[yellow]Engine is not becoming ready after {max_attempts} attempts.[/yellow]"
1863
1909
  )
1910
+ if last_error:
1911
+ console.print(f"[dim]Last issue: {last_error}[/dim]")
1912
+ console.print("\n[yellow]You can try:[/yellow]")
1913
+ console.print(f" 1. Wait a minute and retry: [cyan]dh studio attach {engine['name']}[/cyan]")
1914
+ console.print(f" 2. Check engine status: [cyan]dh engine status {engine['name']}[/cyan]")
1915
+ console.print(f" 3. Repair the engine: [cyan]dh engine repair {engine['name']}[/cyan]")
1864
1916
  return
1865
1917
 
1866
1918
  # Successful attach path
@@ -2453,3 +2505,113 @@ def debug_engine(
2453
2505
 
2454
2506
  except Exception as e:
2455
2507
  console.print(f"[cyan]{name}:[/cyan] [red]ERROR: {e}[/red]\n")
2508
+
2509
+
2510
+ @engine_app.command("repair")
2511
+ def repair_engine(
2512
+ name_or_id: str = typer.Argument(help="Engine name or instance ID"),
2513
+ ):
2514
+ """Repair an engine that's stuck in a bad state (e.g., after GAMI creation)."""
2515
+ check_aws_sso()
2516
+
2517
+ # Get all engines to resolve name
2518
+ response = make_api_request("GET", "/engines")
2519
+ if response.status_code != 200:
2520
+ console.print("[red]❌ Failed to fetch engines[/red]")
2521
+ raise typer.Exit(1)
2522
+
2523
+ engines = response.json().get("engines", [])
2524
+ engine = resolve_engine(name_or_id, engines)
2525
+
2526
+ if engine["state"].lower() != "running":
2527
+ console.print(f"[yellow]⚠️ Engine is {engine['state']}. Must be running to repair.[/yellow]")
2528
+ if engine["state"].lower() == "stopped" and Confirm.ask("Start the engine first?"):
2529
+ response = make_api_request("POST", f"/engines/{engine['instance_id']}/start")
2530
+ if response.status_code != 200:
2531
+ console.print("[red]❌ Failed to start engine[/red]")
2532
+ raise typer.Exit(1)
2533
+ console.print("[green]✓ Engine started[/green]")
2534
+ console.print("Waiting for engine to become ready...")
2535
+ time.sleep(30) # Give it time to boot
2536
+ else:
2537
+ raise typer.Exit(1)
2538
+
2539
+ console.print(f"[bold]Repairing engine [cyan]{engine['name']}[/cyan][/bold]")
2540
+ console.print("[dim]This will restore bootstrap state and ensure all services are running[/dim]\n")
2541
+
2542
+ ssm = boto3.client("ssm", region_name="us-east-1")
2543
+
2544
+ # Repair commands
2545
+ repair_commands = [
2546
+ # Create necessary directories
2547
+ "sudo mkdir -p /opt/dayhoff /opt/dayhoff/state /opt/dayhoff/scripts",
2548
+
2549
+ # Download scripts from S3 if missing
2550
+ "source /etc/engine.env && sudo aws s3 sync s3://${VM_SCRIPTS_BUCKET}/ /opt/dayhoff/scripts/ --exclude '*' --include '*.sh' --quiet",
2551
+ "sudo chmod +x /opt/dayhoff/scripts/*.sh 2>/dev/null || true",
2552
+
2553
+ # Restore bootstrap state
2554
+ "sudo touch /opt/dayhoff/first_boot_complete.sentinel",
2555
+ "echo 'finished' | sudo tee /opt/dayhoff/state/engine-init.stage > /dev/null",
2556
+
2557
+ # Ensure SSM agent is running
2558
+ "sudo systemctl restart amazon-ssm-agent 2>/dev/null || true",
2559
+
2560
+ # Restart idle detector
2561
+ "sudo systemctl restart engine-idle-detector.timer 2>/dev/null || true",
2562
+ "sudo systemctl restart engine-idle-detector.service 2>/dev/null || true",
2563
+
2564
+ # Report status
2565
+ "echo '=== Repair Complete ===' && echo 'Sentinel: ' && ls -la /opt/dayhoff/first_boot_complete.sentinel",
2566
+ "echo 'Stage: ' && cat /opt/dayhoff/state/engine-init.stage",
2567
+ "echo 'Scripts: ' && ls /opt/dayhoff/scripts/*.sh 2>/dev/null | wc -l",
2568
+ ]
2569
+
2570
+ try:
2571
+ with Progress(
2572
+ SpinnerColumn(),
2573
+ TextColumn("[progress.description]{task.description}"),
2574
+ transient=True,
2575
+ ) as progress:
2576
+ task = progress.add_task("Repairing engine...", total=None)
2577
+
2578
+ response = ssm.send_command(
2579
+ InstanceIds=[engine["instance_id"]],
2580
+ DocumentName="AWS-RunShellScript",
2581
+ Parameters={
2582
+ "commands": repair_commands,
2583
+ "executionTimeout": ["60"],
2584
+ },
2585
+ )
2586
+
2587
+ command_id = response["Command"]["CommandId"]
2588
+
2589
+ # Wait for command
2590
+ for _ in range(60):
2591
+ time.sleep(1)
2592
+ result = ssm.get_command_invocation(
2593
+ CommandId=command_id,
2594
+ InstanceId=engine["instance_id"],
2595
+ )
2596
+ if result["Status"] in ["Success", "Failed"]:
2597
+ break
2598
+
2599
+ if result["Status"] == "Success":
2600
+ output = result["StandardOutputContent"]
2601
+ console.print("[green]✓ Engine repaired successfully![/green]\n")
2602
+
2603
+ # Show repair results
2604
+ if "=== Repair Complete ===" in output:
2605
+ repair_section = output.split("=== Repair Complete ===")[1].strip()
2606
+ console.print("[bold]Repair Results:[/bold]")
2607
+ console.print(repair_section)
2608
+
2609
+ console.print("\n[dim]You should now be able to attach studios to this engine.[/dim]")
2610
+ else:
2611
+ console.print(
2612
+ f"[red]❌ Repair failed: {result.get('StandardErrorContent', 'Unknown error')}[/red]"
2613
+ )
2614
+ console.print("\n[yellow]Try running 'dh engine debug' for more information.[/yellow]")
2615
+
2616
+ except Exception as e:
2617
+ console.print(f"[red]❌ Failed to repair engine: {e}[/red]")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dayhoff-tools
3
- Version: 1.5.7
3
+ Version: 1.5.9
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=_WzJpxwGVmN0vem6oVi5FosQkKaEGCLUgoUHoKSWejg,89475
6
+ dayhoff_tools/cli/engine_commands.py,sha256=RSbZK2hN3bTBQrZxAgooqMsqSiw0hnhmzTVkL44xdD4,97147
7
7
  dayhoff_tools/cli/main.py,sha256=tRN7WCBHg6uyNp6rA54pKTCoVmBntta2i0Yas3bUpZ4,4853
8
8
  dayhoff_tools/cli/swarm_commands.py,sha256=5EyKj8yietvT5lfoz8Zx0iQvVaNgc3SJX1z2zQR6o6M,5614
9
9
  dayhoff_tools/cli/utility_commands.py,sha256=FRZTPrjsG_qmIIqoNxd1Q1vVkS_5w8aY33IrVYVNCLg,18131
@@ -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=heaYc64qplgN3_1WVPFmqj53goStioWwY5NqlWc4c0s,24453
30
- dayhoff_tools-1.5.7.dist-info/METADATA,sha256=APBk7rddBNxAjWSsibYM8Fx5B_BRJXRHi61rqjBRGZg,2914
31
- dayhoff_tools-1.5.7.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
- dayhoff_tools-1.5.7.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
33
- dayhoff_tools-1.5.7.dist-info/RECORD,,
30
+ dayhoff_tools-1.5.9.dist-info/METADATA,sha256=mGdK_rLBA7HwjkUI7e2sQ5oGExLsBU8A-GIBD9-xyaM,2914
31
+ dayhoff_tools-1.5.9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
+ dayhoff_tools-1.5.9.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
33
+ dayhoff_tools-1.5.9.dist-info/RECORD,,