seqcounter 1.0.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.
- seqcounter-1.0.0/LICENSE +21 -0
- seqcounter-1.0.0/MANIFEST.in +1 -0
- seqcounter-1.0.0/PKG-INFO +140 -0
- seqcounter-1.0.0/README.md +109 -0
- seqcounter-1.0.0/pyproject.toml +72 -0
- seqcounter-1.0.0/seqcounter/__init__.py +3 -0
- seqcounter-1.0.0/seqcounter/__main__.py +6 -0
- seqcounter-1.0.0/seqcounter/cli.py +184 -0
- seqcounter-1.0.0/seqcounter/counts.py +168 -0
- seqcounter-1.0.0/seqcounter/errors.py +5 -0
- seqcounter-1.0.0/seqcounter/references.py +87 -0
- seqcounter-1.0.0/seqcounter/samples.py +123 -0
- seqcounter-1.0.0/seqcounter/seal.py +283 -0
- seqcounter-1.0.0/seqcounter.egg-info/PKG-INFO +140 -0
- seqcounter-1.0.0/seqcounter.egg-info/SOURCES.txt +26 -0
- seqcounter-1.0.0/seqcounter.egg-info/dependency_links.txt +1 -0
- seqcounter-1.0.0/seqcounter.egg-info/entry_points.txt +2 -0
- seqcounter-1.0.0/seqcounter.egg-info/requires.txt +4 -0
- seqcounter-1.0.0/seqcounter.egg-info/top_level.txt +1 -0
- seqcounter-1.0.0/setup.cfg +4 -0
- seqcounter-1.0.0/tests/__init__.py +0 -0
- seqcounter-1.0.0/tests/conftest.py +81 -0
- seqcounter-1.0.0/tests/test_cli.py +117 -0
- seqcounter-1.0.0/tests/test_counts.py +139 -0
- seqcounter-1.0.0/tests/test_integration.py +180 -0
- seqcounter-1.0.0/tests/test_references.py +81 -0
- seqcounter-1.0.0/tests/test_samples.py +113 -0
- seqcounter-1.0.0/tests/test_seal.py +263 -0
seqcounter-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Marc-Olivier Duceppe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include tests *.py
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seqcounter
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Count the occurrences of reference sequences in sequencing reads or genome assemblies
|
|
5
|
+
Author-email: Marc-Olivier Duceppe <marc-olivier.duceppe@inspection.gc.ca>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/duceppemo/seqcounter
|
|
8
|
+
Project-URL: Documentation, https://github.com/duceppemo/seqcounter/wiki
|
|
9
|
+
Project-URL: Changelog, https://github.com/duceppemo/seqcounter/wiki/Changelog
|
|
10
|
+
Project-URL: Issues, https://github.com/duceppemo/seqcounter/issues
|
|
11
|
+
Keywords: bioinformatics,kmer,counting,reference sequences,fastq,fasta,wgs,BBTools,Seal
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<picture>
|
|
34
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/duceppemo/seqcounter/master/assets/logo_dark.svg">
|
|
35
|
+
<img src="https://raw.githubusercontent.com/duceppemo/seqcounter/master/assets/logo.svg" width="520" alt="seqcounter: count reference sequences in reads or assemblies">
|
|
36
|
+
</picture>
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<a href="https://github.com/duceppemo/seqcounter/actions/workflows/tests.yml"><img src="https://github.com/duceppemo/seqcounter/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
|
|
41
|
+
<a href="https://codecov.io/gh/duceppemo/seqcounter"><img src="https://codecov.io/gh/duceppemo/seqcounter/graph/badge.svg" alt="Coverage"></a>
|
|
42
|
+
<a href="https://github.com/duceppemo/seqcounter/releases/latest"><img src="https://img.shields.io/github/v/release/duceppemo/seqcounter?cacheSeconds=3600" alt="Release"></a>
|
|
43
|
+
<a href="https://pypi.org/project/seqcounter/"><img src="https://img.shields.io/pypi/v/seqcounter?cacheSeconds=3600" alt="PyPI"></a>
|
|
44
|
+
<img src="https://img.shields.io/badge/python-3.10%E2%80%933.14-blue" alt="Python 3.10–3.14">
|
|
45
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/duceppemo/seqcounter" alt="License"></a>
|
|
46
|
+
<a href="https://github.com/duceppemo/seqcounter/wiki"><img src="https://img.shields.io/badge/docs-wiki-informational" alt="Documentation"></a>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
Count how many sequencing reads (fastq) or contigs (fasta) contain each sequence of a reference file, with
|
|
50
|
+
[Seal](https://sourceforge.net/projects/bbmap/) from BBTools. Point it at a reference fasta file and a sample, or a
|
|
51
|
+
folder of samples, and it writes one table of counts per reference sequence and per sample.
|
|
52
|
+
|
|
53
|
+
Any set of short sequences works: spoligotype spacers, marker or resistance genes, primer or probe sequences,
|
|
54
|
+
plasmid or phage fragments, barcodes, spike-ins. seqcounter does not interpret them; it counts them, reproducibly
|
|
55
|
+
and in the same way for reads and for assemblies.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
$ seqcounter -ref refs.fasta -i samples/ -o results/
|
|
59
|
+
Reference assembly no_match paired single_end
|
|
60
|
+
absent 0 0 0 0
|
|
61
|
+
present 1 0 86 34
|
|
62
|
+
twice 1 0 154 65
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
* **Reads or assemblies**: single-end or paired-end fastq, or fasta, gzipped or not. Both mates of a pair are
|
|
67
|
+
counted, once each.
|
|
68
|
+
* **Batch mode**: point it at a folder; fastq and fasta files are found recursively and R1/R2 files paired
|
|
69
|
+
automatically. Several samples can be counted at the same time.
|
|
70
|
+
* **Both strands, and mismatches**: a reference is found on either strand, and with up to `-m` mismatches per k-mer
|
|
71
|
+
(one by default).
|
|
72
|
+
* **Three reports**: the counts table (`refseqcounts.tsv`, the table the first version of seqcounter wrote, with the
|
|
73
|
+
command that produced it on its first line), a per-sample summary of how much was read and how much matched, and a
|
|
74
|
+
JSON report with the parameters, the software versions and the counts.
|
|
75
|
+
* **Nothing is hidden**: references Seal cannot find (shorter than the k-mer size), names that would collide, and
|
|
76
|
+
samples that failed are reported, not silently dropped. A failed sample leaves the others readable.
|
|
77
|
+
* **Nothing to configure**: no database, no index, no reference genome. Only the Python standard library, plus
|
|
78
|
+
BBTools.
|
|
79
|
+
* **Tested**: 105 tests, 100% coverage, end to end on synthetic references, genomes and reads with known counts.
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
```
|
|
83
|
+
conda create -n seqcounter -c conda-forge -c bioconda bbmap python">=3.10"
|
|
84
|
+
conda activate seqcounter
|
|
85
|
+
pip install seqcounter
|
|
86
|
+
```
|
|
87
|
+
BBTools (which provides `seal.sh`) is not on PyPI, so it comes from bioconda; seqcounter itself needs nothing else.
|
|
88
|
+
See [Installation](https://github.com/duceppemo/seqcounter/wiki/Installation) for other options, including running
|
|
89
|
+
it from a clone with `python seqcounter.py`, as the first version did.
|
|
90
|
+
|
|
91
|
+
## Quick start
|
|
92
|
+
```
|
|
93
|
+
seqcounter -ref refs.fasta -r1 sample_R1.fastq.gz -r2 sample_R2.fastq.gz -o results/ # Paired-end reads
|
|
94
|
+
seqcounter -ref refs.fasta -r1 sample.fastq.gz -o results/ # Single-end reads
|
|
95
|
+
seqcounter -ref refs.fasta -r1 assembly.fasta -o results/ # An assembly
|
|
96
|
+
seqcounter -ref refs.fasta -i samples/ -o results/ -p 4 # A folder, 4 at a time
|
|
97
|
+
```
|
|
98
|
+
`-ref` is a fasta file of the sequences to count: each one gives a row, named after the first word of its `>` line.
|
|
99
|
+
The counts are printed and saved in `results/`. To see it work on data with known counts, run
|
|
100
|
+
`bash examples/run_example.sh`, which generates its own references and samples.
|
|
101
|
+
|
|
102
|
+
**What a count is**: the number of reads, or contigs, that contain the reference sequence, not the number of copies
|
|
103
|
+
it has in them. A reference present twice in a single-contig assembly is counted once; in reads, about twice as many
|
|
104
|
+
reads match it. See [How it works](https://github.com/duceppemo/seqcounter/wiki/How-it-works).
|
|
105
|
+
|
|
106
|
+
## Documentation
|
|
107
|
+
The **[wiki](https://github.com/duceppemo/seqcounter/wiki)** covers
|
|
108
|
+
[installation](https://github.com/duceppemo/seqcounter/wiki/Installation),
|
|
109
|
+
[usage and options](https://github.com/duceppemo/seqcounter/wiki/Usage),
|
|
110
|
+
[output files](https://github.com/duceppemo/seqcounter/wiki/Output-files),
|
|
111
|
+
[how it works](https://github.com/duceppemo/seqcounter/wiki/How-it-works),
|
|
112
|
+
[troubleshooting](https://github.com/duceppemo/seqcounter/wiki/Troubleshooting) and the
|
|
113
|
+
[FAQ](https://github.com/duceppemo/seqcounter/wiki/FAQ).
|
|
114
|
+
|
|
115
|
+
## Citing
|
|
116
|
+
If you use seqcounter, please cite it and BBTools, which does the counting:
|
|
117
|
+
|
|
118
|
+
> Duceppe M-O. seqcounter: count the occurrences of reference sequences in sequencing reads or genome assemblies.
|
|
119
|
+
> https://github.com/duceppemo/seqcounter
|
|
120
|
+
|
|
121
|
+
> Bushnell B. BBTools. https://sourceforge.net/projects/bbmap/
|
|
122
|
+
|
|
123
|
+
GitHub's **"Cite this repository"** button gives the citation in APA and BibTeX formats, from
|
|
124
|
+
[`CITATION.cff`](CITATION.cff).
|
|
125
|
+
|
|
126
|
+
## Related
|
|
127
|
+
* [spoligotyper](https://github.com/duceppemo/spoligotyper) — *in silico* spoligotyping of the *M. tuberculosis*
|
|
128
|
+
complex. It counts the 43 spacers the way seqcounter counts any reference, and then reads them as a spoligotype,
|
|
129
|
+
with the species, the lineage and a PDF report.
|
|
130
|
+
* [brucetyper](https://github.com/duceppemo/brucetyper) — *in silico* Bruce-ladder, Suis-ladder, AMOS and RB51
|
|
131
|
+
junction PCR for *Brucella*, from assemblies.
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
Bug reports, questions and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
135
|
+
|
|
136
|
+
## Author
|
|
137
|
+
Marc-Olivier Duceppe, Canadian Food Inspection Agency (CFIA): marc-olivier.duceppe@inspection.gc.ca
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/duceppemo/seqcounter/master/assets/logo_dark.svg">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/duceppemo/seqcounter/master/assets/logo.svg" width="520" alt="seqcounter: count reference sequences in reads or assemblies">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://github.com/duceppemo/seqcounter/actions/workflows/tests.yml"><img src="https://github.com/duceppemo/seqcounter/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
|
|
10
|
+
<a href="https://codecov.io/gh/duceppemo/seqcounter"><img src="https://codecov.io/gh/duceppemo/seqcounter/graph/badge.svg" alt="Coverage"></a>
|
|
11
|
+
<a href="https://github.com/duceppemo/seqcounter/releases/latest"><img src="https://img.shields.io/github/v/release/duceppemo/seqcounter?cacheSeconds=3600" alt="Release"></a>
|
|
12
|
+
<a href="https://pypi.org/project/seqcounter/"><img src="https://img.shields.io/pypi/v/seqcounter?cacheSeconds=3600" alt="PyPI"></a>
|
|
13
|
+
<img src="https://img.shields.io/badge/python-3.10%E2%80%933.14-blue" alt="Python 3.10–3.14">
|
|
14
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/duceppemo/seqcounter" alt="License"></a>
|
|
15
|
+
<a href="https://github.com/duceppemo/seqcounter/wiki"><img src="https://img.shields.io/badge/docs-wiki-informational" alt="Documentation"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
Count how many sequencing reads (fastq) or contigs (fasta) contain each sequence of a reference file, with
|
|
19
|
+
[Seal](https://sourceforge.net/projects/bbmap/) from BBTools. Point it at a reference fasta file and a sample, or a
|
|
20
|
+
folder of samples, and it writes one table of counts per reference sequence and per sample.
|
|
21
|
+
|
|
22
|
+
Any set of short sequences works: spoligotype spacers, marker or resistance genes, primer or probe sequences,
|
|
23
|
+
plasmid or phage fragments, barcodes, spike-ins. seqcounter does not interpret them; it counts them, reproducibly
|
|
24
|
+
and in the same way for reads and for assemblies.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
$ seqcounter -ref refs.fasta -i samples/ -o results/
|
|
28
|
+
Reference assembly no_match paired single_end
|
|
29
|
+
absent 0 0 0 0
|
|
30
|
+
present 1 0 86 34
|
|
31
|
+
twice 1 0 154 65
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
* **Reads or assemblies**: single-end or paired-end fastq, or fasta, gzipped or not. Both mates of a pair are
|
|
36
|
+
counted, once each.
|
|
37
|
+
* **Batch mode**: point it at a folder; fastq and fasta files are found recursively and R1/R2 files paired
|
|
38
|
+
automatically. Several samples can be counted at the same time.
|
|
39
|
+
* **Both strands, and mismatches**: a reference is found on either strand, and with up to `-m` mismatches per k-mer
|
|
40
|
+
(one by default).
|
|
41
|
+
* **Three reports**: the counts table (`refseqcounts.tsv`, the table the first version of seqcounter wrote, with the
|
|
42
|
+
command that produced it on its first line), a per-sample summary of how much was read and how much matched, and a
|
|
43
|
+
JSON report with the parameters, the software versions and the counts.
|
|
44
|
+
* **Nothing is hidden**: references Seal cannot find (shorter than the k-mer size), names that would collide, and
|
|
45
|
+
samples that failed are reported, not silently dropped. A failed sample leaves the others readable.
|
|
46
|
+
* **Nothing to configure**: no database, no index, no reference genome. Only the Python standard library, plus
|
|
47
|
+
BBTools.
|
|
48
|
+
* **Tested**: 105 tests, 100% coverage, end to end on synthetic references, genomes and reads with known counts.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
```
|
|
52
|
+
conda create -n seqcounter -c conda-forge -c bioconda bbmap python">=3.10"
|
|
53
|
+
conda activate seqcounter
|
|
54
|
+
pip install seqcounter
|
|
55
|
+
```
|
|
56
|
+
BBTools (which provides `seal.sh`) is not on PyPI, so it comes from bioconda; seqcounter itself needs nothing else.
|
|
57
|
+
See [Installation](https://github.com/duceppemo/seqcounter/wiki/Installation) for other options, including running
|
|
58
|
+
it from a clone with `python seqcounter.py`, as the first version did.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
```
|
|
62
|
+
seqcounter -ref refs.fasta -r1 sample_R1.fastq.gz -r2 sample_R2.fastq.gz -o results/ # Paired-end reads
|
|
63
|
+
seqcounter -ref refs.fasta -r1 sample.fastq.gz -o results/ # Single-end reads
|
|
64
|
+
seqcounter -ref refs.fasta -r1 assembly.fasta -o results/ # An assembly
|
|
65
|
+
seqcounter -ref refs.fasta -i samples/ -o results/ -p 4 # A folder, 4 at a time
|
|
66
|
+
```
|
|
67
|
+
`-ref` is a fasta file of the sequences to count: each one gives a row, named after the first word of its `>` line.
|
|
68
|
+
The counts are printed and saved in `results/`. To see it work on data with known counts, run
|
|
69
|
+
`bash examples/run_example.sh`, which generates its own references and samples.
|
|
70
|
+
|
|
71
|
+
**What a count is**: the number of reads, or contigs, that contain the reference sequence, not the number of copies
|
|
72
|
+
it has in them. A reference present twice in a single-contig assembly is counted once; in reads, about twice as many
|
|
73
|
+
reads match it. See [How it works](https://github.com/duceppemo/seqcounter/wiki/How-it-works).
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
The **[wiki](https://github.com/duceppemo/seqcounter/wiki)** covers
|
|
77
|
+
[installation](https://github.com/duceppemo/seqcounter/wiki/Installation),
|
|
78
|
+
[usage and options](https://github.com/duceppemo/seqcounter/wiki/Usage),
|
|
79
|
+
[output files](https://github.com/duceppemo/seqcounter/wiki/Output-files),
|
|
80
|
+
[how it works](https://github.com/duceppemo/seqcounter/wiki/How-it-works),
|
|
81
|
+
[troubleshooting](https://github.com/duceppemo/seqcounter/wiki/Troubleshooting) and the
|
|
82
|
+
[FAQ](https://github.com/duceppemo/seqcounter/wiki/FAQ).
|
|
83
|
+
|
|
84
|
+
## Citing
|
|
85
|
+
If you use seqcounter, please cite it and BBTools, which does the counting:
|
|
86
|
+
|
|
87
|
+
> Duceppe M-O. seqcounter: count the occurrences of reference sequences in sequencing reads or genome assemblies.
|
|
88
|
+
> https://github.com/duceppemo/seqcounter
|
|
89
|
+
|
|
90
|
+
> Bushnell B. BBTools. https://sourceforge.net/projects/bbmap/
|
|
91
|
+
|
|
92
|
+
GitHub's **"Cite this repository"** button gives the citation in APA and BibTeX formats, from
|
|
93
|
+
[`CITATION.cff`](CITATION.cff).
|
|
94
|
+
|
|
95
|
+
## Related
|
|
96
|
+
* [spoligotyper](https://github.com/duceppemo/spoligotyper) — *in silico* spoligotyping of the *M. tuberculosis*
|
|
97
|
+
complex. It counts the 43 spacers the way seqcounter counts any reference, and then reads them as a spoligotype,
|
|
98
|
+
with the species, the lineage and a PDF report.
|
|
99
|
+
* [brucetyper](https://github.com/duceppemo/brucetyper) — *in silico* Bruce-ladder, Suis-ladder, AMOS and RB51
|
|
100
|
+
junction PCR for *Brucella*, from assemblies.
|
|
101
|
+
|
|
102
|
+
## Contributing
|
|
103
|
+
Bug reports, questions and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
104
|
+
|
|
105
|
+
## Author
|
|
106
|
+
Marc-Olivier Duceppe, Canadian Food Inspection Agency (CFIA): marc-olivier.duceppe@inspection.gc.ca
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "seqcounter"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Count the occurrences of reference sequences in sequencing reads or genome assemblies"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{name = "Marc-Olivier Duceppe", email = "marc-olivier.duceppe@inspection.gc.ca"}]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
keywords = ["bioinformatics", "kmer", "counting", "reference sequences", "fastq", "fasta", "wgs", "BBTools", "Seal"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Operating System :: MacOS",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
28
|
+
]
|
|
29
|
+
# Seal, from BBTools (https://sourceforge.net/projects/bbmap/), must also be installed: it is not available from
|
|
30
|
+
# PyPI. seqcounter itself needs nothing but the Python standard library.
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
test = ["pytest>=7", "pytest-cov>=5"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/duceppemo/seqcounter"
|
|
38
|
+
Documentation = "https://github.com/duceppemo/seqcounter/wiki"
|
|
39
|
+
Changelog = "https://github.com/duceppemo/seqcounter/wiki/Changelog"
|
|
40
|
+
Issues = "https://github.com/duceppemo/seqcounter/issues"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
seqcounter = "seqcounter.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools]
|
|
46
|
+
packages = ["seqcounter"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.dynamic]
|
|
49
|
+
# "seqcounter.__init__" rather than "seqcounter": the root seqcounter.py shim has the same name as the package,
|
|
50
|
+
# and setuptools resolves the plain name to that file, which holds no version.
|
|
51
|
+
version = {attr = "seqcounter.__init__.__version__"}
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
|
|
56
|
+
[tool.coverage.run]
|
|
57
|
+
source = ["seqcounter"]
|
|
58
|
+
branch = true
|
|
59
|
+
|
|
60
|
+
[tool.coverage.report]
|
|
61
|
+
show_missing = true
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 120
|
|
65
|
+
target-version = "py310"
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
69
|
+
ignore = ["UP031", "UP032"] # str.format() is used throughout
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint.per-file-ignores]
|
|
72
|
+
"tests/*" = ["E501"]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Command line entry point."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from argparse import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .counts import count_samples, print_table, run_info, write_reports
|
|
15
|
+
from .errors import SeqCounterError
|
|
16
|
+
from .references import load_references
|
|
17
|
+
from .samples import find_samples, single_sample
|
|
18
|
+
from .seal import DEFAULT_KMER_SIZE, DEFAULT_MISMATCHES, auto_memory, check_seal
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger('seqcounter')
|
|
21
|
+
|
|
22
|
+
# Expected errors are reported without a traceback
|
|
23
|
+
USER_ERRORS = (SeqCounterError, OSError)
|
|
24
|
+
|
|
25
|
+
EPILOG = '''examples:
|
|
26
|
+
seqcounter -ref refs.fasta -r1 sample_R1.fastq.gz -r2 sample_R2.fastq.gz -o results/
|
|
27
|
+
seqcounter -ref refs.fasta -r1 sample.fastq.gz -o results/
|
|
28
|
+
seqcounter -ref refs.fasta -r1 assembly.fasta -o results/
|
|
29
|
+
seqcounter -ref refs.fasta -i folder_of_fastq_and_fasta/ -o results/
|
|
30
|
+
|
|
31
|
+
documentation: https://github.com/duceppemo/seqcounter/wiki'''
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def setup_logging(verbose=False):
|
|
35
|
+
"""Configure the package logger only, so importing seqcounter never alters the root logger."""
|
|
36
|
+
for handler in list(log.handlers):
|
|
37
|
+
log.removeHandler(handler)
|
|
38
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
39
|
+
handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)-7s %(message)s', datefmt='%H:%M:%S'))
|
|
40
|
+
log.addHandler(handler)
|
|
41
|
+
log.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def positive_int(value):
|
|
45
|
+
try:
|
|
46
|
+
value = int(value)
|
|
47
|
+
except ValueError:
|
|
48
|
+
raise ArgumentTypeError('"{}" is not an integer'.format(value)) from None
|
|
49
|
+
if value < 1:
|
|
50
|
+
raise ArgumentTypeError('must be >= 1')
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def java_memory(value):
|
|
55
|
+
if not value[:-1].isdigit() or value[-1].lower() not in 'mg':
|
|
56
|
+
raise ArgumentTypeError('"{}" is not a Java memory size such as 500m or 2g'.format(value))
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def available_cpus():
|
|
61
|
+
try:
|
|
62
|
+
return len(os.sched_getaffinity(0)) # Respects cgroup/taskset limits on Linux (e.g. SLURM jobs)
|
|
63
|
+
except AttributeError:
|
|
64
|
+
return os.cpu_count() or 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def format_elapsed(seconds):
|
|
68
|
+
"""A readable duration, as the first version of seqcounter printed it: "1h2m3s"."""
|
|
69
|
+
minutes, seconds = divmod(int(round(seconds)), 60)
|
|
70
|
+
hours, minutes = divmod(minutes, 60)
|
|
71
|
+
days, hours = divmod(hours, 24)
|
|
72
|
+
periods = [('d', days), ('h', hours), ('m', minutes), ('s', seconds)]
|
|
73
|
+
return ''.join('{}{}'.format(value, name) for name, value in periods if value) or '0s'
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_parser():
|
|
77
|
+
max_cpu = available_cpus()
|
|
78
|
+
parser = ArgumentParser(prog='seqcounter', formatter_class=RawDescriptionHelpFormatter, epilog=EPILOG,
|
|
79
|
+
description='Count the occurrences of reference sequences in sequencing reads (fastq)\n'
|
|
80
|
+
'or genome assemblies (fasta), with Seal from BBTools.')
|
|
81
|
+
inputs = parser.add_argument_group('input (one sample with -r1, or a folder of samples with -i)')
|
|
82
|
+
source = inputs.add_mutually_exclusive_group(required=True)
|
|
83
|
+
source.add_argument('-r1', '--r1', metavar='FILE',
|
|
84
|
+
help='Reads (single-end or R1 of paired-end) or assembly. fastq or fasta, gzipped or not.')
|
|
85
|
+
source.add_argument('-i', '--input', metavar='FOLDER',
|
|
86
|
+
help='Folder of fastq and fasta files, searched recursively. Each fasta file, single-end '
|
|
87
|
+
'fastq file or pair of R1/R2 fastq files is a sample.')
|
|
88
|
+
inputs.add_argument('-r2', '--r2', metavar='FILE',
|
|
89
|
+
help='R2 reads, for paired-end fastq files.')
|
|
90
|
+
inputs.add_argument('-ref', '--ref', metavar='FILE', required=True,
|
|
91
|
+
help='Fasta file of the reference sequences to count, gzipped or not. One row per sequence '
|
|
92
|
+
'is reported, named after the first word of its ">" line.')
|
|
93
|
+
inputs.add_argument('-s', '--sample', metavar='NAME',
|
|
94
|
+
help='With -r1: sample name used in the reports. Default: the input file name, without '
|
|
95
|
+
'extension and read suffix (_R1, _1, ...).')
|
|
96
|
+
|
|
97
|
+
output = parser.add_argument_group('output')
|
|
98
|
+
output.add_argument('-o', '--output', metavar='FOLDER', required=True,
|
|
99
|
+
help='Folder to hold the reports. Created if needed.')
|
|
100
|
+
|
|
101
|
+
counting = parser.add_argument_group('counting')
|
|
102
|
+
counting.add_argument('-k', '--kmer-size', metavar='N', type=positive_int, default=DEFAULT_KMER_SIZE,
|
|
103
|
+
help='K-mer size used to find the reference sequences. A reference shorter than k cannot '
|
|
104
|
+
'be found. Default: {}.'.format(DEFAULT_KMER_SIZE))
|
|
105
|
+
counting.add_argument('-m', '--mismatch', metavar='N', type=int, default=DEFAULT_MISMATCHES,
|
|
106
|
+
help='Mismatches allowed between a reference k-mer and the input (maximum Hamming '
|
|
107
|
+
'distance). Default: {}.'.format(DEFAULT_MISMATCHES))
|
|
108
|
+
|
|
109
|
+
other = parser.add_argument_group('performance and other options')
|
|
110
|
+
other.add_argument('-t', '--threads', metavar='N', type=positive_int, default=max_cpu,
|
|
111
|
+
help='Number of threads. Default: all available ({}).'.format(max_cpu))
|
|
112
|
+
other.add_argument('-p', '-j', '--parallel', '--jobs', metavar='N', type=positive_int, default=1, dest='parallel',
|
|
113
|
+
help='With -i: number of samples counted at the same time, sharing the threads. Default: 1.')
|
|
114
|
+
other.add_argument('--memory', metavar='SIZE', type=java_memory,
|
|
115
|
+
help='Memory for Seal (Java heap size), per job, e.g. 2g. Default: from the size of the '
|
|
116
|
+
'reference sequences, within 85%% of the machine.')
|
|
117
|
+
other.add_argument('--verbose', action='store_true', help='Show debug messages, including the Seal command.')
|
|
118
|
+
other.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__)
|
|
119
|
+
return parser
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def check_arguments(parser, args):
|
|
123
|
+
if args.input and (args.r2 or args.sample):
|
|
124
|
+
parser.error('-r2 and --sample are only used with -r1')
|
|
125
|
+
if args.r1 and args.parallel > 1:
|
|
126
|
+
parser.error('--parallel is only used with -i')
|
|
127
|
+
if args.mismatch < 0:
|
|
128
|
+
parser.error('--mismatch cannot be negative')
|
|
129
|
+
if args.mismatch >= args.kmer_size:
|
|
130
|
+
parser.error('--mismatch ({}) must be smaller than --kmer-size ({})'.format(args.mismatch, args.kmer_size))
|
|
131
|
+
if args.sample is not None and (not args.sample.strip() or '/' in args.sample or args.sample in ('.', '..')):
|
|
132
|
+
parser.error('--sample must be a name, not a path: "{}"'.format(args.sample))
|
|
133
|
+
if args.sample is not None and re.search(r'[\t\r\n]', args.sample):
|
|
134
|
+
parser.error('--sample cannot contain tabs or line breaks (they would break the table)')
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main(argv=None):
|
|
138
|
+
parser = build_parser()
|
|
139
|
+
args = parser.parse_args(argv)
|
|
140
|
+
check_arguments(parser, args)
|
|
141
|
+
setup_logging(args.verbose)
|
|
142
|
+
command = shlex.join([parser.prog, *(sys.argv[1:] if argv is None else argv)])
|
|
143
|
+
log.debug('seqcounter %s: %s', __version__, command)
|
|
144
|
+
started = datetime.now().astimezone()
|
|
145
|
+
start = time.monotonic()
|
|
146
|
+
output = Path(args.output).expanduser()
|
|
147
|
+
try:
|
|
148
|
+
check_seal()
|
|
149
|
+
references = load_references(args.ref, args.kmer_size)
|
|
150
|
+
if args.input:
|
|
151
|
+
samples = find_samples(args.input, exclude=[output])
|
|
152
|
+
log.info('%d sample(s) found in %s', len(samples), args.input)
|
|
153
|
+
else:
|
|
154
|
+
samples = [single_sample(args.r1, args.r2, name=args.sample)]
|
|
155
|
+
if any(s.file_type == 'fasta' for s in samples):
|
|
156
|
+
log.info('fasta input: the count of a reference sequence is the number of sequences (contigs) that '
|
|
157
|
+
'contain it, not the number of copies it has in them.')
|
|
158
|
+
jobs = min(args.parallel, len(samples))
|
|
159
|
+
memory = args.memory or auto_memory(sum(r.length for r in references), jobs=jobs)
|
|
160
|
+
results = count_samples(samples, args.ref, jobs=jobs, threads=args.threads, kmer_size=args.kmer_size,
|
|
161
|
+
mismatch=args.mismatch, memory=memory)
|
|
162
|
+
run = run_info(command, {'Reference': os.path.abspath(args.ref),
|
|
163
|
+
'Input': ' '.join(os.path.abspath(f) for f in (args.input, args.r1, args.r2) if f),
|
|
164
|
+
'Output folder': str(output.resolve()),
|
|
165
|
+
'K-mer size': args.kmer_size, 'Mismatches': args.mismatch,
|
|
166
|
+
'Threads': args.threads, 'Parallel samples': jobs, 'Seal memory': memory}, started)
|
|
167
|
+
rows, reports = write_reports(results, references, output, command=command, run=run)
|
|
168
|
+
except USER_ERRORS as e:
|
|
169
|
+
log.error('%s', e)
|
|
170
|
+
log.debug('Traceback:', exc_info=True) # Shown with --verbose
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
except KeyboardInterrupt:
|
|
173
|
+
log.error('Interrupted')
|
|
174
|
+
sys.exit(130)
|
|
175
|
+
|
|
176
|
+
# The counts go to stdout, the messages to stderr: "seqcounter ... > counts.tsv" works
|
|
177
|
+
print_table(rows)
|
|
178
|
+
for report in reports:
|
|
179
|
+
log.info('Report saved in %s', report)
|
|
180
|
+
log.info('Done in %s', format_elapsed(time.monotonic() - start))
|
|
181
|
+
failed = [r.sample for r in results if r.error]
|
|
182
|
+
if failed:
|
|
183
|
+
log.error('%d of %d sample(s) failed: %s', len(failed), len(results), ', '.join(failed))
|
|
184
|
+
sys.exit(1)
|