taxdumpy 0.1.1__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,212 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ # More
210
+ .DS_Store
211
+ CLAUDE.md
212
+ .claude
taxdumpy-0.1.1/LICENSE ADDED
@@ -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.
@@ -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,127 @@
1
+ # Taxdumpy 🧬
2
+
3
+ #### NCBI Taxonomy Toolkit for Python
4
+
5
+ *A high-performance parser for NCBI Taxonomy databases with lineage resolution and taxonomy search*
6
+
7
+ ![](https://img.shields.io/badge/license-MIT-blue.svg)
8
+ ![](https://img.shields.io/badge/python-3.10+-blue.svg)
9
+ ![](https://img.shields.io/pypi/v/taxdumpy?color=green)
10
+
11
+ ## Features
12
+
13
+ - **Blazing Fast Parsing**
14
+ Optimized loading of NCBI taxdump files (`nodes.dmp`, `names.dmp`, etc.) with optional pickle caching
15
+
16
+ - **Comprehensive Taxon Operations**
17
+ - TaxID validation and lineage tracing
18
+ - Scientific name resolution
19
+ - Rank-based filtering (species → kingdom)
20
+ - Merged/deleted node handling
21
+
22
+ - **Fuzzy Search**
23
+ Rapid approximate name matching using `rapidfuzz` (supports misspellings)
24
+
25
+ - **Memory Efficient**
26
+ Lazy loading and optimized data structures for large taxonomies
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install taxdumpy
32
+ ```
33
+
34
+ Or from source:
35
+ ```bash
36
+ git clone https://github.com/yourusername/taxdumpy.git
37
+ cd taxdumpy
38
+ pip install -e .
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ from taxdumpy import TaxDb, Taxon
45
+
46
+ # Initialize database (auto-downloads if needed)
47
+ taxdb = TaxDb("/path/to/taxdump")
48
+
49
+ # Create taxon object
50
+ human = Taxon(9606, taxdb) # Homo sapiens
51
+
52
+ # Access lineage
53
+ print(human.name_lineage)
54
+ # ['Homo sapiens', 'Homo', 'Hominidae', ..., 'cellular organisms']
55
+
56
+ # Search organisms
57
+ taxdb._rapid_fuzz("Influenza", limit=5)
58
+ ```
59
+
60
+ ## Command Line Interface
61
+
62
+ ```bash
63
+ # Cache full database
64
+ taxdumpy cache -d /path/to/taxdump
65
+
66
+ # Search organism
67
+ taxdumpy search --fast "Escherichia coli"
68
+
69
+ # Trace lineage
70
+ taxdumpy lineage --fast 511145 # E. coli K-12
71
+ ```
72
+
73
+ ## Database Setup
74
+
75
+ 1. Download NCBI taxdump:
76
+ ```bash
77
+ mkdir -p ~/.taxonkit
78
+ wget ftp://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz -P ~/.taxonkit
79
+ tar -xzf ~/.taxonkit/taxdump.tar.gz -C ~/.taxonkit
80
+ ```
81
+
82
+ 2. (Optional) Create optimized cache:
83
+ ```bash
84
+ taxdumpy cache -d ~/.taxonkit
85
+ ```
86
+
87
+ ## Advanced Usage
88
+
89
+ ### Custom Caching
90
+ ```python
91
+ # Fast cache with specific taxids
92
+ with open("important_taxids.txt", "w") as f:
93
+ f.write("\n".join(["9606", "511145"]))
94
+
95
+ # CLI
96
+ taxdumpy fast-cache -d ~/.taxonkit -f important_taxids.txt
97
+ ```
98
+
99
+ ### API Reference
100
+ ```python
101
+ class Taxon:
102
+ """Represents a taxonomic unit"""
103
+
104
+ @property
105
+ def lineage(self) -> List[Node]: ...
106
+ @property
107
+ def rank_lineage(self) -> List[str]: ...
108
+ @property
109
+ def is_legacy(self) -> bool: ...
110
+ ```
111
+
112
+ ## Performance Tips
113
+
114
+ - Use `fast=True` when loading for ~3x speedup (requires pre-caching)
115
+ - For batch processing, reuse `TaxDb` instances
116
+ - Set `TAXDB_PATH` environment variable to avoid path repetition
117
+
118
+ ## Contributing
119
+
120
+ PRs welcome! Please:
121
+ 1. Format with `black`
122
+ 2. Include type hints
123
+ 3. Add tests under `/tests`
124
+
125
+ ## License
126
+
127
+ MIT © 2025 [Omega HH](https://github.com/omegahh)
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "taxdumpy"
7
+ description = "Python package for efficiently parsing NCBI's taxdump database"
8
+ authors = [
9
+ {name = "Omega HH", email = "omeganju@gmail.com"}
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+ license = {file = "LICENSE"}
22
+ keywords = [
23
+ "metagenomics",
24
+ "taxonomy",
25
+ "tree-of-life",
26
+ "NCBI"
27
+ ]
28
+ readme = "README.md"
29
+ requires-python = ">=3.10"
30
+ dependencies = [
31
+ "rapidfuzz",
32
+ "tqdm"
33
+ ]
34
+ dynamic = ["version"]
35
+
36
+ [project.optional-dependencies]
37
+ dev = ["black", "pytest", "build"]
38
+
39
+ [project.urls]
40
+ Documentation = "https://github.com/omegahh/taxdumpy"
41
+ Issues = "https://github.com/omegahh/taxdumpy/issues"
42
+ Source = "https://github.com/omegahh/taxdumpy"
43
+
44
+ [project.scripts]
45
+ taxdumpy = "taxdumpy.__main__:main"
46
+
47
+ [tool.hatch.metadata]
48
+ allow-direct-references = true
49
+
50
+ [tool.hatch.version]
51
+ path = "src/taxdumpy/__init__.py"
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/taxdumpy"]
@@ -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
+ ]
@@ -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())
@@ -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"
@@ -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
@@ -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())
@@ -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
@@ -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}")
@@ -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
@@ -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}")