taxdumpy 1.2.1__tar.gz → 1.3.0__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.
Files changed (44) hide show
  1. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/.gitignore +6 -0
  2. taxdumpy-1.3.0/AGENTS.md +287 -0
  3. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/PKG-INFO +175 -43
  4. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/README.md +173 -41
  5. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/pytest.ini +1 -1
  6. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/__init__.py +8 -1
  7. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/basic.py +9 -4
  8. taxdumpy-1.3.0/src/taxdumpy/cache.py +43 -0
  9. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/cli.py +15 -55
  10. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/database.py +66 -29
  11. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/functions.py +197 -111
  12. taxdumpy-1.3.0/src/taxdumpy/ranks.py +64 -0
  13. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/taxdb.py +115 -85
  14. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/taxon.py +20 -31
  15. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/taxsqlite.py +126 -92
  16. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/conftest.py +13 -0
  17. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_api_consistency.py +34 -2
  18. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_basic.py +8 -1
  19. taxdumpy-1.3.0/tests/test_cache.py +82 -0
  20. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_cli.py +23 -5
  21. taxdumpy-1.3.0/tests/test_critical_regressions.py +158 -0
  22. taxdumpy-1.3.0/tests/test_descendants.py +137 -0
  23. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_error_handling.py +14 -2
  24. taxdumpy-1.3.0/tests/test_fast_cache.py +44 -0
  25. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_functions.py +142 -43
  26. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_taxdb.py +22 -4
  27. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_taxon.py +35 -5
  28. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_taxsqlite.py +22 -1
  29. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/.coveragerc +0 -0
  30. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/LICENSE +0 -0
  31. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/docs/incomplete_lineages.md +0 -0
  32. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/pyproject.toml +0 -0
  33. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/scripts/release.py +0 -0
  34. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/__main__.py +0 -0
  35. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/src/taxdumpy/ansi.py +0 -0
  36. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/__init__.py +0 -0
  37. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/data/delnodes.dmp +0 -0
  38. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/data/division.dmp +0 -0
  39. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/data/kept_taxids.list +0 -0
  40. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/data/make_smalldb.sh +0 -0
  41. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/data/merged.dmp +0 -0
  42. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/data/names.dmp +0 -0
  43. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/data/nodes.dmp +0 -0
  44. {taxdumpy-1.2.1 → taxdumpy-1.3.0}/tests/test_integration.py +0 -0
@@ -206,6 +206,12 @@ marimo/_static/
206
206
  marimo/_lsp/
207
207
  __marimo__/
208
208
 
209
+ # Taxdump caches generated beside the .dmp files when tests or tools run
210
+ taxdump.pickle
211
+ taxdump_fast.pickle
212
+ taxdump.sqlite
213
+ taxdump.sqlite.*.tmp
214
+
209
215
  # More
210
216
  .DS_Store
211
217
  CLAUDE.md
@@ -0,0 +1,287 @@
1
+ # Repository Guidance
2
+
3
+ Instructions and verified project notes for agents working in this repository.
4
+
5
+ ## Rules
6
+
7
+ - Do not add "Co-Authored-By" messages in git commits
8
+
9
+ ## Project Overview
10
+
11
+ **Taxdumpy** is a high-performance Python toolkit for parsing NCBI Taxonomy databases with lineage resolution and fuzzy search capabilities. Designed for metagenomics, phylogenetics, and bioinformatics applications.
12
+
13
+ - **Version source**: `src/taxdumpy/__init__.py` (Hatch reads the version from this file)
14
+ - **Python**: 3.10+
15
+ - **License**: MIT
16
+ - **Repository visibility**: GitHub repository `omegahh/taxdumpy` is private.
17
+ Codecov coverage uploads require a Codecov account and repository token; the
18
+ current CI upload is non-blocking if Codecov rejects or cannot process it.
19
+
20
+ ## Development Commands
21
+
22
+ ### Installation and Setup
23
+ ```bash
24
+ # Install in development mode
25
+ python -m pip install -e .
26
+
27
+ # Install with dependencies
28
+ python -m pip install -e '.[dev]'
29
+ ```
30
+
31
+ ### Running the CLI
32
+ ```bash
33
+ # Cache taxonomy database (requires NCBI taxdump files)
34
+ taxdumpy cache -d /path/to/taxdump
35
+
36
+ # Search for organisms with fuzzy matching
37
+ taxdumpy search --fast "Escherichia coli"
38
+
39
+ # Get lineage for a specific taxid
40
+ taxdumpy lineage 511145
41
+ ```
42
+
43
+ `search --fast` and `lineage --fast` need a filtered pickle cache created with
44
+ `cache -f FILE`. Without `--fast`, both commands use SQLite to avoid loading the
45
+ full pickle for a single query. Python batch callers should create one `TaxDb`
46
+ instance and reuse it for all queries.
47
+
48
+ ### Testing and Quality
49
+ ```bash
50
+ # Run all tests with coverage (requires 80% minimum)
51
+ pytest
52
+
53
+ # Run specific test categories
54
+ pytest -m "not slow and not integration" # Fast tests only
55
+ pytest -m "slow" # Slow tests only
56
+ pytest -m "integration" # Integration tests only
57
+ pytest -m "unit or not (slow or integration)" # Unit and unmarked non-slow tests
58
+
59
+ # Run tests with coverage report
60
+ pytest --cov=taxdumpy --cov-report=html --cov-report=term-missing
61
+
62
+ # Run a focused test file without the repository-wide coverage threshold
63
+ pytest tests/test_taxdb.py --no-cov
64
+
65
+ # Format code and sort imports with ruff
66
+ ruff format src/ tests/ # Format code
67
+ ruff check src/ tests/ --fix # Fix import sorting
68
+
69
+ # Check formatting and imports in CI mode
70
+ ruff format --check src/ tests/
71
+ ruff check src/ tests/ # Only checks import sorting (I rules)
72
+ ```
73
+
74
+ **Note**: Ruff is configured with `line-length = 120`, and only handles formatting and import sorting (I rules). All other linting is disabled.
75
+
76
+ **Note**: pytest config lives in `pytest.ini` under a `[pytest]` section. It must
77
+ not be `[tool:pytest]` — that is the `setup.cfg` spelling, and pytest silently
78
+ ignores the whole file, taking `--strict-markers` and the 80% coverage gate with
79
+ it. Running `pytest` requires `pytest-cov` to be installed, since `addopts`
80
+ passes `--cov` unconditionally.
81
+
82
+ Test fixtures copy the `.dmp` files into temporary directories, and backend
83
+ caches are written there. Pytest also writes coverage reports in the repository
84
+ root; those reports and cache filenames are gitignored.
85
+
86
+ ### Build and Release
87
+ ```bash
88
+ # Build an sdist and wheel (requires the `build` package)
89
+ python -m build
90
+
91
+ # A pushed v* tag triggers GitHub Release creation; it does not publish to PyPI.
92
+ # Build and upload separately with Twine when a PyPI release is intended.
93
+ # python -m twine upload dist/*
94
+ ```
95
+
96
+ `scripts/release.py` has remote and repository side effects: it requires a clean
97
+ worktree, updates the version, commits it, pushes `origin main` and a version
98
+ tag, removes the existing `dist/` directory, and builds the package. Its
99
+ `--upload` option publishes to PyPI. Read the script and use it only when those
100
+ exact actions are intended.
101
+
102
+ ## Architecture Overview
103
+
104
+ ### Project Structure
105
+ ```
106
+ src/taxdumpy/
107
+ ├── __init__.py # Package initialization and exports
108
+ ├── __main__.py # CLI entry point
109
+ ├── ansi.py # ANSI color formatting utilities
110
+ ├── basic.py # Core data structures (Node) and exception hierarchy
111
+ ├── ranks.py # Shared canonical rank names, levels and codes
112
+ ├── database.py # Abstract base class (TaxonomyDatabase) + factory function
113
+ ├── cache.py # Source signatures and atomic pickle replacement
114
+ ├── taxdb.py # Dictionary-based in-memory backend with pickle caching
115
+ ├── taxsqlite.py # SQLite-based persistent backend
116
+ ├── taxon.py # Taxon entity with lineage resolution
117
+ ├── functions.py # Rank utilities (upper_rank_id, get_canonical_ranks) + subtree traversal (descendants)
118
+ └── cli.py # CLI commands: cache, search, lineage
119
+
120
+ tests/
121
+ ├── conftest.py # Pytest fixtures
122
+ ├── data/ # Sample taxdump files for testing
123
+ ├── test_api_consistency.py
124
+ ├── test_basic.py
125
+ ├── test_cache.py
126
+ ├── test_cli.py
127
+ ├── test_critical_regressions.py # Pins previously-shipped critical defects
128
+ ├── test_descendants.py # Subtree traversal, parametrized over both backends
129
+ ├── test_error_handling.py
130
+ ├── test_fast_cache.py
131
+ ├── test_functions.py
132
+ ├── test_integration.py
133
+ ├── test_taxdb.py
134
+ ├── test_taxon.py
135
+ └── test_taxsqlite.py
136
+ ```
137
+
138
+ ### Core Components
139
+
140
+ #### Database Layer
141
+
142
+ **TaxonomyDatabase** (`database.py`): Abstract base class defining the consistent API
143
+ - Abstract methods: `get_node()`, `_rapid_fuzz()`, `__len__()`, `__repr__()`
144
+ - Shared `search()` returns structured results; `fuzzy_search()` prints them
145
+ - Shared methods: `_validate_taxid()` - validates and converts taxid to integer
146
+ - Abstract cached properties: `all_names`, `name2taxid`, `delnodes`, `max_taxid_strlen`, `max_rank_strlen`
147
+ - Context manager support for connection management
148
+ - Formatting width properties return zero for an empty database
149
+
150
+ **Important**: these five properties use the module's `abstract_cached_property`
151
+ helper, not `@cached_property` over `@abstractmethod`. Stacking those two
152
+ silently breaks enforcement — `cached_property` does not propagate
153
+ `__isabstractmethod__`, so ABCMeta never sees the member and a subclass omitting
154
+ it instantiates fine, returning `None`. Do not "simplify" this back.
155
+
156
+ **TaxDb** (`taxdb.py`): Dictionary-based in-memory database
157
+ - Fast mode (`fast=True`) loads `taxdump_fast.pickle`, built from the taxids in a
158
+ filter file, their ancestors, and their descendants
159
+ - Deserializes a pickle once per initialization; reuse the instance for batches
160
+ - `build_fast_cache(taxids)` validates selections and writes the filtered cache
161
+ - New pickle caches include versioned source signatures; original three-part
162
+ pickles remain readable. Changes in available source sizes or mtimes trigger
163
+ a rebuild; caches remain usable when source files have been removed
164
+ - Pickle writes use atomic replacement. `rebuild=True` forces a full rebuild;
165
+ a failed rebuild preserves the previous cache
166
+ - Missing, stale or corrupt fast caches require an explicit filtered rebuild
167
+ - Data structures: `_taxid2nodes` (dict), `_old2news` (merged taxids), `_delnodes` (set)
168
+
169
+ **TaxSQLite** (`taxsqlite.py`): SQLite-based persistent database
170
+ - Tables: `nodes` (taxid, parent, rank, division), `names` (taxid, name, name_class), `merged` (oldtid, newtid), `delnodes` (taxid)
171
+ - All four tables are listed in `REQUIRED_TABLES`; `_check_tables` validates every one, so a cache built by an older version with a narrower schema is rebuilt rather than trusted
172
+ - The database is self-contained: no `.dmp` file is read at query time
173
+ - Builds go to a `mkstemp` temp path and are `os.replace`d into position, so an interrupted build cannot leave a schema-complete but empty database behind
174
+ - Indexed for fast queries
175
+ - Deleted-taxid checks use an indexed lookup rather than loading the whole set
176
+ - New caches record source signatures in `source_files`; legacy caches use
177
+ source mtimes. `rebuild=True` forces a rebuild with atomic replacement
178
+ - Memory-efficient for large datasets
179
+
180
+ **Factory Function**: `create_database(taxdump_dir, backend="sqlite", ...)`.
181
+ Supported backend names are `sqlite`/`sql` and `dict`/`memory`/`pickle`.
182
+
183
+ #### Data Structures
184
+
185
+ **Node** (`basic.py`): Dataclass with `__slots__` optimization
186
+ - Fields: `taxid`, `parent`, `rank`, `name`, `equal`, `acronym`, `division`
187
+
188
+ **Taxon** (`taxon.py`): Main entity for taxonomy operations
189
+ - Properties: `lineage`, `taxid_lineage`, `rank_lineage`, `name_lineage`
190
+ - Properties: `is_legacy`, `has_species_level`, `species_taxid`
191
+ - Traces lineage from any taxid to root, handles merged taxids automatically
192
+
193
+ #### Exception Hierarchy
194
+
195
+ ```
196
+ TaxdumpyError (base)
197
+ ├── TaxDbError # Database-related issues
198
+ │ ├── TaxdumpFileError # File issues (includes solutions)
199
+ │ └── DatabaseCorruptionError
200
+ ├── TaxidError # Invalid/unknown taxid (includes suggestions)
201
+ ├── TaxRankError # Invalid taxonomic rank (includes valid ranks)
202
+ ├── TaxdumpyMemoryError
203
+ └── ValidationError # Input validation failures
204
+ ```
205
+
206
+ #### Rank Functions (`functions.py`)
207
+
208
+ - Rank tables live in `ranks.py` and are re-exported by `functions.py`
209
+ - `RankName` defines 9 levels plus the `domain` alias of `superkingdom`;
210
+ `RANKNAMES` is derived via `get_args` (species to realm)
211
+ - Realm uses the root-level `R` code to preserve existing integer encodings,
212
+ and `r__` in MPA output. Root itself is omitted from MPA output
213
+ - `EXTENDED_RANKS`: 32 names including sub-ranks and aliases
214
+ - `upper_rank_id()`: Get taxid at specific canonical rank (strict)
215
+ - `get_rank_taxid()`: Safely get taxid for rank (returns None if not found)
216
+ - `get_canonical_ranks()`: Get all canonical ranks in lineage as dict
217
+ - `get_closest_rank()`: Get closest rank at or above target level
218
+ - `get_rank_or_closest()`, `get_all_ranks()`, `find_rank_in_lineage()`
219
+ - `get_rank_distance()`: Calculate distance between two canonical ranks
220
+ - `group_taxids_by_rank()`: Group taxids by rank using their closest ancestor
221
+
222
+ #### Subtree Traversal (`functions.py`)
223
+
224
+ Lineage tracing walks *up*; these walk *down*.
225
+
226
+ - `descendants(taxdb, taxid, max_depth=None)`: every taxid below `taxid`,
227
+ excluding `taxid` itself and excluding deleted/merged taxids. Breadth-first.
228
+ `max_depth=1` gives direct children only
229
+ - `descendant_nodes()`: the same as `Node` objects (one `get_node` per result)
230
+ - `build_children_index(taxdb)`: inverts child→parent in one pass, cached per
231
+ database in a `WeakKeyDictionary` so a closed database does not pin a
232
+ million-entry index. Traversal is iterative, so deep trees cannot exhaust the
233
+ stack
234
+
235
+ These reach into `taxdb._taxid2nodes` and `taxsqlite._conn` for speed, so they
236
+ dispatch on backend type and raise `TaxDbError` for any third backend. Moving
237
+ them onto `TaxonomyDatabase` would remove that coupling.
238
+
239
+ ### Data Flow
240
+ 1. NCBI taxdump files (nodes.dmp, names.dmp, etc.) are parsed
241
+ 2. Data is cached as either pickle files (TaxDb) or SQLite database (TaxSQLite)
242
+ 3. Taxon objects provide lineage tracing and taxonomy operations
243
+ 4. CLI commands leverage either backend for search/lineage operations
244
+
245
+ ### CLI Commands
246
+
247
+ | Command | Description | Key Options |
248
+ |---------|-------------|-------------|
249
+ | `cache` | Build SQLite and full pickle caches; with `-f`, build only a filtered fast pickle | `-d/--directory`, `-f/--filter-file` |
250
+ | `search` | Fuzzy organism name search | `-d/--directory`, `--fast`, `--limit` |
251
+ | `lineage` | Display taxonomic lineage (SQLite by default, filtered pickle with `--fast`) | `-d/--directory`, `--fast` |
252
+
253
+ ### Environment Variables
254
+ - `TAXDB_PATH`: Default path to taxonomy database (defaults to `~/.taxonkit`)
255
+
256
+ ### Performance Features
257
+ - Pickle caching for repeated Python queries; `fast=True` loads a filtered cache
258
+ - SQLite backend for memory efficiency with large datasets
259
+ - Rapidfuzz integration for fuzzy organism name matching
260
+ - Fast cache mode for selected taxids and their related lineage/subtree
261
+
262
+ ### Database Requirements
263
+ Building a new cache from source requires NCBI taxdump files:
264
+ - `nodes.dmp`: Taxonomy tree structure
265
+ - `names.dmp`: Scientific names
266
+ - `merged.dmp`: Legacy taxid mappings
267
+ - `delnodes.dmp`: Deleted nodes
268
+ - `division.dmp`: Division information
269
+
270
+ ### Dependencies
271
+ - **Core**: `rapidfuzz` (fuzzy matching), `tqdm` (progress bars)
272
+ - **Dev**: `ruff`, `pytest`, `pytest-cov`, `pytest-mock`, `build`
273
+
274
+ ## Known Issues
275
+
276
+ These issues are still present in the checked-in implementation. Recheck this
277
+ section when the corresponding code or tests change.
278
+
279
+ **Mutable nodes**: `TaxDb.get_node` returns stored Node objects while SQLite
280
+ returns fresh ones. Mutating dict nodes can invalidate cached search and
281
+ children indexes; treat returned nodes as read-only until an explicit mutation
282
+ API or immutable node contract is introduced.
283
+
284
+ **Importer differences**: malformed input handling differs between backends.
285
+ SQLite construction still holds parsed names in memory, so its build uses more
286
+ memory than steady-state queries. Node and insert rows are streamed, with
287
+ indexes built after bulk insertion.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: taxdumpy
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: Python package for efficiently parsing NCBI's taxdump database
5
5
  Project-URL: Documentation, https://github.com/omegahh/taxdumpy
6
6
  Project-URL: Issues, https://github.com/omegahh/taxdumpy/issues
@@ -61,9 +61,10 @@ _A high-performance Python toolkit for parsing NCBI Taxonomy databases with line
61
61
 
62
62
  ## Features
63
63
 
64
- - **Fast Parsing**: Optimized loading of NCBI taxdump files with pickle caching for ~3x speedup
64
+ - **Cached Loading**: Parse NCBI taxdump files once, then reuse pickle or SQLite caches
65
65
  - **Dual Backends**: In-memory (`TaxDb`) for speed or SQLite (`TaxSQLite`) for memory efficiency
66
66
  - **Lineage Resolution**: Complete lineage tracing with automatic handling of merged/deleted nodes
67
+ - **Subtree Traversal**: Walk *down* the tree with `descendants` — every taxid below a given one
67
68
  - **Rank Codes**: Comparable `RC` class for efficient rank-based operations and grouping
68
69
  - **Fuzzy Search**: Rapid approximate name matching using `rapidfuzz`
69
70
  - **Batch Grouping**: Group taxids by rank level with `group_taxids_by_rank`
@@ -86,7 +87,7 @@ pip install -e .
86
87
  **With Development Dependencies:**
87
88
 
88
89
  ```bash
89
- pip install -e .[dev]
90
+ pip install -e '.[dev]'
90
91
  ```
91
92
 
92
93
  ## Quick Start
@@ -112,7 +113,7 @@ print(human.rank_lineage)
112
113
 
113
114
  # Check taxonomic properties
114
115
  print(f"Rank: {human.rank}") # species
115
- print(f"Division: {human.division}") # Primates
116
+ print(f"Division: {human.division}") # PRI (division code, not full name)
116
117
  print(f"Is legacy: {human.is_legacy}") # False
117
118
  ```
118
119
 
@@ -121,23 +122,54 @@ print(f"Is legacy: {human.is_legacy}") # False
121
122
  ```python
122
123
  from taxdumpy import create_database, Taxon
123
124
 
124
- # Create database with preferred backend
125
- db = create_database(backend="sqlite", taxdump_dir="/path/to/taxdump")
125
+ # taxdump_dir is the first positional argument
126
+ db = create_database("/path/to/taxdump", backend="sqlite")
126
127
  # Or: backend="dict" for in-memory
127
128
 
128
129
  taxon = Taxon(9606, db)
129
130
  print(taxon.name) # Homo sapiens
130
131
  ```
131
132
 
133
+ ### Subtree Traversal
134
+
135
+ ```python
136
+ from taxdumpy import TaxDb, descendants
137
+
138
+ taxdb = TaxDb("/path/to/taxdump")
139
+
140
+ # Every taxid below genus Escherichia (transitively, not just children)
141
+ descendants(taxdb, 561)
142
+ # [562, 208962, 511145, ...]
143
+
144
+ # Direct children only
145
+ descendants(taxdb, 561, max_depth=1)
146
+
147
+ # Deleted and merged taxids are excluded; the queried taxid itself is not
148
+ # included. Combine with existing helpers to filter:
149
+ species = [t for t in descendants(taxdb, 2) if taxdb.get_node(t).rank == "species"]
150
+ ```
151
+
152
+ The first call builds a parent-to-children index in one pass over the database
153
+ and caches it, so later queries cost time proportional to the subtree returned
154
+ rather than to the database size.
155
+
132
156
  ### Fuzzy Search
133
157
 
134
158
  ```python
135
- # Search with typos and partial matches
136
- results = taxdb._rapid_fuzz("Escherichia coli", limit=5)
159
+ # Print formatted, highlighted matches to the console
160
+ taxdb.fuzzy_search("Escherichia coli", limit=5)
161
+
162
+ # Or get the matches as data
163
+ results = taxdb.search("Escherichia coli", limit=5)
137
164
  for match in results:
138
165
  print(f"{match['name']} (TaxID: {match['taxid']}, Score: {match['score']})")
139
166
  ```
140
167
 
168
+ `search` returns dictionaries containing `name`, `taxid`, and a `score` from
169
+ 0 to 100. It preserves separate taxa sharing a name and reuses its candidate
170
+ index across queries. `fuzzy_search` prints the results. Both methods propagate
171
+ validation and database errors to callers.
172
+
141
173
  ### Rank Codes and Grouping
142
174
 
143
175
  ```python
@@ -169,6 +201,10 @@ print(human.mpa_repr)
169
201
 
170
202
  ## Command Line Interface
171
203
 
204
+ By default, `search` and `lineage` use SQLite so individual queries do not load
205
+ the full pickle into memory. `--fast` loads a smaller, filtered pickle instead;
206
+ create that cache first with `cache -f FILE`.
207
+
172
208
  ### Check Version
173
209
 
174
210
  ```bash
@@ -178,13 +214,17 @@ taxdumpy --version
178
214
  ### Cache Database
179
215
 
180
216
  ```bash
181
- # Cache full NCBI taxonomy database
217
+ # Rebuild both taxdump.sqlite and taxdump.pickle from source files
182
218
  taxdumpy cache -d /path/to/taxdump
183
219
 
184
- # Create fast cache with specific organisms
220
+ # Build taxdump_fast.pickle from selected taxids (one per line), their
221
+ # ancestors, and their descendants; blank lines and # comments are allowed
185
222
  taxdumpy cache -d /path/to/taxdump -f important_taxids.txt
186
223
  ```
187
224
 
225
+ The filtered command loads the full `TaxDb` to build the subset. It does not
226
+ rebuild SQLite. Queries outside the subset require the full database.
227
+
188
228
  ### Search Operations
189
229
 
190
230
  ```bash
@@ -250,7 +290,7 @@ export TAXDB_PATH=~/.taxonkit
250
290
  from taxdumpy import TaxSQLite, Taxon
251
291
 
252
292
  # Use SQLite for memory-efficient storage
253
- db = TaxSQLite("/path/to/taxdump") # Creates taxonomy.db in that directory
293
+ db = TaxSQLite("/path/to/taxdump") # Creates taxdump.sqlite in that directory
254
294
 
255
295
  # Same API as TaxDb
256
296
  taxon = Taxon(9606, db)
@@ -264,11 +304,14 @@ with TaxSQLite("/path/to/taxdump") as db:
264
304
 
265
305
  ### Batch Processing
266
306
 
307
+ Load the full pickle once and reuse the same instance for all Python queries.
308
+ Its startup cost is paid once per instance, rather than once per taxid.
309
+
267
310
  ```python
268
311
  from taxdumpy import TaxDb, Taxon
269
312
 
270
- # Reuse database instance for efficiency
271
- taxdb = TaxDb("/path/to/taxdump", fast=True)
313
+ # Load all taxa for arbitrary batch queries
314
+ taxdb = TaxDb("/path/to/taxdump")
272
315
 
273
316
  taxids = [9606, 511145, 7227, 4932] # Human, E.coli, Fly, Yeast
274
317
  for taxid in taxids:
@@ -276,6 +319,43 @@ for taxid in taxids:
276
319
  print(f"{taxon.name}: {' > '.join(taxon.name_lineage[:3])}")
277
320
  ```
278
321
 
322
+ For a known subset, build a filtered cache using the full database, then load
323
+ that smaller cache in subsequent processes:
324
+
325
+ ```python
326
+ count = taxdb.build_fast_cache([9606, 511145, 7227, 4932])
327
+ print(f"Cached {count} nodes, including ancestors and descendants")
328
+
329
+ subset = TaxDb("/path/to/taxdump", fast=True)
330
+ human = Taxon(9606, subset)
331
+ ```
332
+
333
+ ### Cache Freshness and Rebuilding
334
+
335
+ New caches record source file sizes and modification times. Opening a stale
336
+ full pickle or SQLite cache rebuilds it from the taxdump files. Missing, stale,
337
+ or unreadable filtered caches raise an error; recreate them with `cache -f FILE`
338
+ or `build_fast_cache(taxids)` so the selection is explicit.
339
+
340
+ Original three-part pickles remain readable, and legacy caches use source
341
+ modification times to detect changes. Valid caches remain usable after the
342
+ source `.dmp` files are removed. Rebuilding requires the source files again.
343
+
344
+ To force a rebuild from Python:
345
+
346
+ ```python
347
+ from taxdumpy import TaxDb, TaxSQLite
348
+
349
+ taxdb = TaxDb("/path/to/taxdump", rebuild=True)
350
+ with TaxSQLite("/path/to/taxdump", rebuild=True) as db:
351
+ print(len(db))
352
+ ```
353
+
354
+ Each cache is replaced atomically only after its new contents are written
355
+ successfully. A failed rebuild preserves the previous file. `rebuild=True`
356
+ cannot be combined with `fast=True`; rebuild filtered caches through the full
357
+ database's `build_fast_cache` method instead.
358
+
279
359
  ### Handling Merged/Legacy TaxIDs
280
360
 
281
361
  ```python
@@ -293,7 +373,7 @@ print(taxon.node.taxid) # Current/resolved taxid
293
373
  ### Exception Handling
294
374
 
295
375
  ```python
296
- from taxdumpy import TaxDb, Taxon, TaxidError, TaxRankError, TaxDbError
376
+ from taxdumpy import TaxDb, Taxon, TaxidError, TaxRankError, upper_rank_id
297
377
 
298
378
  taxdb = TaxDb("/path/to/taxdump")
299
379
 
@@ -304,7 +384,8 @@ except TaxidError as e:
304
384
  # Includes suggestions for similar taxids
305
385
 
306
386
  try:
307
- rank_id = upper_rank_id(9606, "invalid_rank", taxdb)
387
+ # Signature is (tax, taxdb, rank) — a Taxon, not a bare taxid
388
+ rank_id = upper_rank_id(Taxon(9606, taxdb), taxdb, "invalid_rank")
308
389
  except TaxRankError as e:
309
390
  print(f"Invalid rank: {e}")
310
391
  # Includes list of valid ranks
@@ -317,32 +398,37 @@ except TaxRankError as e:
317
398
  **TaxDb**: In-memory dictionary-based database with pickle caching
318
399
 
319
400
  ```python
320
- TaxDb(taxdump_dir: str, fast: bool = False)
401
+ TaxDb(taxdump_dir: Path | str, fast: bool = False, verbose: bool = False, *, rebuild: bool = False)
321
402
  ```
322
403
 
323
404
  **TaxSQLite**: SQLite-based persistent database
324
405
 
325
406
  ```python
326
- TaxSQLite(taxdump_dir: str)
407
+ TaxSQLite(taxdump_dir: Path | str, verbose: bool = False, *, rebuild: bool = False)
327
408
  ```
328
409
 
329
410
  **Taxon**: Taxonomic unit with lineage resolution
330
411
 
331
412
  ```python
332
- Taxon(taxid: int, taxdb: TaxDb | TaxSQLite)
413
+ Taxon(taxid: int | str, taxdb: TaxDb | TaxSQLite)
333
414
 
334
415
  # Properties
335
- .name: str # Scientific name
336
- .rank: str # Taxonomic rank
337
- .division: str # NCBI division
338
- .parent: int # Parent taxid
339
- .lineage: list[Node] # Complete lineage as Node objects
340
- .name_lineage: list[str] # Names from taxid to root
341
- .rank_lineage: list[str] # Ranks from taxid to root
416
+ .name: str # Scientific name
417
+ .rank: str # Taxonomic rank
418
+ .division: str # NCBI division
419
+ .parent: int # Parent taxid
420
+ .acronym: str | None # Acronym, if any
421
+ .node: Node # Underlying node (resolved, if merged)
422
+ .rc: RC # Rank code for this taxon
423
+ .lineage: list[Node] # Complete lineage as Node objects
424
+ .name_lineage: list[str] # Names from taxid to root
425
+ .rank_lineage: list[str] # Ranks from taxid to root
342
426
  .taxid_lineage: list[int] # Taxids from taxid to root
343
- .is_legacy: bool # True if taxid was merged
344
- .has_species_level: bool # True if lineage contains species rank
345
- .species_taxid: int | None # Taxid at species level
427
+ .rankcode_lineage: list[RC] # Rank codes from taxid to root
428
+ .mpa_repr: str # MetaPhlAn-style lineage string
429
+ .is_legacy: bool # True if taxid was merged
430
+ .has_species_level: bool # True if lineage contains species rank
431
+ .species_taxid: int | None # Taxid at species level, or None if absent (previously 0)
346
432
  ```
347
433
 
348
434
  **Factory Function**
@@ -351,21 +437,52 @@ Taxon(taxid: int, taxdb: TaxDb | TaxSQLite)
351
437
  from taxdumpy import create_database
352
438
 
353
439
  # backend: "sqlite", "sql", "dict", "memory", or "pickle"
354
- db = create_database(backend="sqlite", taxdump_dir="/path/to/taxdump")
440
+ db = create_database("/path/to/taxdump", backend="sqlite")
355
441
  ```
356
442
 
357
443
  ### Rank Functions
358
444
 
359
445
  ```python
360
446
  from taxdumpy import (
361
- upper_rank_id, # Get taxid at rank (raises TaxRankError if not found)
362
- get_rank_taxid, # Get taxid at rank (returns None if not found)
447
+ upper_rank_id, # Get taxid at rank (raises TaxRankError if not found)
448
+ get_rank_taxid, # Get taxid at rank (returns None if not found)
363
449
  get_canonical_ranks, # Get dict of all canonical ranks in lineage
364
- get_closest_rank, # Get closest rank at or above target level
365
- get_rank_distance, # Calculate distance between two ranks
366
- RANKNAMES, # 8 canonical ranks: species → realm
367
- EXTENDED_RANKS, # 30+ ranks including sub-ranks
450
+ get_closest_rank, # Get closest rank at or above target level
451
+ get_rank_or_closest, # Same, returning the taxid alone
452
+ get_all_ranks, # Every rank in the lineage as a dict
453
+ find_rank_in_lineage,# First of several candidate ranks present
454
+ get_rank_distance, # Calculate distance between two ranks
455
+ group_taxids_by_rank,# Group a list of taxids at a given rank
368
456
  )
457
+
458
+ # The rank tables live in taxdumpy.functions, not the package root:
459
+ from taxdumpy.functions import RANKNAMES, EXTENDED_RANKS
460
+ # RANKNAMES: 9 canonical levels, species → realm (domain/superkingdom are aliases)
461
+ # EXTENDED_RANKS: 32 names including sub-ranks and aliases
462
+ ```
463
+
464
+ `domain` and `superkingdom` resolve to the same rank level. Realm uses `r__` in
465
+ MPA output and the root-level `R` rank code, preserving existing integer `RC`
466
+ values. Grouping by `root` always selects taxid 1, including viral lineages.
467
+
468
+ `upper_rank_id` requires the requested higher rank or its subrank to exist in
469
+ the lineage; otherwise it raises `TaxRankError`. Use `get_closest_rank` or
470
+ `get_rank_or_closest` when fallback to a higher canonical rank is desired.
471
+
472
+ ### Subtree Traversal
473
+
474
+ ```python
475
+ from taxdumpy import descendants, descendant_nodes, build_children_index
476
+
477
+ descendants(taxdb, taxid, max_depth=None) -> list[int]
478
+ # Every taxid below `taxid`. Excludes `taxid` itself, and excludes deleted
479
+ # and merged taxids. max_depth=1 gives direct children only. Breadth-first.
480
+
481
+ descendant_nodes(taxdb, taxid, max_depth=None) -> list[Node]
482
+ # Same, as Node objects. Costs one get_node per result.
483
+
484
+ build_children_index(taxdb) -> dict[int, list[int]]
485
+ # The parent-to-children index the above build and cache. Read-only.
369
486
  ```
370
487
 
371
488
  ### Exceptions
@@ -384,11 +501,17 @@ from taxdumpy import (
384
501
 
385
502
  ## Performance Tips
386
503
 
387
- - **Use Fast Mode**: `TaxDb(path, fast=True)` provides ~3x speedup with pre-cached data
388
- - **Reuse Instances**: Create one `TaxDb` instance and reuse for multiple operations
504
+ - **Single CLI Queries**: Use the default SQLite backend to avoid loading the full pickle
505
+ - **Python Batches**: Load one `TaxDb` instance and reuse it for multiple operations
506
+ - **Filtered Data**: Use `TaxDb(path, fast=True)` after building a subset cache;
507
+ startup cost and memory use depend on the subset size
389
508
  - **Environment Variables**: Set `TAXDB_PATH` to avoid repeating database paths
390
509
  - **Choose Backend**: Use `TaxSQLite` for large datasets with limited memory
391
510
  - **Batch Operations**: Process multiple TaxIDs in batches rather than individual calls
511
+ - **Subtree Queries**: Reuse one database instance so `descendants` shares its
512
+ cached children index. Building it costs one pass over the database
513
+ (~0.4 s for 1M nodes); every query afterwards is proportional to the subtree,
514
+ and small subtrees resolve in microseconds
392
515
 
393
516
  ## Use Cases
394
517
 
@@ -412,7 +535,7 @@ from taxdumpy import (
412
535
  ```bash
413
536
  git clone https://github.com/omegahh/taxdumpy.git
414
537
  cd taxdumpy
415
- pip install -e .[dev]
538
+ pip install -e '.[dev]'
416
539
  ```
417
540
 
418
541
  ### Testing
@@ -439,12 +562,21 @@ ruff check src/ tests/ --fix
439
562
 
440
563
  ### Release Process
441
564
 
565
+ A pushed `v*` tag creates a GitHub Release. It does not build or publish a
566
+ package to PyPI. Build and upload separately when publishing a release:
567
+
442
568
  ```bash
443
- # Automated release
444
- python scripts/release.py --version 1.2.3 --upload
569
+ python -m build
570
+ python -m twine upload dist/*
571
+ ```
572
+
573
+ The release helper requires a clean worktree, runs tests and Ruff, updates and
574
+ commits the version, pushes `origin main` and a version tag, removes `dist/`,
575
+ and builds the package. `--upload` also publishes to PyPI. Run it only when
576
+ those repository and remote changes are intended:
445
577
 
446
- # Or manual: create tag and push (GitHub Actions publishes to PyPI)
447
- git tag v1.2.3 && git push origin v1.2.3
578
+ ```bash
579
+ python scripts/release.py --version 1.2.3 --upload
448
580
  ```
449
581
 
450
582
  ## License