Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
429 changes: 229 additions & 200 deletions pypiper/manager.py

Large diffs are not rendered by default.

72 changes: 26 additions & 46 deletions pypiper/ngstk.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,22 +559,22 @@ def temp_func(input_files=input_files, output_files=output_files, paired_end=pai
[int(self.count_reads(input_file, paired_end)) for input_file in input_files]
)
raw_reads = int(total_reads / n_input_files)
self.pm.pipestat.report(values={"Raw_reads": str(raw_reads)})
self.pm.report_result("Raw_reads", str(raw_reads))

total_fastq_reads = sum(
[int(self.count_reads(output_file, paired_end)) for output_file in output_files]
)
fastq_reads = int(total_fastq_reads / n_output_files)

self.pm.pipestat.report(values={"Fastq_reads": fastq_reads})
self.pm.report_result("Fastq_reads", fastq_reads)
input_ext = self.get_input_ext(input_files[0])
# We can only assess pass filter reads in bam files with flags.
if input_ext == ".bam":
num_failed_filter = sum(
[int(self.count_fail_reads(f, paired_end)) for f in input_files]
)
pf_reads = int(raw_reads) - num_failed_filter
self.pm.pipestat.report(values={"PF_reads": str(pf_reads)})
self.pm.report_result("PF_reads", str(pf_reads))
if fastq_reads != int(raw_reads):
raise Exception(
"Fastq conversion error? Number of input reads doesn't number of output reads."
Expand Down Expand Up @@ -610,9 +610,9 @@ def temp_func():
print("WARNING: specified paired-end but no R2 file")

n_trim = float(self.count_reads(trimmed_fastq, paired_end))
self.pm.pipestat.report(values={"Trimmed_reads": int(n_trim)})
self.pm.report_result("Trimmed_reads", int(n_trim))
try:
rr = float(self.pm.pipestat.retrieve("Raw_reads"))
rr = float(self.pm.get_stat("Raw_reads"))
except Exception:
print("Can't calculate trim loss rate without raw read result.")
else:
Expand All @@ -626,27 +626,17 @@ def temp_func():
self.pm.run(cmd, lock_name="trimmed_fastqc", nofail=True)
fname, ext = os.path.splitext(os.path.basename(trimmed_fastq))
fastqc_html = os.path.join(fastqc_folder, fname + "_fastqc.html")
self.pm.pipestat.report(
values={
"FastQC_report_R1": {
"path": fastqc_html,
"title": "FastQC report R1",
}
}
self.pm.report_result(
"FastQC_report_R1", {"path": fastqc_html, "title": "FastQC report R1"}
)

if paired_end and trimmed_fastq_R2:
cmd = self.fastqc(trimmed_fastq_R2, fastqc_folder)
self.pm.run(cmd, lock_name="trimmed_fastqc_R2", nofail=True)
fname, ext = os.path.splitext(os.path.basename(trimmed_fastq_R2))
fastqc_html = os.path.join(fastqc_folder, fname + "_fastqc.html")
self.pm.pipestat.report(
values={
"FastQC_report_R2": {
"path": fastqc_html,
"title": "FastQC report R2",
}
}
self.pm.report_result(
"FastQC_report_R2", {"path": fastqc_html, "title": "FastQC report R2"}
)

return temp_func
Expand Down Expand Up @@ -1886,25 +1876,21 @@ def get_read_type(self, bam_file: str, n: int = 10) -> tuple[str, int]:
else:
return "SE", read_length

def parse_bowtie_stats(self, stats_file: str) -> Any:
def parse_bowtie_stats(self, stats_file: str) -> dict:
"""
Parses Bowtie2 stats file, returns series with values.
Parses Bowtie2 stats file, returns dict with values.

Args:
stats_file (str): Bowtie2 output file with alignment statistics.
"""
import pandas as pd

stats = pd.Series(
index=[
"readCount",
"unpaired",
"unaligned",
"unique",
"multiple",
"alignmentRate",
]
)
stats = {
"readCount": None,
"unpaired": None,
"unaligned": None,
"unique": None,
"multiple": None,
"alignmentRate": None,
}
try:
with open(stats_file) as handle:
content = handle.readlines() # list of strings per line
Expand Down Expand Up @@ -1938,16 +1924,14 @@ def parse_bowtie_stats(self, stats_file: str) -> Any:
pass
return stats

def parse_duplicate_stats(self, stats_file: str) -> Any:
def parse_duplicate_stats(self, stats_file: str) -> dict:
"""
Parses sambamba markdup output, returns series with values.
Parses sambamba markdup output, returns dict with values.

Args:
stats_file (str): sambamba output file with duplicate statistics.
"""
import pandas as pd

series = pd.Series()
series = {}
try:
with open(stats_file) as handle:
content = handle.readlines() # list of strings per line
Expand All @@ -1968,17 +1952,15 @@ def parse_duplicate_stats(self, stats_file: str) -> Any:
pass
return series

def parse_qc(self, qc_file: str) -> Any:
def parse_qc(self, qc_file: str) -> dict:
"""
Parse phantompeakqualtools (spp) QC table and return quality metrics.

Args:
qc_file (str): Path to phantompeakqualtools output file, which
contains sample quality measurements.
"""
import pandas as pd

series = pd.Series()
series = {}
try:
with open(qc_file) as handle:
line = handle.readlines()[0].strip().split("\t") # list of strings per line
Expand All @@ -2001,17 +1983,15 @@ def get_peak_number(self, sample: Any) -> Any:
sample["peakNumber"] = re.sub(r"\D.*", "", out)
return sample

def get_frip(self, sample: Any) -> Any:
def get_frip(self, sample: Any) -> dict:
"""
Calculates the fraction of reads in peaks for a given sample.

Args:
sample (pipelines.Sample): Sample object with "peaks" attribute.
"""
import pandas as pd

with open(sample.frip, "r") as handle:
content = handle.readlines()
reads_in_peaks = int(re.sub(r"\D", "", content[0]))
mapped_reads = sample["readCount"] - sample["unaligned"]
return pd.Series(reads_in_peaks / mapped_reads, index="FRiP")
return {"FRiP": reads_in_peaks / mapped_reads}
11 changes: 5 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "piper"
version = "0.15.0"
version = "0.15.1"
description = "A lightweight python toolkit for gluing together restartable, robust command line pipelines"
readme = "README.md"
license = "BSD-2-Clause"
Expand All @@ -24,12 +24,11 @@ classifiers = [
"Topic :: Scientific/Engineering :: Bio-Informatics",
]
dependencies = [
"logmuse>=0.2.4",
"logmuse>=0.3.0",
"psutil",
"pandas",
"ubiquerg>=0.9.0",
"yacman>=0.9.5",
"pipestat>=0.13.0",
"ubiquerg>=0.9.1",
"yacman>=1.0.0",
"pipestat>=0.13.1",
]

[project.urls]
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ def cleanup_legacy_output_folders(request):

@pytest.fixture
def get_pipe_manager(tmpdir):
"""Provide safe creation of pipeline manager, with multi=True."""
"""Provide safe creation of pipeline manager."""

def get_mgr(**kwargs):
if "outfolder" in kwargs:
kwd_args = kwargs
else:
kwd_args = copy.deepcopy(kwargs)
kwd_args["outfolder"] = tmpdir.strpath
return PipelineManager(multi=True, **kwd_args)
return PipelineManager(**kwd_args)

return get_mgr

Expand Down
2 changes: 1 addition & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class SafeTestPipeline(Pipeline):
"""Pipeline for tests that protects against bad file descriptor."""

def __init__(self, *args, **kwargs):
kwd_args = {"multi": True} # Like interactive mode.
kwd_args = {"multi": True} # Allow shared results file in tests.
kwd_args.update(kwargs)
super(SafeTestPipeline, self).__init__(*args, **kwd_args)

Expand Down
9 changes: 1 addition & 8 deletions tests/pipeline_manager/test_pipeline_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def single_pipeline_manager(tmpdir, request):
"""
outfolder = str(tmpdir.mkdir("pipeline_output"))
pp = pypiper.PipelineManager(
"sample_pipeline", outfolder=outfolder, multi=True, pipestat_schema=TEST_SCHEMA_PATH
"sample_pipeline", outfolder=outfolder, pipestat_schema=TEST_SCHEMA_PATH
)

def cleanup():
Expand Down Expand Up @@ -509,7 +509,6 @@ def test_pipestat_validate_results_default_with_schema(self, tmpdir):
pp = pypiper.PipelineManager(
"test_pipeline",
outfolder=outfolder,
multi=True,
pipestat_schema=TEST_SCHEMA_PATH,
)
assert pp.pipestat.cfg["validate_results"] is True
Expand All @@ -521,7 +520,6 @@ def test_pipestat_validate_results_default_without_schema(self, tmpdir):
pp = pypiper.PipelineManager(
"test_pipeline",
outfolder=outfolder,
multi=True,
)
assert pp.pipestat.cfg["validate_results"] is False
pp.stop_pipeline()
Expand All @@ -532,7 +530,6 @@ def test_pipestat_validate_results_explicit_false(self, tmpdir):
pp = pypiper.PipelineManager(
"test_pipeline",
outfolder=outfolder,
multi=True,
pipestat_schema=TEST_SCHEMA_PATH,
pipestat_validate_results=False,
)
Expand All @@ -545,7 +542,6 @@ def test_pipestat_validate_results_explicit_true(self, tmpdir):
pp = pypiper.PipelineManager(
"test_pipeline",
outfolder=outfolder,
multi=True,
pipestat_schema=TEST_SCHEMA_PATH,
pipestat_validate_results=True,
)
Expand All @@ -558,7 +554,6 @@ def test_pipestat_additional_properties_default(self, tmpdir):
pp = pypiper.PipelineManager(
"test_pipeline",
outfolder=outfolder,
multi=True,
pipestat_schema=TEST_SCHEMA_PATH,
)
# Default per JSON Schema spec should be True
Expand All @@ -571,7 +566,6 @@ def test_pipestat_additional_properties_explicit_false(self, tmpdir):
pp = pypiper.PipelineManager(
"test_pipeline",
outfolder=outfolder,
multi=True,
pipestat_schema=TEST_SCHEMA_PATH,
pipestat_additional_properties=False,
)
Expand All @@ -584,7 +578,6 @@ def test_pipestat_additional_properties_explicit_true(self, tmpdir):
pp = pypiper.PipelineManager(
"test_pipeline",
outfolder=outfolder,
multi=True,
pipestat_schema=TEST_SCHEMA_PATH,
pipestat_additional_properties=True,
)
Expand Down
10 changes: 5 additions & 5 deletions tests/test_context_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,34 @@ class TestContextManager:

def test_context_manager_clean_exit(self, tmpdir):
"""On clean exit, stop_pipeline() is called and status is completed."""
with PipelineManager("test_cm", tmpdir.strpath, multi=True) as pm:
with PipelineManager("test_cm", tmpdir.strpath) as pm:
pass
assert pm._has_exit_status

def test_context_manager_exception_sets_failed(self, tmpdir):
"""Exception inside with block calls fail_pipeline() and propagates."""
with pytest.raises(ValueError, match="test error"):
with PipelineManager("test_cm_fail", tmpdir.strpath, multi=True) as pm:
with PipelineManager("test_cm_fail", tmpdir.strpath) as pm:
raise ValueError("test error")
assert pm._failed

def test_context_manager_yields_self(self, tmpdir):
"""The yielded object is the PipelineManager instance."""
with PipelineManager("test_cm_self", tmpdir.strpath, multi=True) as pm:
with PipelineManager("test_cm_self", tmpdir.strpath) as pm:
assert isinstance(pm, PipelineManager)
pm_ref = pm
assert pm_ref is pm

def test_context_manager_run_command(self, tmpdir):
"""End-to-end: run a command with target inside with, verify target created."""
target = os.path.join(tmpdir.strpath, "output.txt")
with PipelineManager("test_cm_run", tmpdir.strpath, multi=True) as pm:
with PipelineManager("test_cm_run", tmpdir.strpath) as pm:
pm.run(f"echo 'hello' > {target}", target=target)
assert os.path.isfile(target)

def test_context_manager_keyboard_interrupt(self, tmpdir):
"""KeyboardInterrupt triggers fail_pipeline() and propagates."""
with pytest.raises(KeyboardInterrupt):
with PipelineManager("test_cm_kb", tmpdir.strpath, multi=True) as pm:
with PipelineManager("test_cm_kb", tmpdir.strpath) as pm:
raise KeyboardInterrupt()
assert pm._failed
Loading