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.
@@ -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,3 @@
1
+ """Count occurrences of reference sequences in sequencing reads or genome assemblies."""
2
+
3
+ __version__ = '1.0.0'
@@ -0,0 +1,6 @@
1
+ """Allow running the command with "python -m seqcounter"."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
@@ -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)