kleborate 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. kleborate/__init__.py +0 -0
  2. kleborate/__main__.py +497 -0
  3. kleborate/modules/__init__.py +0 -0
  4. kleborate/modules/enterobacterales__species/__init__.py +0 -0
  5. kleborate/modules/enterobacterales__species/data/species_mash_sketches.msh +0 -0
  6. kleborate/modules/enterobacterales__species/enterobacterales__species.py +110 -0
  7. kleborate/modules/enterobacterales__species/test_enterobacterales__species.py +223 -0
  8. kleborate/modules/escherichia__mlst_achtman/__init__.py +0 -0
  9. kleborate/modules/escherichia__mlst_achtman/data/adk.fasta +3562 -0
  10. kleborate/modules/escherichia__mlst_achtman/data/download.sh +12 -0
  11. kleborate/modules/escherichia__mlst_achtman/data/fumC.fasta +4800 -0
  12. kleborate/modules/escherichia__mlst_achtman/data/gyrB.fasta +3052 -0
  13. kleborate/modules/escherichia__mlst_achtman/data/icd.fasta +3820 -0
  14. kleborate/modules/escherichia__mlst_achtman/data/mdh.fasta +3002 -0
  15. kleborate/modules/escherichia__mlst_achtman/data/profiles.tsv +14984 -0
  16. kleborate/modules/escherichia__mlst_achtman/data/purA.fasta +2758 -0
  17. kleborate/modules/escherichia__mlst_achtman/data/recA.fasta +2342 -0
  18. kleborate/modules/escherichia__mlst_achtman/escherichia__mlst_achtman.py +85 -0
  19. kleborate/modules/escherichia__mlst_achtman/test_escherichia__mlst_achtman.py +161 -0
  20. kleborate/modules/escherichia__mlst_pasteur/__init__.py +0 -0
  21. kleborate/modules/escherichia__mlst_pasteur/data/dinB.fasta +460 -0
  22. kleborate/modules/escherichia__mlst_pasteur/data/download.sh +13 -0
  23. kleborate/modules/escherichia__mlst_pasteur/data/icdA.fasta +870 -0
  24. kleborate/modules/escherichia__mlst_pasteur/data/pabB.fasta +498 -0
  25. kleborate/modules/escherichia__mlst_pasteur/data/polB.fasta +532 -0
  26. kleborate/modules/escherichia__mlst_pasteur/data/profiles.tsv +1479 -0
  27. kleborate/modules/escherichia__mlst_pasteur/data/putP.fasta +554 -0
  28. kleborate/modules/escherichia__mlst_pasteur/data/trpA.fasta +574 -0
  29. kleborate/modules/escherichia__mlst_pasteur/data/trpB.fasta +520 -0
  30. kleborate/modules/escherichia__mlst_pasteur/data/uidA.fasta +596 -0
  31. kleborate/modules/escherichia__mlst_pasteur/escherichia__mlst_pasteur.py +85 -0
  32. kleborate/modules/escherichia__mlst_pasteur/test_escherichia__mlst_pasteur.py +161 -0
  33. kleborate/modules/general__contig_stats/__init__.py +0 -0
  34. kleborate/modules/general__contig_stats/data/species_specification.txt +9 -0
  35. kleborate/modules/general__contig_stats/general__contig_stats.py +130 -0
  36. kleborate/modules/general__contig_stats/test_general__contig_stats.py +191 -0
  37. kleborate/modules/klebsiella__abst/__init__.py +0 -0
  38. kleborate/modules/klebsiella__abst/data/download.sh +10 -0
  39. kleborate/modules/klebsiella__abst/data/iucA.fasta +1650 -0
  40. kleborate/modules/klebsiella__abst/data/iucB.fasta +629 -0
  41. kleborate/modules/klebsiella__abst/data/iucC.fasta +1413 -0
  42. kleborate/modules/klebsiella__abst/data/iucD.fasta +897 -0
  43. kleborate/modules/klebsiella__abst/data/iutA.fasta +1824 -0
  44. kleborate/modules/klebsiella__abst/data/profiles.tsv +100 -0
  45. kleborate/modules/klebsiella__abst/klebsiella__abst.py +106 -0
  46. kleborate/modules/klebsiella__abst/test_klebsiella__abst.py +149 -0
  47. kleborate/modules/klebsiella__cbst/__init__.py +0 -0
  48. kleborate/modules/klebsiella__cbst/data/clbA.fasta +140 -0
  49. kleborate/modules/klebsiella__cbst/data/clbB.fasta +11502 -0
  50. kleborate/modules/klebsiella__cbst/data/clbC.fasta +945 -0
  51. kleborate/modules/klebsiella__cbst/data/clbD.fasta +176 -0
  52. kleborate/modules/klebsiella__cbst/data/clbE.fasta +54 -0
  53. kleborate/modules/klebsiella__cbst/data/clbF.fasta +300 -0
  54. kleborate/modules/klebsiella__cbst/data/clbG.fasta +230 -0
  55. kleborate/modules/klebsiella__cbst/data/clbH.fasta +2349 -0
  56. kleborate/modules/klebsiella__cbst/data/clbI.fasta +1248 -0
  57. kleborate/modules/klebsiella__cbst/data/clbL.fasta +260 -0
  58. kleborate/modules/klebsiella__cbst/data/clbM.fasta +250 -0
  59. kleborate/modules/klebsiella__cbst/data/clbN.fasta +2072 -0
  60. kleborate/modules/klebsiella__cbst/data/clbO.fasta +798 -0
  61. kleborate/modules/klebsiella__cbst/data/clbP.fasta +378 -0
  62. kleborate/modules/klebsiella__cbst/data/clbQ.fasta +84 -0
  63. kleborate/modules/klebsiella__cbst/data/download.sh +20 -0
  64. kleborate/modules/klebsiella__cbst/data/profiles.tsv +83 -0
  65. kleborate/modules/klebsiella__cbst/klebsiella__cbst.py +110 -0
  66. kleborate/modules/klebsiella__cbst/test_klebsiella__cbst.py +144 -0
  67. kleborate/modules/klebsiella__rmpa2/data/rmpA2.fasta +122 -0
  68. kleborate/modules/klebsiella__rmpa2/klebsiella__rmpa2.py +84 -0
  69. kleborate/modules/klebsiella__rmpa2/rmpa2.py +40 -0
  70. kleborate/modules/klebsiella__rmst/__init__.py +0 -0
  71. kleborate/modules/klebsiella__rmst/data/download.sh +8 -0
  72. kleborate/modules/klebsiella__rmst/data/profiles.tsv +172 -0
  73. kleborate/modules/klebsiella__rmst/data/rmpA.fasta +1116 -0
  74. kleborate/modules/klebsiella__rmst/data/rmpC.fasta +360 -0
  75. kleborate/modules/klebsiella__rmst/data/rmpD.fasta +308 -0
  76. kleborate/modules/klebsiella__rmst/klebsiella__rmst.py +102 -0
  77. kleborate/modules/klebsiella__rmst/test_klebsiella__rmst.py +195 -0
  78. kleborate/modules/klebsiella__smst/__init__.py +0 -0
  79. kleborate/modules/klebsiella__smst/data/download.sh +9 -0
  80. kleborate/modules/klebsiella__smst/data/iroB.fasta +659 -0
  81. kleborate/modules/klebsiella__smst/data/iroC.fasta +3889 -0
  82. kleborate/modules/klebsiella__smst/data/iroD.fasta +594 -0
  83. kleborate/modules/klebsiella__smst/data/iroN.fasta +1368 -0
  84. kleborate/modules/klebsiella__smst/data/profiles.tsv +50 -0
  85. kleborate/modules/klebsiella__smst/klebsiella__smst.py +103 -0
  86. kleborate/modules/klebsiella__smst/test_klebsiella__smst.py +146 -0
  87. kleborate/modules/klebsiella__ybst/__init__.py +0 -0
  88. kleborate/modules/klebsiella__ybst/data/download.sh +17 -0
  89. kleborate/modules/klebsiella__ybst/data/fyuA.fasta +11060 -0
  90. kleborate/modules/klebsiella__ybst/data/irp1.fasta +101736 -0
  91. kleborate/modules/klebsiella__ybst/data/irp2.fasta +54765 -0
  92. kleborate/modules/klebsiella__ybst/data/profiles.tsv +600 -0
  93. kleborate/modules/klebsiella__ybst/data/ybtA.fasta +1852 -0
  94. kleborate/modules/klebsiella__ybst/data/ybtE.fasta +5068 -0
  95. kleborate/modules/klebsiella__ybst/data/ybtP.fasta +5460 -0
  96. kleborate/modules/klebsiella__ybst/data/ybtQ.fasta +6942 -0
  97. kleborate/modules/klebsiella__ybst/data/ybtS.fasta +3427 -0
  98. kleborate/modules/klebsiella__ybst/data/ybtT.fasta +1796 -0
  99. kleborate/modules/klebsiella__ybst/data/ybtU.fasta +2840 -0
  100. kleborate/modules/klebsiella__ybst/data/ybtX.fasta +3633 -0
  101. kleborate/modules/klebsiella__ybst/klebsiella__ybst.py +106 -0
  102. kleborate/modules/klebsiella__ybst/test_klebsiella__ybst.py +162 -0
  103. kleborate/modules/klebsiella_oxytoca_complex__mlst/__init__.py +0 -0
  104. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/download.sh +12 -0
  105. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/gapA.fasta +104 -0
  106. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/infB.fasta +124 -0
  107. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/mdh.fasta +172 -0
  108. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/pgi.fasta +174 -0
  109. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/phoE.fasta +258 -0
  110. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/profiles.tsv +635 -0
  111. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/rpoB.fasta +118 -0
  112. kleborate/modules/klebsiella_oxytoca_complex__mlst/data/tonB.fasta +240 -0
  113. kleborate/modules/klebsiella_oxytoca_complex__mlst/klebsiella_oxytoca_complex__mlst.py +82 -0
  114. kleborate/modules/klebsiella_oxytoca_complex__mlst/test_klebsiella_oxytoca_complex__mlst.py +143 -0
  115. kleborate/modules/klebsiella_pneumo_complex__amr/__init__.py +0 -0
  116. kleborate/modules/klebsiella_pneumo_complex__amr/col_mutations.py +85 -0
  117. kleborate/modules/klebsiella_pneumo_complex__amr/data/CARD_AMR_clustered.csv +3136 -0
  118. kleborate/modules/klebsiella_pneumo_complex__amr/data/CARD_v3.2.9.fasta +12940 -0
  119. kleborate/modules/klebsiella_pneumo_complex__amr/data/MgrB_and_PmrB.fasta +4 -0
  120. kleborate/modules/klebsiella_pneumo_complex__amr/data/OmpK.fasta +4 -0
  121. kleborate/modules/klebsiella_pneumo_complex__amr/data/QRDR_120.fasta +4 -0
  122. kleborate/modules/klebsiella_pneumo_complex__amr/klebsiella_pneumo_complex__amr.py +105 -0
  123. kleborate/modules/klebsiella_pneumo_complex__amr/omp_mutations.py +78 -0
  124. kleborate/modules/klebsiella_pneumo_complex__amr/qrdr_mutations.py +67 -0
  125. kleborate/modules/klebsiella_pneumo_complex__amr/shv_mutations.py +190 -0
  126. kleborate/modules/klebsiella_pneumo_complex__amr/test/test_mgrb_pmrb_klebsiella_pneumo_complex__amr.py +72 -0
  127. kleborate/modules/klebsiella_pneumo_complex__amr/test/test_omp_klebsiella_pneumo_complex__amr.py +78 -0
  128. kleborate/modules/klebsiella_pneumo_complex__amr/test/test_qrdr_klebsiella_pneumo_complex__amr.py +64 -0
  129. kleborate/modules/klebsiella_pneumo_complex__amr/test/test_res_aac_klebsiella_pneumo_complex__amr.py +75 -0
  130. kleborate/modules/klebsiella_pneumo_complex__amr/test/test_shv_klebsiella_pneumo_complex__amr.py +176 -0
  131. kleborate/modules/klebsiella_pneumo_complex__amr/test/test_tem_klebsiella_pneumo_complex__amr.py +46 -0
  132. kleborate/modules/klebsiella_pneumo_complex__kaptive/klebsiella_pneumo_complex__kaptive.py +175 -0
  133. kleborate/modules/klebsiella_pneumo_complex__mlst/__init__.py +0 -0
  134. kleborate/modules/klebsiella_pneumo_complex__mlst/data/download.sh +12 -0
  135. kleborate/modules/klebsiella_pneumo_complex__mlst/data/gapA.fasta +710 -0
  136. kleborate/modules/klebsiella_pneumo_complex__mlst/data/infB.fasta +544 -0
  137. kleborate/modules/klebsiella_pneumo_complex__mlst/data/mdh.fasta +1004 -0
  138. kleborate/modules/klebsiella_pneumo_complex__mlst/data/pgi.fasta +852 -0
  139. kleborate/modules/klebsiella_pneumo_complex__mlst/data/phoE.fasta +1322 -0
  140. kleborate/modules/klebsiella_pneumo_complex__mlst/data/profiles.tsv +7136 -0
  141. kleborate/modules/klebsiella_pneumo_complex__mlst/data/rpoB.fasta +768 -0
  142. kleborate/modules/klebsiella_pneumo_complex__mlst/data/tonB.fasta +1932 -0
  143. kleborate/modules/klebsiella_pneumo_complex__mlst/klebsiella_pneumo_complex__mlst.py +80 -0
  144. kleborate/modules/klebsiella_pneumo_complex__mlst/test_klebsiella_pneumo_complex__mlst.py +140 -0
  145. kleborate/modules/klebsiella_pneumo_complex__resistance_class_count/__init__.py +0 -0
  146. kleborate/modules/klebsiella_pneumo_complex__resistance_class_count/klebsiella_pneumo_complex__resistance_class_count.py +76 -0
  147. kleborate/modules/klebsiella_pneumo_complex__resistance_gene_count/__init__.py +0 -0
  148. kleborate/modules/klebsiella_pneumo_complex__resistance_gene_count/klebsiella_pneumo_complex__resistance_gene_count.py +82 -0
  149. kleborate/modules/klebsiella_pneumo_complex__resistance_score/__init__.py +0 -0
  150. kleborate/modules/klebsiella_pneumo_complex__resistance_score/klebsiella_pneumo_complex__resistance_score.py +98 -0
  151. kleborate/modules/klebsiella_pneumo_complex__virulence_score/__init__.py +0 -0
  152. kleborate/modules/klebsiella_pneumo_complex__virulence_score/klebsiella_pneumo_complex__virulence_score.py +106 -0
  153. kleborate/modules/klebsiella_pneumo_complex__virulence_score/test_klebsiella_pneumo_complex__virulence_score.py +154 -0
  154. kleborate/modules/klebsiella_pneumo_complex__wzi/data/wzi.fasta +4994 -0
  155. kleborate/modules/klebsiella_pneumo_complex__wzi/data/wzi.txt +556 -0
  156. kleborate/modules/klebsiella_pneumo_complex__wzi/klebsiella_pneumo_complex__wzi.py +85 -0
  157. kleborate/modules/klebsiella_pneumo_complex__wzi/wzi.py +163 -0
  158. kleborate/shared/__init__.py +0 -0
  159. kleborate/shared/alignment.py +331 -0
  160. kleborate/shared/help_formatter.py +175 -0
  161. kleborate/shared/misc.py +88 -0
  162. kleborate/shared/mlst.py +204 -0
  163. kleborate/shared/multi_mlst.py +177 -0
  164. kleborate/shared/resMinimap.py +201 -0
  165. kleborate/shared/species_defs.py +62 -0
  166. kleborate/version.py +20 -0
  167. kleborate-3.0.0.dist-info/LICENSE +674 -0
  168. kleborate-3.0.0.dist-info/METADATA +731 -0
  169. kleborate-3.0.0.dist-info/RECORD +172 -0
  170. kleborate-3.0.0.dist-info/WHEEL +5 -0
  171. kleborate-3.0.0.dist-info/entry_points.txt +2 -0
  172. kleborate-3.0.0.dist-info/top_level.txt +1 -0
kleborate/__init__.py ADDED
File without changes
kleborate/__main__.py ADDED
@@ -0,0 +1,497 @@
1
+ """
2
+ Copyright 2024 Mary Maranga, Kat Holt, Ryan Wick
3
+ https://github.com/klebgenomics/KleborateModular/
4
+
5
+ This file is part of Kleborate. Kleborate is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by the Free Software Foundation,
7
+ either version 3 of the License, or (at your option) any later version. Kleborate is distributed in
8
+ the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
10
+ details. You should have received a copy of the GNU General Public License along with Kleborate. If
11
+ not, see <https://www.gnu.org/licenses/>.
12
+ """
13
+
14
+ import argparse
15
+ import graphlib
16
+ import gzip
17
+ import importlib
18
+ import importlib.metadata
19
+ import os
20
+ import pathlib
21
+ import re
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ import tempfile
26
+ import textwrap
27
+ import uuid
28
+
29
+ from .shared.help_formatter import MyParser, MyHelpFormatter
30
+ from .shared.misc import get_compression_type, load_fasta,reverse_complement
31
+ from .shared.species_defs import is_kp_complex, is_ko_complex, is_escherichia
32
+
33
+
34
+ def parse_arguments(args, all_module_names, modules):
35
+ """
36
+ This function does the CLI argument parsing for Kleborate. Module-specific arguments are added
37
+ by each module's add_cli_options function.
38
+ """
39
+ parser = MyParser(description='Kleborate: a tool for characterising virulence and resistance '
40
+ 'in pathogen assemblies',
41
+ formatter_class=MyHelpFormatter, add_help=False, epilog=paper_refs())
42
+
43
+ if '--helpall' in args or '--allhelp' in args or '--all_help' in args:
44
+ args.append('--help_all')
45
+
46
+ io_args = parser.add_argument_group('Input/output')
47
+ io_args.add_argument('-a', '--assemblies', nargs='+', type=str,
48
+ help='FASTA file(s) for assemblies')
49
+ io_args.add_argument('-o', '--outfile', type=str, default='Kleborate_results.txt',
50
+ help='File for detailed output (default: Kleborate_results.txt)')
51
+
52
+ module_args = parser.add_argument_group('Modules')
53
+ module_args.add_argument('--list_modules', action='store_true',
54
+ help='Print a list of all available modules and then quit')
55
+ module_args.add_argument('-p', '--preset', type=str,
56
+ help=f'Module presets, choose from: ' + ', '.join(get_presets()))
57
+ module_args.add_argument('-m', '--modules', type=str,
58
+ help='Comma-delimited list of Kleborate modules to use')
59
+
60
+ add_module_cli_arguments(parser, args, all_module_names, modules)
61
+
62
+ help_args = parser.add_argument_group('Help')
63
+ help_args.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
64
+ help='Show this help message and exit')
65
+ help_args.add_argument('--help_all', action='help',
66
+ help='Show a help message with all module options')
67
+ help_args.add_argument('--version', action='version', version=f'Kleborate v{get_version()}',
68
+ help="Show program's version number and exit")
69
+
70
+ if not args:
71
+ parser.print_help(file=sys.stderr)
72
+ sys.exit(1)
73
+
74
+ return parser.parse_args(args)
75
+
76
+
77
+ def main():
78
+ all_module_names, modules = import_modules()
79
+ args = parse_arguments(sys.argv[1:], all_module_names, modules)
80
+ print_modules(args, all_module_names, modules)
81
+
82
+ module_names, check_module_list, pass_modules = get_used_module_names(args, all_module_names, get_presets())
83
+
84
+ # Define preset_check_modules
85
+ preset_check_modules = []
86
+ if args.preset:
87
+ presets = get_presets()
88
+ preset_check_modules = [module for module, _ in presets[args.preset]['check']]
89
+
90
+ module_names, module_run_order, external_programs = check_modules(args, modules, module_names, check_module_list, pass_modules)
91
+ check_assemblies(args)
92
+
93
+ full_headers, stdout_headers = get_headers(module_names, modules)
94
+ output_headers(full_headers, stdout_headers, args.outfile)
95
+
96
+ for assembly in args.assemblies:
97
+ with tempfile.TemporaryDirectory() as temp_dir:
98
+ unzipped_assembly = gunzip_assembly_if_necessary(assembly, temp_dir)
99
+ minimap2_index = build_minimap2_index(assembly, unzipped_assembly, external_programs, temp_dir)
100
+ results = {'strain': get_strain_name(assembly)}
101
+
102
+ pass_check = True # default, assume no check and run all modules
103
+
104
+ # if we have 'check' modules in the preset, run these
105
+ if args.preset and len(check_module_list) > 0:
106
+ for module, check in presets[args.preset]['check']:
107
+ try:
108
+ module_results = modules[module].get_results(unzipped_assembly, minimap2_index, args, results)
109
+
110
+ results.update({f'{module}__{header}': result for header, result in module_results.items()})
111
+ check_function = globals()[check]
112
+
113
+ if not check_function(module_results):
114
+ pass_check = False
115
+ print(f"Assembly {assembly} failed in check {check}. Continuing with next assembly.")
116
+ break # Exit the for loop since this assembly failed the check
117
+
118
+ except Exception as e:
119
+ print(f"Error encountered while processing {assembly} with {module}: {e}. Continuing with next assembly.")
120
+ pass_check = False
121
+ break # Exit the for loop since an error occurred
122
+
123
+ # proceed through all other modules
124
+ if pass_check:
125
+ for module in module_run_order:
126
+ if module not in preset_check_modules:
127
+ module_results = modules[module].get_results(unzipped_assembly, minimap2_index, args, results)
128
+
129
+ results.update({f'{module}__{header}': result for header, result in module_results.items()})
130
+
131
+ # write results
132
+ output_results(full_headers, stdout_headers, args.outfile, results)
133
+
134
+
135
+ # def main():
136
+ # all_module_names, modules = import_modules()
137
+ # args = parse_arguments(sys.argv[1:], all_module_names, modules)
138
+ # print_modules(args, all_module_names, modules)
139
+ # module_names, check_module_list, pass_modules = get_used_module_names(args, all_module_names, get_presets())
140
+ # module_names, module_run_order, external_programs = check_modules(args, modules, module_names, check_module_list, pass_modules)
141
+ # check_assemblies(args)
142
+
143
+ # full_headers, stdout_headers = get_headers(module_names, modules)
144
+ # output_headers(full_headers, stdout_headers, args.outfile)
145
+
146
+ # # Define preset_check_modules based on presets
147
+ # presets = get_presets()
148
+ # preset_check_modules = [module for module, _ in presets[args.preset]['check']]
149
+
150
+ # for assembly in args.assemblies:
151
+ # with tempfile.TemporaryDirectory() as temp_dir:
152
+ # unzipped_assembly = gunzip_assembly_if_necessary(assembly, temp_dir)
153
+ # minimap2_index = build_minimap2_index(assembly, unzipped_assembly, external_programs, temp_dir)
154
+ # # results = {'assembly': assembly}
155
+ # results = {'strain': get_strain_name(assembly)}
156
+
157
+ # pass_check = True # default, assume no check and run all modules
158
+
159
+ # # if we have 'check' modules in the preset, run these and see if we pass
160
+ # if len(check_module_list) > 0:
161
+
162
+ # for module, check in presets[args.preset]['check']:
163
+ # try:
164
+ # module_results = modules[module].get_results(unzipped_assembly, minimap2_index, args, results)
165
+
166
+ # results.update({f'{module}__{header}': result for header, result in module_results.items()})
167
+ # check_function = globals()[check]
168
+
169
+ # if not check_function(module_results):
170
+ # pass_check = False
171
+ # print(f"Assembly {assembly} failed in check {check}. Continuing with next assembly.")
172
+ # break # Exit the for loop since this assembly failed the check
173
+
174
+ # except Exception as e:
175
+ # print(f"Error encountered while processing {assembly} with {module}: {e}. Continuing with next assembly.")
176
+ # pass_check = False
177
+ # break # Exit the for loop since an error occurred
178
+
179
+ # # proceed through all other modules
180
+ # if pass_check:
181
+ # for module in module_run_order:
182
+ # if module not in preset_check_modules:
183
+ # module_results = modules[module].get_results(unzipped_assembly, minimap2_index, args, results)
184
+
185
+ # results.update({f'{module}__{header}': result for header, result in module_results.items()})
186
+
187
+ # # write results
188
+ # output_results(full_headers, stdout_headers, args.outfile, results)
189
+
190
+
191
+ def print_modules(args, all_module_names, modules):
192
+ if args.list_modules:
193
+ print()
194
+ print('Available modules for Kleborate')
195
+ print('-------------------------------')
196
+ terminal_width = shutil.get_terminal_size().columns
197
+ end_formatting, bold = '\033[0m', '\033[1m'
198
+ for m in all_module_names:
199
+ description = modules[m].description()
200
+ text = f'{bold}{m}{end_formatting}: {description}'
201
+ print('\n'.join(textwrap.wrap(text, width=terminal_width - 1)))
202
+ print()
203
+ sys.exit(0)
204
+ elif not args.assemblies:
205
+ sys.exit('Error: you must provide one or more assembly files using --assemblies')
206
+
207
+
208
+
209
+ def get_presets():
210
+ kpsc_modules = {
211
+ 'check': [('enterobacterales__species', 'is_kp_complex')],
212
+ 'pass': [
213
+ 'general__contig_stats','klebsiella_pneumo_complex__mlst',
214
+ 'klebsiella__ybst', 'klebsiella__cbst', 'klebsiella__abst', 'klebsiella__smst', 'klebsiella__rmst', 'klebsiella_pneumo_complex__virulence_score',
215
+ 'klebsiella__rmpa2','klebsiella_pneumo_complex__amr', 'klebsiella_pneumo_complex__resistance_score', 'klebsiella_pneumo_complex__resistance_class_count',
216
+ 'klebsiella_pneumo_complex__resistance_gene_count', 'klebsiella_pneumo_complex__wzi','klebsiella_pneumo_complex__kaptive'
217
+ ]
218
+ }
219
+
220
+ kosc_modules = {
221
+ 'check': [('enterobacterales__species', 'is_ko_complex')],
222
+ 'pass': [
223
+ 'general__contig_stats',
224
+ 'klebsiella_oxytoca_complex__mlst', 'klebsiella__ybst', 'klebsiella__cbst', 'klebsiella__abst', 'klebsiella__smst', 'klebsiella__rmst','klebsiella__rmpa2'
225
+ ]
226
+ }
227
+
228
+ escherichia_modules = {
229
+ 'check': [('enterobacterales__species', 'is_escherichia')],
230
+ 'pass': [
231
+ 'general__contig_stats',
232
+ 'escherichia__mlst_achtman', 'escherichia__mlst_pasteur'
233
+ ]
234
+ }
235
+
236
+ return {
237
+ 'kpsc': kpsc_modules,
238
+ 'kosc': kosc_modules,
239
+ 'escherichia': escherichia_modules
240
+ }
241
+
242
+
243
+ def add_module_cli_arguments(parser, args, all_module_names, modules):
244
+ """
245
+ This function add CLI argument for modules. Each modules that has options gets its own argument
246
+ group. These are only displayed in the help text if the user used --help_all, otherwise they
247
+ are hidden.
248
+ """
249
+ for m in all_module_names:
250
+ group = modules[m].add_cli_options(parser)
251
+ if '--help_all' not in args and group is not None:
252
+ for a in group._group_actions:
253
+ a.help = argparse.SUPPRESS
254
+
255
+
256
+ def get_used_module_names(args, all_module_names, presets):
257
+ if args.preset is None and args.modules is None:
258
+ sys.exit('Error: either --preset or --modules is required')
259
+
260
+ # Initialize empty lists for module names, check modules, and pass modules
261
+ module_names = []
262
+ check_modules = []
263
+ pass_modules = []
264
+
265
+ if args.preset:
266
+ if args.preset not in presets:
267
+ sys.exit(f'Error: {args.preset} is not a valid preset')
268
+
269
+ # Assuming presets[args.preset] is a dictionary with 'check' and 'pass' keys
270
+ check_modules = [module[0] for module in presets[args.preset].get('check', [])] # Extract module names from check modules
271
+ pass_modules = presets[args.preset].get('pass', []) # Directly assign pass modules
272
+
273
+ module_names += check_modules + pass_modules # Combine check and pass modules for the overall list
274
+
275
+ if args.modules:
276
+ for m in args.modules.split(','):
277
+ if m not in all_module_names:
278
+ sys.exit(f'Error: {m} is not a valid module name')
279
+ if m not in module_names:
280
+ module_names.append(m)
281
+
282
+ return module_names, check_modules, pass_modules
283
+
284
+
285
+ def get_all_module_names():
286
+ """
287
+ Looks for all Kleborate modules and returns their names. To qualify as a module, it must be in
288
+ the 'modules' directory, in a subdirectory that matches the filename. For example:
289
+ * modules/contig_stats/contig_stats.py <- is a module
290
+ * modules/kpsc_mlst/kpsc_mlst.py <- is a module
291
+ * modules/contig_stats/test_contig_stats.py <- not a module
292
+
293
+ """
294
+ module_dir = pathlib.Path(__file__).parents[0] / 'modules'
295
+ module_names = []
296
+ for module_file in module_dir.glob('*/*.py'):
297
+ dir_name = module_file.parts[-2]
298
+ if module_file.parts[-1][:-3] == dir_name:
299
+ module_names.append(dir_name)
300
+ if 'template' in module_names:
301
+ module_names.remove('template')
302
+ return sorted(module_names)
303
+
304
+
305
+ def get_strain_name(full_path):
306
+ filename = os.path.split(full_path)[1]
307
+ if filename.endswith('_temp_decompress.fasta'):
308
+ filename = filename[:-22]
309
+ if filename.endswith('.gz'):
310
+ filename = filename[:-3]
311
+ return os.path.splitext(filename)[0]
312
+
313
+ def import_modules():
314
+ """
315
+ This function imports all Kleborate modules (whether or not they are used in this run).
316
+ """
317
+ all_module_names = get_all_module_names()
318
+ modules = {}
319
+ for m in all_module_names:
320
+ modules[m] = importlib.import_module(f'..modules.{m}.{m}', __name__)
321
+ return all_module_names, modules
322
+
323
+
324
+ def check_modules(args, modules, module_names, preset_check_modules, preset_pass_modules):
325
+ """
326
+ This function checks the options, prerequisites external requirements of the used modules. If
327
+ any fail, the program will quit with an error. It returns:
328
+ * A list of all module names. Probably the same list as given, but if any prerequisite modules
329
+ were missing, they've been added to the end.
330
+ * A topologically sorted list of module names. This is the run order so each module will have
331
+ its prerequisites run first.
332
+ * A list of all external programs used.
333
+ """
334
+ all_external_programs = set()
335
+
336
+ for m in module_names:
337
+ modules[m].check_cli_options(args)
338
+ all_external_programs.update(modules[m].check_external_programs())
339
+
340
+ new_module_names = module_names.copy()
341
+ for m in module_names:
342
+ for prereq in modules[m].prerequisite_modules():
343
+ if prereq not in new_module_names:
344
+ new_module_names.append(prereq)
345
+ module_names = new_module_names
346
+
347
+ dependency_graph = {m: modules[m].prerequisite_modules() for m in module_names}
348
+
349
+ return module_names, get_run_order(dependency_graph), sorted(all_external_programs)
350
+
351
+
352
+ def get_run_order(dependency_graph):
353
+ try:
354
+ ts = graphlib.TopologicalSorter(dependency_graph)
355
+ return list(ts.static_order())
356
+ except graphlib.CycleError:
357
+ sys.exit('Error: module dependency graph contains a cycle')
358
+
359
+
360
+ def check_assemblies(args):
361
+ """
362
+ This function does a quick check to make sure that the input assemblies look good.
363
+ """
364
+ for assembly in args.assemblies:
365
+ if os.path.isdir(assembly):
366
+ sys.exit('Error: ' + assembly + ' is a directory (please specify assembly files)')
367
+ if not os.path.isfile(assembly):
368
+ sys.exit('Error: could not find ' + assembly)
369
+ fasta = load_fasta(assembly)
370
+ if len(fasta) < 1:
371
+ sys.exit('Error: invalid FASTA file: ' + assembly)
372
+ for _, seq in fasta:
373
+ if len(seq) == 0:
374
+ sys.exit('Error: invalid FASTA file (contains a zero-length sequence): ' + assembly)
375
+
376
+ def get_headers(module_names, modules):
377
+ """
378
+ This function returns three lists of headers:
379
+ * top_headers: for Kleborate's output file, shows module names
380
+ * full_headers: for Kleborate's output file, show column names
381
+ * stdout_headers: for Kleborate's stdout
382
+
383
+ Each used module contributes headers to these lists. To ensure that there aren't any duplicate
384
+ headers, the module name is added before each header in full_headers and stdout_header,
385
+ separated by a double-underscore.
386
+ """
387
+ _,full_headers, stdout_headers = [''], ['strain'], ['strain']
388
+ for module_name in module_names:
389
+ module_full, module_stdout = modules[module_name].get_headers()
390
+ #top_headers.append(module_name)
391
+ #top_headers += [''] * (len(module_full) - 1)
392
+ full_headers += [f'{module_name}__{h}' for h in module_full]
393
+ stdout_headers += [f'{module_name}__{h}' for h in module_stdout]
394
+ return full_headers, stdout_headers
395
+
396
+
397
+ def gunzip_assembly_if_necessary(assembly, temp_dir):
398
+ if get_compression_type(assembly) == 'gz':
399
+ unzipped_assembly = pathlib.Path(temp_dir) / (uuid.uuid4().hex + '.fasta')
400
+ decompress_file(assembly, unzipped_assembly)
401
+ return unzipped_assembly
402
+ else:
403
+ return assembly
404
+
405
+
406
+ def build_minimap2_index(assembly, unzipped_assembly, external_programs, temp_dir):
407
+ """
408
+ A lot of the modules use minimap2 alignment, so pre-building the index for this assembly once
409
+ can save a bit of time.
410
+ """
411
+ if 'minimap2' not in external_programs:
412
+ return None
413
+ minimap2_index = (pathlib.Path(temp_dir) / (uuid.uuid4().hex + '.mmi')).resolve()
414
+ command = ['minimap2', '-d', minimap2_index, unzipped_assembly]
415
+ p = subprocess.run(command, capture_output=True, text=True)
416
+ if p.returncode != 0:
417
+ sys.exit(f'\nError: minimap2 failed to index sample {assembly}:\n{p.stderr}')
418
+ return minimap2_index
419
+
420
+
421
+ def decompress_file(in_file, out_file):
422
+ with gzip.GzipFile(in_file, 'rb') as i, open(out_file, 'wb') as o:
423
+ s = i.read()
424
+ o.write(s)
425
+
426
+ def output_headers(full_headers, stdout_headers, outfile):
427
+ """
428
+ This function prints headers to stdout and writes headers to the output file. Module names are
429
+ trimmed off to make the headers shorter and easier to read.
430
+ """
431
+ trimmed_stdout_headers = [h.split('__')[-1] for h in stdout_headers]
432
+ trimmed_full_headers = [h.split('__')[-1] for h in full_headers]
433
+ print('\t'.join(trimmed_stdout_headers))
434
+ with open(outfile, 'wt') as o:
435
+ o.write('\t'.join(trimmed_full_headers))
436
+
437
+
438
+ def output_results(full_headers, stdout_headers, outfile, results):
439
+ """
440
+ This function writes the results to stdout and the output file.
441
+ """
442
+ print('\t'.join([str(results.get(x, "-")).strip("[] ").replace("assembly", "strain") for x in stdout_headers]))
443
+ with open(outfile, 'at') as o:
444
+ if o.tell() > 0: # Check if the file is not empty
445
+ o.write('\n')
446
+ o.write('\t'.join([str(results.get(x, "-")).strip("[] ").replace("assembly", "strain") for x in full_headers]))
447
+
448
+ for h in results.keys():
449
+ if h not in full_headers:
450
+ sys.exit(f'Error: results contained a value ({h}) that is not covered by the output headers')
451
+
452
+
453
+ def paper_refs():
454
+ """
455
+ This function prepares the references for the program's help text, with clean line wrapping.
456
+ """
457
+ terminal_width = shutil.get_terminal_size().columns
458
+ text = 'If you use Kleborate, please cite the paper:\n' \
459
+ 'Lam MMC, et al. A genomic surveillance framework and genotyping tool for Klebsiella ' \
460
+ 'pneumoniae and its related species complex. Nature Communications. 2021. ' \
461
+ 'doi:10.1038/s41467-021-24448-3.\n\n' \
462
+ 'If you turn on the Kaptive option for full K and O typing, please also cite:\n' \
463
+ 'Wyres KL, et al. Identification of Klebsiella capsule synthesis loci from whole ' \
464
+ 'genome data. Microbial Genomics. 2016. doi:10.1099/mgen.0.000102.'
465
+ wrapped_text = ''
466
+ for line in text.split('\n'):
467
+ wrapped_text += '\n'.join(textwrap.wrap(line, width=terminal_width - 1))
468
+ wrapped_text += '\n'
469
+ return 'R|' + wrapped_text
470
+
471
+
472
+ def get_version():
473
+ """
474
+ This function returns the version of Kleborate as a string, without the leading 'v'.
475
+ """
476
+ # First try to get the version from the pyproject.toml file, in case this is being run directly
477
+ # from the repo using the kleborate-runner.py file.
478
+ pyproject = pathlib.Path(__file__).parents[1] / 'pyproject.toml'
479
+ if pyproject.is_file():
480
+ with open(pyproject, 'rt') as f:
481
+ pyproject_text = f.read()
482
+ if 'name = "kleborate"' in pyproject_text: # make sure it's the right pyproject.toml
483
+ match = re.search(r'version = "(\d+\.\d+\.\d+)"', pyproject_text)
484
+ if match:
485
+ return match.group(1)
486
+
487
+ # If there wasn't a 'pyproject.toml' file, then Kleborate is probably installed, so use
488
+ # importlib to get the version of the installed package.
489
+ try:
490
+ from importlib import metadata
491
+ except ImportError:
492
+ import importlib_metadata as metadata
493
+ return metadata.version(__package__ or __name__)
494
+
495
+
496
+ if __name__ == '__main__':
497
+ main()
File without changes
@@ -0,0 +1,110 @@
1
+ """
2
+ Copyright 2023 Kat Holt
3
+ Copyright 2023 Ryan Wick (rrwick@gmail.com)
4
+ https://github.com/katholt/Kleborate/
5
+
6
+ This file is part of Kleborate. Kleborate is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by the Free Software Foundation,
8
+ either version 3 of the License, or (at your option) any later version. Kleborate is distributed in
9
+ the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
11
+ details. You should have received a copy of the GNU General Public License along with Kleborate. If
12
+ not, see <https://www.gnu.org/licenses/>.
13
+ """
14
+
15
+ import argparse
16
+ import os
17
+ import pathlib
18
+ import shutil
19
+ import sys
20
+
21
+
22
+ def description():
23
+ return 'Mash-based species detection for enterobacterales species'
24
+
25
+
26
+ def prerequisite_modules():
27
+ return []
28
+
29
+
30
+ def get_headers():
31
+ full_headers = ['species', 'species_match']
32
+ stdout_headers = ['species']
33
+ return full_headers, stdout_headers
34
+
35
+
36
+ def test_add_cli_options():
37
+ parser = argparse.ArgumentParser()
38
+ add_cli_options(parser)
39
+ assert '--enterobacterales__species_strong' in parser.format_help()
40
+ assert '--enterobacterales__species_weak' in parser.format_help()
41
+
42
+
43
+ def add_cli_options(parser):
44
+ module_name = os.path.basename(__file__)[:-3]
45
+ group = parser.add_argument_group(f'{module_name} module')
46
+ group.add_argument('--enterobacterales__species_strong', type=float, default=0.02,
47
+ help='Mash distance threshold for a strong species match')
48
+ group.add_argument('--enterobacterales__species_weak', type=float, default=0.04,
49
+ help='Mash distance threshold for a weak species match')
50
+ return group
51
+
52
+
53
+ def check_cli_options(args):
54
+ if args.enterobacterales__species_strong <= 0.0 or args.enterobacterales__species_strong >= 1.0:
55
+ sys.exit('Error: --enterobacterales__species_strong must be between 0.0 and 1.0')
56
+ if args.enterobacterales__species_weak <= 0.0 or args.enterobacterales__species_weak >= 1.0:
57
+ sys.exit('Error: --enterobacterales__species_weak must be between 0.0 and 1.0')
58
+ if args.enterobacterales__species_weak < args.enterobacterales__species_strong:
59
+ sys.exit('Error: --enterobacterales__species_weak must be greater than '
60
+ '--enterobacterales__species_strong')
61
+
62
+
63
+ def check_external_programs():
64
+ if not shutil.which('mash'):
65
+ sys.exit('Error: could not find mash')
66
+ return ['mash']
67
+
68
+
69
+ def get_results(assembly, minimap2_index, args, previous_results):
70
+ #print(previous_results)
71
+ sketch_file = pathlib.Path(__file__).parents[0] / 'data' / 'species_mash_sketches.msh'
72
+ species, distance = get_enterobacterales__species(assembly, sketch_file)
73
+ if distance <= args.enterobacterales__species_strong:
74
+ species_hit_strength = 'strong'
75
+ elif distance <= args.enterobacterales__species_weak:
76
+ species_hit_strength = 'weak'
77
+ else:
78
+ species = 'unknown'
79
+ species_hit_strength = ''
80
+ return {'species': species,
81
+ 'species_match': species_hit_strength}
82
+
83
+
84
+ def get_enterobacterales__species(assembly, sketch_file):
85
+ f = os.popen('mash dist ' + str(sketch_file) + ' ' + str(assembly))
86
+ best_species, best_distance = None, 1.0
87
+ for line in f:
88
+ line_parts = line.split('\t')
89
+ reference = line_parts[0]
90
+ if len(line_parts) >= 3:
91
+ species = reference.split('/')[0]
92
+ distance = float(line_parts[2])
93
+ if distance < best_distance:
94
+ best_distance = distance
95
+ best_species = species
96
+ f.close()
97
+ best_species = clean_species_name(best_species)
98
+ return best_species, best_distance
99
+
100
+
101
+ def clean_species_name(species):
102
+ if species is None:
103
+ return None
104
+ species = species.replace('Escherichia_coli', 'Escherichia coli / Shigella')
105
+ species = species.replace('Raoultella', 'Klebsiella (Raoultella)')
106
+ species = species.replace('_', ' ')
107
+ species = species.replace(' subsp ', ' subsp. ')
108
+ if species.endswith(' unknown'):
109
+ species = species.replace(' unknown', ' (unknown species)')
110
+ return species