seqstatx 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.
seqstats/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
seqstats/cli.py ADDED
@@ -0,0 +1,84 @@
1
+ """Command-line interface for seqstats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from seqstats.core import SeqStats, parse_file
10
+ from seqstats import __version__
11
+
12
+ _COLS = ["file", "seqs", "total_bp", "gc%", "mean_len", "min_len", "max_len", "N50", "N90"]
13
+ _COL_W = [30, 8, 12, 7, 10, 8, 8, 10, 10]
14
+
15
+
16
+ def _fmt(stats: SeqStats) -> list[str]:
17
+ return [
18
+ stats.name[:29],
19
+ str(stats.n_seqs),
20
+ str(stats.total_bases),
21
+ f"{stats.gc_pct:.2f}",
22
+ f"{stats.mean_len:.1f}",
23
+ str(stats.min_len),
24
+ str(stats.max_len),
25
+ str(stats.n50),
26
+ str(stats.n90),
27
+ ]
28
+
29
+
30
+ def _header() -> str:
31
+ return " ".join(c.ljust(w) for c, w in zip(_COLS, _COL_W))
32
+
33
+
34
+ def _row(stats: SeqStats) -> str:
35
+ return " ".join(v.ljust(w) for v, w in zip(_fmt(stats), _COL_W))
36
+
37
+
38
+ def _tsv_header() -> str:
39
+ return "\t".join(_COLS)
40
+
41
+
42
+ def _tsv_row(stats: SeqStats) -> str:
43
+ return "\t".join(_fmt(stats))
44
+
45
+
46
+ def main(argv: list[str] | None = None) -> None:
47
+ parser = argparse.ArgumentParser(
48
+ prog="seqstatx",
49
+ description="Compute sequence statistics for FASTA/FASTQ files.",
50
+ formatter_class=argparse.RawDescriptionHelpFormatter,
51
+ epilog="""examples:
52
+ seqstatx genome.fa
53
+ seqstatx *.fastq.gz
54
+ seqstatx --tsv reads.fq.gz > stats.tsv
55
+ seqstatx --tsv sample1.fa sample2.fa | column -t""",
56
+ )
57
+ parser.add_argument("files", nargs="+", type=Path, metavar="FILE")
58
+ parser.add_argument("--tsv", action="store_true", help="output tab-separated values")
59
+ parser.add_argument("--version", action="version", version=f"seqstatx {__version__}")
60
+
61
+ args = parser.parse_args(argv)
62
+
63
+ missing = [f for f in args.files if not f.exists()]
64
+ if missing:
65
+ for f in missing:
66
+ print(f"[seqstats] file not found: {f}", file=sys.stderr)
67
+ sys.exit(1)
68
+
69
+ if args.tsv:
70
+ print(_tsv_header())
71
+ else:
72
+ print(_header())
73
+ print("-" * sum(_COL_W) + "-" * (2 * (len(_COL_W) - 1)))
74
+
75
+ for path in args.files:
76
+ stats = parse_file(path)
77
+ if args.tsv:
78
+ print(_tsv_row(stats))
79
+ else:
80
+ print(_row(stats))
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
seqstats/core.py ADDED
@@ -0,0 +1,125 @@
1
+ """Core sequence statistics logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gzip
6
+ import sys
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass
12
+ class SeqStats:
13
+ name: str
14
+ n_seqs: int = 0
15
+ total_bases: int = 0
16
+ gc_count: int = 0
17
+ _lengths: list[int] = field(default_factory=list, repr=False)
18
+
19
+ def add(self, seq: str) -> None:
20
+ n = len(seq)
21
+ self.n_seqs += 1
22
+ self.total_bases += n
23
+ self.gc_count += seq.count("G") + seq.count("C") + seq.count("g") + seq.count("c")
24
+ self._lengths.append(n)
25
+
26
+ @property
27
+ def gc_pct(self) -> float:
28
+ return 100.0 * self.gc_count / self.total_bases if self.total_bases else 0.0
29
+
30
+ @property
31
+ def mean_len(self) -> float:
32
+ return self.total_bases / self.n_seqs if self.n_seqs else 0.0
33
+
34
+ @property
35
+ def min_len(self) -> int:
36
+ return min(self._lengths) if self._lengths else 0
37
+
38
+ @property
39
+ def max_len(self) -> int:
40
+ return max(self._lengths) if self._lengths else 0
41
+
42
+ @property
43
+ def n50(self) -> int:
44
+ if not self._lengths:
45
+ return 0
46
+ sorted_lens = sorted(self._lengths, reverse=True)
47
+ threshold = self.total_bases / 2
48
+ cumsum = 0
49
+ for length in sorted_lens:
50
+ cumsum += length
51
+ if cumsum >= threshold:
52
+ return length
53
+ return 0
54
+
55
+ @property
56
+ def n90(self) -> int:
57
+ if not self._lengths:
58
+ return 0
59
+ sorted_lens = sorted(self._lengths, reverse=True)
60
+ threshold = self.total_bases * 0.9
61
+ cumsum = 0
62
+ for length in sorted_lens:
63
+ cumsum += length
64
+ if cumsum >= threshold:
65
+ return length
66
+ return 0
67
+
68
+
69
+ def _open(path: Path):
70
+ """Open plain or gzipped file."""
71
+ if path.suffix in (".gz", ".gzip"):
72
+ return gzip.open(path, "rt")
73
+ return open(path, "r")
74
+
75
+
76
+ def parse_fasta(path: Path) -> SeqStats:
77
+ stats = SeqStats(name=path.name)
78
+ current: list[str] = []
79
+
80
+ def flush():
81
+ if current:
82
+ stats.add("".join(current))
83
+ current.clear()
84
+
85
+ with _open(path) as fh:
86
+ for line in fh:
87
+ line = line.rstrip()
88
+ if not line:
89
+ continue
90
+ if line.startswith(">"):
91
+ flush()
92
+ else:
93
+ current.append(line)
94
+ flush()
95
+ return stats
96
+
97
+
98
+ def parse_fastq(path: Path) -> SeqStats:
99
+ stats = SeqStats(name=path.name)
100
+ with _open(path) as fh:
101
+ while True:
102
+ header = fh.readline()
103
+ if not header:
104
+ break
105
+ seq = fh.readline().rstrip()
106
+ fh.readline() # +
107
+ fh.readline() # quality
108
+ if seq:
109
+ stats.add(seq)
110
+ return stats
111
+
112
+
113
+ def parse_file(path: Path) -> SeqStats:
114
+ """Detect format by extension and parse."""
115
+ name = path.name.lower()
116
+ if any(name.endswith(ext) for ext in (".fa", ".fna", ".fasta", ".fa.gz", ".fna.gz", ".fasta.gz")):
117
+ return parse_fasta(path)
118
+ if any(name.endswith(ext) for ext in (".fq", ".fastq", ".fq.gz", ".fastq.gz")):
119
+ return parse_fastq(path)
120
+ # fallback: try FASTA
121
+ try:
122
+ return parse_fasta(path)
123
+ except Exception:
124
+ print(f"[seqstats] could not detect format for {path.name}", file=sys.stderr)
125
+ sys.exit(1)
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: seqstatx
3
+ Version: 0.1.0
4
+ Summary: Fast sequence statistics for FASTA/FASTQ files — N50, GC%, length distributions and more
5
+ Author-email: Wendy Bui <wendybuinta@gmail.com>
6
+ License: MIT
7
+ Keywords: bioinformatics,genomics,fasta,fastq,sequence,qc
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest; extra == "dev"
15
+ Requires-Dist: pytest-cov; extra == "dev"
16
+
17
+ # seqstats
18
+
19
+ [![CI](https://github.com/perhapsstrawberries/seqstats/actions/workflows/ci.yml/badge.svg)](https://github.com/perhapsstrawberries/seqstats/actions/workflows/ci.yml)
20
+ [![PyPI](https://img.shields.io/pypi/v/seqstatx)](https://pypi.org/project/seqstatx/)
21
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
22
+ ![License](https://img.shields.io/badge/license-MIT-green)
23
+
24
+ Fast sequence statistics for FASTA and FASTQ files — works on plain or gzipped inputs, no dependencies.
25
+
26
+ ```
27
+ file seqs total_bp gc% mean_len min_len max_len N50 N90
28
+ -----------------------------------------------------------------------------------------------------------------
29
+ GRCh38.primary_assembly.fa 194 3,088,286,401 40.93 15,918,992 970 248,956,422 153,373,213 40,103,529
30
+ SRR10045678_1.fastq.gz 10000000 1,510,000,000 50.21 151.0 151 151 151 151
31
+ ```
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install seqstatx
37
+ ```
38
+
39
+ Or for development:
40
+
41
+ ```bash
42
+ git clone https://github.com/perhapsstrawberries/seqstats.git
43
+ cd seqstats
44
+ pip install -e .
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```bash
50
+ # single file
51
+ seqstatx genome.fa
52
+
53
+ # multiple files, gzipped FASTQ
54
+ seqstatx sample1.fastq.gz sample2.fastq.gz
55
+
56
+ # TSV output for downstream parsing
57
+ seqstatx --tsv *.fa > stats.tsv
58
+
59
+ # pipe to column for alignment
60
+ seqstatx --tsv *.fastq.gz | column -t
61
+ ```
62
+
63
+ ## Metrics
64
+
65
+ | Column | Description |
66
+ |--------|-------------|
67
+ | `seqs` | Number of sequences / reads |
68
+ | `total_bp` | Total base pairs |
69
+ | `gc%` | GC content (%) |
70
+ | `mean_len` | Mean sequence length |
71
+ | `min_len` / `max_len` | Shortest / longest sequence |
72
+ | `N50` | 50% of total assembly is in sequences ≥ this length |
73
+ | `N90` | 90% of total assembly is in sequences ≥ this length |
74
+
75
+ ## Supported formats
76
+
77
+ | Extension | Format |
78
+ |-----------|--------|
79
+ | `.fa` `.fna` `.fasta` | FASTA |
80
+ | `.fq` `.fastq` | FASTQ |
81
+ | `.fa.gz` `.fastq.gz` etc. | gzipped variants |
82
+
83
+ ## Why
84
+
85
+ Existing tools (seqkit, seqtk) are great but require installation of compiled binaries.
86
+ `seqstats` is pure Python 3.10+, zero dependencies, pip-installable from any HPC or Conda environment.
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,8 @@
1
+ seqstats/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ seqstats/cli.py,sha256=G1j416HXic3FElg_v6JraqdRuUpHNwmOq9-Sv3UOtoE,2234
3
+ seqstats/core.py,sha256=JMA1Ynrk41igrGYReqXv02wjVdpi3XBv5hs4GrYtyvk,3367
4
+ seqstatx-0.1.0.dist-info/METADATA,sha256=TQ5gGX89VHJ6bQ42z1cPfkv-n_vOFGSXUg-rN6DIDBY,2796
5
+ seqstatx-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ seqstatx-0.1.0.dist-info/entry_points.txt,sha256=hltTdEO16NlBN8dFELLix6DGhq6E8TSrFnGAjfqTMu4,47
7
+ seqstatx-0.1.0.dist-info/top_level.txt,sha256=pu4R79NmGkId0R2KIUWXSCVKLiTPU5mgKR37uyRr5P0,9
8
+ seqstatx-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ seqstatx = seqstats.cli:main
@@ -0,0 +1 @@
1
+ seqstats