from __future__ import annotations
import logging
import os
import shutil
from contextvars import ContextVar
from pathlib import Path
from typing import Any, Iterable
import bio_tools
from bio_tools import (
LaunchType,
Process,
catalog_process,
catalog_spec,
)
from .environments import (
environment_python,
environment_script,
process_executables_root,
)
from .status_check import probe_command
logger = logging.getLogger(__name__)
AMINO_ACIDS = frozenset("ABCDEFGHIKLMNPQRSTVWXYZ")
DNA_BASES = frozenset("ACGTUNRYKMSWBDHV")
PROCESS_EXECUTABLES = process_executables_root()
RUN_LOGS = PROCESS_EXECUTABLES / "run_logs"
_RUN_LOG_NAME: ContextVar[str | None] = ContextVar("tool_run_log_name", default=None)
TOOL_RUN_TIMEOUT = 86_400
class ToolInputError(ValueError):
class ToolExecutionError(RuntimeError):
class ToolUnavailable(RuntimeError):
ERROR_CODES: tuple[tuple[type[Exception], str], ...] = (
(ToolInputError, "invalid_input"),
(ToolUnavailable, "tool_unavailable"),
(ToolExecutionError, "execution_failed"),
)
def job_error(tool_name: str, error: Exception) -> dict[str, str]:
for exc_type, code in ERROR_CODES:
if isinstance(error, exc_type):
return {"code": code, "message": str(error)}
logger.exception("Unexpected %s tool failure", tool_name, exc_info=error)
return {"code": "internal_error", "message": "The tool failed unexpectedly."}
def run_tool(tool: Process, payload: dict[str, Any]) -> Any:
token = _RUN_LOG_NAME.set(tool.spec.slug)
try:
return tool.module.run(preset_payload(tool.spec.slug, payload))
finally:
_RUN_LOG_NAME.reset(token)
def preset_payload(slug: str, payload: dict[str, Any]) -> dict[str, Any]:
preset_id = payload.get("preset")
if not preset_id:
return payload
try:
return bio_tools.catalog_preset(
slug,
str(preset_id),
overrides={key: value for key, value in payload.items() if key != "preset"},
)
except ValueError as exc:
raise ToolInputError(str(exc)) from exc
from .field_processing import Field, Option, text
def tool_fields(
slug: str, *, dynamic_options: dict[str, list[tuple[str, str]]] | None = None
) -> list[Field]:
return bio_tools.catalog_fields(
slug,
field_type=Field,
option_type=Option,
dynamic_options=dynamic_options,
)
def tool_tasks(slug: str) -> list[Option]:
return bio_tools.catalog_tasks(slug, option_type=Option)
def _bundled_candidates(name: str) -> Iterable[Path]:
for filename in (name, f"{name}.exe") if os.name == "nt" else (name,):
yield PROCESS_EXECUTABLES / filename
yield PROCESS_EXECUTABLES / "bin" / filename
yield from sorted(PROCESS_EXECUTABLES.glob(f"*/{filename}"))
yield from sorted(PROCESS_EXECUTABLES.glob(f"*/bin/{filename}"))
def executable(env_name: str, *names: str) -> str:
configured = os.getenv(env_name)
if configured:
resolved = shutil.which(configured)
if resolved:
return str(Path(resolved).resolve())
configured_path = Path(configured).expanduser()
if configured_path.is_file():
return str(configured_path.resolve())
raise ToolUnavailable(
f"{env_name} points to an executable that cannot be found."
)
for name in names:
for candidate in _bundled_candidates(name):
if candidate.is_file() and os.access(candidate, os.X_OK):
return str(candidate.resolve())
for name in names:
resolved = shutil.which(name)
if resolved:
return str(Path(resolved).resolve())
friendly = ", ".join(names)
raise ToolUnavailable(
f"No executable found for {friendly}. Install it under {PROCESS_EXECUTABLES}, "
f"set {env_name} to its full path, or add it to PATH."
)
def _override(env_name: str) -> str | None:
configured = os.getenv(env_name)
if not configured:
return None
resolved = shutil.which(configured)
if resolved:
return str(Path(resolved).resolve())
path = Path(configured).expanduser()
if path.is_file():
return str(path.resolve())
raise ToolUnavailable(f"{env_name} points to a file that cannot be found.")
def _setup_hint(environment: str) -> str:
return (
f"Run `python install_tools.py {environment}` "
"(setup_system.sh does this through bio_tools)"
)
def tool_script(environment: str, script: str, env_name: str) -> str:
override = _override(env_name)
if override:
return override
path = environment_script(environment, script)
if path.is_file():
return str(path.resolve())
raise ToolUnavailable(
f"{script} is not installed in the {environment} environment. "
f"{_setup_hint(environment)}, or set {env_name} to its full path."
)
def tool_python(environment: str, env_name: str) -> str:
override = _override(env_name)
if override:
return override
path = environment_python(environment)
if path.is_file():
return str(path)
raise ToolUnavailable(
f"No Python environment for {environment}. {_setup_hint(environment)}, "
f"or set {env_name} to an interpreter that can run it."
)
def configured_python(env_name: str, tool: str) -> str:
override = _override(env_name)
if override:
return override
raise ToolUnavailable(
f"Set {env_name} to the Python interpreter of the {tool} Conda "
f"environment (for example `conda run -n {tool} which python`)."
)
def run_command(
command: list[str],
*,
cwd: Path,
timeout: int = TOOL_RUN_TIMEOUT,
stdin: str | None = None,
env: dict[str, str] | None = None,
artifacts: Iterable[Path] | None = None,
) -> dict[str, Any]:
try:
completed = bio_tools.CommandSpec(
command,
cwd=cwd,
timeout=timeout,
stdin=stdin,
env=env,
check=True,
output_limit=100_000,
run_log_dir=RUN_LOGS,
run_name=_RUN_LOG_NAME.get() or Path(command[0]).stem,
artifacts=list(artifacts) if artifacts is not None else [cwd],
).run()
except bio_tools.RunError as exc:
raise ToolExecutionError(str(exc)) from exc
return {
"command": completed.command,
"return_code": completed.return_code,
"stdout": completed.stdout,
"stderr": completed.stderr,
"run_log_dir": str(completed.run_log_dir),
}
def readable_files(root: Path, *, maximum_files: int = 30) -> list[str]:
files: list[str] = []
for path in sorted(root.rglob("*")):
if path.is_file():
files.append(path.relative_to(root).as_posix())
if len(files) >= maximum_files:
break
return files
def _device_probe(
python: str | Path, snippet: str, *, timeout: int = 300
) -> str | None:
code, output = probe_command([str(python), "-c", snippet], timeout=timeout)
if code != 0:
return None
lines = [line.strip() for line in output.splitlines() if line.strip()]
device = lines[-1] if lines else ""
return device if device in {"GPU", "CPU"} else None
def torch_device(python: str | Path) -> str | None:
return _device_probe(
python, "import torch; print('GPU' if torch.cuda.is_available() else 'CPU')"
)
def jax_device(python: str | Path) -> str | None:
return _device_probe(
python,
"import jax; print('GPU' if any(d.platform == 'gpu' for d in jax.devices()) else 'CPU')",
)