taxdumpy 0.1.1__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.
- taxdumpy/__init__.py +19 -0
- taxdumpy/__main__.py +8 -0
- taxdumpy/ansi.py +5 -0
- taxdumpy/basic.py +21 -0
- taxdumpy/cli.py +104 -0
- taxdumpy/functions.py +48 -0
- taxdumpy/taxdb.py +171 -0
- taxdumpy/taxon.py +116 -0
- taxdumpy/taxsqlite.py +252 -0
- taxdumpy-0.1.1.dist-info/METADATA +175 -0
- taxdumpy-0.1.1.dist-info/RECORD +14 -0
- taxdumpy-0.1.1.dist-info/WHEEL +4 -0
- taxdumpy-0.1.1.dist-info/entry_points.txt +2 -0
- taxdumpy-0.1.1.dist-info/licenses/LICENSE +21 -0
taxdumpy/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Taxdumpy: A Python package for parsing NCBI taxdump database and resolving taxonomy lineage.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.1"
|
|
5
|
+
|
|
6
|
+
from taxdumpy.taxdb import TaxDb, TaxDbError, TaxidError
|
|
7
|
+
from taxdumpy.taxon import Taxon
|
|
8
|
+
from taxdumpy.functions import upper_rank_id
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
# Exceptions
|
|
12
|
+
"TaxDbError",
|
|
13
|
+
"TaxidError",
|
|
14
|
+
# Data classes
|
|
15
|
+
"TaxDb",
|
|
16
|
+
"Taxon",
|
|
17
|
+
# utilities
|
|
18
|
+
"upper_rank_id"
|
|
19
|
+
]
|
taxdumpy/__main__.py
ADDED
taxdumpy/ansi.py
ADDED
taxdumpy/basic.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
@dataclass
|
|
4
|
+
class Node:
|
|
5
|
+
__slots__ = ("taxid","parent","rank","name","equal","acronym","legacy","division")
|
|
6
|
+
taxid: int
|
|
7
|
+
parent: int
|
|
8
|
+
rank: str
|
|
9
|
+
name: str
|
|
10
|
+
equal: str|None
|
|
11
|
+
acronym: str|None
|
|
12
|
+
division: str
|
|
13
|
+
|
|
14
|
+
class TaxDbError(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class TaxidError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class TaxRankError(Exception):
|
|
21
|
+
pass
|
taxdumpy/cli.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Taxdumpy command-line interface
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import pickle
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, List
|
|
12
|
+
|
|
13
|
+
from taxdumpy.taxdb import TaxDb
|
|
14
|
+
from taxdumpy.taxsqlite import TaxSQLite
|
|
15
|
+
from taxdumpy.taxon import Taxon
|
|
16
|
+
from taxdumpy.ansi import u_str
|
|
17
|
+
|
|
18
|
+
def _params_parser():
|
|
19
|
+
TAXDB_PATH = os.environ.get("TAXDB_PATH", Path.home() / ".taxonkit")
|
|
20
|
+
parser = argparse.ArgumentParser(description="Toolkit for parsing NCBI taxonomy and resolving taxon lineage")
|
|
21
|
+
sub_parsers = parser.add_subparsers(dest='command')
|
|
22
|
+
|
|
23
|
+
cache_parser = sub_parsers.add_parser('cache')
|
|
24
|
+
cache_parser.add_argument('-d', dest='taxdb', help=f'path of upzipped taxonomy database, default: {TAXDB_PATH}', default=TAXDB_PATH)
|
|
25
|
+
cache_parser.add_argument('-f', dest='taxidf', help=f"If specified, cache a tiny version of TaxDb which only contains taxids in the given file.")
|
|
26
|
+
|
|
27
|
+
trace_parser = sub_parsers.add_parser('lineage')
|
|
28
|
+
trace_parser.add_argument('-d', dest='taxdb', help=f'path of upzipped taxonomy database, default: {TAXDB_PATH}', default=TAXDB_PATH)
|
|
29
|
+
trace_parser.add_argument("--fast", action="store_true")
|
|
30
|
+
trace_parser.add_argument("taxid", help=f"NCBI taxonomy id", type=int)
|
|
31
|
+
|
|
32
|
+
sname_parser = sub_parsers.add_parser('search')
|
|
33
|
+
sname_parser.add_argument('-d', dest='taxdb', help=f'path of upzipped taxonomy database, default: {TAXDB_PATH}', default=TAXDB_PATH)
|
|
34
|
+
sname_parser.add_argument("--fast", action="store_true")
|
|
35
|
+
sname_parser.add_argument("keyword", help=f"organism name", type=str)
|
|
36
|
+
|
|
37
|
+
return parser
|
|
38
|
+
|
|
39
|
+
# Main Entry Point
|
|
40
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
41
|
+
parsed_args = _params_parser().parse_args(args)
|
|
42
|
+
taxdb_path = Path(parsed_args.taxdb)
|
|
43
|
+
if not taxdb_path.is_dir():
|
|
44
|
+
print(f"Input Path {taxdb_path=} is not exist. aborting!")
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
if parsed_args.command == 'cache':
|
|
48
|
+
sqlite_file = taxdb_path / "taxdump.sqlite"
|
|
49
|
+
sqlite_file.unlink(missing_ok=True)
|
|
50
|
+
TAXSQL = TaxSQLite(taxdb_path)
|
|
51
|
+
print(TAXSQL)
|
|
52
|
+
if not parsed_args.taxidf:
|
|
53
|
+
pickle_file = taxdb_path / "taxdump.pickle"
|
|
54
|
+
pickle_file.unlink(missing_ok=True)
|
|
55
|
+
TAXDB = TaxDb(taxdb_path)
|
|
56
|
+
TAXDB.dump_taxdump()
|
|
57
|
+
else:
|
|
58
|
+
taxid_file = Path(parsed_args.taxidf)
|
|
59
|
+
assert taxid_file.is_file(), FileExistsError(f"Input file {u_str(taxid_file)} is not found")
|
|
60
|
+
try:
|
|
61
|
+
with open(taxid_file, 'r') as fin:
|
|
62
|
+
all_taxids = [int(line.strip()) for line in fin]
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"Error in parsing taxid file:\n\t{u_str(taxid_file)}")
|
|
65
|
+
raise e
|
|
66
|
+
all_taxids = frozenset(all_taxids)
|
|
67
|
+
print(f"Loaded {len(all_taxids):,} taxids...")
|
|
68
|
+
|
|
69
|
+
pickle_file = taxdb_path / "taxdump_fast.pickle"
|
|
70
|
+
pickle_file.unlink(missing_ok=True)
|
|
71
|
+
TAXDB = TaxDb(taxdb_path)
|
|
72
|
+
print(f"Loading the full version of TAXDB, which contains {len(TAXDB):,} taxids.")
|
|
73
|
+
kept_taxids = []
|
|
74
|
+
for taxid in tqdm(TAXDB._taxid2nodes.keys(), ncols=120):
|
|
75
|
+
taxon = Taxon(taxid, TAXDB)
|
|
76
|
+
for lineage_taxid in taxon.taxid_lineage:
|
|
77
|
+
if lineage_taxid in all_taxids:
|
|
78
|
+
kept_taxids.extend(taxon.taxid_lineage)
|
|
79
|
+
break
|
|
80
|
+
kept_taxids = frozenset(kept_taxids)
|
|
81
|
+
print(f"Number of kept taxids: {len(kept_taxids):,}")
|
|
82
|
+
kept_taxid2nodes = {k: v for k,v in TAXDB._taxid2nodes.items() if k in kept_taxids}
|
|
83
|
+
print(f"Writing to {u_str(pickle_file)}")
|
|
84
|
+
with open(pickle_file, 'wb') as fout:
|
|
85
|
+
pickle.dump([kept_taxid2nodes,
|
|
86
|
+
TAXDB._old2news,
|
|
87
|
+
TAXDB.delnodes], fout, protocol=pickle.HIGHEST_PROTOCOL)
|
|
88
|
+
print(f"Fast Taxdump dumped!")
|
|
89
|
+
|
|
90
|
+
elif parsed_args.command == 'lineage':
|
|
91
|
+
TAXDB = TaxSQLite(taxdb_path)
|
|
92
|
+
taxid = parsed_args.taxid
|
|
93
|
+
taxon = Taxon(taxid, TAXDB)
|
|
94
|
+
print(taxon.__repr__())
|
|
95
|
+
|
|
96
|
+
elif parsed_args.command == 'search':
|
|
97
|
+
TAXDB = TaxSQLite(taxdb_path)
|
|
98
|
+
keyword = parsed_args.keyword
|
|
99
|
+
TAXDB.fuzzy_search(kw=keyword)
|
|
100
|
+
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
sys.exit(main())
|
taxdumpy/functions.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description: Functions for Taxdumpy
|
|
3
|
+
Author: Hao Hong (omeganju@gmail.com)
|
|
4
|
+
Created: 2025-07-06 17:07:57
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from taxdumpy.basic import TaxRankError
|
|
10
|
+
from taxdumpy.taxon import Taxon
|
|
11
|
+
from taxdumpy.taxdb import TaxDb
|
|
12
|
+
# from taxdumpy.taxsqlite import TaxSQLite
|
|
13
|
+
|
|
14
|
+
RANKNAMES = ["species", "genus", "family", "order", "class", "phylum", "superkingdom", "realm"]
|
|
15
|
+
RANK2LEVEL = {k: i for i,k in enumerate(RANKNAMES)}
|
|
16
|
+
LEVEL2RANK = {i: k for i,k in enumerate(RANKNAMES)}
|
|
17
|
+
def upper_rank_id(tax: Taxon,
|
|
18
|
+
taxdb: TaxDb,
|
|
19
|
+
rank: Literal["species", "genus", "family", "order",
|
|
20
|
+
"class", "phylum", "superkingdom", "realm"],
|
|
21
|
+
) -> int:
|
|
22
|
+
curr_rank = tax.rank
|
|
23
|
+
if curr_rank not in RANKNAMES:
|
|
24
|
+
raise TaxRankError(f"TAXID={tax.taxid} ({curr_rank=}) is a non-canonical phylogenetic rank")
|
|
25
|
+
assert rank in RANKNAMES, TaxRankError(f"{rank=} is a non-canonical phylogenetic rank")
|
|
26
|
+
# and check rank levels
|
|
27
|
+
curr_level = RANK2LEVEL[curr_rank]
|
|
28
|
+
high_level = RANK2LEVEL[rank]
|
|
29
|
+
if high_level <= curr_level:
|
|
30
|
+
raise RuntimeError(f"{rank=} is in lower/equal level than {curr_rank=} in phylogenetic tree")
|
|
31
|
+
# Get upper rank iteratively
|
|
32
|
+
if rank in tax.rank_lineage:
|
|
33
|
+
return tax.taxid_lineage[tax.rank_lineage.index(rank)]
|
|
34
|
+
elif f"sub{rank}" in tax.rank_lineage:
|
|
35
|
+
return tax.taxid_lineage[tax.rank_lineage.index(f"sub{rank}")]
|
|
36
|
+
else:
|
|
37
|
+
# neither rank nor sub-rank in the lineage
|
|
38
|
+
temp_level = high_level
|
|
39
|
+
temp_rank = LEVEL2RANK[temp_level]
|
|
40
|
+
while temp_level > curr_level:
|
|
41
|
+
temp_level -= 1
|
|
42
|
+
temp_rank = LEVEL2RANK[temp_level]
|
|
43
|
+
if temp_rank in tax.rank_lineage:
|
|
44
|
+
break;
|
|
45
|
+
jump_taxid = tax.taxid_lineage[tax.rank_lineage.index(temp_rank)]
|
|
46
|
+
for _ in range(high_level - temp_level):
|
|
47
|
+
jump_taxid = Taxon(jump_taxid, taxdb).parent
|
|
48
|
+
return jump_taxid
|
taxdumpy/taxdb.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description: NCBI Taxonomy toolkit for parsing database, display taxid, show-lineage, etc,.
|
|
3
|
+
Author: Hao Hong (omeganju@gmail.com)
|
|
4
|
+
Created: 2025-06-30 19:51:00
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import pickle
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from functools import cached_property
|
|
12
|
+
|
|
13
|
+
from rapidfuzz import process, fuzz
|
|
14
|
+
|
|
15
|
+
from taxdumpy.basic import Node, TaxDbError, TaxidError
|
|
16
|
+
from taxdumpy.ansi import u_str, m_color
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TaxDb:
|
|
20
|
+
"""
|
|
21
|
+
Create an object of the TaxDb class.
|
|
22
|
+
"""
|
|
23
|
+
def __init__(self, taxdump_dir: Path|str, fast: bool = False) -> None:
|
|
24
|
+
self._taxdump_dir = Path(taxdump_dir).resolve()
|
|
25
|
+
self._fast = fast
|
|
26
|
+
|
|
27
|
+
self._taxdbf = [self._taxdump_dir / 'merged.dmp',
|
|
28
|
+
self._taxdump_dir / 'delnodes.dmp',
|
|
29
|
+
self._taxdump_dir / 'division.dmp',
|
|
30
|
+
self._taxdump_dir / 'nodes.dmp',
|
|
31
|
+
self._taxdump_dir / 'names.dmp'
|
|
32
|
+
]
|
|
33
|
+
self._mrgef, self._delnf, self._divsf, self._nodef, self._namef = self._taxdbf
|
|
34
|
+
self._pickle = self._taxdump_dir / ('taxdump_fast.pickle' if fast else 'taxdump.pickle')
|
|
35
|
+
|
|
36
|
+
self._taxid2nodes, self._old2news, self._delnodes = self._load_taxdump()
|
|
37
|
+
|
|
38
|
+
def _check_taxdump(self):
|
|
39
|
+
if not (self._pickle.is_file() or all([f.is_file() for f in self._taxdbf])):
|
|
40
|
+
raise TaxDbError("required taxdump files are incomplete")
|
|
41
|
+
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return f"Taxdump (Dict) Database from {u_str(self._taxdump_dir)}"
|
|
44
|
+
|
|
45
|
+
def __len__(self) -> int:
|
|
46
|
+
return len(self._taxid2nodes)
|
|
47
|
+
|
|
48
|
+
def _load_taxdump(self):
|
|
49
|
+
if self._pickle.is_file():
|
|
50
|
+
with open(self._pickle, 'rb') as fin:
|
|
51
|
+
taxid2nodes, old2news, del_taxids = pickle.load(fin)
|
|
52
|
+
else:
|
|
53
|
+
taxid2nodes, old2news, del_taxids = self._import_nodes()
|
|
54
|
+
|
|
55
|
+
return taxid2nodes, old2news, del_taxids
|
|
56
|
+
|
|
57
|
+
@cached_property
|
|
58
|
+
def all_names(self) -> list[str]:
|
|
59
|
+
return [node.name for node in self._taxid2nodes.values()]
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def name2taxid(self) -> dict[str, int]:
|
|
63
|
+
return {node.name:taxid for taxid,node in self._taxid2nodes.items()}
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def delnodes(self) -> set[int]:
|
|
67
|
+
return self._delnodes
|
|
68
|
+
|
|
69
|
+
@cached_property
|
|
70
|
+
def max_taxid_strlen(self) -> int:
|
|
71
|
+
return len(str(max(list(self._taxid2nodes.keys()))))
|
|
72
|
+
|
|
73
|
+
@cached_property
|
|
74
|
+
def max_rank_strlen(self) -> int:
|
|
75
|
+
return max(set([len(n.rank) for n in self._taxid2nodes.values()]))
|
|
76
|
+
|
|
77
|
+
def _import_merged(self) -> dict[int, int]:
|
|
78
|
+
old2new = {}
|
|
79
|
+
with open(self._mrgef, "r") as f:
|
|
80
|
+
for L in f:
|
|
81
|
+
old, new = L.split('\t')[0], L.split('\t')[2]
|
|
82
|
+
old2new[int(old)] = int(new)
|
|
83
|
+
return old2new
|
|
84
|
+
|
|
85
|
+
def _import_divcodes(self) -> dict[int, str]:
|
|
86
|
+
div2codes = {}
|
|
87
|
+
with open(self._divsf, 'r') as f:
|
|
88
|
+
for L in f:
|
|
89
|
+
div, code = L.split('\t')[0], L.split('\t')[2]
|
|
90
|
+
div2codes[int(div)] = code
|
|
91
|
+
return div2codes
|
|
92
|
+
|
|
93
|
+
def _import_delnodes(self) -> set[int]:
|
|
94
|
+
del_taxids = set()
|
|
95
|
+
with open(self._delnf, 'r') as f:
|
|
96
|
+
for L in f:
|
|
97
|
+
del_taxids.add(int(L.split('\t')[0]))
|
|
98
|
+
return del_taxids
|
|
99
|
+
|
|
100
|
+
def _import_names(self) -> tuple[dict, dict, dict]:
|
|
101
|
+
sci_names, eq_names, acr_names = {}, {}, {}
|
|
102
|
+
with open(self._namef, "r") as f:
|
|
103
|
+
for L in f:
|
|
104
|
+
sp = L.split('\t')
|
|
105
|
+
tid = int(sp[0]); name = sp[2]; clas = sp[6]
|
|
106
|
+
if clas == "scientific name":
|
|
107
|
+
sci_names[tid] = name
|
|
108
|
+
elif clas == "equivalent name":
|
|
109
|
+
eq_names.setdefault(tid, []).append(name)
|
|
110
|
+
elif clas == "acronym":
|
|
111
|
+
acr_names.setdefault(tid, []).append(name)
|
|
112
|
+
return sci_names, eq_names, acr_names
|
|
113
|
+
|
|
114
|
+
def _import_nodes(self) -> tuple[dict[int, Node], dict[int, int], set[int]]:
|
|
115
|
+
old2news = self._import_merged()
|
|
116
|
+
div2codes = self._import_divcodes()
|
|
117
|
+
del_taxids = self._import_delnodes()
|
|
118
|
+
old_taxids = set(old2news.keys())
|
|
119
|
+
sci_names, eq_names, acr_names = self._import_names()
|
|
120
|
+
taxid2nodes = {}
|
|
121
|
+
with open(self._nodef, "r") as f:
|
|
122
|
+
for line in f:
|
|
123
|
+
line = line.split('\t')
|
|
124
|
+
taxid = int(line[0])
|
|
125
|
+
parent = int(line[2])
|
|
126
|
+
rank = line[4]
|
|
127
|
+
divid = int(line[8])
|
|
128
|
+
|
|
129
|
+
node = Node(
|
|
130
|
+
taxid = taxid,
|
|
131
|
+
parent = old2news[parent] if parent in old_taxids else parent,
|
|
132
|
+
rank = rank,
|
|
133
|
+
name = sci_names[taxid],
|
|
134
|
+
equal = "|".join(eq_names[taxid]) if taxid in eq_names.keys() else None,
|
|
135
|
+
acronym = "|".join(acr_names[taxid]) if taxid in acr_names.keys() else None,
|
|
136
|
+
division=div2codes[divid]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
taxid2nodes[taxid] = node
|
|
140
|
+
# Finished import all current taxid nodes, extending old_taxids (for compact)
|
|
141
|
+
cur_taxids = set(taxid2nodes.keys())
|
|
142
|
+
assert not old_taxids.intersection(cur_taxids), TaxDbError("current taxids have legacy taxids")
|
|
143
|
+
|
|
144
|
+
return taxid2nodes, old2news, del_taxids
|
|
145
|
+
|
|
146
|
+
def dump_taxdump(self):
|
|
147
|
+
print(f"Writing to {self._pickle}")
|
|
148
|
+
with open(self._pickle, 'wb') as fout:
|
|
149
|
+
pickle.dump([self._taxid2nodes,
|
|
150
|
+
self._old2news,
|
|
151
|
+
self.delnodes], fout, protocol=pickle.HIGHEST_PROTOCOL)
|
|
152
|
+
print(f"Taxdump dumped!")
|
|
153
|
+
|
|
154
|
+
def get_node(self, taxid: int) -> Node:
|
|
155
|
+
if taxid in self._old2news:
|
|
156
|
+
taxid = self._old2news[taxid]
|
|
157
|
+
|
|
158
|
+
if taxid in self.delnodes:
|
|
159
|
+
raise TaxidError(f"{taxid=} is a deleted taxonomy identifier")
|
|
160
|
+
try:
|
|
161
|
+
return self._taxid2nodes[taxid]
|
|
162
|
+
except KeyError:
|
|
163
|
+
raise TaxidError(f"{taxid=} is not a valid taxonomy identifier")
|
|
164
|
+
|
|
165
|
+
def fuzzy_search(self, kw: str, limit: int = 10):
|
|
166
|
+
results = process.extract(kw, self.all_names, scorer=fuzz.WRatio, limit=limit)
|
|
167
|
+
for name, _, _ in results:
|
|
168
|
+
tid = self.name2taxid[name]
|
|
169
|
+
node = self.get_node(tid)
|
|
170
|
+
col = re.sub(kw, m_color, name, flags=re.IGNORECASE)
|
|
171
|
+
print(f"{tid:<{self.max_taxid_strlen}}\t{node.rank:<{self.max_rank_strlen}}\t{col}")
|
taxdumpy/taxon.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description: NCBI Taxonomy toolkit for parsing database, display taxid, show-lineage, etc,.
|
|
3
|
+
Author: Hao Hong (omeganju@gmail.com)
|
|
4
|
+
Created: 2025-06-30 19:51:00
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from taxdumpy.ansi import b_str, r_str
|
|
8
|
+
from taxdumpy.taxdb import TaxDb
|
|
9
|
+
from taxdumpy.taxsqlite import Node, TaxSQLite
|
|
10
|
+
|
|
11
|
+
class Taxon:
|
|
12
|
+
"""
|
|
13
|
+
Create an object of the Taxon class.
|
|
14
|
+
"""
|
|
15
|
+
# @line_profiler.profile
|
|
16
|
+
def __init__(self, taxid: int, taxdb: TaxDb|TaxSQLite):
|
|
17
|
+
# taxid, taxonomy node, and lineage
|
|
18
|
+
self._taxid = taxid
|
|
19
|
+
self._node = taxdb.get_node(taxid)
|
|
20
|
+
self._lineage = self._find_lineage(taxdb)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def taxid(self) -> int:
|
|
24
|
+
return self._taxid
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def node(self) -> Node:
|
|
28
|
+
return self._node
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def lineage(self) -> list[Node]:
|
|
32
|
+
return self._lineage
|
|
33
|
+
|
|
34
|
+
# other infered taxon attributes
|
|
35
|
+
@property
|
|
36
|
+
def update_taxid(self) -> int:
|
|
37
|
+
return self.node.taxid
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def name(self) -> str:
|
|
41
|
+
return self.node.name
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def rank(self) -> str:
|
|
45
|
+
return self.node.rank
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def parent(self) -> int:
|
|
49
|
+
return self.node.parent
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def acronym(self) -> str|None:
|
|
53
|
+
return self.node.acronym
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def division(self) -> str:
|
|
57
|
+
return self.node.division
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_legacy(self) -> bool:
|
|
61
|
+
return self.taxid != self.node.taxid
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def has_species_level(self) -> bool:
|
|
65
|
+
return "species" in self.rank_lineage
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def species_taxid(self) -> int:
|
|
69
|
+
return self.taxid_lineage[self.rank_lineage.index("species")] if self.has_species_level else 0
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def taxid_lineage(self) -> list[int]:
|
|
73
|
+
return [node.taxid for node in self.lineage]
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def rank_lineage(self) -> list[str]:
|
|
77
|
+
return [node.rank for node in self.lineage]
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def name_lineage(self) -> list[str]:
|
|
81
|
+
return [node.name for node in self.lineage]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def _max_tlen(self) -> int:
|
|
85
|
+
return max([len(str(t)) for t in self.taxid_lineage])
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def _max_rlen(self) -> int:
|
|
89
|
+
return max([len(r) for r in self.rank_lineage])
|
|
90
|
+
|
|
91
|
+
def __repr__(self):
|
|
92
|
+
lineage_str_list = []
|
|
93
|
+
for t, r, n in zip(self.taxid_lineage, self.rank_lineage, self.name_lineage):
|
|
94
|
+
current_str = f"{t:<{self._max_tlen}}\t{r:<{self._max_rlen}}\t{n}"
|
|
95
|
+
if t == self.node.taxid:
|
|
96
|
+
if self.acronym:
|
|
97
|
+
current_str += f" ({self.acronym})"
|
|
98
|
+
if self.is_legacy:
|
|
99
|
+
current_str += r_str(f" <- [{self.taxid} has been merged!]")
|
|
100
|
+
current_str = b_str(current_str)
|
|
101
|
+
lineage_str_list.append(current_str)
|
|
102
|
+
return "\n".join(reversed(lineage_str_list))
|
|
103
|
+
|
|
104
|
+
def __str__(self):
|
|
105
|
+
print_str = f"{self.update_taxid:<10}\t{self.rank}\t{self.name}"
|
|
106
|
+
if self.is_legacy:
|
|
107
|
+
print_str += r_str(f" <- [{self.taxid} has been merged!]")
|
|
108
|
+
return print_str
|
|
109
|
+
|
|
110
|
+
def _find_lineage(self, taxdb: TaxDb|TaxSQLite) -> list[Node]:
|
|
111
|
+
lineage = [self.node]
|
|
112
|
+
current_taxid = lineage[-1].taxid
|
|
113
|
+
while lineage[-1].parent != current_taxid:
|
|
114
|
+
current_taxid = lineage[-1].parent
|
|
115
|
+
lineage.append(taxdb.get_node(current_taxid))
|
|
116
|
+
return lineage
|
taxdumpy/taxsqlite.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description: NCBI Taxonomy toolkit for parsing database, and build up a SQLite database, etc,.
|
|
3
|
+
Author: Hao Hong (omeganju@gmail.com)
|
|
4
|
+
Created: 2025-06-30 19:51:00
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import sqlite3
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from rapidfuzz import process, fuzz
|
|
12
|
+
from functools import cached_property
|
|
13
|
+
|
|
14
|
+
from taxdumpy.ansi import u_str, m_color
|
|
15
|
+
from taxdumpy.basic import Node, TaxidError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaxSQLite:
|
|
19
|
+
"""
|
|
20
|
+
Create an object of the TaxDb class.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, taxdump_dir: Path|str):
|
|
23
|
+
self._taxdump_dir = Path(taxdump_dir).resolve()
|
|
24
|
+
|
|
25
|
+
self._db_path = self._taxdump_dir / "taxdump.sqlite"
|
|
26
|
+
|
|
27
|
+
# open a sqlite connection, and use row_factory for element access, r['name'] for example
|
|
28
|
+
self._conn = sqlite3.connect(self._db_path)
|
|
29
|
+
self._conn.row_factory = sqlite3.Row
|
|
30
|
+
if not self._check_tables():
|
|
31
|
+
self._build_database()
|
|
32
|
+
|
|
33
|
+
def __enter__(self):
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
37
|
+
if hasattr(self, '_conn'):
|
|
38
|
+
self._conn.close()
|
|
39
|
+
|
|
40
|
+
def close(self):
|
|
41
|
+
"""Close SQLite connection"""
|
|
42
|
+
if hasattr(self, '_conn'):
|
|
43
|
+
self._conn.close()
|
|
44
|
+
|
|
45
|
+
def _check_tables(self) -> bool:
|
|
46
|
+
c = self._conn.execute(
|
|
47
|
+
"SELECT name FROM sqlite_master WHERE type='table' and name='nodes'")
|
|
48
|
+
return c.fetchone() is not None
|
|
49
|
+
|
|
50
|
+
def __repr__(self):
|
|
51
|
+
repr_str = f"Taxdump (SQLite) Database from {u_str(self._taxdump_dir)}\n"
|
|
52
|
+
nodes_num = self._conn.execute("SELECT COUNT(*) FROM nodes").fetchone()[0]
|
|
53
|
+
repr_str += f"\tImported nodes: {nodes_num:,}\n"
|
|
54
|
+
merged_num = self._conn.execute("SELECT COUNT(*) FROM merged").fetchone()[0]
|
|
55
|
+
repr_str += f"\tLegacy nodes: {merged_num:,}\n"
|
|
56
|
+
delete_num = len(self.delnodes)
|
|
57
|
+
repr_str += f"\tDeleted nodes: {delete_num:,}"
|
|
58
|
+
return repr_str
|
|
59
|
+
|
|
60
|
+
def _build_database(self):
|
|
61
|
+
try:
|
|
62
|
+
print("Build up TAXDUMP SQLite Database...")
|
|
63
|
+
c = self._conn.cursor()
|
|
64
|
+
# 1) Create Tables
|
|
65
|
+
c.executescript(
|
|
66
|
+
"""
|
|
67
|
+
CREATE TABLE nodes (
|
|
68
|
+
taxid INTEGER PRIMARY KEY,
|
|
69
|
+
parent INTEGER NOT NULL,
|
|
70
|
+
rank TEXT NOT NULL,
|
|
71
|
+
division TEXT NOT NULL
|
|
72
|
+
);
|
|
73
|
+
CREATE TABLE names (
|
|
74
|
+
taxid INTEGER NOT NULL,
|
|
75
|
+
name TEXT NOT NULL,
|
|
76
|
+
name_class TEXT NOT NULL,
|
|
77
|
+
PRIMARY KEY (taxid, name_class, name)
|
|
78
|
+
);
|
|
79
|
+
CREATE TABLE merged (
|
|
80
|
+
oldtid INTEGER PRIMARY KEY,
|
|
81
|
+
newtid INTEGER NOT NULL
|
|
82
|
+
);
|
|
83
|
+
CREATE INDEX idx_names_name ON names(name);
|
|
84
|
+
CREATE INDEX idx_names_taxid ON names(taxid);
|
|
85
|
+
CREATE INDEX idx_names_taxid_class ON names(taxid, name_class);
|
|
86
|
+
CREATE INDEX idx_nodes_parent ON nodes(parent);
|
|
87
|
+
"""
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# 2) Parsing merged.dmp, division.dmp, delnodes.dmp, names.dmp
|
|
91
|
+
old2new = self._import_merged()
|
|
92
|
+
div2code = self._import_divcodes()
|
|
93
|
+
sci_names, eq_names, acr_names = self._import_names()
|
|
94
|
+
|
|
95
|
+
# 3) Insert Nodes
|
|
96
|
+
rows_nodes = []
|
|
97
|
+
with open(self._taxdump_dir / "nodes.dmp", 'r') as f:
|
|
98
|
+
for line in f:
|
|
99
|
+
cols = line.split('\t')
|
|
100
|
+
taxid = int(cols[0])
|
|
101
|
+
parent = int(cols[2])
|
|
102
|
+
parent = old2new[parent] if parent in old2new else parent
|
|
103
|
+
rank = cols[4]
|
|
104
|
+
divc = div2code[int(cols[8])]
|
|
105
|
+
rows_nodes.append((taxid, parent, rank, divc))
|
|
106
|
+
c.executemany(
|
|
107
|
+
"INSERT INTO nodes VALUES (?,?,?,?)", rows_nodes
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 4) Insert Names
|
|
111
|
+
rows_names = []
|
|
112
|
+
for taxid, nm in sci_names.items():
|
|
113
|
+
rows_names.append((taxid, nm, "scientific"))
|
|
114
|
+
for taxid, lst in eq_names.items():
|
|
115
|
+
for nm in lst:
|
|
116
|
+
rows_names.append((taxid, nm, "equivalent"))
|
|
117
|
+
for taxid, lst in acr_names.items():
|
|
118
|
+
for nm in lst:
|
|
119
|
+
rows_names.append((taxid, nm, "acronym"))
|
|
120
|
+
c.executemany(
|
|
121
|
+
"INSERT INTO names VALUES (?,?,?)",
|
|
122
|
+
rows_names
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# 5) Insert Legacy Taxids
|
|
126
|
+
rows_legacy = [(old,new) for old,new in old2new.items()]
|
|
127
|
+
c.executemany("INSERT INTO merged VALUES (?,?)", rows_legacy)
|
|
128
|
+
self._conn.commit()
|
|
129
|
+
except Exception:
|
|
130
|
+
self._conn.rollback()
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
@cached_property
|
|
134
|
+
def all_names(self) -> list[str]:
|
|
135
|
+
# Loading all scientific names
|
|
136
|
+
all_names = [
|
|
137
|
+
row["name"]
|
|
138
|
+
for row in self._conn.execute(
|
|
139
|
+
"SELECT name FROM names WHERE name_class='scientific'")
|
|
140
|
+
]
|
|
141
|
+
return all_names
|
|
142
|
+
|
|
143
|
+
@cached_property
|
|
144
|
+
def name2taxid(self) -> dict[str, int]:
|
|
145
|
+
# create name->taxid map
|
|
146
|
+
name2taxid = {
|
|
147
|
+
row["name"]: row["taxid"]
|
|
148
|
+
for row in self._conn.execute(
|
|
149
|
+
"SELECT taxid,name FROM names WHERE name_class='scientific'"
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
return name2taxid
|
|
153
|
+
|
|
154
|
+
@cached_property
|
|
155
|
+
def delnodes(self) -> set[int]:
|
|
156
|
+
return self._import_delnodes()
|
|
157
|
+
|
|
158
|
+
@cached_property
|
|
159
|
+
def max_taxid_strlen(self) -> int:
|
|
160
|
+
max_taxid = self._conn.execute(
|
|
161
|
+
"SELECT MAX(taxid) AS max_taxid FROM nodes"
|
|
162
|
+
).fetchone()["max_taxid"]
|
|
163
|
+
return len(str(max_taxid))
|
|
164
|
+
|
|
165
|
+
@cached_property
|
|
166
|
+
def max_rank_strlen(self) -> int:
|
|
167
|
+
row = self._conn.execute(
|
|
168
|
+
"SELECT MAX(LENGTH(rank)) FROM nodes"
|
|
169
|
+
).fetchone()
|
|
170
|
+
return row[0]
|
|
171
|
+
|
|
172
|
+
def _import_merged(self) -> dict[int,int]:
|
|
173
|
+
old2new = {}
|
|
174
|
+
with open(self._taxdump_dir / "merged.dmp", 'r') as f:
|
|
175
|
+
for line in f:
|
|
176
|
+
parts = line.split('\t')
|
|
177
|
+
old2new[int(parts[0])] = int(parts[2])
|
|
178
|
+
return old2new
|
|
179
|
+
|
|
180
|
+
def _import_divcodes(self) -> dict[int,str]:
|
|
181
|
+
div2codes = {}
|
|
182
|
+
with open(self._taxdump_dir / "division.dmp", 'r') as f:
|
|
183
|
+
for L in f:
|
|
184
|
+
div, code = L.split('\t')[0], L.split('\t')[2]
|
|
185
|
+
div2codes[int(div)] = code
|
|
186
|
+
return div2codes
|
|
187
|
+
|
|
188
|
+
def _import_delnodes(self) -> set[int]:
|
|
189
|
+
s = set()
|
|
190
|
+
with open(self._taxdump_dir / "delnodes.dmp", 'r') as f:
|
|
191
|
+
for L in f:
|
|
192
|
+
s.add(int(L.split('\t')[0]))
|
|
193
|
+
return s
|
|
194
|
+
|
|
195
|
+
def _import_names(self):
|
|
196
|
+
sci_names, eq_names, acr_names = {}, {}, {}
|
|
197
|
+
with open(self._taxdump_dir / "names.dmp", 'r') as f:
|
|
198
|
+
for L in f:
|
|
199
|
+
sp = L.split('\t')
|
|
200
|
+
tid = int(sp[0]); name = sp[2]; clas = sp[6]
|
|
201
|
+
if clas == "scientific name":
|
|
202
|
+
sci_names[tid] = name
|
|
203
|
+
elif clas == "equivalent name":
|
|
204
|
+
eq_names.setdefault(tid, []).append(name)
|
|
205
|
+
elif clas == "acronym":
|
|
206
|
+
acr_names.setdefault(tid, []).append(name)
|
|
207
|
+
return sci_names, eq_names, acr_names
|
|
208
|
+
|
|
209
|
+
def get_node(self, taxid: int) -> Node:
|
|
210
|
+
r1 = self._conn.execute(
|
|
211
|
+
"SELECT newtid FROM merged WHERE oldtid=?", (taxid,)
|
|
212
|
+
).fetchone()
|
|
213
|
+
if r1:
|
|
214
|
+
taxid = r1['newtid']
|
|
215
|
+
|
|
216
|
+
r = self._conn.execute(
|
|
217
|
+
"SELECT * FROM nodes WHERE taxid=?", (taxid,)
|
|
218
|
+
).fetchone()
|
|
219
|
+
if not r:
|
|
220
|
+
if taxid in self.delnodes:
|
|
221
|
+
raise TaxidError(f"{taxid=} is a deleted taxonomy identifier")
|
|
222
|
+
else:
|
|
223
|
+
raise TaxidError(f"{taxid=} is a wrong taxonomy identifier")
|
|
224
|
+
|
|
225
|
+
name = self._conn.execute(
|
|
226
|
+
"SELECT name FROM names WHERE taxid=? AND name_class='scientific'", (taxid,)
|
|
227
|
+
).fetchone()["name"]
|
|
228
|
+
eqv = self._conn.execute(
|
|
229
|
+
"SELECT group_concat(name,'|') AS a FROM names WHERE taxid=? AND name_class='equivalent'",
|
|
230
|
+
(taxid,)
|
|
231
|
+
).fetchone()["a"]
|
|
232
|
+
acr = self._conn.execute(
|
|
233
|
+
"SELECT group_concat(name,'|') AS a FROM names WHERE taxid=? AND name_class='acronym'",
|
|
234
|
+
(taxid,)
|
|
235
|
+
).fetchone()["a"]
|
|
236
|
+
return Node(
|
|
237
|
+
taxid = r["taxid"],
|
|
238
|
+
parent = r["parent"],
|
|
239
|
+
rank = r["rank"],
|
|
240
|
+
name = name,
|
|
241
|
+
equal = eqv,
|
|
242
|
+
acronym = acr,
|
|
243
|
+
division = r["division"]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def fuzzy_search(self, kw: str, limit: int = 10):
|
|
247
|
+
results = process.extract(kw, self.all_names, scorer=fuzz.WRatio, limit=limit)
|
|
248
|
+
for name, _, _ in results:
|
|
249
|
+
tid = self.name2taxid[name]
|
|
250
|
+
node = self.get_node(tid)
|
|
251
|
+
col = re.sub(kw, m_color, name, flags=re.IGNORECASE)
|
|
252
|
+
print(f"{tid:<{self.max_taxid_strlen}}\t{node.rank:<{self.max_rank_strlen}}\t{col}")
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taxdumpy
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python package for efficiently parsing NCBI's taxdump database
|
|
5
|
+
Project-URL: Documentation, https://github.com/omegahh/taxdumpy
|
|
6
|
+
Project-URL: Issues, https://github.com/omegahh/taxdumpy/issues
|
|
7
|
+
Project-URL: Source, https://github.com/omegahh/taxdumpy
|
|
8
|
+
Author-email: Omega HH <omeganju@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2025 Omega HH
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: NCBI,metagenomics,taxonomy,tree-of-life
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Programming Language :: Python
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Requires-Dist: rapidfuzz
|
|
42
|
+
Requires-Dist: tqdm
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: black; extra == 'dev'
|
|
45
|
+
Requires-Dist: build; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# Taxdumpy 🧬
|
|
50
|
+
|
|
51
|
+
#### NCBI Taxonomy Toolkit for Python
|
|
52
|
+
|
|
53
|
+
*A high-performance parser for NCBI Taxonomy databases with lineage resolution and taxonomy search*
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+

|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- **Blazing Fast Parsing**
|
|
62
|
+
Optimized loading of NCBI taxdump files (`nodes.dmp`, `names.dmp`, etc.) with optional pickle caching
|
|
63
|
+
|
|
64
|
+
- **Comprehensive Taxon Operations**
|
|
65
|
+
- TaxID validation and lineage tracing
|
|
66
|
+
- Scientific name resolution
|
|
67
|
+
- Rank-based filtering (species → kingdom)
|
|
68
|
+
- Merged/deleted node handling
|
|
69
|
+
|
|
70
|
+
- **Fuzzy Search**
|
|
71
|
+
Rapid approximate name matching using `rapidfuzz` (supports misspellings)
|
|
72
|
+
|
|
73
|
+
- **Memory Efficient**
|
|
74
|
+
Lazy loading and optimized data structures for large taxonomies
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install taxdumpy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or from source:
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/yourusername/taxdumpy.git
|
|
85
|
+
cd taxdumpy
|
|
86
|
+
pip install -e .
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quick Start
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from taxdumpy import TaxDb, Taxon
|
|
93
|
+
|
|
94
|
+
# Initialize database (auto-downloads if needed)
|
|
95
|
+
taxdb = TaxDb("/path/to/taxdump")
|
|
96
|
+
|
|
97
|
+
# Create taxon object
|
|
98
|
+
human = Taxon(9606, taxdb) # Homo sapiens
|
|
99
|
+
|
|
100
|
+
# Access lineage
|
|
101
|
+
print(human.name_lineage)
|
|
102
|
+
# ['Homo sapiens', 'Homo', 'Hominidae', ..., 'cellular organisms']
|
|
103
|
+
|
|
104
|
+
# Search organisms
|
|
105
|
+
taxdb._rapid_fuzz("Influenza", limit=5)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Command Line Interface
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Cache full database
|
|
112
|
+
taxdumpy cache -d /path/to/taxdump
|
|
113
|
+
|
|
114
|
+
# Search organism
|
|
115
|
+
taxdumpy search --fast "Escherichia coli"
|
|
116
|
+
|
|
117
|
+
# Trace lineage
|
|
118
|
+
taxdumpy lineage --fast 511145 # E. coli K-12
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Database Setup
|
|
122
|
+
|
|
123
|
+
1. Download NCBI taxdump:
|
|
124
|
+
```bash
|
|
125
|
+
mkdir -p ~/.taxonkit
|
|
126
|
+
wget ftp://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz -P ~/.taxonkit
|
|
127
|
+
tar -xzf ~/.taxonkit/taxdump.tar.gz -C ~/.taxonkit
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
2. (Optional) Create optimized cache:
|
|
131
|
+
```bash
|
|
132
|
+
taxdumpy cache -d ~/.taxonkit
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Advanced Usage
|
|
136
|
+
|
|
137
|
+
### Custom Caching
|
|
138
|
+
```python
|
|
139
|
+
# Fast cache with specific taxids
|
|
140
|
+
with open("important_taxids.txt", "w") as f:
|
|
141
|
+
f.write("\n".join(["9606", "511145"]))
|
|
142
|
+
|
|
143
|
+
# CLI
|
|
144
|
+
taxdumpy fast-cache -d ~/.taxonkit -f important_taxids.txt
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### API Reference
|
|
148
|
+
```python
|
|
149
|
+
class Taxon:
|
|
150
|
+
"""Represents a taxonomic unit"""
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def lineage(self) -> List[Node]: ...
|
|
154
|
+
@property
|
|
155
|
+
def rank_lineage(self) -> List[str]: ...
|
|
156
|
+
@property
|
|
157
|
+
def is_legacy(self) -> bool: ...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Performance Tips
|
|
161
|
+
|
|
162
|
+
- Use `fast=True` when loading for ~3x speedup (requires pre-caching)
|
|
163
|
+
- For batch processing, reuse `TaxDb` instances
|
|
164
|
+
- Set `TAXDB_PATH` environment variable to avoid path repetition
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
PRs welcome! Please:
|
|
169
|
+
1. Format with `black`
|
|
170
|
+
2. Include type hints
|
|
171
|
+
3. Add tests under `/tests`
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT © 2025 [Omega HH](https://github.com/omegahh)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
taxdumpy/__init__.py,sha256=T1aAYXM2Y20HyTOJlYEItvEuAtpnYwRlyJ4cNCYde9U,448
|
|
2
|
+
taxdumpy/__main__.py,sha256=xYqzJ5ZUEtTmupOYRyeYyF6uDMvjh5E-LAHnbGkYtQo,125
|
|
3
|
+
taxdumpy/ansi.py,sha256=9-8dhq3R6m5nF4S2qVLAmROchLS5fjorGLIwQmQ6tgs,202
|
|
4
|
+
taxdumpy/basic.py,sha256=F0UOwUf-cTSLqAXG_O2eJrYr2jgn_KReK1wUD_RWLE4,400
|
|
5
|
+
taxdumpy/cli.py,sha256=UDtfei175DpglKno72y55gQP2uPnnDkhedln0yJcKeg,4358
|
|
6
|
+
taxdumpy/functions.py,sha256=1ussuuoYzPQhWj0DQlclMg6ElLjUkG5me2QVAHh65i0,1988
|
|
7
|
+
taxdumpy/taxdb.py,sha256=C1WkfkhB4eIghvJs8Z5MM2gzkzj2VhcjIMy3zdi2BAk,6452
|
|
8
|
+
taxdumpy/taxon.py,sha256=xrLyW0nc_DzDnrF2LB_GcAf0wwWEbvd-Nowq6WkYDM0,3386
|
|
9
|
+
taxdumpy/taxsqlite.py,sha256=eTFzw_vTYB3kbvIwiKXUfAUSLAuAWHJjmi7dACTJodo,9401
|
|
10
|
+
taxdumpy-0.1.1.dist-info/METADATA,sha256=VzimuS1zHueNzvOKBQsvCINE5vtjPfMKu1rxDWDx8Jk,5121
|
|
11
|
+
taxdumpy-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
+
taxdumpy-0.1.1.dist-info/entry_points.txt,sha256=GGWyhLcaFw6VxEHSC84ZDmeqsFBpZpEqY0Q35iGk2Kc,52
|
|
13
|
+
taxdumpy-0.1.1.dist-info/licenses/LICENSE,sha256=a6TSIZkfV5s38RTMhkHaO0erKe-QbfNM1CeRbDovzvw,1065
|
|
14
|
+
taxdumpy-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Omega HH
|
|
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.
|