selexprep 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.
- selexprep/__init__.py +3 -0
- selexprep/_common.py +78 -0
- selexprep/_io.py +80 -0
- selexprep/benchmark/__init__.py +20 -0
- selexprep/benchmark/corpus_audit.py +741 -0
- selexprep/benchmark/eligibility.py +472 -0
- selexprep/benchmark/equivalence.py +228 -0
- selexprep/benchmark/figure_a.py +155 -0
- selexprep/benchmark/figure_b.py +164 -0
- selexprep/benchmark/metrics.py +1214 -0
- selexprep/catalog/__init__.py +24 -0
- selexprep/catalog/cli.py +149 -0
- selexprep/catalog/data/__init__.py +1 -0
- selexprep/catalog/data/bioprojects.csv +294 -0
- selexprep/catalog/data/bioprojects_excluded.csv +62 -0
- selexprep/catalog/filter.py +97 -0
- selexprep/catalog/reader.py +62 -0
- selexprep/catalog/rebuild.py +627 -0
- selexprep/cli.py +695 -0
- selexprep/count/__init__.py +1 -0
- selexprep/count/counter.py +639 -0
- selexprep/extract/__init__.py +6 -0
- selexprep/extract/demux.py +355 -0
- selexprep/extract/runner.py +752 -0
- selexprep/extract/strand.py +181 -0
- selexprep/extract/trim.py +309 -0
- selexprep/fetch/__init__.py +27 -0
- selexprep/fetch/discover.py +1145 -0
- selexprep/fetch/download.py +844 -0
- selexprep/fetch/inspect.py +195 -0
- selexprep/fetch/library_strategy.py +203 -0
- selexprep/fetch/metadata.py +459 -0
- selexprep/fetch/plan.py +243 -0
- selexprep/fetch/runner.py +403 -0
- selexprep/library/__init__.py +25 -0
- selexprep/library/adapters.py +127 -0
- selexprep/library/audit.py +258 -0
- selexprep/library/detect.py +1038 -0
- selexprep/library/report.py +393 -0
- selexprep/manifest.py +286 -0
- selexprep/qc/__init__.py +1 -0
- selexprep/qc/consistency.py +320 -0
- selexprep/qc/coverage.py +188 -0
- selexprep/qc/diversity.py +99 -0
- selexprep/qc/flags.py +464 -0
- selexprep/qc/plots.py +227 -0
- selexprep/qc/readiness.py +887 -0
- selexprep/qc/runner.py +156 -0
- selexprep/run/__init__.py +5 -0
- selexprep/run/runner.py +638 -0
- selexprep-0.1.0.dist-info/METADATA +408 -0
- selexprep-0.1.0.dist-info/RECORD +55 -0
- selexprep-0.1.0.dist-info/WHEEL +4 -0
- selexprep-0.1.0.dist-info/entry_points.txt +2 -0
- selexprep-0.1.0.dist-info/licenses/LICENSE +21 -0
selexprep/__init__.py
ADDED
selexprep/_common.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Shared helpers for selexprep modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Canonical per-SRR FASTQ filename conventions produced by kingfisher /
|
|
12
|
+
# sra-toolkit. Single-end runs produce `{srr}.fastq.gz`; paired-end runs
|
|
13
|
+
# produce `{srr}_1.fastq.gz` and `{srr}_2.fastq.gz`. Match EXACTLY to avoid
|
|
14
|
+
# prefix collisions (SRR1234 must not match SRR12345).
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def iter_srr_files(root: Path, srr: str, suffix: str = ".fastq.gz") -> list[Path]:
|
|
18
|
+
"""Return FASTQ files for `srr` under `root`, matching exact filenames only.
|
|
19
|
+
|
|
20
|
+
Supports both single-end (`{srr}{suffix}`) and paired-end
|
|
21
|
+
(`{srr}_1{suffix}`, `{srr}_2{suffix}`) layouts.
|
|
22
|
+
"""
|
|
23
|
+
if not root.exists():
|
|
24
|
+
return []
|
|
25
|
+
valid_names = {f"{srr}{suffix}", f"{srr}_1{suffix}", f"{srr}_2{suffix}"}
|
|
26
|
+
return [p for p in root.rglob(f"*{suffix}") if p.name in valid_names]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_csv(path: Path) -> list[dict[str, str]]:
|
|
30
|
+
"""Load a CSV file into a list of dicts. Returns `[]` if the file is missing."""
|
|
31
|
+
try:
|
|
32
|
+
with open(path, encoding="utf-8") as f:
|
|
33
|
+
return list(csv.DictReader(f))
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
logging.getLogger(__name__).warning("File not found: %s", path)
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_round_number(parquet_path: Path) -> int | str:
|
|
40
|
+
"""Extract the round number from a per-round parquet filename.
|
|
41
|
+
|
|
42
|
+
Handles `.counts.parquet` and `.clusters.parquet` suffixes. Returns the
|
|
43
|
+
integer round when the trailing token parses as int, otherwise returns
|
|
44
|
+
the bare stem (so callers can detect and route non-round files).
|
|
45
|
+
"""
|
|
46
|
+
stem = parquet_path.stem.replace(".counts", "").replace(".clusters", "")
|
|
47
|
+
tail = stem.split("_")[-1]
|
|
48
|
+
try:
|
|
49
|
+
return int(tail)
|
|
50
|
+
except ValueError:
|
|
51
|
+
return stem
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def setup_logging(
|
|
55
|
+
logger_name: str,
|
|
56
|
+
log_dir: Path | None = None,
|
|
57
|
+
log_prefix: str | None = None,
|
|
58
|
+
) -> logging.Logger:
|
|
59
|
+
"""Configure root logging with optional rotating per-run file handler.
|
|
60
|
+
|
|
61
|
+
Idempotent: only attaches a stream handler once. When `log_dir` is given,
|
|
62
|
+
a timestamped log file is added on top of the stream handler.
|
|
63
|
+
"""
|
|
64
|
+
root = logging.getLogger()
|
|
65
|
+
if not root.handlers:
|
|
66
|
+
logging.basicConfig(
|
|
67
|
+
level=logging.INFO,
|
|
68
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
69
|
+
handlers=[logging.StreamHandler(sys.stdout)],
|
|
70
|
+
)
|
|
71
|
+
if log_dir is not None:
|
|
72
|
+
log_dir.mkdir(exist_ok=True)
|
|
73
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
74
|
+
prefix = log_prefix or logger_name
|
|
75
|
+
fh = logging.FileHandler(log_dir / f"{prefix}_{ts}.log", encoding="utf-8")
|
|
76
|
+
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))
|
|
77
|
+
root.addHandler(fh)
|
|
78
|
+
return logging.getLogger(logger_name)
|
selexprep/_io.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Deterministic I/O helpers for reproducible-output guarantees.
|
|
2
|
+
|
|
3
|
+
The selexprep manifest promises bit-identical SHA256 across reruns for
|
|
4
|
+
FASTA/FASTQ/TSV/JSON outputs. The default ``gzip.open`` writer embeds the
|
|
5
|
+
current ``mtime`` in the gzip header, which breaks this guarantee silently.
|
|
6
|
+
``open_gzip_text_deterministic`` forces ``mtime=0`` so the output bytes are
|
|
7
|
+
reproducible across machines and across reruns on the same machine.
|
|
8
|
+
|
|
9
|
+
See the plan's "Reproducible-output discipline" section: every ``.gz`` write
|
|
10
|
+
must go through this module.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import gzip
|
|
16
|
+
import hashlib
|
|
17
|
+
import io
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import IO
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def open_gzip_text_deterministic(
|
|
25
|
+
path: Path,
|
|
26
|
+
encoding: str = "utf-8",
|
|
27
|
+
) -> io.TextIOWrapper:
|
|
28
|
+
"""Open a deterministic-gzip writer in text mode.
|
|
29
|
+
|
|
30
|
+
The gzip format normally embeds two non-deterministic bits in the header:
|
|
31
|
+
|
|
32
|
+
1. ``mtime`` — the wall-clock time of writing.
|
|
33
|
+
2. ``FNAME`` — the destination filename (so two runs writing to ``a.gz``
|
|
34
|
+
and ``b.gz`` produce different headers even for identical content).
|
|
35
|
+
|
|
36
|
+
We suppress both: ``mtime=0`` and an empty ``filename=""`` (with an
|
|
37
|
+
explicit ``fileobj``) prevent either field from making it into the
|
|
38
|
+
header. The resulting bytes depend only on the payload, so any two
|
|
39
|
+
invocations with the same content yield byte-for-byte identical output
|
|
40
|
+
— which is the property the manifest's SHA256 checks rely on.
|
|
41
|
+
|
|
42
|
+
The returned ``TextIOWrapper`` is the caller's to close. Closing it
|
|
43
|
+
cascades through the inner ``GzipFile`` to the underlying raw file
|
|
44
|
+
because we set ``gz.myfileobj = raw``, which is how stdlib ``gzip``
|
|
45
|
+
flags file ownership for its own close path.
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
with open_gzip_text_deterministic(Path("out.fastq.gz")) as fh:
|
|
50
|
+
fh.write("@read_0\\nACGT\\n+\\nIIII\\n")
|
|
51
|
+
"""
|
|
52
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
raw = open(path, "wb") # noqa: SIM115 — closed via GzipFile.myfileobj cascade
|
|
54
|
+
gz = gzip.GzipFile(filename="", mode="wb", mtime=0, fileobj=raw)
|
|
55
|
+
# Force GzipFile.close() to close `raw` for us so the caller-visible close
|
|
56
|
+
# on the TextIOWrapper produces a clean cascade with no file-handle leak.
|
|
57
|
+
gz.myfileobj = raw # type: ignore[attr-defined]
|
|
58
|
+
return io.TextIOWrapper(gz, encoding=encoding, write_through=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@contextmanager
|
|
62
|
+
def write_gzip_text_deterministic(
|
|
63
|
+
path: Path,
|
|
64
|
+
encoding: str = "utf-8",
|
|
65
|
+
) -> Iterator[IO[str]]:
|
|
66
|
+
"""Context-managed version of :func:`open_gzip_text_deterministic`."""
|
|
67
|
+
fh = open_gzip_text_deterministic(path, encoding=encoding)
|
|
68
|
+
try:
|
|
69
|
+
yield fh
|
|
70
|
+
finally:
|
|
71
|
+
fh.close()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def sha256_file(path: Path, chunk: int = 1 << 20) -> str:
|
|
75
|
+
"""Stream-SHA256 a file in 1 MiB chunks (memory-bounded for multi-GB inputs)."""
|
|
76
|
+
h = hashlib.sha256()
|
|
77
|
+
with open(path, "rb") as fh:
|
|
78
|
+
for block in iter(lambda: fh.read(chunk), b""):
|
|
79
|
+
h.update(block)
|
|
80
|
+
return h.hexdigest()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Benchmark scaffolding for primer-recovery metrics + result tables.
|
|
2
|
+
|
|
3
|
+
Public surface kept minimal — most callers should reach for the named
|
|
4
|
+
functions in the submodules directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from selexprep.benchmark.corpus_audit import (
|
|
8
|
+
CorpusAuditReport,
|
|
9
|
+
aggregate_audit_from_run_outputs,
|
|
10
|
+
sample_corpus,
|
|
11
|
+
)
|
|
12
|
+
from selexprep.benchmark.equivalence import EquivalenceResult, primer_equivalent
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CorpusAuditReport",
|
|
16
|
+
"EquivalenceResult",
|
|
17
|
+
"aggregate_audit_from_run_outputs",
|
|
18
|
+
"primer_equivalent",
|
|
19
|
+
"sample_corpus",
|
|
20
|
+
]
|