Skip to content

Commit 97fc507

Browse files
Merge pull request #150 from wfcommons/snakemake
Snakemake translator and logger implementation and tests
2 parents d3d9c67 + da45fd8 commit 97fc507

7 files changed

Lines changed: 397 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# docker build --platform amd64 -t wfcommons-dev-snakemake -f Dockerfile.snakemake .
2+
# docker run -it --rm -v `pwd`:/home/wfcommons wfcommons-dev-snakemake /bin/bash
3+
4+
FROM amd64/ubuntu:noble
5+
6+
LABEL org.containers.image.authors="henric@hawaii.edu"
7+
8+
# update repositories
9+
RUN apt-get update
10+
11+
# set timezone
12+
RUN echo "America/Los_Angeles" > /etc/timezone && export DEBIAN_FRONTEND=noninteractive && apt-get install -y tzdata
13+
14+
# install useful stuff
15+
RUN apt-get -y install pkg-config
16+
RUN apt-get -y install git
17+
RUN apt-get -y install wget
18+
RUN apt-get -y install curl
19+
RUN apt-get -y install make
20+
RUN apt-get -y install cmake
21+
RUN apt-get -y install cmake-data
22+
RUN apt-get -y install sudo
23+
RUN apt-get -y install vim --fix-missing
24+
RUN apt-get -y install gcc
25+
RUN apt-get -y install gcc-multilib
26+
RUN apt-get -y install graphviz libgraphviz-dev
27+
28+
29+
# Python stuff
30+
RUN apt-get -y install python3 python3-pip
31+
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3 1
32+
RUN python3 -m pip install --break-system-packages pathos pandas filelock
33+
RUN python3 -m pip install --break-system-packages networkx scipy matplotlib pygraphviz
34+
RUN python3 -m pip install --break-system-packages pyyaml jsonschema requests
35+
RUN python3 -m pip install --break-system-packages --upgrade setuptools
36+
37+
# Stress-ng
38+
RUN apt-get -y install stress-ng
39+
40+
# Add wfcommons user
41+
RUN useradd -ms /bin/bash wfcommons
42+
RUN adduser wfcommons sudo
43+
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
44+
ENV PATH="$PATH:/home/wfcommons/.local/bin/"
45+
46+
USER wfcommons
47+
WORKDIR /home/wfcommons
48+
# Making this directory world rwx to facilitate testing
49+
RUN chmod -R 777 /home/wfcommons
50+
51+
52+
# Install Pixi
53+
RUN wget -qO- https://pixi.sh/install.sh | sh
54+
ENV PATH="$PATH:/home/wfcommons/.pixi/bin"
55+
56+
# Install snakemake
57+
RUN pixi global install snakemake conda -c conda-forge -c bioconda
58+
RUN pixi global install snakedeploy -c conda-forge -c bioconda
59+
RUN ~/.pixi/envs/snakemake/bin/python -m ensurepip && \
60+
~/.pixi/envs/snakemake/bin/python -m pip install snakemake-logger-plugin-snkmt
61+

tests/translators_loggers/test_translators_loggers.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from wfcommons.wfbench import BashTranslator
3636
from wfcommons.wfbench import TaskVineTranslator
3737
from wfcommons.wfbench import MakeflowTranslator
38+
from wfcommons.wfbench import SnakemakeTranslator
3839
from wfcommons.wfbench import CWLTranslator
3940
from wfcommons.wfbench import StreamflowTranslator
4041
from wfcommons.wfbench import PegasusTranslator
@@ -44,6 +45,7 @@
4445
from wfcommons.wfinstances.logs import TaskVineLogsParser
4546
from wfcommons.wfinstances.logs import MakeflowLogsParser
4647
from wfcommons.wfinstances.logs import ROCrateLogsParser
48+
from wfcommons.wfinstances.logs import SnakemakeLogsParser
4749

4850

4951
def _create_workflow_benchmark() -> (WorkflowBenchmark, int):
@@ -135,6 +137,7 @@ def _additional_setup_swiftt(container):
135137
"bash": noop,
136138
"taskvine": _additional_setup_taskvine,
137139
"makeflow": noop,
140+
"snakemake": noop,
138141
"cwl": noop,
139142
"streamflow": noop,
140143
"pegasus": _additional_setup_pegasus,
@@ -201,6 +204,15 @@ def run_workflow_makeflow(container, num_tasks, str_dirpath):
201204
num_completed_jobs = len(re.findall(r'job \d+ completed', output.decode()))
202205
assert (num_completed_jobs == num_tasks)
203206

207+
def run_workflow_snakemake(container, num_tasks, str_dirpath):
208+
# Run the workflow (with full logging)
209+
exit_code, output = container.exec_run(cmd=["bash", "-c", "snakemake -s ./workflow.smk --cores 1 --logger snkmt --logger-snkmt-db ./snkmt.sqlite"],
210+
user="wfcommons", stdout=True, stderr=True)
211+
# Check sanity
212+
assert (exit_code == 0)
213+
num_completed_jobs = len(re.findall(r'Finished jobid: \d+', output.decode()))
214+
assert (num_completed_jobs - 1 == num_tasks) # Discounting the "all_tasks" rule
215+
204216
def run_workflow_cwl(container, num_tasks, str_dirpath):
205217
# Run the workflow!
206218
# Note that the input file is hardcoded and Blast-specific
@@ -261,6 +273,7 @@ def run_workflow_swiftt(container, num_tasks, str_dirpath):
261273
"bash": run_workflow_bash,
262274
"taskvine": run_workflow_taskvine,
263275
"makeflow": run_workflow_makeflow,
276+
"snakemake": run_workflow_snakemake,
264277
"cwl": run_workflow_cwl,
265278
"streamflow": run_workflow_streamflow,
266279
"pegasus": run_workflow_pegasus,
@@ -276,6 +289,7 @@ def run_workflow_swiftt(container, num_tasks, str_dirpath):
276289
"bash": BashTranslator,
277290
"taskvine": TaskVineTranslator,
278291
"makeflow": MakeflowTranslator,
292+
"snakemake": SnakemakeTranslator,
279293
"cwl": CWLTranslator,
280294
"streamflow": StreamflowTranslator,
281295
"pegasus": PegasusTranslator,
@@ -297,6 +311,7 @@ class TestTranslators:
297311
"bash",
298312
"taskvine",
299313
"makeflow",
314+
"snakemake",
300315
"cwl",
301316
"streamflow",
302317
"pegasus",
@@ -356,6 +371,8 @@ def test_translator(self, backend) -> None:
356371
steps_to_ignore=["main.cwl#compile_output_files", "main.cwl#compile_log_files"],
357372
file_extensions_to_ignore=[".out", ".err"],
358373
instruments_to_ignore=["shell.cwl"])
374+
elif backend == "snakemake":
375+
parser = SnakemakeLogsParser(dirpath, snkmt_db=dirpath / "snkmt.sqlite", rules_to_ignore=["all_wfbench_tasks"])
359376

360377
if parser is not None:
361378
sys.stderr.write(f"[{backend}] Parsing the logs...\n")

wfcommons/wfbench/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
SwiftTTranslator,
1919
TaskVineTranslator,
2020
MakeflowTranslator,
21+
SnakemakeTranslator,
2122
CWLTranslator,
2223
StreamflowTranslator,
2324
PyCompssTranslator)

wfcommons/wfbench/translator/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
from .swift_t import SwiftTTranslator
2121
from .taskvine import TaskVineTranslator
2222
from .makeflow import MakeflowTranslator
23+
from .snakemake import SnakemakeTranslator
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (c) 2024-2025 The WfCommons Team.
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
11+
import pathlib
12+
import shutil
13+
14+
from logging import Logger
15+
from typing import Optional, Union
16+
17+
from .abstract_translator import Translator
18+
from ...common import Workflow
19+
20+
this_dir = pathlib.Path(__file__).resolve().parent
21+
22+
class SnakemakeTranslator(Translator):
23+
"""
24+
A WfFormat parser for creating Snakemake workflow applications.
25+
26+
:param workflow: Workflow benchmark object or path to the workflow benchmark JSON instance.
27+
:type workflow: Union[Workflow, pathlib.Path],
28+
:param logger: The logger where to log information/warning or errors (optional).
29+
:type logger: Logger
30+
"""
31+
def __init__(self,
32+
workflow: Union[Workflow, pathlib.Path],
33+
logger: Optional[Logger] = None) -> None:
34+
"""Create an object of the translator."""
35+
super().__init__(workflow, logger)
36+
self._script = ""
37+
38+
def translate(self, output_folder: pathlib.Path) -> None:
39+
"""
40+
Translate a workflow benchmark description (WfFormat) into an actual workflow application.
41+
42+
:param output_folder: The path to the folder in which the workflow benchmark will be generated.
43+
:type output_folder: pathlib.Path
44+
"""
45+
46+
# Generate code
47+
self._generate_code()
48+
49+
# write benchmark files
50+
output_folder.mkdir(parents=True)
51+
with open(output_folder.joinpath("workflow.smk"), "w") as fp:
52+
fp.write(self._script)
53+
54+
# additional files
55+
self._copy_binary_files(output_folder)
56+
self._generate_input_files(output_folder)
57+
58+
# README file
59+
self._write_readme_file(output_folder)
60+
61+
def _generate_code(self):
62+
"""
63+
Generate the Makeflow code
64+
65+
:return: the code
66+
:rtype: str
67+
"""
68+
all_rule = ("# Rule to force all task executions\n"
69+
"rule all_wfbench_tasks:\n"
70+
"\tinput:\n")
71+
72+
self._script = "\n# WfBench task rules\n"
73+
for task_name, task in self.workflow.tasks.items():
74+
rule = f"rule {task_name}:\n"
75+
# input files
76+
rule += "\tinput:\n"
77+
for input_file in task.input_files:
78+
rule += f"\t\t\"data/{input_file.file_id}\",\n"
79+
# output files
80+
rule += "\toutput:\n"
81+
for output_file in task.output_files:
82+
all_rule += f"\t\t\"data/{output_file.file_id}\",\n"
83+
rule += f"\t\t\"data/{output_file.file_id}\",\n"
84+
# shell
85+
rule += "\tshell:\n"
86+
rule += "\t\t'" + task.program + " '\n"
87+
88+
input_spec = "\\'["
89+
for file in task.input_files:
90+
input_spec += f"\"data/{file.file_id}\","
91+
input_spec = input_spec[:-1] + "]\\'"
92+
93+
output_spec = "\\'{{"
94+
for file in task.output_files:
95+
output_spec += f"\"data/{file.file_id}\":{str(file.size)},"
96+
output_spec = output_spec[:-1] + "}}\\'"
97+
98+
args = []
99+
for a in task.args:
100+
if "--output-files" in a:
101+
args.append(f"--output-files {output_spec}")
102+
elif "--input-files" in a:
103+
args.append(f"--input-files {input_spec}")
104+
else:
105+
args.append(a)
106+
107+
for a in args:
108+
rule += "\t\t'" + a + " '\n"
109+
110+
self._script += rule + "\n\n"
111+
112+
self._script = all_rule + self._script
113+
return
114+
115+
def _write_readme_file(self, output_folder: pathlib.Path) -> None:
116+
"""
117+
Write the README file.
118+
119+
:param output_folder: The path of the output folder.
120+
:type output_folder: pathlib.Path
121+
"""
122+
readme_file_path = output_folder.joinpath("README")
123+
with open(readme_file_path, "w") as out:
124+
out.write(f"In directory {str(output_folder)}:\n")
125+
out.write(f" - The Snakemake file: workflow.smk\n")
126+
out.write(f" - Run the workflow: snakemake -s workflow.smk --cores 1 [--logger snkmt --logger-snkmt-db ./snkmt.sqlite]\n")

wfcommons/wfinstances/logs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
from .pegasus import PegasusLogsParser
1515
from .pegasusrec import HierarchicalPegasusLogsParser
1616
from .ro_crate import ROCrateLogsParser
17+
from .snakemake import SnakemakeLogsParser

0 commit comments

Comments
 (0)