from __future__ import annotations
import logging
import bio_tools
import shlex
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from enum import Enum
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from main.tools import Process
from .environments import process_executables_root
class CheckResult(Enum):
PASS = "Pass"
NOT_INSTALLED = "Can't find"
ERROR = "Error"
NOT_CHECKED = "Not checked"
@property
def rank(self) -> int:
return {
self.PASS: 0,
self.ERROR: 1,
self.NOT_INSTALLED: 2,
self.NOT_CHECKED: 3,
}[self]
@dataclass
class ToolStatus:
result: CheckResult
detail: str = ""
device: str | None = None
def probe_command(
command: list[str],
*,
cwd: Path | None = None,
timeout: int = 120,
) -> tuple[int | None, str]:
try:
completed = bio_tools.CommandSpec(
command,
cwd=cwd,
timeout=timeout,
check=False,
output_limit=4_000,
).run()
except bio_tools.RunError as exc:
return None, str(exc)
output = (completed.stdout + completed.stderr).strip()[-4_000:]
return completed.return_code, output
def _is_crash(output: str) -> bool:
return output.lstrip().startswith("Traceback (most recent call last):")
def probe_cli(
command: list[str], *, cwd: Path | None = None, timeout: int = 120
) -> ToolStatus:
last_detail = "Produced no output for --version or --help."
for flag in ("--version", "--help"):
code, output = probe_command([*command, flag], cwd=cwd, timeout=timeout)
if code is None:
return ToolStatus(CheckResult.NOT_INSTALLED, output)
if output and not _is_crash(output):
return ToolStatus(CheckResult.PASS, output.splitlines()[0][:200])
if output:
last_detail = output.splitlines()[-1][:200]
else:
last_detail = (
f"`{shlex.join([*command, flag])}` exited {code} with no output."
)
return ToolStatus(CheckResult.ERROR, last_detail)
def probe_python_package(
python: str | Path,
distribution: str,
module: str,
*,
timeout: int = 120,
) -> ToolStatus:
code, output = probe_command(
[
str(python),
"-c",
(
"import importlib, importlib.metadata as m, sys; "
"importlib.import_module(sys.argv[2]); "
"print(sys.argv[1] + ' ' + m.version(sys.argv[1]))"
),
distribution,
module,
],
timeout=timeout,
)
if code is None:
return ToolStatus(CheckResult.NOT_INSTALLED, output)
if code != 0:
detail = output.splitlines()[-1] if output else f"Could not import {module}."
return ToolStatus(CheckResult.ERROR, detail[:200])
return ToolStatus(CheckResult.PASS, output.splitlines()[-1][:200])
def require_gpu_status(
status: ToolStatus, device: str | None, runtime: str
) -> ToolStatus:
if status.result != CheckResult.PASS:
return status
if device == "GPU":
status.device = device
return status
detail = (
f"{runtime} is installed, but its GPU probe did not complete, so its "
"CUDA support could not be determined."
if device is None
else f"{runtime} is installed, but it does not report a usable CUDA GPU."
)
return ToolStatus(CheckResult.ERROR, detail, device=device)
def _check_one(tool: Process, *, full: bool) -> tuple[Process, ToolStatus]:
started = time.perf_counter()
try:
installable = bio_tools.Tool(tool.spec.slug)
probe = installable.status_full if full else installable.status_quick
native = probe(process_executables_root())
status = ToolStatus(CheckResult(native.result), native.detail, native.device)
except Exception as exc:
status = ToolStatus(CheckResult.ERROR, f"bio_tools status() raised: {exc}")
elapsed = time.perf_counter() - started
level = "Full status" if full else "Quick status"
logging.warning(f"{level} ({tool.name}, {elapsed:.1f}s)")
return tool, status
def check_statuses(
tools: list[Process] | None = None,
*,
full: bool = False,
) -> list[tuple[Process, ToolStatus]]:
from main.tools import all_tools
if tools is None:
tools = all_tools()
if not tools:
return []
started = time.perf_counter()
with ThreadPoolExecutor(max_workers=max(len(tools), 1)) as pool:
statuses = list(pool.map(partial(_check_one, full=full), tools))
level = "full" if full else "quick"
logging.warning(
f"Checked {len(statuses)} {level} tool statuses in "
f"{time.perf_counter() - started:.1f}s"
)
return statuses