catomatic 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Predicting AMR de novo
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,119 @@
1
+ Metadata-Version: 2.1
2
+ Name: catomatic
3
+ Version: 0.1.0
4
+ Summary: A tool for automatically building catalogues of antibiotic resistance-associated variants
5
+ Home-page: https://github.com/fowler-lab/catomatic
6
+ Author: Dylan Adlard, Philip W Fowler
7
+ Author-email: philip.fowler@ndm.ox.ac.uk
8
+ License: MIT
9
+ Keywords: resistance catalogue,tuberculosis,clinical microbiology
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: piezo
15
+ Requires-Dist: numpy
16
+ Requires-Dist: scipy
17
+ Requires-Dist: pandas
18
+
19
+ [![codecov](https://codecov.io/gh/fowler-lab/catomatic/branch/main/graph/badge.svg?token=8fnOy6rHCd)](https://codecov.io/gh/fowler-lab/catomatic)
20
+
21
+ # catomatic
22
+
23
+ Python code that algorithmically builds antimicrobial resistance catalogues of mutations.
24
+
25
+ ## Introduction
26
+
27
+ This method relies on the logic that mutations that do not cause resistance can co-occur with those that do, and if a mutation in isolation (solo) does not cause resistance, then it will also not contribute to the phenotype when not in isolation.
28
+
29
+ Mutations that occur in isolation across specified genes are traversed in sequence, and if their proportion of drug-susceptibility (vs resistance) passes the specified statistical test, they are characterized as benign and removed from the dataset. This step repeats while there are susceptible mutations in isolation. Once the dataset has been 'cleaned' of benign mutations, resistant mutations are classified via their proportions by the specified test, failing which they are added to the catalogue as 'Unclassified'.
30
+
31
+ Construction can either rely on homogenous susceptibility for the particular mutation (and no explicit phenotyping is carried out, other than to unlock susceptible variants), use a Binomial test where the proportion of resistance is tested against a specified background rate, or a Fisher's test where the proportion of resistance is tested against a calculated background rate.
32
+
33
+ Although the method is entirely algorithmic, there are 2 entry points for intervention. Firstly, one is able to 'seed' the method with neutral mutations (such as those gathered in a literature search - often helpful if a gene contains phylogenetic mutations with high prevalence that add noise), and secondly one can add or overwrite classifications and entries to the catalogue, although not recommended unless aggregating.
34
+
35
+ Because the method uses and understands GARC1 grammar, one can supply 'rules' to the catalogue post-hoc - such as `{rpoB@*_fs:R}` for frameshifts in rpoB, which can either simply be added (and would have lower prediction priority to finer grain mutations, such as `rpoB@44_ins`) or can replace any mutations that fall under that rule, effectively aggregating relevant variants.
36
+
37
+ The generated catalogue can be returned either as a dictionary, or as a Pandas dataframe which can be exported in a Piezo compatible format for rapid parsing and resistance predictions.
38
+
39
+ Contingency tables, proportions, p_values, and Wilson's Confidence Intervals are logged under the 'EVIDENCE' column of the catalogue.
40
+
41
+ A workflow with example parameters:
42
+
43
+ ![Catalogue Diagram](docs/workflow.png)
44
+
45
+ ## Installation
46
+
47
+ ### Using Conda
48
+
49
+ It is recommended to manage the Python environment and dependencies through Conda. You can install Catomatic within a Conda environment by following these steps:
50
+
51
+ #### Create and Activate Environment
52
+
53
+ First, ensure that you have Conda installed. Then, create and activate a new environment, and install catomatic:
54
+
55
+ ```bash
56
+ conda env create -f env.yml
57
+ conda activate catomatic
58
+ pip install .
59
+ ```
60
+
61
+ ## Running catomatic
62
+
63
+ At the most basic level, the method takes 2 input dataframes: a `samples dataframe` which contains 1 row per sample with 'R' vs 'S' binary phenotypes, and a `mutations dataframe` which contains 1 row per mutation. They have to be joinable on their `UNIQUEID` columns.
64
+
65
+ If exporting to Piezo format, the `MUTATION` column must contain GARC1 grammer (ie `gene@mutation`). One must also supply a path to the `wildcards.json` file, which should contain Piezo wildcards in a json object/dictionary (example file in `/data/bdq_wildcards.json`).
66
+
67
+ If seeding or updating the catalogue, the mutation grammar must match that of the `MUTATION` column.
68
+
69
+ ### CLI
70
+
71
+ After installation, the simplest way to run the catomatic catalogue builder is via the command line interface. ` --to_piezo` or `--to_json ` flags will need to specified to save the catalogue (with additional arguments if using --to_piezo)
72
+
73
+ `BuildCatalogue --samples path/to/samples.csv --mutations path/to/mutations.csv --to_json --outfile path/to/out/catalogue.json`
74
+
75
+ or
76
+
77
+ `BuildCatalogue --samples path/to/samples.csv --mutations path/to/mutations.csv --to_piezo --outfile path/to/out/catalogue.csv --genbank_ref '...' --catalogue_name '...' --version '...' --drug '...' --wildcards path/to/wildcards.json`
78
+
79
+ ### Python/Jupyter notebook
80
+
81
+ Should you wish to run catomatic in a notebook, for example, you can do so simply by calling BuildCatalogue after import.
82
+
83
+ ```python
84
+ from catomatic.CatalogueBuilder import BuildCatalogue
85
+
86
+ #instantiate a catalogue object - this will build the catalogue
87
+ catalogue = BuildCatalogue(samples = samples_df, mutations = mutations_df)
88
+
89
+ #return the catalogue as a dictionary in order of variant addition
90
+ catalogue.return_catalogue()
91
+
92
+ #return the catalogue as a piezo-structured dataframe
93
+ catalogue.build_piezo(genbank_ref='...', catalogue_name='...', version='...', drug='...', wildcards='path/to/wildcards.json')
94
+ ```
95
+
96
+ More detailed examples on running catomatic can be found in `examples/demo.ipynb`
97
+
98
+ ### CLI Parameters
99
+
100
+ | Parameter | Type | Description |
101
+ | ------------------ | ------- | ------------------------------------------------------------------------------------------------- |
102
+ | `--samples` | `str` | Path to the samples file. Required. |
103
+ | `--mutations` | `str` | Path to the mutations file. Required. |
104
+ | `--FRS` | `float` | Fraction Read Support threshold. Optional. |
105
+ | `--seed` | `list` | List of seed mutations using GARC grammar. Optional. |
106
+ | `--test` | `str` | Type of statistical test to run: `None`, `Binomial`, or `Fisher`. Optional. |
107
+ | `--background` | `float` | Background mutation rate for the binomial test. Required if using test = Binomial. Optional. |
108
+ | `--p` | `float` | Significance level for statistical testing. Optional. Defaults to `0.95`. |
109
+ | `--strict_unlock` | `bool` | Enforce strict unlocking for classifications, which requires p < 0.05. Optional. |
110
+ | `--to_json` | `bool` | Export the catalogue to JSON format. Optional. |
111
+ | `--outfile` | `str` | Path to output file for exporting the catalogue. Used with `--to_json` or `--to_piezo`. Optional. |
112
+ | `--to_piezo` | `bool` | Export catalogue to Piezo format. Optional. |
113
+ | `--genbank_ref` | `str` | GenBank reference for the catalogue. Required if to_piezo = True. Optional. |
114
+ | `--catalogue_name` | `str` | Name of the catalogue. Required if to_piezo = True. Optional. |
115
+ | `--version` | `str` | Version of the catalogue. Required if to_piezo = True. Optional. |
116
+ | `--drug` | `str` | Drug associated with the mutations. Required if to_piezo = True. Optional. |
117
+ | `--wildcards` | `str` | JSON file with wildcard rules. Required if to_piezo = True. Optional. |
118
+ | `--grammar` | `str` | Grammar used in the catalogue. Optional. Defaults to `GARC1`. |
119
+ | `--values` | `str` | Values used for predictions in the catalogue. Optional. Defaults to `RUS`. |
@@ -0,0 +1,8 @@
1
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ tests/test_BuildCatalogue.py,sha256=e0YO2sDztw2w-NoCE4mBVZ9hDCn34t-xoBobIzZYtEE,16272
3
+ catomatic-0.1.0.dist-info/LICENSE,sha256=dQ5CwvYEfzkz1MKqqJoH6BJCJUHZg0aKEZCbMyn2RRI,1079
4
+ catomatic-0.1.0.dist-info/METADATA,sha256=3p5vKH0mvl-slylw4IfUWLDfV3HIcW1JLrmzsDnO6J4,8319
5
+ catomatic-0.1.0.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
6
+ catomatic-0.1.0.dist-info/entry_points.txt,sha256=7UFUEqi9Ml-nS8NWgeNQt-O7bHufdQX7KB-8xKfCoh4,67
7
+ catomatic-0.1.0.dist-info/top_level.txt,sha256=EdW7283x-lr_cbuivW8Ij7ANAP-ZJ9sLtILQseEPxXg,6
8
+ catomatic-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (72.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ BuildCatalogue = catomatic.CatalogueBuilder:main
@@ -0,0 +1 @@
1
+ tests
tests/__init__.py ADDED
File without changes
@@ -0,0 +1,537 @@
1
+ import sys
2
+ import pytest
3
+ import json
4
+ import subprocess
5
+ import pandas as pd
6
+ from catomatic.CatalogueBuilder import BuildCatalogue
7
+ from scipy.stats import norm, binomtest, fisher_exact
8
+ from unittest.mock import patch
9
+
10
+
11
+ # a left join of phenotypes and mutations will give:
12
+ """
13
+ UNIQUEID PHENOTYPE MUTATION
14
+ 0 1 R gene@A1S
15
+ 1 2 S gene@A1S
16
+ 2 3 S NaN
17
+ 3 4 S gene@A2S
18
+ 4 5 R NaN
19
+ 5 6 S NaN
20
+ 6 7 S gene@A3S
21
+ 7 8 S gene@A3S
22
+ 8 9 R gene@A3S
23
+ 9 10 R NaN
24
+ """
25
+ # this should allow for pretty robust statistical condition testing
26
+ # have pushed background very R to test fisher S classifications
27
+ # the only function that uses the backgrounds is test_fisher_build_S (R/U uses explicit tables)
28
+
29
+
30
+ @pytest.fixture
31
+ def mutation_data():
32
+ return pd.DataFrame(
33
+ {
34
+ "UNIQUEID": [1, 2, 4, 7, 8, 9],
35
+ "MUTATION": [
36
+ "gene@A1S",
37
+ "gene@A1S",
38
+ "gene@A2S",
39
+ "gene@A3S",
40
+ "gene@A3S",
41
+ "gene@A3S",
42
+ ],
43
+ }
44
+ )
45
+
46
+
47
+ @pytest.fixture
48
+ def phenotype_data():
49
+ return pd.DataFrame(
50
+ {
51
+ "UNIQUEID": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
52
+ "PHENOTYPE": ["R", "S", "R", "S", "R", "S", "S", "S", "R", "R"],
53
+ }
54
+ )
55
+
56
+
57
+ @pytest.fixture
58
+ def phenotypes_file(phenotype_data, tmp_path):
59
+ file = tmp_path / "phenotypes.csv"
60
+ phenotype_data.to_csv(file, index=False)
61
+ return str(file)
62
+
63
+
64
+ @pytest.fixture
65
+ def mutations_file(mutation_data, tmp_path):
66
+ file = tmp_path / "mutations.csv"
67
+ mutation_data.to_csv(file, index=False)
68
+ return str(file)
69
+
70
+
71
+ @pytest.fixture
72
+ def output_file(tmp_path):
73
+ return str(tmp_path / "output.json")
74
+
75
+
76
+ @pytest.fixture
77
+ def solo_data(phenotype_data, mutation_data):
78
+ return pd.merge(phenotype_data, mutation_data, on=["UNIQUEID"], how="left")
79
+
80
+
81
+ @pytest.fixture
82
+ def wildcards():
83
+ return {"gene@-*?": {"pred": "U", "evid": {}}, "gene@*=": {"pred": "S", "evid": {}}}
84
+
85
+
86
+ @pytest.fixture
87
+ def builder(request, phenotype_data, mutation_data):
88
+ # default parameters
89
+ test_type = None
90
+ background = 0.1
91
+ p = 0.95
92
+ strict_unlock = False
93
+
94
+ # check if the test that's calling this fixture is parametrized
95
+ if hasattr(request, "param"):
96
+ # override defaults
97
+ test_type = request.param.get("test", test_type)
98
+ background = request.param.get("background", background)
99
+ p = request.param.get("p", p)
100
+ strict_unlock = request.param.get("strict_unlock", strict_unlock)
101
+
102
+ return BuildCatalogue(
103
+ phenotype_data,
104
+ mutation_data,
105
+ p=p,
106
+ background=background,
107
+ strict_unlock=strict_unlock,
108
+ test=test_type,
109
+ )
110
+
111
+
112
+ def test_add_mutation(builder):
113
+ # using default parameterisation
114
+
115
+ mutation = "mut4"
116
+ prediction = "R"
117
+ evidence = {"detail": "test"}
118
+
119
+ builder.add_mutation(mutation, prediction, evidence)
120
+ assert mutation in builder.catalogue
121
+ assert builder.catalogue[mutation]["pred"] == prediction
122
+ assert builder.catalogue[mutation]["evid"] == evidence
123
+
124
+
125
+ def test_build_contingency(solo_data):
126
+ # for R + S
127
+ x_mut1 = BuildCatalogue.build_contingency(solo_data, "gene@A1S")
128
+ assert x_mut1 == [[1, 1], [3, 1]]
129
+ # for R
130
+ x_mut2 = BuildCatalogue.build_contingency(solo_data, "gene@A2S")
131
+ assert x_mut2 == [[0, 1], [3, 1]]
132
+ # For non-existent mutation - although should be filtered out beforehand
133
+ x_mut4 = BuildCatalogue.build_contingency(solo_data, "gene@A4S")
134
+ assert x_mut4 == [[0, 0], [3, 1]]
135
+
136
+
137
+ def test_calc_proportion():
138
+ x_tests = [
139
+ ([[10, 5], [3, 7]], 10 / (10 + 5)),
140
+ ([[20, 0], [5, 5]], 1),
141
+ ([[0, 30], [5, 5]], 0),
142
+ ]
143
+
144
+ for x, expected in x_tests:
145
+ assert (
146
+ BuildCatalogue.calc_proportion(x) == expected
147
+ ), f"Failed for contingency {x}"
148
+
149
+
150
+ def test_calc_oddsRatio():
151
+ x_tests = [
152
+ ([[10, 5], [3, 7]], 45 / 11),
153
+ ([[20, 0], [5, 5]], 41),
154
+ ([[0, 30], [5, 5]], 1 / 61),
155
+ ]
156
+
157
+ for x, expected in x_tests:
158
+ assert (
159
+ BuildCatalogue.calc_oddsRatio(x) == expected
160
+ ), f"Failed for contingency {x}"
161
+
162
+
163
+ @pytest.mark.parametrize("builder", [{"p": 0.95}], indirect=True)
164
+ def test_calc_confidenceInterval(builder):
165
+
166
+ x_tests = [
167
+ ([[10, 5], [3, 7]], [0.4171, 0.8482]),
168
+ ([[20, 0], [5, 5]], [0.8389, 1]),
169
+ ([[0, 30], [5, 5]], [0, 0.1135]),
170
+ ]
171
+
172
+ for x, expected in x_tests:
173
+ ci = builder.calc_confidenceInterval(x)
174
+ ci = [round(ci[0], 4), round(ci[1], 4)]
175
+
176
+ assert ci == expected, f"Failed for contingency {x}"
177
+
178
+
179
+ @pytest.mark.parametrize("builder", [{"p": 0.95}], indirect=True)
180
+ def test_skeleton_build(builder):
181
+ # uses default parameters (ie test=None), which should fire the skeleton build
182
+
183
+ # now test addition when run_iter = False
184
+ mutation = "mut4"
185
+ x = [[27, 21], [3, 22]]
186
+
187
+ builder.skeleton_build(mutation, x)
188
+ # check its actually been added
189
+ assert mutation in builder.catalogue
190
+ # check if the correct phenotype has been added
191
+ assert builder.catalogue[mutation]["pred"] == 'U'
192
+
193
+ expected_e = {
194
+ "proportion": builder.calc_proportion(x),
195
+ "confidence": builder.calc_confidenceInterval(x),
196
+ "contingency": x,
197
+ }
198
+
199
+ assert builder.catalogue[mutation]["evid"][0] == expected_e
200
+
201
+
202
+ @pytest.mark.parametrize(
203
+ "builder",
204
+ [{"test": "Binomial", "background": 0.1, "strict_unlock": False, "p": 0.95}],
205
+ indirect=True,
206
+ )
207
+ def test_binomial_build_RU(builder):
208
+ """tests when run_iter = False (ie, classiyfing R and U variants)"""
209
+
210
+ # now test additions when run_iter = False
211
+ mutations = ["mut4", "mut5"]
212
+ data_tests = [
213
+ ([[27, 21], [3, 22]], "R"),
214
+ ([[5, 21], [3, 22]], "U"),
215
+ ]
216
+
217
+ for mut in range(len(mutations)):
218
+ mutation = mutations[mut]
219
+ phenotype = data_tests[mut][1]
220
+ x = data_tests[mut][0]
221
+
222
+ p_expected = binomtest(
223
+ x[0][0], (x[0][0] + x[0][1]), 0.1, alternative="two-sided"
224
+ ).pvalue
225
+
226
+ expected_e = {
227
+ "proportion": builder.calc_proportion(x),
228
+ "confidence": builder.calc_confidenceInterval(x),
229
+ "p_value": p_expected,
230
+ "contingency": x,
231
+ }
232
+
233
+ builder.binomial_build(mutation, x)
234
+ # make sure mutaiton was added
235
+ assert mutation in builder.catalogue, f"Failed for contingency {x}"
236
+ # make sure phenotype was determined correclty
237
+ assert (
238
+ builder.catalogue[mutation]["pred"] == phenotype
239
+ ), f"Failed for contingency {x}"
240
+ # make sure evidence was added correclty
241
+ assert (
242
+ builder.catalogue[mutation]["evid"][0] == expected_e
243
+ ), f"Failed for contingency {x}"
244
+
245
+ # fyi haven't tested entire decision tree for run_iter=True, but they're are almost identical
246
+
247
+
248
+ @pytest.mark.parametrize(
249
+ "builder",
250
+ [{"test": "Fisher", "strict_unlock": False, "p": 0.95}],
251
+ indirect=True,
252
+ )
253
+ def test_fisher_build_RU(builder):
254
+ """tests when run_iter = False (ie classifying R and U variants)"""
255
+
256
+ # now test additions when run_iter = False
257
+ mutations = ["mut4", "mut5"]
258
+ data_tests = [
259
+ ([[27, 21], [3, 22]], "R"),
260
+ ([[5, 21], [3, 22]], "U"),
261
+ ]
262
+
263
+ for mut in range(len(mutations)):
264
+ mutation = mutations[mut]
265
+ phenotype = data_tests[mut][1]
266
+ x = data_tests[mut][0]
267
+
268
+ _, p_expected = fisher_exact(x)
269
+
270
+ expected_e = {
271
+ "proportion": builder.calc_proportion(x),
272
+ "confidence": builder.calc_confidenceInterval(x),
273
+ "p_value": p_expected,
274
+ "contingency": x,
275
+ }
276
+
277
+ builder.fishers_build(mutation, x)
278
+ # make sure mutaiton was added
279
+ assert mutation in builder.catalogue, f"Failed for contingency {x}"
280
+ # make sure phenotype was determined correclty
281
+ assert (
282
+ builder.catalogue[mutation]["pred"] == phenotype
283
+ ), f"Failed for contingency {x}"
284
+ # make sure evidence was added correclty
285
+ assert (
286
+ builder.catalogue[mutation]["evid"][0] == expected_e
287
+ ), f"Failed for contingency {x}"
288
+
289
+
290
+ # the S tests below are conceptually more complex than the RU tests, as these use the
291
+ # instantiation data, and therefore implicitly also test the classify function
292
+
293
+
294
+ @pytest.mark.parametrize(
295
+ "builder",
296
+ [{"test": "Binomial", "background": 0.9, "strict_unlock": False, "p": 0.95}],
297
+ indirect=True,
298
+ )
299
+ def test_binomial_build_S(builder):
300
+ # test proportion 0 (not strict) + p_calc<self.p (added and not added)
301
+ # using background = 0.9 to force it without needing tonnes of data
302
+ # havent tested propotion 0 (strict) but i think its fine
303
+
304
+ s_entries = {
305
+ key: val for key, val in builder.catalogue.items() if val["pred"] == "S"
306
+ }
307
+ # check only mut2 and mut3 were added
308
+ assert "gene@A1S" not in s_entries
309
+ assert "gene@A2S" in s_entries
310
+ assert "gene@A3S" in s_entries
311
+
312
+
313
+ @pytest.mark.parametrize(
314
+ "builder",
315
+ [{"test": "Fisher", "strict_unlock": False, "p": 0.5}],
316
+ indirect=True,
317
+ )
318
+ def test_fisher_build_S(builder):
319
+ # test proportion 0 (not strict) + p_calc<self.p (added and not added)
320
+ # using bp value 0.5 and very high R background to force S classifications
321
+ # havent tested propotion 0 (strict) but i think its fine
322
+
323
+ s_entries = {
324
+ key: val for key, val in builder.catalogue.items() if val["pred"] == "S"
325
+ }
326
+ # check only mut2 and mut3 were added
327
+ assert "gene@A1S" not in s_entries
328
+ assert "gene@A2S" in s_entries
329
+ assert "gene@A3S" in s_entries
330
+
331
+
332
+ @pytest.mark.parametrize("builder", [{"p": 0.95}], indirect=True)
333
+ def test_classify(builder):
334
+ # this a bit redundant as the test_S functions above inplicity test the classify function
335
+
336
+ # check that once there is 1 susceptible entry in the catalogue (gene@A1S), run_iter switches to false
337
+ s_entries = {
338
+ key: val for key, val in builder.catalogue.items() if val["pred"] == "S"
339
+ }
340
+ assert len(s_entries) == 1
341
+ assert "gene@A2S" in s_entries.keys()
342
+ assert builder.run_iter == False
343
+
344
+
345
+ @pytest.mark.parametrize("builder", [{"p": 0.95}], indirect=True)
346
+ def test_update(builder, wildcards):
347
+
348
+ # check addition to the catalogue with replacement
349
+ assert builder.catalogue["gene@A1S"]["pred"] == 'U'
350
+ builder.update({"gene@A1S": "R"})
351
+ assert builder.catalogue["gene@A1S"]["pred"] == "R"
352
+ # check addition to the catalogue with wildcard and replacement
353
+ builder.update({"gene@*?": "S"}, wildcards, replace=True)
354
+ assert builder.catalogue["gene@*?"]["pred"] == "S"
355
+ assert "gene@A1S" not in builder.catalogue.keys()
356
+ print(builder.catalogue)
357
+ # check addition to the catalogue without replacement
358
+ builder.update({"gene@A5S": "R"}, wildcards, replace=False)
359
+ assert builder.catalogue["gene@A5S"]["pred"] == "R"
360
+ assert builder.catalogue["gene@*?"]["pred"] == "S"
361
+
362
+
363
+ @pytest.mark.parametrize("builder", [{"p": 0.95}], indirect=True)
364
+ def test_build_piezo(builder, wildcards):
365
+ # build a piezo compitable catalogue df
366
+ catalogue = builder.build_piezo(
367
+ "genbank", "test", "1", "drug", wildcards, grammar="TEST", values="SUR"
368
+ )
369
+ # test basic argument additions to catalogue df
370
+ assert catalogue.GENBANK_REFERENCE[0] == "genbank"
371
+ assert catalogue.CATALOGUE_NAME[0] == "test"
372
+ assert catalogue.CATALOGUE_VERSION[0] == "1"
373
+ assert catalogue.DRUG[0] == "drug"
374
+ # check mutations are in the catalogue, , with correct classification
375
+ assert catalogue[catalogue.MUTATION == "gene@A2S"].PREDICTION.values[0] == "S"
376
+ # check wildcards are in the catalogue, with correct classification
377
+ assert catalogue[catalogue.MUTATION == "gene@*="].PREDICTION.values[0] == "S"
378
+
379
+
380
+ def test_cli_help():
381
+ result = subprocess.run(
382
+ [sys.executable, "-m", "catomatic.CatalogueBuilder", "--help"],
383
+ stdout=subprocess.PIPE,
384
+ stderr=subprocess.PIPE,
385
+ check=True,
386
+ )
387
+ assert "usage:" in result.stdout.decode()
388
+
389
+
390
+ def test_cli_execution(phenotypes_file, mutations_file, output_file):
391
+ result = subprocess.run(
392
+ [
393
+ sys.executable,
394
+ "-m",
395
+ "catomatic.CatalogueBuilder",
396
+ "--samples",
397
+ phenotypes_file,
398
+ "--mutations",
399
+ mutations_file,
400
+ "--to_json",
401
+ "--outfile",
402
+ output_file,
403
+ "--test",
404
+ "Binomial",
405
+ "--background",
406
+ "0.1",
407
+ "--p",
408
+ "0.95",
409
+ "--strict_unlock",
410
+ ],
411
+ stdout=subprocess.PIPE,
412
+ stderr=subprocess.PIPE,
413
+ check=False,
414
+ )
415
+
416
+ assert result.returncode == 0, "Subprocess failed with exit status: {}".format(
417
+ result.returncode
418
+ )
419
+
420
+
421
+ def test_to_json_output(phenotypes_file, mutations_file, output_file):
422
+ result = subprocess.run(
423
+ [
424
+ sys.executable,
425
+ "-m",
426
+ "catomatic.CatalogueBuilder",
427
+ "--samples",
428
+ phenotypes_file,
429
+ "--mutations",
430
+ mutations_file,
431
+ "--to_json",
432
+ "--outfile",
433
+ output_file,
434
+ ],
435
+ stdout=subprocess.PIPE,
436
+ stderr=subprocess.PIPE,
437
+ check=False,
438
+ )
439
+ assert result.returncode == 0, "Subprocess failed with exit status: {}".format(
440
+ result.returncode
441
+ )
442
+
443
+ # Load and verify the JSON output
444
+ with open(output_file, "r") as f:
445
+ data = json.load(f)
446
+ assert isinstance(data, dict)
447
+ assert "gene@A1S" in data
448
+ assert "gene@A2S" in data
449
+ assert "gene@A3S" in data
450
+
451
+
452
+ def test_missing_piezo(phenotypes_file, mutations_file, output_file):
453
+ result = subprocess.run(
454
+ [
455
+ sys.executable,
456
+ "-m",
457
+ "coverage",
458
+ "run",
459
+ "-m",
460
+ "catomatic.CatalogueBuilder",
461
+ "--samples",
462
+ phenotypes_file,
463
+ "--mutations",
464
+ mutations_file,
465
+ "--to_piezo",
466
+ "--outfile",
467
+ output_file,
468
+ "--genbank_ref",
469
+ "genbank",
470
+ "--catalogue_name",
471
+ "test",
472
+ "--version",
473
+ "1",
474
+ ], # missing drug and wildcards
475
+ stdout=subprocess.PIPE,
476
+ stderr=subprocess.PIPE,
477
+ check=False,
478
+ )
479
+ assert result.returncode != 0 # error
480
+
481
+
482
+ def test_to_piezo_output(phenotypes_file, mutations_file, output_file, tmp_path):
483
+ wildcards_file = tmp_path / "wildcards.json"
484
+ wildcards_file.write_text(
485
+ json.dumps(
486
+ {
487
+ "gene@-*?": {"pred": "U", "evid": {}},
488
+ "gene@*=": {"pred": "S", "evid": {}},
489
+ }
490
+ )
491
+ )
492
+
493
+ result = subprocess.run(
494
+ [
495
+ sys.executable,
496
+ "-m",
497
+ "coverage",
498
+ "run",
499
+ "-m",
500
+ "catomatic.CatalogueBuilder",
501
+ "--samples",
502
+ phenotypes_file,
503
+ "--mutations",
504
+ mutations_file,
505
+ "--to_piezo",
506
+ "--outfile",
507
+ output_file,
508
+ "--genbank_ref",
509
+ "genbank",
510
+ "--catalogue_name",
511
+ "test",
512
+ "--version",
513
+ "1",
514
+ "--drug",
515
+ "drug",
516
+ "--wildcards",
517
+ str(wildcards_file),
518
+ ],
519
+ stdout=subprocess.PIPE,
520
+ stderr=subprocess.PIPE,
521
+ check=False,
522
+ )
523
+ if result.returncode != 0:
524
+ print("Error output:", result.stderr.decode())
525
+ assert result.returncode == 0, "Subprocess failed with exit status: {}".format(
526
+ result.returncode
527
+ )
528
+
529
+ # Load and verify the Piezo CSV output
530
+ piezo_df = pd.read_csv(output_file)
531
+ assert "GENBANK_REFERENCE" in piezo_df.columns
532
+ assert piezo_df.loc[0, "GENBANK_REFERENCE"] == "genbank"
533
+ assert piezo_df.loc[0, "CATALOGUE_NAME"] == "test"
534
+ assert piezo_df.loc[0, "CATALOGUE_VERSION"] == 1
535
+ assert piezo_df.loc[0, "DRUG"] == "drug"
536
+ assert "gene@A2S" in piezo_df["MUTATION"].values
537
+ assert "gene@*=" in piezo_df["MUTATION"].values