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.
Files changed (47) hide show
  1. mirna_toolkit/__init__.py +17 -0
  2. mirna_toolkit/alignment/__init__.py +6 -0
  3. mirna_toolkit/alignment/bowtie.py +21 -0
  4. mirna_toolkit/alignment/hisat2.py +21 -0
  5. mirna_toolkit/alignment/star.py +37 -0
  6. mirna_toolkit/batch.py +178 -0
  7. mirna_toolkit/cli.py +190 -0
  8. mirna_toolkit/disease/__init__.py +5 -0
  9. mirna_toolkit/disease/hmdd.py +51 -0
  10. mirna_toolkit/disease/mircancer.py +21 -0
  11. mirna_toolkit/io/__init__.py +21 -0
  12. mirna_toolkit/io/bam.py +32 -0
  13. mirna_toolkit/io/downloaders.py +27 -0
  14. mirna_toolkit/io/fastq.py +41 -0
  15. mirna_toolkit/io/gtf.py +41 -0
  16. mirna_toolkit/ml/__init__.py +5 -0
  17. mirna_toolkit/ml/classifiers.py +22 -0
  18. mirna_toolkit/ml/explainability.py +26 -0
  19. mirna_toolkit/ml/preprocessing.py +11 -0
  20. mirna_toolkit/pipeline.py +120 -0
  21. mirna_toolkit/prediction/__init__.py +16 -0
  22. mirna_toolkit/prediction/confidence.py +18 -0
  23. mirna_toolkit/prediction/mirecords_api.py +21 -0
  24. mirna_toolkit/prediction/mirtarbase_api.py +21 -0
  25. mirna_toolkit/prediction/rnahybrid.py +15 -0
  26. mirna_toolkit/prediction/seed_match.py +11 -0
  27. mirna_toolkit/prediction/targetscan_api.py +27 -0
  28. mirna_toolkit/quantification/__init__.py +15 -0
  29. mirna_toolkit/quantification/counts.py +19 -0
  30. mirna_toolkit/quantification/normalization.py +42 -0
  31. mirna_toolkit/quantification/novel.py +40 -0
  32. mirna_toolkit/utils/__init__.py +17 -0
  33. mirna_toolkit/utils/config.py +31 -0
  34. mirna_toolkit/utils/http.py +77 -0
  35. mirna_toolkit/utils/logging.py +13 -0
  36. mirna_toolkit/utils/plugins.py +22 -0
  37. mirna_toolkit/utils/versioning.py +29 -0
  38. mirna_toolkit/visualization/__init__.py +17 -0
  39. mirna_toolkit/visualization/heatmaps.py +8 -0
  40. mirna_toolkit/visualization/networks.py +82 -0
  41. mirna_toolkit/visualization/pathway.py +12 -0
  42. mirna_toolkit/visualization/volcano.py +15 -0
  43. mirna_toolkit-0.1.0.dist-info/METADATA +147 -0
  44. mirna_toolkit-0.1.0.dist-info/RECORD +47 -0
  45. mirna_toolkit-0.1.0.dist-info/WHEEL +5 -0
  46. mirna_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
  47. 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,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
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,5 @@
1
+ from . import hmdd, mircancer
2
+ from .hmdd import get_associations as hmdd_associations
3
+ from .mircancer import get_associations as mircancer_associations
4
+
5
+ __all__ = ["hmdd", "hmdd_associations", "mircancer", "mircancer_associations"]
@@ -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
+ ]
@@ -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