#!/usr/bin/env python
"""Docker backend implementation for SAMMY execution."""
import shutil
import subprocess
import textwrap
from datetime import datetime
from pathlib import Path
from uuid import uuid4
from pleiades.sammy.config import DockerSammyConfig
from pleiades.sammy.interface import (
EnvironmentPreparationError,
SammyExecutionError,
SammyExecutionResult,
SammyFiles,
SammyRunner,
)
from pleiades.utils.logger import loguru_logger
logger = loguru_logger.bind(name=__name__)
[docs]
class DockerSammyRunner(SammyRunner):
"""Implementation of SAMMY runner for Docker container."""
def __init__(self, config: DockerSammyConfig):
super().__init__(config)
self.config = config
self._moved_files = []
[docs]
def prepare_environment(self, files: SammyFiles) -> None:
"""
Prepare environment for Docker SAMMY execution.
Args:
files: Container with paths to required input files
Raises:
EnvironmentPreparationError: If preparation fails
"""
try:
# Validate input files exist
files.validate()
# Validate docker is available
if shutil.which("docker") is None:
raise EnvironmentPreparationError("Docker not found in PATH")
# Verify docker image exists
result = subprocess.run(
["docker", "image", "inspect", self.config.image_name], capture_output=True, text=True
)
if result.returncode != 0:
raise EnvironmentPreparationError(f"Docker image not found: {self.config.image_name}")
except EnvironmentPreparationError:
raise # Reraise to avoid double logging
except Exception as e:
raise EnvironmentPreparationError(f"Environment preparation failed: {str(e)}")
[docs]
def execute_sammy(self, files: SammyFiles) -> SammyExecutionResult:
"""
Execute SAMMY using Docker container.
Args:
files: Container with paths to required input files
Returns:
SammyExecutionResult containing execution status and outputs
Raises:
SammyExecutionError: If execution fails
"""
execution_id = str(uuid4())
start_time = datetime.now()
logger.info(f"Starting SAMMY execution {execution_id} in Docker")
logger.debug(f"Input files from: {files.input_file.parent}")
logger.debug(f"Working directory: {self.config.working_dir}")
# Prepare container paths for input files
container_input = str(self.config.container_data_dir / files.input_file.name)
container_params = str(self.config.container_data_dir / files.parameter_file.name)
container_data = str(self.config.container_data_dir / files.data_file.name)
# Construct docker run command
docker_cmd = [
"docker",
"run",
"--rm", # Remove container after execution
"-i", # Interactive mode for heredoc input
# Mount working directory
"-v",
f"{self.config.working_dir}:{self.config.container_working_dir}",
# Mount data directory
"-v",
f"{files.input_file.parent}:{self.config.container_data_dir}",
# Set working directory
"-w",
str(self.config.container_working_dir),
# Image name
self.config.image_name,
# SAMMY command (will receive input via stdin)
"sammy",
]
# Prepare SAMMY input
sammy_input = textwrap.dedent(f"""\
{container_input}
{container_params}
{container_data}
""")
try:
process = subprocess.run(docker_cmd, input=sammy_input, text=True, capture_output=True)
end_time = datetime.now()
console_output = process.stdout + process.stderr
if process.returncode != 0:
logger.error(f"Docker execution failed with code {process.returncode}")
return SammyExecutionResult(
success=False,
execution_id=execution_id,
start_time=start_time,
end_time=end_time,
console_output=console_output,
error_message=f"Docker execution failed with code {process.returncode}",
)
# Check SAMMY output for success
success = " Normal finish to SAMMY" in console_output
error_message = None if success else "SAMMY execution failed"
if not success:
logger.error(f"SAMMY execution failed for {execution_id}")
else:
logger.info(f"SAMMY execution completed successfully for {execution_id}")
return SammyExecutionResult(
success=success,
execution_id=execution_id,
start_time=start_time,
end_time=end_time,
console_output=console_output,
error_message=error_message,
)
except Exception as e:
logger.exception(f"Docker execution failed for {execution_id}")
raise SammyExecutionError(f"Docker execution failed: {str(e)}")
[docs]
def cleanup(self) -> None:
"""Clean up after execution."""
logger.debug("Performing cleanup")
self._moved_files = []
[docs]
def validate_config(self) -> bool:
"""Validate the configuration."""
return self.config.validate()
if __name__ == "__main__":
# Setup paths
test_data_dir = Path(__file__).parents[4] / "tests/data/ex012"
working_dir = Path.home() / "tmp" / "sammy_docker_run"
output_dir = working_dir / "output"
try:
# Create and validate config
config = DockerSammyConfig(
image_name="kedokudo/sammy-docker:1.0.0", working_dir=working_dir, output_dir=output_dir
)
config.validate()
# Create files container
files = SammyFiles(
input_file=test_data_dir / "ex012a.inp",
parameter_file=test_data_dir / "ex012a.par",
data_file=test_data_dir / "ex012a.dat",
)
# Create and use runner
runner = DockerSammyRunner(config)
# Execute pipeline
runner.prepare_environment(files)
result = runner.execute_sammy(files)
# Process results
if result.success:
print(f"SAMMY execution successful (runtime: {result.runtime_seconds:.2f}s)")
runner.collect_outputs(result)
print(f"Output files available in: {output_dir}")
else:
print("SAMMY execution failed:")
print(result.error_message)
print("\nConsole output:")
print(result.console_output)
except Exception as e:
print(f"Error running SAMMY: {str(e)}")
finally:
runner.cleanup()