mirna-toolkit 0.1.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.
- mirna_toolkit/__init__.py +17 -0
- mirna_toolkit/alignment/__init__.py +6 -0
- mirna_toolkit/alignment/bowtie.py +21 -0
- mirna_toolkit/alignment/hisat2.py +21 -0
- mirna_toolkit/alignment/star.py +37 -0
- mirna_toolkit/batch.py +178 -0
- mirna_toolkit/cli.py +190 -0
- mirna_toolkit/disease/__init__.py +5 -0
- mirna_toolkit/disease/hmdd.py +51 -0
- mirna_toolkit/disease/mircancer.py +21 -0
- mirna_toolkit/io/__init__.py +21 -0
- mirna_toolkit/io/bam.py +32 -0
- mirna_toolkit/io/downloaders.py +27 -0
- mirna_toolkit/io/fastq.py +41 -0
- mirna_toolkit/io/gtf.py +41 -0
- mirna_toolkit/ml/__init__.py +5 -0
- mirna_toolkit/ml/classifiers.py +22 -0
- mirna_toolkit/ml/explainability.py +26 -0
- mirna_toolkit/ml/preprocessing.py +11 -0
- mirna_toolkit/pipeline.py +120 -0
- mirna_toolkit/prediction/__init__.py +16 -0
- mirna_toolkit/prediction/confidence.py +18 -0
- mirna_toolkit/prediction/mirecords_api.py +21 -0
- mirna_toolkit/prediction/mirtarbase_api.py +21 -0
- mirna_toolkit/prediction/rnahybrid.py +15 -0
- mirna_toolkit/prediction/seed_match.py +11 -0
- mirna_toolkit/prediction/targetscan_api.py +27 -0
- mirna_toolkit/quantification/__init__.py +15 -0
- mirna_toolkit/quantification/counts.py +19 -0
- mirna_toolkit/quantification/normalization.py +42 -0
- mirna_toolkit/quantification/novel.py +40 -0
- mirna_toolkit/utils/__init__.py +17 -0
- mirna_toolkit/utils/config.py +31 -0
- mirna_toolkit/utils/http.py +77 -0
- mirna_toolkit/utils/logging.py +13 -0
- mirna_toolkit/utils/plugins.py +22 -0
- mirna_toolkit/utils/versioning.py +29 -0
- mirna_toolkit/visualization/__init__.py +17 -0
- mirna_toolkit/visualization/heatmaps.py +8 -0
- mirna_toolkit/visualization/networks.py +82 -0
- mirna_toolkit/visualization/pathway.py +12 -0
- mirna_toolkit/visualization/volcano.py +15 -0
- mirna_toolkit-0.1.0.dist-info/METADATA +147 -0
- mirna_toolkit-0.1.0.dist-info/RECORD +47 -0
- mirna_toolkit-0.1.0.dist-info/WHEEL +5 -0
- mirna_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- mirna_toolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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
|
mirna_toolkit/batch.py
ADDED
|
@@ -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"}
|
mirna_toolkit/cli.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from textwrap import dedent
|
|
6
|
+
|
|
7
|
+
from . import batch as batch_module
|
|
8
|
+
from .pipeline import PipelineConfig, run_end_to_end
|
|
9
|
+
|
|
10
|
+
EXAMPLES = dedent(
|
|
11
|
+
"""
|
|
12
|
+
Examples:
|
|
13
|
+
mirna-toolkit --fastq sample.fastq --reference mirbase.fa --annotation mirbase.gtf
|
|
14
|
+
mirna-toolkit run --fastq sample.fastq --reference mirbase.fa --annotation mirbase.gtf --trim
|
|
15
|
+
mirna-toolkit batch --manifest samples.csv --output-dir batch_runs
|
|
16
|
+
mirna-toolkit help
|
|
17
|
+
mirna-toolkit help run
|
|
18
|
+
mirna-toolkit help batch
|
|
19
|
+
"""
|
|
20
|
+
).strip()
|
|
21
|
+
|
|
22
|
+
RUN_HELP = dedent(
|
|
23
|
+
"""
|
|
24
|
+
Run a single sample through preprocessing, alignment, quantification, and normalization.
|
|
25
|
+
|
|
26
|
+
Required arguments:
|
|
27
|
+
--fastq Input FASTQ path
|
|
28
|
+
--reference Reference index or genome directory
|
|
29
|
+
--annotation Annotation GTF/GFF path
|
|
30
|
+
|
|
31
|
+
Useful options:
|
|
32
|
+
--aligner bowtie | hisat2 | star
|
|
33
|
+
--normalize tpm | rpm
|
|
34
|
+
--run-qc Run FastQC before alignment
|
|
35
|
+
--trim Trim adapters with fastp before alignment
|
|
36
|
+
"""
|
|
37
|
+
).strip()
|
|
38
|
+
|
|
39
|
+
BATCH_HELP = dedent(
|
|
40
|
+
"""
|
|
41
|
+
Run multiple samples from a CSV or JSON manifest.
|
|
42
|
+
|
|
43
|
+
Manifest columns or keys:
|
|
44
|
+
sample_id, fastq_path, reference, annotation, output_dir, aligner,
|
|
45
|
+
normalization_method, threads, run_qc, trim
|
|
46
|
+
|
|
47
|
+
Minimal CSV row example:
|
|
48
|
+
sample_id,fastq_path,reference,annotation
|
|
49
|
+
s1,s1.fastq,mirbase.fa,mirbase.gtf
|
|
50
|
+
"""
|
|
51
|
+
).strip()
|
|
52
|
+
|
|
53
|
+
HELP_TEXT = dedent(
|
|
54
|
+
f"""
|
|
55
|
+
miRNA-Toolkit command line interface
|
|
56
|
+
|
|
57
|
+
Available commands:
|
|
58
|
+
run Process one sample
|
|
59
|
+
batch Process multiple samples from a manifest
|
|
60
|
+
help Show detailed examples and topic-specific usage
|
|
61
|
+
|
|
62
|
+
{EXAMPLES}
|
|
63
|
+
"""
|
|
64
|
+
).strip()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(argv: list[str] | None = None) -> int:
|
|
68
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
69
|
+
|
|
70
|
+
if not args:
|
|
71
|
+
print(HELP_TEXT)
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
if args[0] == "help":
|
|
75
|
+
_print_help(args[1] if len(args) > 1 else None)
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
if args[0] == "batch":
|
|
79
|
+
return _run_batch(args[1:])
|
|
80
|
+
|
|
81
|
+
if args[0] == "run":
|
|
82
|
+
return _run_run(args[1:])
|
|
83
|
+
|
|
84
|
+
return _run_legacy(args)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _run_legacy(argv: list[str]) -> int:
|
|
88
|
+
parser = _build_run_parser(prog="mirna-toolkit", description="Run an end-to-end miRNA-Toolkit workflow")
|
|
89
|
+
args = parser.parse_args(argv)
|
|
90
|
+
return _execute_run(args)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _run_run(argv: list[str]) -> int:
|
|
94
|
+
parser = _build_run_parser(prog="mirna-toolkit run", description="Run an end-to-end miRNA-Toolkit workflow")
|
|
95
|
+
args = parser.parse_args(argv)
|
|
96
|
+
return _execute_run(args)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_batch(argv: list[str]) -> int:
|
|
100
|
+
parser = argparse.ArgumentParser(
|
|
101
|
+
prog="mirna-toolkit batch",
|
|
102
|
+
description="Run multiple samples from a manifest",
|
|
103
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
104
|
+
epilog=BATCH_HELP,
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument("--manifest", required=True, help="CSV or JSON manifest path")
|
|
107
|
+
parser.add_argument("--output-dir", default="batch_outputs", help="Base output directory")
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--db-versions-json",
|
|
110
|
+
default="",
|
|
111
|
+
help="JSON file mapping database names to versions, for batch metadata",
|
|
112
|
+
)
|
|
113
|
+
args = parser.parse_args(argv)
|
|
114
|
+
|
|
115
|
+
db_versions = _load_db_versions(args.db_versions_json)
|
|
116
|
+
result = batch_module.run_batch_workflow(
|
|
117
|
+
args.manifest,
|
|
118
|
+
output_dir=args.output_dir,
|
|
119
|
+
database_versions=db_versions,
|
|
120
|
+
)
|
|
121
|
+
print(result.summary_json_path)
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _execute_run(args: argparse.Namespace) -> int:
|
|
126
|
+
db_versions = _load_db_versions(getattr(args, "db_versions_json", ""))
|
|
127
|
+
|
|
128
|
+
config = PipelineConfig(
|
|
129
|
+
fastq_path=args.fastq,
|
|
130
|
+
reference=args.reference,
|
|
131
|
+
annotation=args.annotation,
|
|
132
|
+
output_dir=args.output_dir,
|
|
133
|
+
aligner=args.aligner,
|
|
134
|
+
normalization_method=args.normalize,
|
|
135
|
+
threads=args.threads,
|
|
136
|
+
run_qc=args.run_qc,
|
|
137
|
+
trim=args.trim,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
result = run_end_to_end(config, database_versions=db_versions)
|
|
141
|
+
print(result.metadata_path)
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _build_run_parser(*, prog: str, description: str) -> argparse.ArgumentParser:
|
|
146
|
+
parser = argparse.ArgumentParser(
|
|
147
|
+
prog=prog,
|
|
148
|
+
description=description,
|
|
149
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
150
|
+
epilog=RUN_HELP + "\n\n" + EXAMPLES,
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument("--fastq", required=True, help="Input FASTQ path")
|
|
153
|
+
parser.add_argument("--reference", required=True, help="Reference index/path")
|
|
154
|
+
parser.add_argument("--annotation", required=True, help="Annotation GTF/GFF path")
|
|
155
|
+
parser.add_argument("--output-dir", default="outputs", help="Output directory")
|
|
156
|
+
parser.add_argument("--aligner", default="bowtie", choices=["bowtie", "hisat2", "star"])
|
|
157
|
+
parser.add_argument("--normalize", default="tpm", choices=["tpm", "rpm"])
|
|
158
|
+
parser.add_argument("--threads", type=int, default=4)
|
|
159
|
+
parser.add_argument("--run-qc", action="store_true")
|
|
160
|
+
parser.add_argument("--trim", action="store_true")
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--db-versions-json",
|
|
163
|
+
default="",
|
|
164
|
+
help="JSON file mapping database names to versions, for run metadata",
|
|
165
|
+
)
|
|
166
|
+
return parser
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _load_db_versions(path_value: str) -> dict[str, str] | None:
|
|
170
|
+
if not path_value:
|
|
171
|
+
return None
|
|
172
|
+
return json.loads(Path(path_value).read_text(encoding="utf-8"))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _print_help(topic: str | None) -> None:
|
|
176
|
+
if topic == "run":
|
|
177
|
+
print(RUN_HELP)
|
|
178
|
+
print()
|
|
179
|
+
print(EXAMPLES)
|
|
180
|
+
return
|
|
181
|
+
if topic == "batch":
|
|
182
|
+
print(BATCH_HELP)
|
|
183
|
+
print()
|
|
184
|
+
print(EXAMPLES)
|
|
185
|
+
return
|
|
186
|
+
print(HELP_TEXT)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..utils.http import safe_get_json
|
|
7
|
+
|
|
8
|
+
HMDD_ENDPOINT = "https://www.cuilab.cn/hmdd/api/v1/associations"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_associations(mirna_id: str, timeout: int = 60, strict: bool = False) -> list[dict[str, Any]]:
|
|
12
|
+
payload = safe_get_json(HMDD_ENDPOINT, params={"mirna": mirna_id}, timeout=timeout, strict=strict)
|
|
13
|
+
if payload is None:
|
|
14
|
+
return []
|
|
15
|
+
if isinstance(payload, dict) and "data" in payload:
|
|
16
|
+
return list(payload["data"])
|
|
17
|
+
if isinstance(payload, list):
|
|
18
|
+
return payload
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_network_edges(associations: list[dict[str, Any]]) -> list[tuple[str, str]]:
|
|
23
|
+
edges: list[tuple[str, str]] = []
|
|
24
|
+
for row in associations:
|
|
25
|
+
mirna = str(row.get("mirna") or row.get("miRNA") or "")
|
|
26
|
+
disease = str(row.get("disease") or row.get("disease_name") or "")
|
|
27
|
+
if mirna and disease:
|
|
28
|
+
edges.append((mirna, disease))
|
|
29
|
+
return edges
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def export_associations(associations: list[dict[str, Any]], output_path: str | Path) -> Path:
|
|
33
|
+
output = Path(output_path)
|
|
34
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
if output.suffix.lower() == ".json":
|
|
37
|
+
output.write_text(json.dumps(associations, indent=2), encoding="utf-8")
|
|
38
|
+
return output
|
|
39
|
+
|
|
40
|
+
if output.suffix.lower() == ".csv":
|
|
41
|
+
if associations:
|
|
42
|
+
fields = sorted({k for row in associations for k in row.keys()})
|
|
43
|
+
with output.open("w", newline="", encoding="utf-8") as handle:
|
|
44
|
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
|
45
|
+
writer.writeheader()
|
|
46
|
+
writer.writerows(associations)
|
|
47
|
+
else:
|
|
48
|
+
output.write_text("", encoding="utf-8")
|
|
49
|
+
return output
|
|
50
|
+
|
|
51
|
+
raise ValueError("output_path must end with .json or .csv")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ..utils.http import safe_get_json
|
|
4
|
+
|
|
5
|
+
MIRCANCER_ENDPOINT = "https://mircancer.ecu.edu/api/v1/associations"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_associations(mirna_id: str, timeout: int = 60, strict: bool = False) -> list[dict[str, Any]]:
|
|
9
|
+
payload = safe_get_json(
|
|
10
|
+
MIRCANCER_ENDPOINT,
|
|
11
|
+
params={"mirna": mirna_id},
|
|
12
|
+
timeout=timeout,
|
|
13
|
+
strict=strict,
|
|
14
|
+
)
|
|
15
|
+
if payload is None:
|
|
16
|
+
return []
|
|
17
|
+
if isinstance(payload, dict) and "data" in payload:
|
|
18
|
+
return list(payload["data"])
|
|
19
|
+
if isinstance(payload, list):
|
|
20
|
+
return payload
|
|
21
|
+
return []
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from . import bam, downloaders, fastq, gtf
|
|
2
|
+
from .bam import bam_to_fasta, fastq_to_bam
|
|
3
|
+
from .downloaders import download_ensembl_release, download_geo_series, download_mirbase_release
|
|
4
|
+
from .fastq import parse_fastq, run_fastqc, trim_adapters
|
|
5
|
+
from .gtf import parse_gtf
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"bam",
|
|
9
|
+
"bam_to_fasta",
|
|
10
|
+
"downloaders",
|
|
11
|
+
"download_ensembl_release",
|
|
12
|
+
"download_geo_series",
|
|
13
|
+
"download_mirbase_release",
|
|
14
|
+
"fastq",
|
|
15
|
+
"fastq_to_bam",
|
|
16
|
+
"gtf",
|
|
17
|
+
"parse_fastq",
|
|
18
|
+
"parse_gtf",
|
|
19
|
+
"run_fastqc",
|
|
20
|
+
"trim_adapters",
|
|
21
|
+
]
|
mirna_toolkit/io/bam.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def fastq_to_bam(
|
|
6
|
+
fastq_path: str | Path,
|
|
7
|
+
reference: str | Path,
|
|
8
|
+
output_bam: str | Path,
|
|
9
|
+
aligner: str = "bowtie",
|
|
10
|
+
) -> Path:
|
|
11
|
+
"""Convert FASTQ to BAM through selected aligner + samtools."""
|
|
12
|
+
out = Path(output_bam)
|
|
13
|
+
sam_path = out.with_suffix(".sam")
|
|
14
|
+
|
|
15
|
+
if aligner == "bowtie":
|
|
16
|
+
cmd = ["bowtie", "-S", str(reference), str(fastq_path), str(sam_path)]
|
|
17
|
+
elif aligner == "hisat2":
|
|
18
|
+
cmd = ["hisat2", "-x", str(reference), "-U", str(fastq_path), "-S", str(sam_path)]
|
|
19
|
+
elif aligner == "star":
|
|
20
|
+
cmd = ["STAR", "--genomeDir", str(reference), "--readFilesIn", str(fastq_path), "--outFileNamePrefix", str(sam_path.with_suffix(""))]
|
|
21
|
+
else:
|
|
22
|
+
raise ValueError("aligner must be one of: bowtie, hisat2, star")
|
|
23
|
+
|
|
24
|
+
subprocess.run(cmd, check=True)
|
|
25
|
+
subprocess.run(["samtools", "view", "-bS", str(sam_path), "-o", str(out)], check=True)
|
|
26
|
+
return out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def bam_to_fasta(bam_path: str | Path, output_fasta: str | Path) -> Path:
|
|
30
|
+
out = Path(output_fasta)
|
|
31
|
+
subprocess.run(["samtools", "fasta", str(bam_path), "-o", str(out)], check=True)
|
|
32
|
+
return out
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from ..utils.http import download_file
|
|
4
|
+
|
|
5
|
+
MIRBASE_BASE = "https://www.mirbase.org/ftp"
|
|
6
|
+
GEO_BASE = "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi"
|
|
7
|
+
ENSEMBL_BASE = "https://ftp.ensembl.org/pub"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _download(url: str, output_path: str | Path) -> Path:
|
|
11
|
+
return download_file(url, output_path)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def download_mirbase_release(filename: str, output_dir: str | Path = "data") -> Path:
|
|
15
|
+
output = Path(output_dir) / filename
|
|
16
|
+
return _download(f"{MIRBASE_BASE}/{filename}", output)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def download_geo_series(gse_id: str, output_path: str | Path) -> Path:
|
|
20
|
+
url = f"{GEO_BASE}?acc={gse_id}&targ=self&form=text&view=quick"
|
|
21
|
+
return _download(url, output_path)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def download_ensembl_release(release_path: str, output_dir: str | Path = "data") -> Path:
|
|
25
|
+
filename = Path(release_path).name
|
|
26
|
+
output = Path(output_dir) / filename
|
|
27
|
+
return _download(f"{ENSEMBL_BASE}/{release_path}", output)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class FastqRecord:
|
|
8
|
+
header: str
|
|
9
|
+
sequence: str
|
|
10
|
+
plus: str
|
|
11
|
+
quality: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_fastq(path: str | Path):
|
|
15
|
+
"""Yield FASTQ records from a file."""
|
|
16
|
+
with Path(path).open("r", encoding="utf-8") as handle:
|
|
17
|
+
while True:
|
|
18
|
+
header = handle.readline().rstrip("\n")
|
|
19
|
+
if not header:
|
|
20
|
+
break
|
|
21
|
+
sequence = handle.readline().rstrip("\n")
|
|
22
|
+
plus = handle.readline().rstrip("\n")
|
|
23
|
+
quality = handle.readline().rstrip("\n")
|
|
24
|
+
if not quality:
|
|
25
|
+
raise ValueError("Malformed FASTQ: incomplete record")
|
|
26
|
+
yield FastqRecord(header=header, sequence=sequence, plus=plus, quality=quality)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_fastqc(fastq_path: str | Path, output_dir: str | Path) -> Path:
|
|
30
|
+
output = Path(output_dir)
|
|
31
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
cmd = ["fastqc", str(fastq_path), "-o", str(output)]
|
|
33
|
+
subprocess.run(cmd, check=True)
|
|
34
|
+
return output
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def trim_adapters(fastq_path: str | Path, output_fastq: str | Path, threads: int = 4) -> Path:
|
|
38
|
+
out = Path(output_fastq)
|
|
39
|
+
cmd = ["fastp", "-i", str(fastq_path), "-o", str(out), "-w", str(threads)]
|
|
40
|
+
subprocess.run(cmd, check=True)
|
|
41
|
+
return out
|