mirna-toolkit 0.1.0__tar.gz

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.
Files changed (62) hide show
  1. mirna_toolkit-0.1.0/PKG-INFO +147 -0
  2. mirna_toolkit-0.1.0/README.md +111 -0
  3. mirna_toolkit-0.1.0/pyproject.toml +70 -0
  4. mirna_toolkit-0.1.0/setup.cfg +4 -0
  5. mirna_toolkit-0.1.0/src/mirna_toolkit/__init__.py +17 -0
  6. mirna_toolkit-0.1.0/src/mirna_toolkit/alignment/__init__.py +6 -0
  7. mirna_toolkit-0.1.0/src/mirna_toolkit/alignment/bowtie.py +21 -0
  8. mirna_toolkit-0.1.0/src/mirna_toolkit/alignment/hisat2.py +21 -0
  9. mirna_toolkit-0.1.0/src/mirna_toolkit/alignment/star.py +37 -0
  10. mirna_toolkit-0.1.0/src/mirna_toolkit/batch.py +178 -0
  11. mirna_toolkit-0.1.0/src/mirna_toolkit/cli.py +190 -0
  12. mirna_toolkit-0.1.0/src/mirna_toolkit/disease/__init__.py +5 -0
  13. mirna_toolkit-0.1.0/src/mirna_toolkit/disease/hmdd.py +51 -0
  14. mirna_toolkit-0.1.0/src/mirna_toolkit/disease/mircancer.py +21 -0
  15. mirna_toolkit-0.1.0/src/mirna_toolkit/io/__init__.py +21 -0
  16. mirna_toolkit-0.1.0/src/mirna_toolkit/io/bam.py +32 -0
  17. mirna_toolkit-0.1.0/src/mirna_toolkit/io/downloaders.py +27 -0
  18. mirna_toolkit-0.1.0/src/mirna_toolkit/io/fastq.py +41 -0
  19. mirna_toolkit-0.1.0/src/mirna_toolkit/io/gtf.py +41 -0
  20. mirna_toolkit-0.1.0/src/mirna_toolkit/ml/__init__.py +5 -0
  21. mirna_toolkit-0.1.0/src/mirna_toolkit/ml/classifiers.py +22 -0
  22. mirna_toolkit-0.1.0/src/mirna_toolkit/ml/explainability.py +26 -0
  23. mirna_toolkit-0.1.0/src/mirna_toolkit/ml/preprocessing.py +11 -0
  24. mirna_toolkit-0.1.0/src/mirna_toolkit/pipeline.py +120 -0
  25. mirna_toolkit-0.1.0/src/mirna_toolkit/prediction/__init__.py +16 -0
  26. mirna_toolkit-0.1.0/src/mirna_toolkit/prediction/confidence.py +18 -0
  27. mirna_toolkit-0.1.0/src/mirna_toolkit/prediction/mirecords_api.py +21 -0
  28. mirna_toolkit-0.1.0/src/mirna_toolkit/prediction/mirtarbase_api.py +21 -0
  29. mirna_toolkit-0.1.0/src/mirna_toolkit/prediction/rnahybrid.py +15 -0
  30. mirna_toolkit-0.1.0/src/mirna_toolkit/prediction/seed_match.py +11 -0
  31. mirna_toolkit-0.1.0/src/mirna_toolkit/prediction/targetscan_api.py +27 -0
  32. mirna_toolkit-0.1.0/src/mirna_toolkit/quantification/__init__.py +15 -0
  33. mirna_toolkit-0.1.0/src/mirna_toolkit/quantification/counts.py +19 -0
  34. mirna_toolkit-0.1.0/src/mirna_toolkit/quantification/normalization.py +42 -0
  35. mirna_toolkit-0.1.0/src/mirna_toolkit/quantification/novel.py +40 -0
  36. mirna_toolkit-0.1.0/src/mirna_toolkit/utils/__init__.py +17 -0
  37. mirna_toolkit-0.1.0/src/mirna_toolkit/utils/config.py +31 -0
  38. mirna_toolkit-0.1.0/src/mirna_toolkit/utils/http.py +77 -0
  39. mirna_toolkit-0.1.0/src/mirna_toolkit/utils/logging.py +13 -0
  40. mirna_toolkit-0.1.0/src/mirna_toolkit/utils/plugins.py +22 -0
  41. mirna_toolkit-0.1.0/src/mirna_toolkit/utils/versioning.py +29 -0
  42. mirna_toolkit-0.1.0/src/mirna_toolkit/visualization/__init__.py +17 -0
  43. mirna_toolkit-0.1.0/src/mirna_toolkit/visualization/heatmaps.py +8 -0
  44. mirna_toolkit-0.1.0/src/mirna_toolkit/visualization/networks.py +82 -0
  45. mirna_toolkit-0.1.0/src/mirna_toolkit/visualization/pathway.py +12 -0
  46. mirna_toolkit-0.1.0/src/mirna_toolkit/visualization/volcano.py +15 -0
  47. mirna_toolkit-0.1.0/src/mirna_toolkit.egg-info/PKG-INFO +147 -0
  48. mirna_toolkit-0.1.0/src/mirna_toolkit.egg-info/SOURCES.txt +60 -0
  49. mirna_toolkit-0.1.0/src/mirna_toolkit.egg-info/dependency_links.txt +1 -0
  50. mirna_toolkit-0.1.0/src/mirna_toolkit.egg-info/entry_points.txt +2 -0
  51. mirna_toolkit-0.1.0/src/mirna_toolkit.egg-info/requires.txt +20 -0
  52. mirna_toolkit-0.1.0/src/mirna_toolkit.egg-info/top_level.txt +1 -0
  53. mirna_toolkit-0.1.0/tests/test_batch.py +60 -0
  54. mirna_toolkit-0.1.0/tests/test_cli.py +54 -0
  55. mirna_toolkit-0.1.0/tests/test_external_wrappers.py +67 -0
  56. mirna_toolkit-0.1.0/tests/test_http_utils.py +34 -0
  57. mirna_toolkit-0.1.0/tests/test_normalization.py +12 -0
  58. mirna_toolkit-0.1.0/tests/test_novel.py +9 -0
  59. mirna_toolkit-0.1.0/tests/test_pipeline.py +39 -0
  60. mirna_toolkit-0.1.0/tests/test_plugins.py +12 -0
  61. mirna_toolkit-0.1.0/tests/test_prediction.py +12 -0
  62. mirna_toolkit-0.1.0/tests/test_versioning.py +12 -0
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: mirna-toolkit
3
+ Version: 0.1.0
4
+ Summary: Python-native end-to-end toolkit for miRNA analysis
5
+ Author: miRNA-Toolkit Contributors
6
+ License-Expression: MIT
7
+ Keywords: mirna,bioinformatics,rna,genomics,small-rna
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests>=2.31
19
+ Requires-Dist: numpy>=1.24
20
+ Requires-Dist: pandas>=2.0
21
+ Requires-Dist: networkx>=3.2
22
+ Requires-Dist: plotly>=5.20
23
+ Requires-Dist: matplotlib>=3.8
24
+ Requires-Dist: seaborn>=0.13
25
+ Provides-Extra: full
26
+ Requires-Dist: pysam>=0.22; extra == "full"
27
+ Requires-Dist: scikit-learn>=1.4; extra == "full"
28
+ Requires-Dist: shap>=0.45; extra == "full"
29
+ Requires-Dist: lime>=0.2.0.1; extra == "full"
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1.2; extra == "dev"
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.5; extra == "dev"
35
+ Requires-Dist: mypy>=1.10; extra == "dev"
36
+
37
+ # miRNA-Toolkit
38
+
39
+ miRNA-Toolkit is a Python-native library for end-to-end miRNA analysis, including preprocessing, alignment, quantification, target prediction, disease association, and visualization.
40
+
41
+ Install name: `mirna-toolkit`
42
+ Import name: `mirna_toolkit`
43
+
44
+ ## Features
45
+
46
+ - I/O support for FASTQ, FASTA, BAM, and GTF/GFF-oriented workflows.
47
+ - Wrappers for FastQC, fastp, Bowtie, HISAT2, and STAR (small-RNA mode).
48
+ - Quantification and normalization (TPM, RPM, DESeq2-style size factors).
49
+ - Target prediction (seed match + RNAhybrid-style scoring + external connectors).
50
+ - Disease association integration (HMDD, miRCancer) and network export.
51
+ - Visualization utilities for interaction networks, heatmaps, volcano plots, and pathway enrichment plots.
52
+ - Optional ML utilities for classification, feature ranking, and explainability.
53
+ - End-to-end reproducible workflow runner (`pipeline.run_end_to_end`) and CLI (`mirna-toolkit`).
54
+ - Manifest-based batch workflow for multiple samples (`batch.run_batch_workflow`).
55
+ - Retry-aware HTTP connectors with configurable strict mode for robust integrations.
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install mirna-toolkit
61
+ ```
62
+
63
+ For local development:
64
+
65
+ ```bash
66
+ pip install -e .
67
+ ```
68
+
69
+ Optional extras:
70
+
71
+ ```bash
72
+ pip install -e .[full,dev]
73
+ ```
74
+
75
+ ## Quick Start
76
+
77
+ ```python
78
+ from mirna_toolkit import alignment, quantification, prediction, disease, visualization
79
+
80
+ bam_file = alignment.bowtie.align("sample.fastq", reference="mirbase.fa")
81
+ counts = quantification.counts.from_bam(bam_file, annotation="mirbase.gtf")
82
+ normalized = quantification.normalization.tpm(counts)
83
+
84
+ targets = prediction.targetscan_api.get_targets("hsa-miR-21")
85
+ assoc = disease.hmdd.get_associations("hsa-miR-21")
86
+
87
+ fig = visualization.networks.plot_mirna_targets("hsa-miR-21", targets)
88
+ fig.show()
89
+ ```
90
+
91
+ ## End-to-End Pipeline
92
+
93
+ ```python
94
+ from mirna_toolkit.pipeline import PipelineConfig, run_end_to_end
95
+
96
+ config = PipelineConfig(
97
+ fastq_path="sample.fastq",
98
+ reference="mirbase_index",
99
+ annotation="mirbase.gtf",
100
+ output_dir="run_001",
101
+ aligner="bowtie",
102
+ normalization_method="tpm",
103
+ run_qc=True,
104
+ trim=True,
105
+ )
106
+
107
+ result = run_end_to_end(config, database_versions={"miRBase": "22.1", "TargetScan": "8.0"})
108
+ print(result.metadata_path)
109
+ ```
110
+
111
+ ## CLI
112
+
113
+ ```bash
114
+ mirna-toolkit --fastq sample.fastq --reference mirbase_index --annotation mirbase.gtf --output-dir run_001 --aligner bowtie --normalize tpm --run-qc --trim
115
+ ```
116
+
117
+ Helpful CLI topics:
118
+
119
+ ```bash
120
+ mirna-toolkit help
121
+ mirna-toolkit help run
122
+ mirna-toolkit help batch
123
+ ```
124
+
125
+ ## Batch Workflow
126
+
127
+ Manifest example (`samples.csv`):
128
+
129
+ ```csv
130
+ sample_id,fastq_path,reference,annotation,run_qc,trim
131
+ s1,s1.fastq,mirbase_index,mirbase.gtf,true,true
132
+ s2,s2.fastq,mirbase_index,mirbase.gtf,false,false
133
+ ```
134
+
135
+ Run it with:
136
+
137
+ ```bash
138
+ mirna-toolkit batch --manifest samples.csv --output-dir batch_runs
139
+ ```
140
+
141
+ ## Reproducibility
142
+
143
+ Use `mirna_toolkit.utils.versioning.DatabaseVersionTracker` to record exact versions/releases of external databases used in analyses.
144
+
145
+ ## Status
146
+
147
+ This repository provides a production-ready scaffold with working core APIs and wrappers. External command wrappers require the corresponding tools (for example, `bowtie`, `fastqc`, `fastp`) to be installed and available on `PATH`.
@@ -0,0 +1,111 @@
1
+ # miRNA-Toolkit
2
+
3
+ miRNA-Toolkit is a Python-native library for end-to-end miRNA analysis, including preprocessing, alignment, quantification, target prediction, disease association, and visualization.
4
+
5
+ Install name: `mirna-toolkit`
6
+ Import name: `mirna_toolkit`
7
+
8
+ ## Features
9
+
10
+ - I/O support for FASTQ, FASTA, BAM, and GTF/GFF-oriented workflows.
11
+ - Wrappers for FastQC, fastp, Bowtie, HISAT2, and STAR (small-RNA mode).
12
+ - Quantification and normalization (TPM, RPM, DESeq2-style size factors).
13
+ - Target prediction (seed match + RNAhybrid-style scoring + external connectors).
14
+ - Disease association integration (HMDD, miRCancer) and network export.
15
+ - Visualization utilities for interaction networks, heatmaps, volcano plots, and pathway enrichment plots.
16
+ - Optional ML utilities for classification, feature ranking, and explainability.
17
+ - End-to-end reproducible workflow runner (`pipeline.run_end_to_end`) and CLI (`mirna-toolkit`).
18
+ - Manifest-based batch workflow for multiple samples (`batch.run_batch_workflow`).
19
+ - Retry-aware HTTP connectors with configurable strict mode for robust integrations.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install mirna-toolkit
25
+ ```
26
+
27
+ For local development:
28
+
29
+ ```bash
30
+ pip install -e .
31
+ ```
32
+
33
+ Optional extras:
34
+
35
+ ```bash
36
+ pip install -e .[full,dev]
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ from mirna_toolkit import alignment, quantification, prediction, disease, visualization
43
+
44
+ bam_file = alignment.bowtie.align("sample.fastq", reference="mirbase.fa")
45
+ counts = quantification.counts.from_bam(bam_file, annotation="mirbase.gtf")
46
+ normalized = quantification.normalization.tpm(counts)
47
+
48
+ targets = prediction.targetscan_api.get_targets("hsa-miR-21")
49
+ assoc = disease.hmdd.get_associations("hsa-miR-21")
50
+
51
+ fig = visualization.networks.plot_mirna_targets("hsa-miR-21", targets)
52
+ fig.show()
53
+ ```
54
+
55
+ ## End-to-End Pipeline
56
+
57
+ ```python
58
+ from mirna_toolkit.pipeline import PipelineConfig, run_end_to_end
59
+
60
+ config = PipelineConfig(
61
+ fastq_path="sample.fastq",
62
+ reference="mirbase_index",
63
+ annotation="mirbase.gtf",
64
+ output_dir="run_001",
65
+ aligner="bowtie",
66
+ normalization_method="tpm",
67
+ run_qc=True,
68
+ trim=True,
69
+ )
70
+
71
+ result = run_end_to_end(config, database_versions={"miRBase": "22.1", "TargetScan": "8.0"})
72
+ print(result.metadata_path)
73
+ ```
74
+
75
+ ## CLI
76
+
77
+ ```bash
78
+ mirna-toolkit --fastq sample.fastq --reference mirbase_index --annotation mirbase.gtf --output-dir run_001 --aligner bowtie --normalize tpm --run-qc --trim
79
+ ```
80
+
81
+ Helpful CLI topics:
82
+
83
+ ```bash
84
+ mirna-toolkit help
85
+ mirna-toolkit help run
86
+ mirna-toolkit help batch
87
+ ```
88
+
89
+ ## Batch Workflow
90
+
91
+ Manifest example (`samples.csv`):
92
+
93
+ ```csv
94
+ sample_id,fastq_path,reference,annotation,run_qc,trim
95
+ s1,s1.fastq,mirbase_index,mirbase.gtf,true,true
96
+ s2,s2.fastq,mirbase_index,mirbase.gtf,false,false
97
+ ```
98
+
99
+ Run it with:
100
+
101
+ ```bash
102
+ mirna-toolkit batch --manifest samples.csv --output-dir batch_runs
103
+ ```
104
+
105
+ ## Reproducibility
106
+
107
+ Use `mirna_toolkit.utils.versioning.DatabaseVersionTracker` to record exact versions/releases of external databases used in analyses.
108
+
109
+ ## Status
110
+
111
+ This repository provides a production-ready scaffold with working core APIs and wrappers. External command wrappers require the corresponding tools (for example, `bowtie`, `fastqc`, `fastp`) to be installed and available on `PATH`.
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mirna-toolkit"
7
+ version = "0.1.0"
8
+ description = "Python-native end-to-end toolkit for miRNA analysis"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "miRNA-Toolkit Contributors" }
14
+ ]
15
+ keywords = ["mirna", "bioinformatics", "rna", "genomics", "small-rna"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Science/Research",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Topic :: Scientific/Engineering :: Bio-Informatics"
25
+ ]
26
+ dependencies = [
27
+ "requests>=2.31",
28
+ "numpy>=1.24",
29
+ "pandas>=2.0",
30
+ "networkx>=3.2",
31
+ "plotly>=5.20",
32
+ "matplotlib>=3.8",
33
+ "seaborn>=0.13"
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ full = [
38
+ "pysam>=0.22",
39
+ "scikit-learn>=1.4",
40
+ "shap>=0.45",
41
+ "lime>=0.2.0.1"
42
+ ]
43
+ dev = [
44
+ "build>=1.2",
45
+ "pytest>=8.0",
46
+ "pytest-cov>=5.0",
47
+ "ruff>=0.5",
48
+ "mypy>=1.10"
49
+ ]
50
+
51
+ [project.scripts]
52
+ mirna-toolkit = "mirna_toolkit.cli:main"
53
+
54
+ [tool.setuptools]
55
+ package-dir = {"" = "src"}
56
+
57
+ [tool.setuptools.packages.find]
58
+ where = ["src"]
59
+
60
+ [tool.pytest.ini_options]
61
+ minversion = "8.0"
62
+ addopts = "-ra -q"
63
+ testpaths = ["tests"]
64
+
65
+ [tool.ruff]
66
+ line-length = 150
67
+ target-version = "py39"
68
+
69
+ [tool.ruff.lint]
70
+ select = ["E", "F", "I", "UP"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """miRNA-Toolkit public package API."""
2
+
3
+ from . import alignment, batch, disease, io, ml, pipeline, prediction, quantification, visualization
4
+
5
+ __all__ = [
6
+ "alignment",
7
+ "batch",
8
+ "disease",
9
+ "io",
10
+ "ml",
11
+ "pipeline",
12
+ "prediction",
13
+ "quantification",
14
+ "visualization",
15
+ ]
16
+
17
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ from . import bowtie, hisat2, star
2
+ from .bowtie import align as bowtie_align
3
+ from .hisat2 import align as hisat2_align
4
+ from .star import align as star_align
5
+
6
+ __all__ = ["bowtie", "bowtie_align", "hisat2", "hisat2_align", "star", "star_align"]
@@ -0,0 +1,21 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+
5
+ def align(
6
+ fastq_path: str | Path,
7
+ reference: str | Path,
8
+ output_bam: str | Path | None = None,
9
+ threads: int = 4,
10
+ ) -> Path:
11
+ """Align reads with Bowtie and return BAM path."""
12
+ fastq = Path(fastq_path)
13
+ output = Path(output_bam) if output_bam else fastq.with_suffix(".bowtie.bam")
14
+ sam = output.with_suffix(".sam")
15
+
16
+ subprocess.run(
17
+ ["bowtie", "-p", str(threads), "-S", str(reference), str(fastq), str(sam)],
18
+ check=True,
19
+ )
20
+ subprocess.run(["samtools", "view", "-bS", str(sam), "-o", str(output)], check=True)
21
+ return output
@@ -0,0 +1,21 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+
5
+ def align(
6
+ fastq_path: str | Path,
7
+ reference: str | Path,
8
+ output_bam: str | Path | None = None,
9
+ threads: int = 4,
10
+ ) -> Path:
11
+ """Align reads with HISAT2 and return BAM path."""
12
+ fastq = Path(fastq_path)
13
+ output = Path(output_bam) if output_bam else fastq.with_suffix(".hisat2.bam")
14
+ sam = output.with_suffix(".sam")
15
+
16
+ subprocess.run(
17
+ ["hisat2", "-p", str(threads), "-x", str(reference), "-U", str(fastq), "-S", str(sam)],
18
+ check=True,
19
+ )
20
+ subprocess.run(["samtools", "view", "-bS", str(sam), "-o", str(output)], check=True)
21
+ return output
@@ -0,0 +1,37 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+
5
+ def align(
6
+ fastq_path: str | Path,
7
+ reference: str | Path,
8
+ output_bam: str | Path | None = None,
9
+ threads: int = 4,
10
+ small_rna_mode: bool = True,
11
+ ) -> Path:
12
+ """Align reads with STAR in small-RNA-friendly settings and return BAM path."""
13
+ fastq = Path(fastq_path)
14
+ output = Path(output_bam) if output_bam else fastq.with_suffix(".star.bam")
15
+ prefix = output.with_suffix("")
16
+
17
+ cmd = [
18
+ "STAR",
19
+ "--genomeDir",
20
+ str(reference),
21
+ "--readFilesIn",
22
+ str(fastq),
23
+ "--runThreadN",
24
+ str(threads),
25
+ "--outSAMtype",
26
+ "BAM",
27
+ "Unsorted",
28
+ "--outFileNamePrefix",
29
+ f"{prefix}",
30
+ ]
31
+ if small_rna_mode:
32
+ cmd.extend(["--alignIntronMax", "1", "--outFilterMismatchNmax", "1"])
33
+
34
+ subprocess.run(cmd, check=True)
35
+ produced_bam = Path(f"{prefix}Aligned.out.bam")
36
+ produced_bam.replace(output)
37
+ return output
@@ -0,0 +1,178 @@
1
+ import csv
2
+ import json
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from . import pipeline
8
+ from .pipeline import PipelineConfig, PipelineResult
9
+
10
+
11
+ @dataclass
12
+ class BatchSampleConfig:
13
+ sample_id: str
14
+ fastq_path: str | Path
15
+ reference: str | Path
16
+ annotation: str | Path
17
+ output_dir: str | Path | None = None
18
+ aligner: str = "bowtie"
19
+ normalization_method: str = "tpm"
20
+ threads: int = 4
21
+ run_qc: bool = False
22
+ trim: bool = False
23
+
24
+
25
+ @dataclass
26
+ class BatchSampleResult:
27
+ sample_id: str
28
+ bam_path: Path
29
+ counts_path: Path
30
+ normalized_path: Path
31
+ metadata_path: Path
32
+ counts: dict[str, int]
33
+ normalized: dict[str, float]
34
+
35
+
36
+ @dataclass
37
+ class BatchRunResult:
38
+ manifest_path: Path
39
+ summary_json_path: Path
40
+ summary_csv_path: Path
41
+ samples: list[BatchSampleResult]
42
+
43
+
44
+ def load_manifest(manifest_path: str | Path) -> list[BatchSampleConfig]:
45
+ path = Path(manifest_path)
46
+ suffix = path.suffix.lower()
47
+
48
+ if suffix == ".csv":
49
+ return _load_csv_manifest(path)
50
+ if suffix == ".json":
51
+ return _load_json_manifest(path)
52
+ raise ValueError("manifest_path must end with .csv or .json")
53
+
54
+
55
+ def run_batch_workflow(
56
+ manifest_path: str | Path,
57
+ output_dir: str | Path | None = None,
58
+ database_versions: dict[str, str] | None = None,
59
+ ) -> BatchRunResult:
60
+ manifest_file = Path(manifest_path)
61
+ base_output_dir = Path(output_dir) if output_dir is not None else manifest_file.parent / "batch_outputs"
62
+ base_output_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ sample_configs = load_manifest(manifest_file)
65
+ sample_results: list[BatchSampleResult] = []
66
+
67
+ for sample in sample_configs:
68
+ sample_output_dir = Path(sample.output_dir) if sample.output_dir is not None else base_output_dir / sample.sample_id
69
+ config = PipelineConfig(
70
+ fastq_path=sample.fastq_path,
71
+ reference=sample.reference,
72
+ annotation=sample.annotation,
73
+ output_dir=sample_output_dir,
74
+ aligner=sample.aligner,
75
+ normalization_method=sample.normalization_method,
76
+ threads=sample.threads,
77
+ run_qc=sample.run_qc,
78
+ trim=sample.trim,
79
+ )
80
+
81
+ result = pipeline.run_end_to_end(config, database_versions=database_versions)
82
+ sample_results.append(_to_sample_result(sample.sample_id, result))
83
+
84
+ summary_json_path = base_output_dir / "batch_summary.json"
85
+ summary_csv_path = base_output_dir / "batch_summary.csv"
86
+ _write_summary(sample_results, summary_json_path, summary_csv_path)
87
+
88
+ return BatchRunResult(
89
+ manifest_path=manifest_file,
90
+ summary_json_path=summary_json_path,
91
+ summary_csv_path=summary_csv_path,
92
+ samples=sample_results,
93
+ )
94
+
95
+
96
+ def _load_csv_manifest(path: Path) -> list[BatchSampleConfig]:
97
+ with path.open("r", encoding="utf-8", newline="") as handle:
98
+ reader = csv.DictReader(handle)
99
+ rows = list(reader)
100
+ return [_row_to_sample(row, index=index) for index, row in enumerate(rows, start=1)]
101
+
102
+
103
+ def _load_json_manifest(path: Path) -> list[BatchSampleConfig]:
104
+ payload = json.loads(path.read_text(encoding="utf-8"))
105
+ if isinstance(payload, dict):
106
+ rows = payload.get("samples", [])
107
+ elif isinstance(payload, list):
108
+ rows = payload
109
+ else:
110
+ raise ValueError("JSON manifest must be a list or a dict with a samples key")
111
+ return [_row_to_sample(row, index=index) for index, row in enumerate(rows, start=1)]
112
+
113
+
114
+ def _row_to_sample(row: dict[str, Any], index: int) -> BatchSampleConfig:
115
+ sample_id = str(row.get("sample_id") or row.get("sample") or row.get("id") or f"sample_{index}")
116
+ fastq_path = row.get("fastq_path") or row.get("fastq")
117
+ reference = row.get("reference")
118
+ annotation = row.get("annotation")
119
+
120
+ if not fastq_path or not reference or not annotation:
121
+ raise ValueError("Each manifest row must include fastq_path, reference, and annotation")
122
+
123
+ return BatchSampleConfig(
124
+ sample_id=sample_id,
125
+ fastq_path=fastq_path,
126
+ reference=reference,
127
+ annotation=annotation,
128
+ output_dir=row.get("output_dir") or None,
129
+ aligner=str(row.get("aligner") or "bowtie"),
130
+ normalization_method=str(row.get("normalization_method") or row.get("normalize") or "tpm"),
131
+ threads=int(row.get("threads") or 4),
132
+ run_qc=_parse_bool(row.get("run_qc") or row.get("qc") or False),
133
+ trim=_parse_bool(row.get("trim") or False),
134
+ )
135
+
136
+
137
+ def _to_sample_result(sample_id: str, result: PipelineResult) -> BatchSampleResult:
138
+ return BatchSampleResult(
139
+ sample_id=sample_id,
140
+ bam_path=result.bam_path,
141
+ counts_path=result.counts_path,
142
+ normalized_path=result.normalized_path,
143
+ metadata_path=result.metadata_path,
144
+ counts=result.counts,
145
+ normalized=result.normalized,
146
+ )
147
+
148
+
149
+ def _write_summary(samples: list[BatchSampleResult], json_path: Path, csv_path: Path) -> None:
150
+ summary_rows = [
151
+ {
152
+ "sample_id": sample.sample_id,
153
+ "bam_path": str(sample.bam_path),
154
+ "counts_path": str(sample.counts_path),
155
+ "normalized_path": str(sample.normalized_path),
156
+ "metadata_path": str(sample.metadata_path),
157
+ }
158
+ for sample in samples
159
+ ]
160
+
161
+ json_path.write_text(json.dumps(summary_rows, indent=2), encoding="utf-8")
162
+
163
+ with csv_path.open("w", encoding="utf-8", newline="") as handle:
164
+ writer = csv.DictWriter(handle, fieldnames=["sample_id", "bam_path", "counts_path", "normalized_path", "metadata_path"])
165
+ writer.writeheader()
166
+ writer.writerows(summary_rows)
167
+
168
+
169
+ def _parse_bool(value: Any) -> bool:
170
+ if isinstance(value, bool):
171
+ return value
172
+ if value is None:
173
+ return False
174
+ if isinstance(value, (int, float)):
175
+ return value != 0
176
+
177
+ normalized = str(value).strip().lower()
178
+ return normalized in {"1", "true", "yes", "y", "on"}