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 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
@@ -0,0 +1,8 @@
1
+ """Taxdumpy main entry point.
2
+ """
3
+
4
+ import sys
5
+ from taxdumpy.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
taxdumpy/ansi.py ADDED
@@ -0,0 +1,5 @@
1
+
2
+ b_str = lambda x: f"\033[1m{x}\033[0m" # Bold
3
+ u_str = lambda x: f"\033[1;4m{x}\033[0m" # UnderLine
4
+ r_str = lambda x: f"\033[1;31m{x}\033[0m" # Red
5
+ m_color = lambda m: f"\033[1;31m{m.group(0)}\033[0m"
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
+ ![](https://img.shields.io/badge/license-MIT-blue.svg)
56
+ ![](https://img.shields.io/badge/python-3.10+-blue.svg)
57
+ ![](https://img.shields.io/pypi/v/taxdumpy?color=green)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ taxdumpy = taxdumpy.__main__:main
@@ -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.