viralunity 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- viralunity/__init__.py +4 -0
- viralunity/_orchestrator.py +135 -0
- viralunity/_subprocess.py +52 -0
- viralunity/config_generator.py +587 -0
- viralunity/constants.py +151 -0
- viralunity/exceptions.py +100 -0
- viralunity/logging_config.py +101 -0
- viralunity/provenance.py +102 -0
- viralunity/scripts/__init__.py +0 -0
- viralunity/scripts/consensus_illumina.smk +142 -0
- viralunity/scripts/consensus_illumina_segmented.smk +203 -0
- viralunity/scripts/consensus_nanopore.smk +128 -0
- viralunity/scripts/consensus_nanopore_segmented.smk +172 -0
- viralunity/scripts/envs/alignment.yaml +11 -0
- viralunity/scripts/envs/assembly.yaml +6 -0
- viralunity/scripts/envs/clair3.yaml +7 -0
- viralunity/scripts/envs/consensus.yaml +10 -0
- viralunity/scripts/envs/genome_selection.yaml +8 -0
- viralunity/scripts/envs/medaka.yaml +6 -0
- viralunity/scripts/envs/qc.yaml +8 -0
- viralunity/scripts/envs/taxonomy.yaml +8 -0
- viralunity/scripts/envs/utils.yaml +10 -0
- viralunity/scripts/metagenomics_illumina.smk +279 -0
- viralunity/scripts/metagenomics_nanopore.smk +278 -0
- viralunity/scripts/python/__init__.py +0 -0
- viralunity/scripts/python/add_RPM_to_summary.py +143 -0
- viralunity/scripts/python/add_negative_control_enrichment.py +418 -0
- viralunity/scripts/python/add_rpkm_to_summary.py +125 -0
- viralunity/scripts/python/annotate_diamond_taxonomy.py +90 -0
- viralunity/scripts/python/apply_max_rpm_bleed_filter.py +172 -0
- viralunity/scripts/python/build_genome_length_table.py +197 -0
- viralunity/scripts/python/calculate_assembly_stats.py +151 -0
- viralunity/scripts/python/convert_diamond_output_to_krona_input.py +142 -0
- viralunity/scripts/python/filter_diamond_by_idxstats.py +131 -0
- viralunity/scripts/python/filter_krona_by_pass_taxids.py +224 -0
- viralunity/scripts/python/filter_taxids.py +82 -0
- viralunity/scripts/python/rename_sequences.py +67 -0
- viralunity/scripts/python/select_reference_genomes.py +470 -0
- viralunity/scripts/python/summarize_krona_taxa.py +163 -0
- viralunity/scripts/python/taxonomy.py +77 -0
- viralunity/scripts/rules/__init__.py +0 -0
- viralunity/scripts/rules/alignment_illumina.smk +73 -0
- viralunity/scripts/rules/alignment_nanopore.smk +66 -0
- viralunity/scripts/rules/consensus_illumina.smk +105 -0
- viralunity/scripts/rules/consensus_illumina_common.smk +86 -0
- viralunity/scripts/rules/consensus_nanopore.smk +67 -0
- viralunity/scripts/rules/consensus_nanopore_common.smk +57 -0
- viralunity/scripts/rules/metagenomics_assembly_illumina.smk +39 -0
- viralunity/scripts/rules/metagenomics_assembly_nanopore.smk +109 -0
- viralunity/scripts/rules/metagenomics_dehost_illumina.smk +130 -0
- viralunity/scripts/rules/metagenomics_dehost_nanopore.smk +97 -0
- viralunity/scripts/rules/metagenomics_diamond_contigs_illumina.smk +338 -0
- viralunity/scripts/rules/metagenomics_diamond_contigs_nanopore.smk +379 -0
- viralunity/scripts/rules/metagenomics_diamond_reads_illumina.smk +226 -0
- viralunity/scripts/rules/metagenomics_diamond_reads_nanopore.smk +226 -0
- viralunity/scripts/rules/metagenomics_genome_lengths.smk +37 -0
- viralunity/scripts/rules/metagenomics_kraken2_contigs_illumina.smk +210 -0
- viralunity/scripts/rules/metagenomics_kraken2_contigs_nanopore.smk +210 -0
- viralunity/scripts/rules/metagenomics_kraken2_reads_illumina.smk +214 -0
- viralunity/scripts/rules/metagenomics_kraken2_reads_nanopore.smk +213 -0
- viralunity/scripts/rules/metagenomics_multiqc_illumina.smk +16 -0
- viralunity/scripts/rules/metagenomics_reference_assembly.smk +151 -0
- viralunity/scripts/rules/qc_illumina.smk +65 -0
- viralunity/scripts/rules/stats.smk +28 -0
- viralunity/validators.py +614 -0
- viralunity/viralunity_build_deacon_index_cli.py +60 -0
- viralunity/viralunity_cli.py +60 -0
- viralunity/viralunity_consensus.py +160 -0
- viralunity/viralunity_consensus_cli.py +423 -0
- viralunity/viralunity_create_samplesheet.py +277 -0
- viralunity/viralunity_get_databases_cli.py +862 -0
- viralunity/viralunity_meta.py +200 -0
- viralunity/viralunity_meta_cli.py +469 -0
- viralunity/viralunity_setup_cli.py +212 -0
- viralunity-1.2.0.dist-info/METADATA +121 -0
- viralunity-1.2.0.dist-info/RECORD +80 -0
- viralunity-1.2.0.dist-info/WHEEL +5 -0
- viralunity-1.2.0.dist-info/entry_points.txt +2 -0
- viralunity-1.2.0.dist-info/licenses/LICENSE +21 -0
- viralunity-1.2.0.dist-info/top_level.txt +1 -0
viralunity/__init__.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Shared pipeline-orchestration helpers used by ``viralunity_consensus`` and
|
|
2
|
+
``viralunity_meta``.
|
|
3
|
+
|
|
4
|
+
Each pipeline still owns its own ``validate_args``, ``generate_config_file``,
|
|
5
|
+
``run_snakemake_workflow``, and ``main`` so that existing test patches at
|
|
6
|
+
those module-level names continue to work; the helpers in this module are
|
|
7
|
+
called from inside those functions to remove what would otherwise be
|
|
8
|
+
duplicated boilerplate.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any, Callable, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from snakemake import snakemake
|
|
16
|
+
|
|
17
|
+
from viralunity.config_generator import ConfigGenerator
|
|
18
|
+
from viralunity.exceptions import ValidationError, ViralUnityError
|
|
19
|
+
from viralunity.provenance import write_run_manifest
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def start_config(args: Dict[str, Any], samples: Dict[str, list]) -> ConfigGenerator:
|
|
25
|
+
"""Instantiate a ``ConfigGenerator`` and add the keys both pipelines
|
|
26
|
+
always set: samples, output, threads.
|
|
27
|
+
|
|
28
|
+
The caller is responsible for adding pipeline-specific keys (consensus
|
|
29
|
+
or metagenomics settings), per-rule resource settings, and calling
|
|
30
|
+
``generator.save()``.
|
|
31
|
+
"""
|
|
32
|
+
logger.info("Generating configuration file")
|
|
33
|
+
generator = ConfigGenerator(args["config_file"])
|
|
34
|
+
generator.add_samples(samples, args["data_type"])
|
|
35
|
+
generator.add_output(args["output"], args["run_name"])
|
|
36
|
+
generator.add_threads(args["threads"])
|
|
37
|
+
return generator
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run_workflow(workflow_path: str, args: Dict[str, Any]) -> bool:
|
|
41
|
+
"""Run a Snakemake workflow with the kwargs both pipelines share.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
workflow_path: Absolute path to the ``.smk`` file to execute.
|
|
45
|
+
args: Pipeline argument dict. Must contain ``config_file`` and
|
|
46
|
+
``threads_total``.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValidationError: If ``workflow_path`` does not exist.
|
|
50
|
+
"""
|
|
51
|
+
logger.info("Starting Snakemake workflow")
|
|
52
|
+
|
|
53
|
+
if not os.path.isfile(workflow_path):
|
|
54
|
+
raise ValidationError(f"Workflow file not found: {workflow_path}")
|
|
55
|
+
|
|
56
|
+
successful = snakemake(
|
|
57
|
+
workflow_path,
|
|
58
|
+
configfiles=[args["config_file"]],
|
|
59
|
+
cores=args["threads_total"],
|
|
60
|
+
use_conda=True,
|
|
61
|
+
conda_prefix=args.get("conda_prefix"),
|
|
62
|
+
targets=["all"],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if successful:
|
|
66
|
+
logger.info("Snakemake workflow completed successfully")
|
|
67
|
+
else:
|
|
68
|
+
logger.error("Snakemake workflow failed")
|
|
69
|
+
|
|
70
|
+
return successful
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_pipeline(
|
|
74
|
+
args: Dict[str, Any],
|
|
75
|
+
*,
|
|
76
|
+
resolve_paths: Callable[[Dict[str, Any]], None],
|
|
77
|
+
validate: Callable[[Dict[str, Any]], Optional[Dict[str, list]]],
|
|
78
|
+
generate_config: Callable[[Dict[str, list], Dict[str, Any]], None],
|
|
79
|
+
run_workflow_fn: Callable[[Dict[str, Any]], bool],
|
|
80
|
+
skip_when_no_samples: bool = False,
|
|
81
|
+
) -> int:
|
|
82
|
+
"""The try/except ``main`` skeleton shared by both pipelines.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
args: Pipeline argument dict.
|
|
86
|
+
resolve_paths: Called first to convert relative path args to
|
|
87
|
+
absolute paths.
|
|
88
|
+
validate: Called to validate args and return the samples dict.
|
|
89
|
+
generate_config: Called to write the Snakemake config file.
|
|
90
|
+
run_workflow_fn: Called to execute the Snakemake workflow. The
|
|
91
|
+
pipeline-specific module passes its own
|
|
92
|
+
``run_snakemake_workflow`` so test patches at that name
|
|
93
|
+
keep working.
|
|
94
|
+
skip_when_no_samples: If ``True``, return 0 (success) when the
|
|
95
|
+
validated samples dict is empty. ``viralunity_meta`` opts in;
|
|
96
|
+
``viralunity_consensus`` does not.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Exit code (0 for success, 1 for failure).
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
resolve_paths(args)
|
|
103
|
+
samples = validate(args)
|
|
104
|
+
|
|
105
|
+
if skip_when_no_samples and (samples is None or len(samples) == 0):
|
|
106
|
+
logger.warning("No samples were provided.")
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
generate_config(samples, args)
|
|
110
|
+
|
|
111
|
+
if args.get("create_config_only", False):
|
|
112
|
+
logger.info("Config file created. Exiting without running workflow.")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
# Provenance: record version, config, and input checksums for the run.
|
|
116
|
+
# Best-effort — a manifest failure must never abort an analysis.
|
|
117
|
+
try:
|
|
118
|
+
manifest_path = write_run_manifest(args, samples)
|
|
119
|
+
logger.info(f"Wrote run manifest: {manifest_path}")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.warning(f"Could not write run manifest: {e}")
|
|
122
|
+
|
|
123
|
+
successful = run_workflow_fn(args)
|
|
124
|
+
return 0 if successful else 1
|
|
125
|
+
|
|
126
|
+
except ViralUnityError as e:
|
|
127
|
+
# Expected, user-facing failures (bad inputs, missing files/DBs, config
|
|
128
|
+
# errors). Log a clean message with the machine-readable error code so a
|
|
129
|
+
# caller/service can key off it; no stack trace for these.
|
|
130
|
+
logger.error(f"[{e.code}] {e}")
|
|
131
|
+
return 1
|
|
132
|
+
except Exception as e:
|
|
133
|
+
# Genuinely unexpected: keep the traceback for debugging.
|
|
134
|
+
logger.exception(f"Unexpected error: {e}")
|
|
135
|
+
return 1
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Shared subprocess helper for ViralUnity CLI commands.
|
|
2
|
+
|
|
3
|
+
Internal module — not part of the public API. Used by the `get-databases`
|
|
4
|
+
and `build-deacon-index` subcommands to invoke external tools (`wget`,
|
|
5
|
+
`tar`, `diamond`, `deacon`, `datasets`, ...) with consistent echoing and
|
|
6
|
+
error handling.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import Optional, Sequence
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
#: Default wall-clock ceiling for a single external command (seconds). Long
|
|
17
|
+
#: enough for large downloads/DB builds, but bounded so a hung tool cannot block
|
|
18
|
+
#: a run (or a service worker) indefinitely. Override per-call via ``timeout``.
|
|
19
|
+
DEFAULT_TIMEOUT = 6 * 60 * 60 # 6 hours
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_command(
|
|
23
|
+
cmd: Sequence[object],
|
|
24
|
+
cwd: Optional[str] = None,
|
|
25
|
+
timeout: Optional[float] = DEFAULT_TIMEOUT,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Run a subprocess command, streaming output and raising on failure.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
cmd: Argument list (no shell). Each element is coerced to ``str``
|
|
31
|
+
when echoed; pass paths and flags as separate elements.
|
|
32
|
+
cwd: Optional working directory in which to run the command.
|
|
33
|
+
timeout: Wall-clock ceiling in seconds. Defaults to
|
|
34
|
+
``DEFAULT_TIMEOUT``; pass ``None`` to disable.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
click.ClickException: If the subprocess exits non-zero or times out.
|
|
38
|
+
The exit code/timeout and command are included in the message so
|
|
39
|
+
the failure surfaces cleanly in the CLI output.
|
|
40
|
+
"""
|
|
41
|
+
click.echo(f"$ {' '.join(str(c) for c in cmd)}")
|
|
42
|
+
try:
|
|
43
|
+
result = subprocess.run(list(cmd), cwd=cwd, timeout=timeout)
|
|
44
|
+
except subprocess.TimeoutExpired as e:
|
|
45
|
+
raise click.ClickException(
|
|
46
|
+
f"Command timed out after {e.timeout:.0f}s: " f"{' '.join(str(c) for c in cmd)}"
|
|
47
|
+
) from e
|
|
48
|
+
if result.returncode != 0:
|
|
49
|
+
raise click.ClickException(
|
|
50
|
+
f"Command failed with exit code {result.returncode}: "
|
|
51
|
+
f"{' '.join(str(c) for c in cmd)}"
|
|
52
|
+
)
|