A performance and robustness release: ingest rebuilt around a streaming Rust producer, relation queries rewritten around what DuckDB can index, loops over a live iterator prefetched, and the noisy files real annotation sources publish read as they were meant.
pip install --upgrade gffbaseabi3 wheels for Linux (x86_64, aarch64), macOS (Intel, Apple Silicon) and Windows (x86_64) cover CPython 3.10 through 3.14; no Rust toolchain is needed. No database migration is needed: 0.3.0 reads and writes the same schema as 0.2.x.
Faster, in far less memory
Against gffutils 0.14 (one canonical run on the installed wheel, exhaustive validation, correctness signatures matching on every row):
| Corpus | gffbase 0.3.0 | gffutils 0.14 | speedup | database size vs SQLite |
|---|---|---|---|---|
| GENCODE v49 GFF3 | 3 min 13 s | 9 min 45 s | 3.03× | 0.61× |
| GENCODE v49 GTF | 3 min 36 s | 6 min 54 s | 1.92× | 0.64× |
| RefSeq GRCh38.p14 | 2 min 5 s | 6 min 31 s | 3.13× | 0.73× |
| CHESS 3.1.3 | 37.0 s | 2 min 14 s | 3.62× | 0.89× |
| MANE v1.5 | 15.6 s | 45.1 s | 2.89× | 0.70× |
Against 0.2.1 (ingest only, same machine and cores): GENCODE v49 GFF3 537 s and 9.7 GiB peak → 192 s and 2.8 GiB; RefSeq 347 s and 6.9 GiB → 119 s and 2.0 GiB; MANE 36 s and 1.7 GiB → 15 s and 1.0 GiB. Databases are about half the size they were.
- Ingest is rebuilt. The Rust parser streams its input instead of decompressing the whole file first; a Rust producer resolves ids and duplicates and hands each batch to DuckDB as Arrow with no Python object per record; indexes are single-column, built over sorted input, one at a time; the closure is built level by level. Ingest into a file runs under a 512 MB DuckDB budget, raised only for the step that needs more.
children()andparents()are 3–25× faster per call: one single-table lookup, then the features by key, instead of a join DuckDB answered by scanning the whole closure.- Loops are prefetched. A
children(),parents()ordb[id]call on a feature an open gffbase iterator holds is answered, with its neighbours', by one query — nested loops too — so the canonical gffutils loops run 4–21× faster than calling per id, 1.4–3.5× behind gffutils 0.14. Answers are identical to the per-call path (a Hypothesis state machine interleaving streams, calls and writes holds them to it). A scattered call on an id from your own list is still a DuckDB statement; batch those withchildren_batched. - DuckDB uses at most 8 threads by default (
GFFBASE_THREADSoverrides): more bought no time and cost memory.
Noisy files, read as meant
Byte-order marks, lone-CR line endings, whitespace-only lines, . in column 9, Parent=a,, spaced =; CDS-only and AUGUSTUS GTF; gene_id without transcript_id; UCSC GTFs with one transcript id on several contigs; Liftoff _1/_2 ids; FlyBase's gzipped tar; NUL bytes; truncated gzip (GFFFormatError(kind="ReadError") in both engines). Each is on the new Noisy and non-standard files page, and every change to what a query returns is declared in tests/parity/deviations.toml. Nine more annotation sources — Ensembl mouse, NCBI E. coli, FlyBase, WormBase, UCSC knownGene and ncbiRefSeq, a T2T Liftoff annotation — are ingested and fully validated by the test suite.
Behaviour you may notice
- Inside a loop over a gffbase iterator,
children(),parents()anddb[id]may be answered from a prefetch. Writes throughFeatureDBinvalidate it; SQL run directly ondb.conninside such a loop is not seen by that loop's prefetched answers. - The ingest connection's DuckDB memory limit is managed by gffbase unless
pragmassets one; the handle you get back uses DuckDB's default. DuplicateIDErrornames both lines; the CLI reports a user error in one line and exits 1; the dialect is voted per key, weighted by attribute count, as gffutils does.
The full list is in the changelog; tuning advice is on the new Memory and performance tuning page.
How this release was verified
- Qualified in CI on Linux, macOS and Windows across Python 3.10–3.14 on both spatial index paths, with a five-corpus correctness gate, the gffutils parity suite, the pure-Python engine without the extension, and the minimum supported dependencies.
- Each rebuilt path is held to the one it replaced by an equivalence oracle: the Python ingest loop (producer), the row-by-row duplicate pass, the recursive closure query, the closure join (relations) and the per-call path (prefetch).
- The benchmark table above is generated from one canonical run on the installed wheel, with exhaustive validation, and publishes a ratio only where both engines' correctness signatures match.
- The binaries attached here are byte-identical to the files on PyPI.
| File | SHA-256 |
|---|---|
gffbase-0.3.0.tar.gz
| 961a30f1baa3218ee0491f76217d4780f50e467ba078440930aecfad72779c4a
|
…manylinux_2_17_x86_64…whl
| 718a926e2be6d105df7332f32830c361c742ea626f5c3eb041f7b8dd6b9d2898
|
…manylinux_2_17_aarch64…whl
| d99ed16e19ed5a12706b40c88e613f2000c8444979d885574d7573743d43c3ef
|
…macosx_11_0_arm64…whl
| 85c8cf75dee4bea214370550ca17b23834366506c600f9b776fe21d9a7bed81c
|
…macosx_10_12_x86_64…whl
| b37d4cb2fc315f540a975a00c2c2796058a9347061a35db767c0254825714c9d
|
…win_amd64…whl
| 7d6b4b3496fb34469b237734a063407e666cda6ab7922155bf72e16b0d812af5
|
Docs: https://khchao.com/gffbase/ · Changelog: CHANGELOG.md · PyPI: https://pypi.org/project/gffbase/0.3.0/