isiteuk 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2024 Ben Woodcroft
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
isiteuk-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: isiteuk
3
+ Version: 0.0.2
4
+ Summary: In-situ identification of eukaryotes in metagenomic data
5
+ Author-email: Ben Woodcroft <benjwoodcroft@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/wwood/isiteuk
8
+ Keywords: metagenomics,bioinformatics,eukaryotes
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Topic :: Scientific/Engineering
11
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENCE.txt
16
+ Requires-Dist: singlem>=0.16.0
17
+ Requires-Dist: polars>=0.20
18
+ Requires-Dist: zenodo_backpack>=0.3
19
+ Dynamic: license-file
20
+
21
+ # isiteuk
22
+
23
+ isiteuk classifies genomes by domain (Bacteria, Archaea, Eukaryota) using [SingleM](https://github.com/wwood/SingleM) marker genes.
24
+
25
+ ## Installation
26
+
27
+ ### Conda (recommended)
28
+
29
+ ```bash
30
+ conda create -c conda-forge -c bioconda --override-channels --name isiteuk isiteuk
31
+ conda activate isiteuk
32
+ ```
33
+
34
+ ### PyPI
35
+
36
+ ```bash
37
+ pip install isiteuk
38
+ ```
39
+
40
+ Non-Python dependencies (SingleM, DIAMOND) must be installed separately — see `pixi.toml` for the full list.
41
+
42
+ ### GitHub (development)
43
+
44
+ ```bash
45
+ git clone https://github.com/wwood/isiteuk
46
+ cd isiteuk
47
+ pixi shell
48
+ isiteuk --help
49
+ ```
50
+
51
+ This installs isiteuk and all dependencies (including SingleM) into a managed environment via [pixi](https://pixi.sh).
52
+
53
+ ## Reference data
54
+
55
+ Download the isiteuk metapackage from Zenodo:
56
+
57
+ ```bash
58
+ isiteuk data --output-directory /path/to/isiteuk-data
59
+ export ISITEUK_METAPACKAGE_PATH=/path/to/isiteuk-data/isiteuk-backpack-0.0.1
60
+ ```
61
+
62
+ Add the `export` line to your `.bashrc` or equivalent to avoid repeating it. To verify the download:
63
+
64
+ ```bash
65
+ isiteuk data --verify-only
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### Classify genomes
71
+
72
+ ```bash
73
+ isiteuk process \
74
+ --output results.tsv \
75
+ --genome-list genomes.txt \
76
+ --threads 64
77
+ ```
78
+
79
+ Or pass genomes directly:
80
+
81
+ ```bash
82
+ isiteuk process \
83
+ --output results.tsv \
84
+ --genomes genome1.fna.gz genome2.fna.gz \
85
+ --threads 64
86
+ ```
87
+
88
+ The `--metapackage` flag can override `ISITEUK_METAPACKAGE_PATH` for a one-off run:
89
+
90
+ ```bash
91
+ isiteuk process \
92
+ --metapackage /path/to/isiteuk-0.0.1.smpkg \
93
+ --output results.tsv \
94
+ --genomes genome1.fna.gz
95
+ ```
96
+
97
+ To resume an interrupted run:
98
+
99
+ ```bash
100
+ isiteuk process --continue \
101
+ --output results.tsv \
102
+ --genome-list genomes.txt
103
+ ```
104
+
105
+ ### Output format
106
+
107
+ Tab-separated file with one row per genome per domain detected:
108
+
109
+ | genome | domain | num_in_target_domain | num_not_in_target_domain |
110
+ |--------|--------|---------------------|--------------------------|
111
+ | genome1 | d__Bacteria | 14.2 | 0.0 |
112
+ | genome2 | d__Eukaryota | 8.7 | 1.3 |
113
+
114
+ ## Running tests
115
+
116
+ Quick tests (no metapackage required):
117
+
118
+ ```bash
119
+ pixi run test
120
+ ```
121
+
122
+ Full integration tests (requires `ISITEUK_METAPACKAGE_PATH` to be set):
123
+
124
+ ```bash
125
+ pixi run test-expensive
126
+ ```
127
+
128
+ Run a single test by name:
129
+
130
+ ```bash
131
+ pixi run run-a-test test_bacterial_genome
132
+ ```
@@ -0,0 +1,112 @@
1
+ # isiteuk
2
+
3
+ isiteuk classifies genomes by domain (Bacteria, Archaea, Eukaryota) using [SingleM](https://github.com/wwood/SingleM) marker genes.
4
+
5
+ ## Installation
6
+
7
+ ### Conda (recommended)
8
+
9
+ ```bash
10
+ conda create -c conda-forge -c bioconda --override-channels --name isiteuk isiteuk
11
+ conda activate isiteuk
12
+ ```
13
+
14
+ ### PyPI
15
+
16
+ ```bash
17
+ pip install isiteuk
18
+ ```
19
+
20
+ Non-Python dependencies (SingleM, DIAMOND) must be installed separately — see `pixi.toml` for the full list.
21
+
22
+ ### GitHub (development)
23
+
24
+ ```bash
25
+ git clone https://github.com/wwood/isiteuk
26
+ cd isiteuk
27
+ pixi shell
28
+ isiteuk --help
29
+ ```
30
+
31
+ This installs isiteuk and all dependencies (including SingleM) into a managed environment via [pixi](https://pixi.sh).
32
+
33
+ ## Reference data
34
+
35
+ Download the isiteuk metapackage from Zenodo:
36
+
37
+ ```bash
38
+ isiteuk data --output-directory /path/to/isiteuk-data
39
+ export ISITEUK_METAPACKAGE_PATH=/path/to/isiteuk-data/isiteuk-backpack-0.0.1
40
+ ```
41
+
42
+ Add the `export` line to your `.bashrc` or equivalent to avoid repeating it. To verify the download:
43
+
44
+ ```bash
45
+ isiteuk data --verify-only
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ### Classify genomes
51
+
52
+ ```bash
53
+ isiteuk process \
54
+ --output results.tsv \
55
+ --genome-list genomes.txt \
56
+ --threads 64
57
+ ```
58
+
59
+ Or pass genomes directly:
60
+
61
+ ```bash
62
+ isiteuk process \
63
+ --output results.tsv \
64
+ --genomes genome1.fna.gz genome2.fna.gz \
65
+ --threads 64
66
+ ```
67
+
68
+ The `--metapackage` flag can override `ISITEUK_METAPACKAGE_PATH` for a one-off run:
69
+
70
+ ```bash
71
+ isiteuk process \
72
+ --metapackage /path/to/isiteuk-0.0.1.smpkg \
73
+ --output results.tsv \
74
+ --genomes genome1.fna.gz
75
+ ```
76
+
77
+ To resume an interrupted run:
78
+
79
+ ```bash
80
+ isiteuk process --continue \
81
+ --output results.tsv \
82
+ --genome-list genomes.txt
83
+ ```
84
+
85
+ ### Output format
86
+
87
+ Tab-separated file with one row per genome per domain detected:
88
+
89
+ | genome | domain | num_in_target_domain | num_not_in_target_domain |
90
+ |--------|--------|---------------------|--------------------------|
91
+ | genome1 | d__Bacteria | 14.2 | 0.0 |
92
+ | genome2 | d__Eukaryota | 8.7 | 1.3 |
93
+
94
+ ## Running tests
95
+
96
+ Quick tests (no metapackage required):
97
+
98
+ ```bash
99
+ pixi run test
100
+ ```
101
+
102
+ Full integration tests (requires `ISITEUK_METAPACKAGE_PATH` to be set):
103
+
104
+ ```bash
105
+ pixi run test-expensive
106
+ ```
107
+
108
+ Run a single test by name:
109
+
110
+ ```bash
111
+ pixi run run-a-test test_bacterial_genome
112
+ ```
@@ -0,0 +1 @@
1
+ __version__ = '0.0.2'
@@ -0,0 +1,251 @@
1
+ ###############################################################################
2
+ #
3
+ # Copyright (C) 2024 Ben Woodcroft
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+ ###############################################################################
19
+
20
+ import argparse
21
+ import logging
22
+ import sys
23
+ import os
24
+ import polars as pl
25
+
26
+ import isiteuk
27
+ from isiteuk.metapackage import Metapackage as IsiteukMetapackage, DATA_ENVIRONMENT_VARIABLE
28
+
29
+ import singlem.metapackage
30
+ import singlem.pipe
31
+ import singlem.taxonomy
32
+
33
+
34
+ def process_cmd(args):
35
+ logging.info("isiteuk version {}".format(isiteuk.__version__))
36
+
37
+ if args.metapackage:
38
+ metapackage_object = singlem.metapackage.Metapackage.acquire(args.metapackage)
39
+ else:
40
+ metapackage_object = IsiteukMetapackage.acquire_default()
41
+
42
+ marker_to_target_domains = {}
43
+ domain_total_counts = {}
44
+ for pkg in metapackage_object.singlem_packages:
45
+ name = pkg.base_directory()
46
+ target_domains = pkg.target_domains()
47
+ marker_to_target_domains[name] = target_domains
48
+ for domain in target_domains:
49
+ if domain not in domain_total_counts:
50
+ domain_total_counts[domain] = 0
51
+ domain_total_counts[domain] += 1
52
+ log_str = []
53
+ for domain, count in domain_total_counts.items():
54
+ log_str.append(f"{domain}: {count}")
55
+ logging.info("Number of markers for each domain: " + ', '.join(log_str))
56
+
57
+ if args.genomes:
58
+ genomes = args.genomes
59
+ else:
60
+ with open(args.genome_list) as f:
61
+ genomes = [line.strip() for line in f.readlines()]
62
+ logging.info(f"Processing {len(genomes)} genomes")
63
+
64
+ if args.continuing:
65
+ completed_genomes = set()
66
+ if not os.path.exists(args.output):
67
+ logging.error(f"Output file {args.output} does not exist. Cannot continue.")
68
+ sys.exit(1)
69
+ with open(args.output) as f:
70
+ for line in f:
71
+ if line.split("\t")[1] != "domain":
72
+ completed_genomes.add(line.split("\t")[0])
73
+ logging.info(f"Skipping {len(completed_genomes)} genomes already completed")
74
+ genomes = [g for g in genomes if os.path.splitext(os.path.basename(g))[0] not in completed_genomes]
75
+
76
+ genome_chunks = [genomes[i:i + args.chunk_size] for i in range(0, len(genomes), args.chunk_size)]
77
+
78
+ for i, genome_chunk in enumerate(genome_chunks):
79
+ logging.info(f"Processing chunk {i+1} of {len(genome_chunks)}")
80
+ first_line = (i == 0) and (not args.continuing)
81
+
82
+ logging.info("Running SingleM pipe to get an OTU table")
83
+ otu_table = singlem.pipe.SearchPipe().run_to_otu_table(
84
+ genomes=genome_chunk,
85
+ metapackage_object=metapackage_object,
86
+ threads=args.threads,
87
+ min_orf_length=singlem.pipe.SearchPipe.DEFAULT_GENOME_MIN_ORF_LENGTH,
88
+ diamond_prefilter=True,
89
+ assignment_method=singlem.taxonomy.DIAMOND_ASSIGNMENT_METHOD,
90
+ )
91
+
92
+ gene_counts = []
93
+ for otu in otu_table:
94
+ tax_split = otu.taxonomy.split('; ')
95
+ if len(tax_split) == 1:
96
+ continue
97
+ domain = tax_split[1]
98
+ gene_counts.append((otu.sample_name, otu.marker, domain))
99
+
100
+ if not gene_counts:
101
+ logging.info(f"No reads found for chunk {i+1}, writing empty results to mark genomes as completed")
102
+ open_type = 'w' if first_line else 'a'
103
+ with open(args.output, open_type) as f:
104
+ if first_line:
105
+ print('genome\tdomain\tnum_in_target_domain\tnum_not_in_target_domain', file=f)
106
+ for genome in genome_chunk:
107
+ genome_name = os.path.splitext(os.path.basename(genome))[0]
108
+ print(f'{genome_name}\t\t0\t0', file=f)
109
+ continue
110
+
111
+ df = pl.DataFrame(gene_counts).transpose()
112
+ df.columns = ['genome', 'gene', 'domain']
113
+
114
+ genes_and_domains = set()
115
+ for gene, domains in marker_to_target_domains.items():
116
+ for domain in domains:
117
+ genes_and_domains.add(
118
+ os.path.basename(gene).replace('.spkg', '') + '_d__' + domain
119
+ )
120
+ df = df.with_columns(
121
+ (pl.col('gene') + '_' + pl.col('domain')).is_in(genes_and_domains).alias('is_target_domain')
122
+ )
123
+
124
+ domain_cumulative = {}
125
+ for _, group in df.group_by(['genome', 'gene', 'domain', 'is_target_domain']).len().group_by(['genome', 'gene']):
126
+ total_hits = sum(group['len'])
127
+ for row in group.rows(named=True):
128
+ genome = row['genome']
129
+ domain = row['domain']
130
+ count = row['len']
131
+ is_target_domain = row['is_target_domain']
132
+ if genome not in domain_cumulative:
133
+ domain_cumulative[genome] = {}
134
+ if domain not in domain_cumulative[genome]:
135
+ domain_cumulative[genome][domain] = {}
136
+ if is_target_domain not in domain_cumulative[genome][domain]:
137
+ domain_cumulative[genome][domain][True] = 0
138
+ domain_cumulative[genome][domain][False] = 0
139
+ domain_cumulative[genome][domain][is_target_domain] += count / total_hits
140
+
141
+ logging.info(f"Writing results to {args.output}")
142
+ domains = df.unique("domain").get_column("domain").to_list()
143
+ open_type = 'w' if first_line else 'a'
144
+ with open(args.output, open_type) as f:
145
+ if first_line:
146
+ print('genome\tdomain\tnum_in_target_domain\tnum_not_in_target_domain', file=f)
147
+ for genome, domain_counts in domain_cumulative.items():
148
+ for domain in domains:
149
+ if domain not in domain_counts:
150
+ domain_counts[domain] = {True: 0, False: 0}
151
+ for domain, is_target_domain_count in domain_counts.items():
152
+ print(
153
+ f'{genome}\t{domain}\t{is_target_domain_count[True]}\t{is_target_domain_count[False]}',
154
+ file=f
155
+ )
156
+ genomes_with_hits = set(domain_cumulative.keys())
157
+ for genome in genome_chunk:
158
+ genome_name = os.path.splitext(os.path.basename(genome))[0]
159
+ if genome_name not in genomes_with_hits:
160
+ print(f'{genome_name}\t\t0\t0', file=f)
161
+
162
+
163
+ def data_cmd(args):
164
+ if args.verify_only:
165
+ IsiteukMetapackage.verify(output_directory=args.output_directory)
166
+ else:
167
+ IsiteukMetapackage.download(output_directory=args.output_directory)
168
+
169
+
170
+ def main():
171
+ parent_parser = argparse.ArgumentParser(
172
+ prog='isiteuk',
173
+ description='In-situ identification of eukaryotes in metagenomic data',
174
+ )
175
+ parent_parser.add_argument('--debug', help='output debug information', action='store_true')
176
+ parent_parser.add_argument('--quiet', help='only output errors', action='store_true')
177
+ parent_parser.add_argument('--version', action='version', version=f'%(prog)s {isiteuk.__version__}')
178
+
179
+ subparsers = parent_parser.add_subparsers(
180
+ title='subcommands',
181
+ dest='subparser_name',
182
+ description='choose one of these subcommands',
183
+ )
184
+
185
+ # process subcommand
186
+ process_parser = subparsers.add_parser(
187
+ 'process',
188
+ help='Classify genomes by domain using SingleM',
189
+ description='Classify genomes by domain using SingleM marker genes',
190
+ )
191
+ process_parser.add_argument('--threads', type=int, default=1,
192
+ help='Number of threads to use [default: %(default)s]')
193
+ process_parser.add_argument('--output', required=True,
194
+ help='Output TSV file')
195
+ process_parser.add_argument('--chunk-size', type=int, default=10**4,
196
+ help='Number of genomes to process per chunk [default: %(default)s]')
197
+ process_parser.add_argument(
198
+ '--metapackage',
199
+ help=f'SingleM metapackage to use [default: use {DATA_ENVIRONMENT_VARIABLE} env var]',
200
+ )
201
+ process_parser.add_argument('--continue', dest='continuing',
202
+ help='Continue from a previous incomplete run', action='store_true')
203
+ genome_group = process_parser.add_argument_group('Genomes')
204
+ genome_group_mut = genome_group.add_mutually_exclusive_group(required=True)
205
+ genome_group_mut.add_argument('--genomes', nargs='+',
206
+ help='Genome(s) to classify in FASTA format')
207
+ genome_group_mut.add_argument('--genome-list',
208
+ help='File containing newline-separated list of genome paths in FASTA format')
209
+
210
+ # data subcommand
211
+ data_parser = subparsers.add_parser(
212
+ 'data',
213
+ help='Download reference metapackage data',
214
+ description='Download the isiteuk reference metapackage from Zenodo',
215
+ )
216
+ data_parser.add_argument(
217
+ '--output-directory',
218
+ help=f'Directory to download the metapackage to [required unless using --verify-only]',
219
+ )
220
+ data_parser.add_argument(
221
+ '--verify-only',
222
+ help=f'Check that the data in {DATA_ENVIRONMENT_VARIABLE} is up to date with correct checksums',
223
+ action='store_true',
224
+ default=False,
225
+ )
226
+
227
+ args = parent_parser.parse_args()
228
+
229
+ if args.debug:
230
+ loglevel = logging.DEBUG
231
+ elif args.quiet:
232
+ loglevel = logging.ERROR
233
+ else:
234
+ loglevel = logging.INFO
235
+ logging.basicConfig(
236
+ level=loglevel,
237
+ format='%(asctime)s %(levelname)s: %(message)s',
238
+ datefmt='%Y/%m/%d %I:%M:%S %p',
239
+ )
240
+
241
+ if args.subparser_name == 'process':
242
+ process_cmd(args)
243
+ elif args.subparser_name == 'data':
244
+ data_cmd(args)
245
+ else:
246
+ parent_parser.print_help()
247
+ sys.exit(1)
248
+
249
+
250
+ if __name__ == '__main__':
251
+ main()
@@ -0,0 +1,70 @@
1
+ import os
2
+ import logging
3
+ import zenodo_backpack
4
+
5
+ DATA_ENVIRONMENT_VARIABLE = 'ISITEUK_METAPACKAGE_PATH'
6
+ DATA_DEFAULT_VERSION = '0.0.1'
7
+ # TODO: Update DATA_DOI after uploading the metapackage to Zenodo
8
+ DATA_DOI = '10.5281/zenodo.20755874'
9
+
10
+
11
+ class Metapackage:
12
+ @staticmethod
13
+ def acquire_default():
14
+ import singlem.metapackage
15
+ env_path = os.environ.get(DATA_ENVIRONMENT_VARIABLE)
16
+ if env_path is None:
17
+ raise Exception(
18
+ f"No metapackage specified. Either set the {DATA_ENVIRONMENT_VARIABLE} "
19
+ "environment variable, use --metapackage, or run 'isiteuk data' to download."
20
+ )
21
+ # Support both a zenodo backpack directory and a direct .smpkg path.
22
+ # Check for zenodo_backpack_version specifically — .smpkg dirs also have CONTENTS.json.
23
+ import json
24
+ contents_path = os.path.join(env_path, 'CONTENTS.json')
25
+ is_backpack = False
26
+ if os.path.exists(contents_path):
27
+ try:
28
+ with open(contents_path) as f:
29
+ is_backpack = 'zenodo_backpack_version' in json.load(f)
30
+ except Exception:
31
+ pass
32
+ if is_backpack:
33
+ backpack = zenodo_backpack.acquire(path=env_path, version=DATA_DEFAULT_VERSION)
34
+ return singlem.metapackage.Metapackage.acquire(
35
+ backpack.payload_directory_string(enter_single_payload_directory=True)
36
+ )
37
+ else:
38
+ return singlem.metapackage.Metapackage.acquire(env_path)
39
+
40
+ @staticmethod
41
+ def download(output_directory=None):
42
+ if not output_directory:
43
+ raise Exception("Must specify an output directory to download the isiteuk metapackage to")
44
+ logging.info(f"Downloading isiteuk metapackage version {DATA_DEFAULT_VERSION} from {DATA_DOI} ...")
45
+ backpack = zenodo_backpack.ZenodoBackpackDownloader().download_and_extract(
46
+ output_directory,
47
+ DATA_DOI,
48
+ progress_bar=True,
49
+ )
50
+ logging.info("Finished downloading data")
51
+ logging.info(f"export {DATA_ENVIRONMENT_VARIABLE}='{os.path.abspath(backpack.base_directory)}'")
52
+
53
+ @staticmethod
54
+ def verify(output_directory=None):
55
+ if output_directory is not None:
56
+ raise Exception(
57
+ f"Verification does not take an output directory. "
58
+ f"Set {DATA_ENVIRONMENT_VARIABLE} to the downloaded data location."
59
+ )
60
+ if DATA_ENVIRONMENT_VARIABLE not in os.environ:
61
+ raise Exception(
62
+ f"The {DATA_ENVIRONMENT_VARIABLE} environment variable is not set. "
63
+ "Run 'isiteuk data' to download the metapackage first."
64
+ )
65
+ backpack = zenodo_backpack.acquire(
66
+ env_var_name=DATA_ENVIRONMENT_VARIABLE,
67
+ version=DATA_DEFAULT_VERSION,
68
+ )
69
+ zenodo_backpack.ZenodoBackpackDownloader().verify(backpack)
70
+ logging.info("Metapackage verified successfully.")
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: isiteuk
3
+ Version: 0.0.2
4
+ Summary: In-situ identification of eukaryotes in metagenomic data
5
+ Author-email: Ben Woodcroft <benjwoodcroft@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/wwood/isiteuk
8
+ Keywords: metagenomics,bioinformatics,eukaryotes
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Topic :: Scientific/Engineering
11
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENCE.txt
16
+ Requires-Dist: singlem>=0.16.0
17
+ Requires-Dist: polars>=0.20
18
+ Requires-Dist: zenodo_backpack>=0.3
19
+ Dynamic: license-file
20
+
21
+ # isiteuk
22
+
23
+ isiteuk classifies genomes by domain (Bacteria, Archaea, Eukaryota) using [SingleM](https://github.com/wwood/SingleM) marker genes.
24
+
25
+ ## Installation
26
+
27
+ ### Conda (recommended)
28
+
29
+ ```bash
30
+ conda create -c conda-forge -c bioconda --override-channels --name isiteuk isiteuk
31
+ conda activate isiteuk
32
+ ```
33
+
34
+ ### PyPI
35
+
36
+ ```bash
37
+ pip install isiteuk
38
+ ```
39
+
40
+ Non-Python dependencies (SingleM, DIAMOND) must be installed separately — see `pixi.toml` for the full list.
41
+
42
+ ### GitHub (development)
43
+
44
+ ```bash
45
+ git clone https://github.com/wwood/isiteuk
46
+ cd isiteuk
47
+ pixi shell
48
+ isiteuk --help
49
+ ```
50
+
51
+ This installs isiteuk and all dependencies (including SingleM) into a managed environment via [pixi](https://pixi.sh).
52
+
53
+ ## Reference data
54
+
55
+ Download the isiteuk metapackage from Zenodo:
56
+
57
+ ```bash
58
+ isiteuk data --output-directory /path/to/isiteuk-data
59
+ export ISITEUK_METAPACKAGE_PATH=/path/to/isiteuk-data/isiteuk-backpack-0.0.1
60
+ ```
61
+
62
+ Add the `export` line to your `.bashrc` or equivalent to avoid repeating it. To verify the download:
63
+
64
+ ```bash
65
+ isiteuk data --verify-only
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### Classify genomes
71
+
72
+ ```bash
73
+ isiteuk process \
74
+ --output results.tsv \
75
+ --genome-list genomes.txt \
76
+ --threads 64
77
+ ```
78
+
79
+ Or pass genomes directly:
80
+
81
+ ```bash
82
+ isiteuk process \
83
+ --output results.tsv \
84
+ --genomes genome1.fna.gz genome2.fna.gz \
85
+ --threads 64
86
+ ```
87
+
88
+ The `--metapackage` flag can override `ISITEUK_METAPACKAGE_PATH` for a one-off run:
89
+
90
+ ```bash
91
+ isiteuk process \
92
+ --metapackage /path/to/isiteuk-0.0.1.smpkg \
93
+ --output results.tsv \
94
+ --genomes genome1.fna.gz
95
+ ```
96
+
97
+ To resume an interrupted run:
98
+
99
+ ```bash
100
+ isiteuk process --continue \
101
+ --output results.tsv \
102
+ --genome-list genomes.txt
103
+ ```
104
+
105
+ ### Output format
106
+
107
+ Tab-separated file with one row per genome per domain detected:
108
+
109
+ | genome | domain | num_in_target_domain | num_not_in_target_domain |
110
+ |--------|--------|---------------------|--------------------------|
111
+ | genome1 | d__Bacteria | 14.2 | 0.0 |
112
+ | genome2 | d__Eukaryota | 8.7 | 1.3 |
113
+
114
+ ## Running tests
115
+
116
+ Quick tests (no metapackage required):
117
+
118
+ ```bash
119
+ pixi run test
120
+ ```
121
+
122
+ Full integration tests (requires `ISITEUK_METAPACKAGE_PATH` to be set):
123
+
124
+ ```bash
125
+ pixi run test-expensive
126
+ ```
127
+
128
+ Run a single test by name:
129
+
130
+ ```bash
131
+ pixi run run-a-test test_bacterial_genome
132
+ ```
@@ -0,0 +1,13 @@
1
+ LICENCE.txt
2
+ README.md
3
+ pyproject.toml
4
+ isiteuk/__init__.py
5
+ isiteuk/main.py
6
+ isiteuk/metapackage.py
7
+ isiteuk.egg-info/PKG-INFO
8
+ isiteuk.egg-info/SOURCES.txt
9
+ isiteuk.egg-info/dependency_links.txt
10
+ isiteuk.egg-info/entry_points.txt
11
+ isiteuk.egg-info/requires.txt
12
+ isiteuk.egg-info/top_level.txt
13
+ test/test_isiteuk.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ isiteuk = isiteuk.main:main
@@ -0,0 +1,3 @@
1
+ singlem>=0.16.0
2
+ polars>=0.20
3
+ zenodo_backpack>=0.3
@@ -0,0 +1 @@
1
+ isiteuk
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=45"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "isiteuk"
7
+ version = "0.0.2"
8
+ description = "In-situ identification of eukaryotes in metagenomic data"
9
+ readme = "README.md"
10
+ license = "GPL-3.0-or-later"
11
+ license-files = ["LICENCE.txt"]
12
+ authors = [
13
+ {name = "Ben Woodcroft", email = "benjwoodcroft@gmail.com"},
14
+ ]
15
+ keywords = ["metagenomics", "bioinformatics", "eukaryotes"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Topic :: Scientific/Engineering",
19
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
20
+ "Programming Language :: Python :: 3",
21
+ ]
22
+ requires-python = ">=3.7"
23
+ dependencies = [
24
+ "singlem>=0.16.0",
25
+ "polars>=0.20",
26
+ "zenodo_backpack>=0.3",
27
+ ]
28
+
29
+ [project.scripts]
30
+ isiteuk = "isiteuk.main:main"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/wwood/isiteuk"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["."]
37
+ include = ["isiteuk*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,226 @@
1
+ import pytest
2
+ import subprocess
3
+ import sys
4
+ import os
5
+
6
+ import isiteuk
7
+
8
+ TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data')
9
+ BACTERIAL_GENOME = os.path.join(TEST_DATA_DIR, 'GCF_002008365.1_genomic.fna.gz')
10
+ ARCHAEAL_GENOME = os.path.join(TEST_DATA_DIR, 'GCA_003139855.1_genomic.fna.gz')
11
+ EUK_GENOME = os.path.join(TEST_DATA_DIR, 'binchicken_co8412.34_euk.fna.gz')
12
+
13
+ DEFAULT_METAPACKAGE = '/work/microbiome/db/isiteuk/isiteuk-0.0.1.smpkg'
14
+
15
+ ISITEUK_CMD = [sys.executable, '-m', 'isiteuk.main']
16
+
17
+ EXPECTED_HEADER = 'genome\tdomain\tnum_in_target_domain\tnum_not_in_target_domain'
18
+
19
+
20
+ def _is_zenodo_backpack(path):
21
+ import json
22
+ contents = os.path.join(path, 'CONTENTS.json')
23
+ if not os.path.exists(contents):
24
+ return False
25
+ try:
26
+ with open(contents) as f:
27
+ return 'zenodo_backpack_version' in json.load(f)
28
+ except Exception:
29
+ return False
30
+
31
+
32
+ def _resolve_metapackage():
33
+ """Return the path to the .smpkg, unwrapping a zenodo backpack if needed."""
34
+ from isiteuk.metapackage import DATA_ENVIRONMENT_VARIABLE
35
+ import zenodo_backpack
36
+
37
+ env_path = os.environ.get(DATA_ENVIRONMENT_VARIABLE)
38
+ candidates = [p for p in [env_path, DEFAULT_METAPACKAGE] if p]
39
+
40
+ for path in candidates:
41
+ if not os.path.isdir(path):
42
+ continue
43
+ if _is_zenodo_backpack(path):
44
+ backpack = zenodo_backpack.acquire(path=path)
45
+ return backpack.payload_directory_string(enter_single_payload_directory=True)
46
+ return path
47
+
48
+ return None
49
+
50
+
51
+ class TestVersion:
52
+ def test_version(self):
53
+ assert isiteuk.__version__ is not None
54
+
55
+ def test_cli_version(self):
56
+ result = subprocess.run(
57
+ ISITEUK_CMD + ['--version'],
58
+ capture_output=True, text=True,
59
+ )
60
+ assert result.returncode == 0
61
+ assert isiteuk.__version__ in result.stdout
62
+
63
+
64
+ def run_isiteuk(*args):
65
+ return subprocess.run(
66
+ ISITEUK_CMD + list(args),
67
+ capture_output=True,
68
+ text=True,
69
+ )
70
+
71
+
72
+ def _parse_output(content):
73
+ """Parse isiteuk TSV output into {genome: {domain: total_hits}}."""
74
+ result = {}
75
+ for line in content.splitlines()[1:]: # skip header
76
+ parts = line.split('\t')
77
+ if len(parts) < 4:
78
+ continue
79
+ genome, domain, in_target, not_in_target = parts[0], parts[1], parts[2], parts[3]
80
+ if not domain:
81
+ continue
82
+ result.setdefault(genome, {})[domain] = float(in_target) + float(not_in_target)
83
+ return result
84
+
85
+
86
+ def _dominant_domain(hits_by_domain):
87
+ """Return the domain with the most total hits."""
88
+ return max(hits_by_domain, key=hits_by_domain.get)
89
+
90
+
91
+ @pytest.fixture(scope='module')
92
+ def metapackage():
93
+ from isiteuk.metapackage import DATA_ENVIRONMENT_VARIABLE
94
+ path = _resolve_metapackage()
95
+ if path is None:
96
+ pytest.skip(
97
+ f'Metapackage not found. Set {DATA_ENVIRONMENT_VARIABLE} to a zenodo backpack '
98
+ f'directory (after running isiteuk data) or directly to a .smpkg path.'
99
+ )
100
+ return path
101
+
102
+
103
+ @pytest.mark.expensive
104
+ class TestProcess:
105
+ def test_bacterial_genome(self, metapackage, tmp_path):
106
+ output = str(tmp_path / 'output.tsv')
107
+ result = run_isiteuk(
108
+ 'process',
109
+ '--metapackage', metapackage,
110
+ '--output', output,
111
+ '--genomes', BACTERIAL_GENOME,
112
+ '--threads', '12',
113
+ )
114
+ assert result.returncode == 0, result.stderr
115
+ with open(output) as f:
116
+ lines = f.readlines()
117
+ assert lines[0].rstrip() == EXPECTED_HEADER
118
+ genome_names = [line.split('\t')[0] for line in lines[1:]]
119
+ assert 'GCF_002008365.1_genomic' in genome_names
120
+
121
+ def test_archaeal_genome(self, metapackage, tmp_path):
122
+ output = str(tmp_path / 'output.tsv')
123
+ result = run_isiteuk(
124
+ 'process',
125
+ '--metapackage', metapackage,
126
+ '--output', output,
127
+ '--genomes', ARCHAEAL_GENOME,
128
+ '--threads', '12',
129
+ )
130
+ assert result.returncode == 0, result.stderr
131
+ with open(output) as f:
132
+ lines = f.readlines()
133
+ assert lines[0].rstrip() == EXPECTED_HEADER
134
+ genome_names = [line.split('\t')[0] for line in lines[1:]]
135
+ assert 'GCA_003139855.1_genomic' in genome_names
136
+
137
+ def test_eukaryotic_genome(self, metapackage, tmp_path):
138
+ output = str(tmp_path / 'output.tsv')
139
+ result = run_isiteuk(
140
+ 'process',
141
+ '--metapackage', metapackage,
142
+ '--output', output,
143
+ '--genomes', EUK_GENOME,
144
+ '--threads', '12',
145
+ )
146
+ assert result.returncode == 0, result.stderr
147
+ with open(output) as f:
148
+ lines = f.readlines()
149
+ assert lines[0].rstrip() == EXPECTED_HEADER
150
+ genome_names = [line.split('\t')[0] for line in lines[1:]]
151
+ assert 'binchicken_co8412.34_euk' in genome_names
152
+
153
+ def test_all_three_genomes(self, metapackage, tmp_path):
154
+ output = str(tmp_path / 'output.tsv')
155
+ result = run_isiteuk(
156
+ 'process',
157
+ '--metapackage', metapackage,
158
+ '--output', output,
159
+ '--genomes', BACTERIAL_GENOME, ARCHAEAL_GENOME, EUK_GENOME,
160
+ '--threads', '12',
161
+ )
162
+ assert result.returncode == 0, result.stderr
163
+ with open(output) as f:
164
+ content = f.read()
165
+ assert EXPECTED_HEADER in content
166
+
167
+ hits = _parse_output(content)
168
+
169
+ bac = hits['GCF_002008365.1_genomic']
170
+ assert 'Bacteria' in _dominant_domain(bac), \
171
+ f'Expected bacterial genome to have most hits in a Bacteria domain, got: {bac}'
172
+
173
+ arc = hits['GCA_003139855.1_genomic']
174
+ assert 'Archaea' in _dominant_domain(arc), \
175
+ f'Expected archaeal genome to have most hits in an Archaea domain, got: {arc}'
176
+
177
+ euk = hits['binchicken_co8412.34_euk']
178
+ euk_dominant = _dominant_domain(euk)
179
+ assert 'Bacteria' not in euk_dominant and 'Archaea' not in euk_dominant, \
180
+ f'Expected eukaryotic genome dominant domain to be non-prokaryotic, got: {euk}'
181
+
182
+ def test_continue_flag(self, metapackage, tmp_path):
183
+ output = str(tmp_path / 'output.tsv')
184
+ # First run: process just the bacterial genome
185
+ result = run_isiteuk(
186
+ 'process',
187
+ '--metapackage', metapackage,
188
+ '--output', output,
189
+ '--genomes', BACTERIAL_GENOME,
190
+ '--threads', '12',
191
+ )
192
+ assert result.returncode == 0, result.stderr
193
+
194
+ # Second run: continue and add the archaeal genome
195
+ result = run_isiteuk(
196
+ 'process',
197
+ '--metapackage', metapackage,
198
+ '--output', output,
199
+ '--genomes', BACTERIAL_GENOME, ARCHAEAL_GENOME,
200
+ '--continue',
201
+ '--threads', '12',
202
+ )
203
+ assert result.returncode == 0, result.stderr
204
+ with open(output) as f:
205
+ content = f.read()
206
+ assert 'GCF_002008365.1_genomic' in content
207
+ assert 'GCA_003139855.1_genomic' in content
208
+
209
+ def test_genome_list_file(self, metapackage, tmp_path):
210
+ genome_list = str(tmp_path / 'genomes.txt')
211
+ output = str(tmp_path / 'output.tsv')
212
+ with open(genome_list, 'w') as f:
213
+ f.write(BACTERIAL_GENOME + '\n')
214
+ f.write(ARCHAEAL_GENOME + '\n')
215
+ result = run_isiteuk(
216
+ 'process',
217
+ '--metapackage', metapackage,
218
+ '--output', output,
219
+ '--genome-list', genome_list,
220
+ '--threads', '12',
221
+ )
222
+ assert result.returncode == 0, result.stderr
223
+ with open(output) as f:
224
+ content = f.read()
225
+ assert 'GCF_002008365.1_genomic' in content
226
+ assert 'GCA_003139855.1_genomic' in content