brucellaid 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.
Files changed (40) hide show
  1. brucellaid-1.0.0/LICENSE +21 -0
  2. brucellaid-1.0.0/MANIFEST.in +2 -0
  3. brucellaid-1.0.0/PKG-INFO +163 -0
  4. brucellaid-1.0.0/README.md +129 -0
  5. brucellaid-1.0.0/brucellaid/__init__.py +3 -0
  6. brucellaid-1.0.0/brucellaid/__main__.py +6 -0
  7. brucellaid-1.0.0/brucellaid/cli.py +233 -0
  8. brucellaid-1.0.0/brucellaid/data/README.md +34 -0
  9. brucellaid-1.0.0/brucellaid/data/mlst_alleles.fasta +2605 -0
  10. brucellaid-1.0.0/brucellaid/data/mlst_profiles.tsv +123 -0
  11. brucellaid-1.0.0/brucellaid/data/references.msh +0 -0
  12. brucellaid-1.0.0/brucellaid/data/references.tsv +22 -0
  13. brucellaid-1.0.0/brucellaid/errors.py +5 -0
  14. brucellaid-1.0.0/brucellaid/mash.py +231 -0
  15. brucellaid-1.0.0/brucellaid/mlst.py +224 -0
  16. brucellaid-1.0.0/brucellaid/pipeline.py +148 -0
  17. brucellaid-1.0.0/brucellaid/references.py +140 -0
  18. brucellaid-1.0.0/brucellaid/report.py +235 -0
  19. brucellaid-1.0.0/brucellaid/samples.py +129 -0
  20. brucellaid-1.0.0/brucellaid/seal.py +293 -0
  21. brucellaid-1.0.0/brucellaid.egg-info/PKG-INFO +163 -0
  22. brucellaid-1.0.0/brucellaid.egg-info/SOURCES.txt +38 -0
  23. brucellaid-1.0.0/brucellaid.egg-info/dependency_links.txt +1 -0
  24. brucellaid-1.0.0/brucellaid.egg-info/entry_points.txt +2 -0
  25. brucellaid-1.0.0/brucellaid.egg-info/requires.txt +8 -0
  26. brucellaid-1.0.0/brucellaid.egg-info/top_level.txt +1 -0
  27. brucellaid-1.0.0/pyproject.toml +79 -0
  28. brucellaid-1.0.0/setup.cfg +4 -0
  29. brucellaid-1.0.0/tests/__init__.py +0 -0
  30. brucellaid-1.0.0/tests/conftest.py +169 -0
  31. brucellaid-1.0.0/tests/test_cli.py +154 -0
  32. brucellaid-1.0.0/tests/test_integration.py +150 -0
  33. brucellaid-1.0.0/tests/test_main_module.py +32 -0
  34. brucellaid-1.0.0/tests/test_mash.py +194 -0
  35. brucellaid-1.0.0/tests/test_mlst.py +160 -0
  36. brucellaid-1.0.0/tests/test_pipeline.py +149 -0
  37. brucellaid-1.0.0/tests/test_references.py +93 -0
  38. brucellaid-1.0.0/tests/test_report.py +158 -0
  39. brucellaid-1.0.0/tests/test_samples.py +118 -0
  40. brucellaid-1.0.0/tests/test_seal.py +270 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 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,2 @@
1
+ recursive-include brucellaid/data *
2
+ recursive-include tests *.py
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: brucellaid
3
+ Version: 1.0.0
4
+ Summary: Identify the Brucella species of a read set with MASH, and type it with MLST
5
+ Author-email: Marc-Olivier Duceppe <marc-olivier.duceppe@inspection.gc.ca>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/duceppemo/BrucellaID
8
+ Project-URL: Documentation, https://github.com/duceppemo/BrucellaID/wiki
9
+ Project-URL: Changelog, https://github.com/duceppemo/BrucellaID/wiki/Changelog
10
+ Project-URL: Issues, https://github.com/duceppemo/BrucellaID/issues
11
+ Keywords: bioinformatics,brucella,species identification,MLST,MASH,BBTools,Seal,fastq,wgs
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: xlsx
28
+ Requires-Dist: xlsxwriter>=3; extra == "xlsx"
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=7; extra == "test"
31
+ Requires-Dist: pytest-cov>=5; extra == "test"
32
+ Requires-Dist: xlsxwriter>=3; extra == "test"
33
+ Dynamic: license-file
34
+
35
+ <p align="center">
36
+ <picture>
37
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/duceppemo/BrucellaID/main/assets/logo_dark.svg">
38
+ <img src="https://raw.githubusercontent.com/duceppemo/BrucellaID/main/assets/logo.svg" width="520" alt="BrucellaID: Brucella species identification and MLST from reads">
39
+ </picture>
40
+ </p>
41
+
42
+ <p align="center">
43
+ <a href="https://github.com/duceppemo/BrucellaID/actions/workflows/tests.yml"><img src="https://github.com/duceppemo/BrucellaID/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
44
+ <a href="https://codecov.io/gh/duceppemo/BrucellaID"><img src="https://codecov.io/gh/duceppemo/BrucellaID/graph/badge.svg" alt="Coverage"></a>
45
+ <a href="https://github.com/duceppemo/BrucellaID/releases/latest"><img src="https://img.shields.io/github/v/release/duceppemo/BrucellaID?cacheSeconds=3600" alt="Release"></a>
46
+ <a href="https://pypi.org/project/brucellaid/"><img src="https://img.shields.io/pypi/v/brucellaid?cacheSeconds=3600" alt="PyPI"></a>
47
+ <img src="https://img.shields.io/badge/python-3.10%E2%80%933.14-blue" alt="Python 3.10–3.14">
48
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/duceppemo/BrucellaID" alt="License"></a>
49
+ <a href="https://github.com/duceppemo/BrucellaID/wiki"><img src="https://img.shields.io/badge/docs-wiki-informational" alt="Documentation"></a>
50
+ </p>
51
+
52
+ Identify the *Brucella* species of a fastq sample by its [MASH](https://github.com/marbl/mash) distance to 16
53
+ reference genomes, and type it with the nine-locus
54
+ [Whatmore MLST scheme](https://doi.org/10.1186/1471-2180-7-34), whose alleles are called from the reads with
55
+ [Seal](https://sourceforge.net/projects/bbmap/) from BBTools. Point it at a sample, or a folder of samples.
56
+
57
+ ```
58
+ $ brucellaid -r1 sample_R1.fastq.gz -r2 sample_R2.fastq.gz -o results/
59
+ Sample Species Biovar ClosestReference MashDistance SharedHashes SequenceType AlleleProfile
60
+ sample Brucella abortus 1 NC_006932-NC_006933.fasta 0.000024 999/1000 1 2-1-1-2-1-3-1-1-1
61
+ ```
62
+
63
+ Both the reference sketch and the MLST database come with the package: nothing to download, index or
64
+ configure.
65
+
66
+ ## Features
67
+ * **Two independent answers**: the species, from the closest of 16 *Brucella* reference genomes, and the
68
+ sequence type, from the nine MLST alleles. Neither call corrects the other, and both come with the
69
+ numbers they rest on.
70
+ * **Nothing is hidden**: the distance to every reference, the reads behind every allele call and the
71
+ runner-up that call had to beat, a note when a second reference was nearly as close, and a tie reported as
72
+ a tie rather than broken by a coin toss.
73
+ * **Batch mode**: point it at a folder; fastq files are found recursively, R1/R2 paired, and several samples
74
+ run at a time. A sample that fails leaves the others readable.
75
+ * **Current reference data**: the MLST database is PubMLST's, 309 alleles and 122 sequence types, rebuilt by
76
+ a script in the repository. The sketch is vSNP's 16 *Brucella* genomes, with the species, biovar, strain
77
+ and accession of each.
78
+ * **Backwards compatible where it counts**: the species identification is vSNP's, down to the threshold of
79
+ 150 shared hashes, and `--xlsx` still writes the `stat_alignment_summary_<date>.xlsx` of the first
80
+ version.
81
+ * **Nothing to configure**: only the Python standard library, plus MASH and BBTools.
82
+ * **Tested**: 194 tests, 100% coverage, end to end on a synthetic genome and on reads tiled over the real
83
+ MLST alleles.
84
+ * **Validated** on 16 genomes with a known answer: every species, biovar and closest reference right, and
85
+ all 14 sequence types the one the genome carries — read off the assembly by exact match, which shares no
86
+ code with the read-based caller. The nearest neighbouring species shares 4 of 1000 hashes and comes back
87
+ as not identified ([Validation](https://github.com/duceppemo/BrucellaID/wiki/Validation)).
88
+
89
+ ## Installation
90
+ ```
91
+ conda create -n brucellaid -c conda-forge -c bioconda brucellaid
92
+ conda activate brucellaid
93
+ ```
94
+ MASH and BBTools (which provides `seal.sh`) are not on PyPI, so they come from bioconda; BrucellaID itself
95
+ needs nothing else. See [Installation](https://github.com/duceppemo/BrucellaID/wiki/Installation) for pip
96
+ and for running it from a clone.
97
+
98
+ ## Quick start
99
+ ```
100
+ brucellaid -r1 sample_R1.fastq.gz -r2 sample_R2.fastq.gz -o results/ # Paired-end reads
101
+ brucellaid -r1 sample.fastq.gz -o results/ # Single-end reads
102
+ brucellaid -i reads/ -o results/ -p 4 # A folder, 4 samples at a time
103
+ brucellaid -i reads/ -o results/ --no-mlst # Species only
104
+ ```
105
+ The results are printed and saved in `results/`: one row per sample, one row per locus, every MASH distance,
106
+ and a JSON report. To see it work on data with a known answer, run `bash examples/run_example.sh`, which
107
+ builds its own reads from the alleles of a sequence type.
108
+
109
+ **What the two answers mean**: the species is the species of the closest reference genome, and the biovar is
110
+ that reference's biovar, which is not the same as a biovar call — the *Brucella* biovar references differ by
111
+ very little. The sequence type is `new` when the nine alleles are called but the combination is not in the
112
+ table, and `ND` when a locus could not be called. See
113
+ [How it works](https://github.com/duceppemo/BrucellaID/wiki/How-it-works).
114
+
115
+ ## Documentation
116
+ The **[wiki](https://github.com/duceppemo/BrucellaID/wiki)** covers
117
+ [installation](https://github.com/duceppemo/BrucellaID/wiki/Installation), a
118
+ [tutorial](https://github.com/duceppemo/BrucellaID/wiki/Tutorial) on public read sets,
119
+ [usage and options](https://github.com/duceppemo/BrucellaID/wiki/Usage),
120
+ [output files](https://github.com/duceppemo/BrucellaID/wiki/Output-files),
121
+ [how it works](https://github.com/duceppemo/BrucellaID/wiki/How-it-works),
122
+ [validation](https://github.com/duceppemo/BrucellaID/wiki/Validation),
123
+ [troubleshooting](https://github.com/duceppemo/BrucellaID/wiki/Troubleshooting) and the
124
+ [FAQ](https://github.com/duceppemo/BrucellaID/wiki/FAQ).
125
+
126
+ ## Citing
127
+ If you use BrucellaID, please cite it together with the tools and the data it rests on:
128
+
129
+ > Duceppe M-O. BrucellaID: *Brucella* species identification and MLST from sequencing reads.
130
+ > https://github.com/duceppemo/BrucellaID
131
+
132
+ > Ondov BD, Treangen TJ, Melsted P, et al. Mash: fast genome and metagenome distance estimation using
133
+ > MinHash. *Genome Biology* 2016;17:132. https://doi.org/10.1186/s13059-016-0997-x
134
+
135
+ > Bushnell B. BBTools. https://sourceforge.net/projects/bbmap/
136
+
137
+ > Whatmore AM, Perrett LL, MacMillan AP. Characterisation of the genetic diversity of *Brucella* by
138
+ > multilocus sequencing. *BMC Microbiology* 2007;7:34. https://doi.org/10.1186/1471-2180-7-34
139
+
140
+ > Jolley KA, Bray JE, Maiden MCJ. Open-access bacterial population genomics: BIGSdb software, the
141
+ > PubMLST.org website and their applications. *Wellcome Open Research* 2018;3:124.
142
+ > https://doi.org/10.12688/wellcomeopenres.14826.1
143
+
144
+ GitHub's **"Cite this repository"** button gives the citation in APA and BibTeX formats, from
145
+ [`CITATION.cff`](CITATION.cff).
146
+
147
+ ## Related
148
+ * [brucetyper](https://github.com/duceppemo/brucetyper) — *in silico* Bruce-ladder, Suis-ladder, AMOS and
149
+ RB51 junction PCR for *Brucella*, from assemblies. Where BrucellaID gives the species from a distance and
150
+ the sequence type from the alleles, brucetyper reads the assays a laboratory would run on a gel.
151
+ * [seqcounter](https://github.com/duceppemo/seqcounter) — count the occurrences of any reference sequences
152
+ in reads or assemblies.
153
+ * [spoligotyper](https://github.com/duceppemo/spoligotyper) — *in silico* spoligotyping of the
154
+ *M. tuberculosis* complex.
155
+
156
+ ## Contributing
157
+ Bug reports, questions and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
158
+
159
+ ## Author
160
+ Marc-Olivier Duceppe, Canadian Food Inspection Agency (CFIA): marc-olivier.duceppe@inspection.gc.ca
161
+
162
+ ## License
163
+ [MIT](LICENSE)
@@ -0,0 +1,129 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/duceppemo/BrucellaID/main/assets/logo_dark.svg">
4
+ <img src="https://raw.githubusercontent.com/duceppemo/BrucellaID/main/assets/logo.svg" width="520" alt="BrucellaID: Brucella species identification and MLST from reads">
5
+ </picture>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://github.com/duceppemo/BrucellaID/actions/workflows/tests.yml"><img src="https://github.com/duceppemo/BrucellaID/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
10
+ <a href="https://codecov.io/gh/duceppemo/BrucellaID"><img src="https://codecov.io/gh/duceppemo/BrucellaID/graph/badge.svg" alt="Coverage"></a>
11
+ <a href="https://github.com/duceppemo/BrucellaID/releases/latest"><img src="https://img.shields.io/github/v/release/duceppemo/BrucellaID?cacheSeconds=3600" alt="Release"></a>
12
+ <a href="https://pypi.org/project/brucellaid/"><img src="https://img.shields.io/pypi/v/brucellaid?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/BrucellaID" alt="License"></a>
15
+ <a href="https://github.com/duceppemo/BrucellaID/wiki"><img src="https://img.shields.io/badge/docs-wiki-informational" alt="Documentation"></a>
16
+ </p>
17
+
18
+ Identify the *Brucella* species of a fastq sample by its [MASH](https://github.com/marbl/mash) distance to 16
19
+ reference genomes, and type it with the nine-locus
20
+ [Whatmore MLST scheme](https://doi.org/10.1186/1471-2180-7-34), whose alleles are called from the reads with
21
+ [Seal](https://sourceforge.net/projects/bbmap/) from BBTools. Point it at a sample, or a folder of samples.
22
+
23
+ ```
24
+ $ brucellaid -r1 sample_R1.fastq.gz -r2 sample_R2.fastq.gz -o results/
25
+ Sample Species Biovar ClosestReference MashDistance SharedHashes SequenceType AlleleProfile
26
+ sample Brucella abortus 1 NC_006932-NC_006933.fasta 0.000024 999/1000 1 2-1-1-2-1-3-1-1-1
27
+ ```
28
+
29
+ Both the reference sketch and the MLST database come with the package: nothing to download, index or
30
+ configure.
31
+
32
+ ## Features
33
+ * **Two independent answers**: the species, from the closest of 16 *Brucella* reference genomes, and the
34
+ sequence type, from the nine MLST alleles. Neither call corrects the other, and both come with the
35
+ numbers they rest on.
36
+ * **Nothing is hidden**: the distance to every reference, the reads behind every allele call and the
37
+ runner-up that call had to beat, a note when a second reference was nearly as close, and a tie reported as
38
+ a tie rather than broken by a coin toss.
39
+ * **Batch mode**: point it at a folder; fastq files are found recursively, R1/R2 paired, and several samples
40
+ run at a time. A sample that fails leaves the others readable.
41
+ * **Current reference data**: the MLST database is PubMLST's, 309 alleles and 122 sequence types, rebuilt by
42
+ a script in the repository. The sketch is vSNP's 16 *Brucella* genomes, with the species, biovar, strain
43
+ and accession of each.
44
+ * **Backwards compatible where it counts**: the species identification is vSNP's, down to the threshold of
45
+ 150 shared hashes, and `--xlsx` still writes the `stat_alignment_summary_<date>.xlsx` of the first
46
+ version.
47
+ * **Nothing to configure**: only the Python standard library, plus MASH and BBTools.
48
+ * **Tested**: 194 tests, 100% coverage, end to end on a synthetic genome and on reads tiled over the real
49
+ MLST alleles.
50
+ * **Validated** on 16 genomes with a known answer: every species, biovar and closest reference right, and
51
+ all 14 sequence types the one the genome carries — read off the assembly by exact match, which shares no
52
+ code with the read-based caller. The nearest neighbouring species shares 4 of 1000 hashes and comes back
53
+ as not identified ([Validation](https://github.com/duceppemo/BrucellaID/wiki/Validation)).
54
+
55
+ ## Installation
56
+ ```
57
+ conda create -n brucellaid -c conda-forge -c bioconda brucellaid
58
+ conda activate brucellaid
59
+ ```
60
+ MASH and BBTools (which provides `seal.sh`) are not on PyPI, so they come from bioconda; BrucellaID itself
61
+ needs nothing else. See [Installation](https://github.com/duceppemo/BrucellaID/wiki/Installation) for pip
62
+ and for running it from a clone.
63
+
64
+ ## Quick start
65
+ ```
66
+ brucellaid -r1 sample_R1.fastq.gz -r2 sample_R2.fastq.gz -o results/ # Paired-end reads
67
+ brucellaid -r1 sample.fastq.gz -o results/ # Single-end reads
68
+ brucellaid -i reads/ -o results/ -p 4 # A folder, 4 samples at a time
69
+ brucellaid -i reads/ -o results/ --no-mlst # Species only
70
+ ```
71
+ The results are printed and saved in `results/`: one row per sample, one row per locus, every MASH distance,
72
+ and a JSON report. To see it work on data with a known answer, run `bash examples/run_example.sh`, which
73
+ builds its own reads from the alleles of a sequence type.
74
+
75
+ **What the two answers mean**: the species is the species of the closest reference genome, and the biovar is
76
+ that reference's biovar, which is not the same as a biovar call — the *Brucella* biovar references differ by
77
+ very little. The sequence type is `new` when the nine alleles are called but the combination is not in the
78
+ table, and `ND` when a locus could not be called. See
79
+ [How it works](https://github.com/duceppemo/BrucellaID/wiki/How-it-works).
80
+
81
+ ## Documentation
82
+ The **[wiki](https://github.com/duceppemo/BrucellaID/wiki)** covers
83
+ [installation](https://github.com/duceppemo/BrucellaID/wiki/Installation), a
84
+ [tutorial](https://github.com/duceppemo/BrucellaID/wiki/Tutorial) on public read sets,
85
+ [usage and options](https://github.com/duceppemo/BrucellaID/wiki/Usage),
86
+ [output files](https://github.com/duceppemo/BrucellaID/wiki/Output-files),
87
+ [how it works](https://github.com/duceppemo/BrucellaID/wiki/How-it-works),
88
+ [validation](https://github.com/duceppemo/BrucellaID/wiki/Validation),
89
+ [troubleshooting](https://github.com/duceppemo/BrucellaID/wiki/Troubleshooting) and the
90
+ [FAQ](https://github.com/duceppemo/BrucellaID/wiki/FAQ).
91
+
92
+ ## Citing
93
+ If you use BrucellaID, please cite it together with the tools and the data it rests on:
94
+
95
+ > Duceppe M-O. BrucellaID: *Brucella* species identification and MLST from sequencing reads.
96
+ > https://github.com/duceppemo/BrucellaID
97
+
98
+ > Ondov BD, Treangen TJ, Melsted P, et al. Mash: fast genome and metagenome distance estimation using
99
+ > MinHash. *Genome Biology* 2016;17:132. https://doi.org/10.1186/s13059-016-0997-x
100
+
101
+ > Bushnell B. BBTools. https://sourceforge.net/projects/bbmap/
102
+
103
+ > Whatmore AM, Perrett LL, MacMillan AP. Characterisation of the genetic diversity of *Brucella* by
104
+ > multilocus sequencing. *BMC Microbiology* 2007;7:34. https://doi.org/10.1186/1471-2180-7-34
105
+
106
+ > Jolley KA, Bray JE, Maiden MCJ. Open-access bacterial population genomics: BIGSdb software, the
107
+ > PubMLST.org website and their applications. *Wellcome Open Research* 2018;3:124.
108
+ > https://doi.org/10.12688/wellcomeopenres.14826.1
109
+
110
+ GitHub's **"Cite this repository"** button gives the citation in APA and BibTeX formats, from
111
+ [`CITATION.cff`](CITATION.cff).
112
+
113
+ ## Related
114
+ * [brucetyper](https://github.com/duceppemo/brucetyper) — *in silico* Bruce-ladder, Suis-ladder, AMOS and
115
+ RB51 junction PCR for *Brucella*, from assemblies. Where BrucellaID gives the species from a distance and
116
+ the sequence type from the alleles, brucetyper reads the assays a laboratory would run on a gel.
117
+ * [seqcounter](https://github.com/duceppemo/seqcounter) — count the occurrences of any reference sequences
118
+ in reads or assemblies.
119
+ * [spoligotyper](https://github.com/duceppemo/spoligotyper) — *in silico* spoligotyping of the
120
+ *M. tuberculosis* complex.
121
+
122
+ ## Contributing
123
+ Bug reports, questions and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
124
+
125
+ ## Author
126
+ Marc-Olivier Duceppe, Canadian Food Inspection Agency (CFIA): marc-olivier.duceppe@inspection.gc.ca
127
+
128
+ ## License
129
+ [MIT](LICENSE)
@@ -0,0 +1,3 @@
1
+ """Identify the Brucella species of a read set with MASH, and type it with MLST."""
2
+
3
+ __version__ = '1.0.0'
@@ -0,0 +1,6 @@
1
+ """Allow running the command with "python -m brucellaid"."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
@@ -0,0 +1,233 @@
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 .errors import BrucellaIDError
15
+ from .mash import DEFAULT_MIN_HASHES, check_mash, sketch_references
16
+ from .mlst import DEFAULT_MIN_READS, alleles_fasta, load_profiles
17
+ from .pipeline import analyse_samples
18
+ from .references import compare_with_sketch, load_references, reference_sketch
19
+ from .report import print_table, run_info, write_reports
20
+ from .samples import find_samples, single_sample
21
+ from .seal import DEFAULT_KMER_SIZE, DEFAULT_MISMATCHES, auto_memory, check_seal
22
+
23
+ log = logging.getLogger('brucellaid')
24
+
25
+ # Expected errors are reported without a traceback
26
+ USER_ERRORS = (BrucellaIDError, OSError)
27
+
28
+ EPILOG = '''examples:
29
+ brucellaid -r1 sample_R1.fastq.gz -r2 sample_R2.fastq.gz -o results/
30
+ brucellaid -r1 sample.fastq.gz -o results/
31
+ brucellaid -i folder_of_fastq/ -o results/
32
+ brucellaid -i folder_of_fastq/ -o results/ -p 4 -t 16 --xlsx
33
+
34
+ documentation: https://github.com/duceppemo/BrucellaID/wiki'''
35
+
36
+
37
+ def setup_logging(verbose=False):
38
+ """Configure the package logger only, so importing brucellaid never alters the root logger."""
39
+ for handler in list(log.handlers):
40
+ log.removeHandler(handler)
41
+ handler = logging.StreamHandler(sys.stderr)
42
+ handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)-7s %(message)s', datefmt='%H:%M:%S'))
43
+ log.addHandler(handler)
44
+ log.setLevel(logging.DEBUG if verbose else logging.INFO)
45
+
46
+
47
+ def positive_int(value):
48
+ try:
49
+ value = int(value)
50
+ except ValueError:
51
+ raise ArgumentTypeError('"{}" is not an integer'.format(value)) from None
52
+ if value < 1:
53
+ raise ArgumentTypeError('must be >= 1')
54
+ return value
55
+
56
+
57
+ def java_memory(value):
58
+ if not value[:-1].isdigit() or value[-1].lower() not in 'mg':
59
+ raise ArgumentTypeError('"{}" is not a Java memory size such as 500m or 2g'.format(value))
60
+ return value
61
+
62
+
63
+ def available_cpus():
64
+ try:
65
+ return len(os.sched_getaffinity(0)) # Respects cgroup/taskset limits on Linux (e.g. SLURM jobs)
66
+ except AttributeError:
67
+ return os.cpu_count() or 1
68
+
69
+
70
+ def format_elapsed(seconds):
71
+ """A readable duration: "1h2m3s"."""
72
+ minutes, seconds = divmod(int(round(seconds)), 60)
73
+ hours, minutes = divmod(minutes, 60)
74
+ days, hours = divmod(hours, 24)
75
+ periods = [('d', days), ('h', hours), ('m', minutes), ('s', seconds)]
76
+ return ''.join('{}{}'.format(value, name) for name, value in periods if value) or '0s'
77
+
78
+
79
+ def build_parser():
80
+ max_cpu = available_cpus()
81
+ parser = ArgumentParser(prog='brucellaid', formatter_class=RawDescriptionHelpFormatter, epilog=EPILOG,
82
+ description='Identify the Brucella species of sequencing reads by their MASH distance\n'
83
+ 'to 16 reference genomes, and type them with the nine-locus MLST scheme.')
84
+ inputs = parser.add_argument_group('input (one sample with -r1, or a folder of samples with -i)')
85
+ source = inputs.add_mutually_exclusive_group(required=True)
86
+ source.add_argument('-r1', '--r1', metavar='FILE',
87
+ help='Reads: single-end, or R1 of paired-end. fastq, gzipped or not.')
88
+ source.add_argument('-i', '--input', metavar='FOLDER',
89
+ help='Folder of fastq files, searched recursively. Each single-end fastq file or pair of '
90
+ 'R1/R2 fastq files is a sample.')
91
+ inputs.add_argument('-r2', '--r2', metavar='FILE',
92
+ help='R2 reads, for paired-end fastq files.')
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
+ output.add_argument('--xlsx', action='store_true',
101
+ help='Also write the legacy vSNP-style report, stat_alignment_summary_<date>.xlsx. Needs '
102
+ 'the xlsxwriter package.')
103
+
104
+ calling = parser.add_argument_group('identification and typing')
105
+ calling.add_argument('--min-hashes', metavar='N', type=int, default=DEFAULT_MIN_HASHES,
106
+ help='Minimum number of the reference\'s 1000 min-hashes the reads must share for a '
107
+ 'species to be reported. Default: {}.'.format(DEFAULT_MIN_HASHES))
108
+ calling.add_argument('--min-reads', metavar='N', type=int, default=DEFAULT_MIN_READS,
109
+ help='Minimum number of reads assigned to an MLST allele for the allele to be called. '
110
+ 'Default: {}.'.format(DEFAULT_MIN_READS))
111
+ calling.add_argument('--no-mlst', action='store_true',
112
+ help='Identify the species only, and skip the MLST typing.')
113
+ calling.add_argument('-k', '--kmer-size', metavar='N', type=positive_int, default=DEFAULT_KMER_SIZE,
114
+ help='K-mer size used to assign the reads to the MLST alleles. '
115
+ 'Default: {}.'.format(DEFAULT_KMER_SIZE))
116
+ calling.add_argument('-m', '--mismatch', metavar='N', type=int, default=DEFAULT_MISMATCHES,
117
+ help='Mismatches allowed between an allele k-mer and a read (maximum Hamming distance). '
118
+ 'Default: {}, as the alleles of a locus differ by a few bases.'
119
+ .format(DEFAULT_MISMATCHES))
120
+
121
+ data = parser.add_argument_group('reference data (the package ships all three)')
122
+ data.add_argument('--sketch', metavar='FILE',
123
+ help='MASH sketch of the reference genomes, instead of the one of the package.')
124
+ data.add_argument('--references', metavar='FILE', dest='references',
125
+ help='Table of the reference genomes and their species, instead of the one of the package.')
126
+ data.add_argument('--alleles', metavar='FILE',
127
+ help='Fasta file of the MLST alleles, instead of the one of the package.')
128
+ data.add_argument('--profiles', metavar='FILE',
129
+ help='Table of the MLST profiles, instead of the one of the package.')
130
+
131
+ other = parser.add_argument_group('performance and other options')
132
+ other.add_argument('-t', '--threads', metavar='N', type=positive_int, default=max_cpu,
133
+ help='Number of threads. Default: all available ({}).'.format(max_cpu))
134
+ other.add_argument('-p', '-j', '--parallel', '--jobs', metavar='N', type=positive_int, default=1, dest='parallel',
135
+ help='With -i: number of samples analysed at the same time, sharing the threads. Default: 1.')
136
+ other.add_argument('--memory', metavar='SIZE', type=java_memory,
137
+ help='Memory for Seal (Java heap size), per job, e.g. 2g. Default: from the size of the '
138
+ 'allele file, within 85%% of the machine.')
139
+ other.add_argument('--verbose', action='store_true', help='Show debug messages, including the commands run.')
140
+ other.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__)
141
+ return parser
142
+
143
+
144
+ def check_arguments(parser, args):
145
+ if args.input and (args.r2 or args.sample):
146
+ parser.error('-r2 and --sample are only used with -r1')
147
+ if args.r1 and args.parallel > 1:
148
+ parser.error('--parallel is only used with -i')
149
+ if args.min_hashes < 1:
150
+ parser.error('--min-hashes must be >= 1')
151
+ if args.min_reads < 1:
152
+ parser.error('--min-reads must be >= 1')
153
+ if args.mismatch < 0:
154
+ parser.error('--mismatch cannot be negative')
155
+ if args.mismatch >= args.kmer_size:
156
+ parser.error('--mismatch ({}) must be smaller than --kmer-size ({})'.format(args.mismatch, args.kmer_size))
157
+ if args.sample is not None and (not args.sample.strip() or '/' in args.sample or args.sample in ('.', '..')):
158
+ parser.error('--sample must be a name, not a path: "{}"'.format(args.sample))
159
+ if args.sample is not None and re.search(r'[\t\r\n]', args.sample):
160
+ parser.error('--sample cannot contain tabs or line breaks (they would break the table)')
161
+
162
+
163
+ def file_bases(path):
164
+ """Rough number of bases in a fasta file: its size, which is close enough to size the Seal heap."""
165
+ try:
166
+ return Path(path).stat().st_size
167
+ except OSError:
168
+ return 0
169
+
170
+
171
+ def main(argv=None):
172
+ parser = build_parser()
173
+ args = parser.parse_args(argv)
174
+ check_arguments(parser, args)
175
+ setup_logging(args.verbose)
176
+ command = shlex.join([parser.prog, *(sys.argv[1:] if argv is None else argv)])
177
+ log.debug('brucellaid %s: %s', __version__, command)
178
+ started = datetime.now().astimezone()
179
+ start = time.monotonic()
180
+ output = Path(args.output).expanduser()
181
+ mlst = not args.no_mlst
182
+ try:
183
+ check_mash()
184
+ if mlst:
185
+ check_seal()
186
+ sketch = reference_sketch(args.sketch)
187
+ references = load_references(args.references)
188
+ alleles = alleles_fasta(args.alleles)
189
+ loci = load_profiles(args.profiles)[0] if mlst else ()
190
+ log.info('%d reference genome(s) in %s', len(sketch_references(sketch)), sketch)
191
+ for message in compare_with_sketch(sketch_references(sketch), references):
192
+ log.warning('%s', message)
193
+ if mlst:
194
+ log.info('%d MLST locus/loci: %s', len(loci), ', '.join(loci))
195
+ if args.input:
196
+ samples = find_samples(args.input, exclude=[output])
197
+ log.info('%d sample(s) found in %s', len(samples), args.input)
198
+ else:
199
+ samples = [single_sample(args.r1, args.r2, name=args.sample)]
200
+ jobs = min(args.parallel, len(samples))
201
+ memory = args.memory or auto_memory(file_bases(alleles), jobs=jobs)
202
+ results = analyse_samples(samples, jobs=jobs, threads=args.threads, sketch=sketch, references=references,
203
+ min_hashes=args.min_hashes, min_reads=args.min_reads, kmer_size=args.kmer_size,
204
+ mismatch=args.mismatch, memory=memory, alleles=args.alleles,
205
+ profiles=args.profiles, mlst=mlst)
206
+ run = run_info(command, {'Input': ' '.join(os.path.abspath(f) for f in (args.input, args.r1, args.r2) if f),
207
+ 'Output folder': str(output.resolve()),
208
+ 'Reference sketch': os.path.abspath(sketch),
209
+ 'MLST alleles': os.path.abspath(alleles) if mlst else None,
210
+ 'Minimum shared hashes': args.min_hashes,
211
+ 'Minimum reads per allele': args.min_reads if mlst else None,
212
+ 'K-mer size': args.kmer_size, 'Mismatches': args.mismatch,
213
+ 'MLST': mlst, 'Threads': args.threads, 'Parallel samples': jobs,
214
+ 'Seal memory': memory}, started)
215
+ rows, reports = write_reports(results, references, output, started, command=command, run=run,
216
+ xlsx=args.xlsx)
217
+ except USER_ERRORS as e:
218
+ log.error('%s', e)
219
+ log.debug('Traceback:', exc_info=True) # Shown with --verbose
220
+ sys.exit(1)
221
+ except KeyboardInterrupt:
222
+ log.error('Interrupted')
223
+ sys.exit(130)
224
+
225
+ # The results go to stdout, the messages to stderr: "brucellaid ... > results.tsv" works
226
+ print_table(rows)
227
+ for report in reports:
228
+ log.info('Report saved in %s', report)
229
+ log.info('Done in %s', format_elapsed(time.monotonic() - start))
230
+ failed = [r.sample for r in results if r.error]
231
+ if failed:
232
+ log.error('%d of %d sample(s) failed: %s', len(failed), len(results), ', '.join(failed))
233
+ sys.exit(1)
@@ -0,0 +1,34 @@
1
+ # Reference data
2
+
3
+ Four files, about 290 kB. `brucellaid --sketch/--references/--alleles/--profiles` take files of your own
4
+ instead of these.
5
+
6
+ | File | What it is | Where it comes from |
7
+ | --- | --- | --- |
8
+ | `references.msh` | MASH sketch of 16 *Brucella* reference genomes (k = 21, 1000 hashes) | The *Brucella* part of [vSNP](https://github.com/USDA-VS/vsnp)'s reference set, as `vsnp_reference.msh` |
9
+ | `references.tsv` | What each genome of the sketch is: vSNP species code, species, biovar, strain, NCBI accessions | Written for BrucellaID, from the sketch and vSNP's `species_accessions.csv` |
10
+ | `mlst_alleles.fasta` | 309 alleles of the nine MLST loci | [PubMLST](https://pubmlst.org/organisms/brucella-spp), "MLST (9 locus)", downloaded 2026-10-31 |
11
+ | `mlst_profiles.tsv` | 122 sequence types | The same scheme, the same day |
12
+
13
+ The sketch ID of a genome is the name of the file it was built from, which is what `mash dist` reports and
14
+ what `references.tsv` keys on.
15
+
16
+ Two genomes of the sketch, `Bceti1Cudo.fasta` (*B. ceti* strain Cudo) and `B-REF-BS4-40.fasta` (*B. suis*
17
+ bv. 4 strain 40), are distributed by vSNP under its own names rather than as accessions, so their
18
+ `accessions` column is empty and `scripts/make_reference_data.py` cannot download them.
19
+
20
+ ## Rebuilding
21
+ ```bash
22
+ python3 scripts/make_mlst_data.py # the two MLST files, from PubMLST
23
+ python3 scripts/make_reference_data.py --genomes /tmp/refs --out /tmp/references.msh # the sketch
24
+ ```
25
+ PubMLST serves the data submitted up to a cut-off date unless you authenticate; the script prints the date
26
+ it was given. The MLST data here was downloaded on 2026-10-31, with data submitted up to 2024-12-31.
27
+
28
+ The scheme is Whatmore, Perrett and MacMillan (2007),
29
+ [doi:10.1186/1471-2180-7-34](https://doi.org/10.1186/1471-2180-7-34). PubMLST is maintained at the
30
+ University of Oxford; publications using its data should cite Jolley, Bray and Maiden (2018),
31
+ [doi:10.12688/wellcomeopenres.14826.1](https://doi.org/10.12688/wellcomeopenres.14826.1).
32
+
33
+ A sketch must keep MASH's defaults (k = 21, 1000 hashes): the threshold of 150 shared hashes
34
+ (`--min-hashes`) is read against that sketch size.