tors
Fast, GIL-free text and document operations for Python, backed by Rust:
normalization, Unicode segmentation, diffing, fuzzy and phonetic matching,
multi-pattern search and redaction, chunking, and lightweight retrieval
(TF-IDF, BM25, SimHash, Merkle integrity). An optional extra adds
cross-format document extraction (PDF, Office, RTF/ODF/EPUB, CSV, HTML to
markdown or plain text). In the spirit of orjson for JSON or polars for
dataframes.
Full documentation lives at
azx-pbc-oss.github.io/tors and in
docs/ in this repo: the API reference,
Documents, Performance,
Async use, Design and scope,
Dependencies and licensing, and three worked
recipes.
Why
Python's re module and str methods never release the GIL, no matter how
large the input is: a multi-megabyte text transform runs as one long GIL-held
call that stalls every other thread and the asyncio event loop for its whole
duration. tors does the same kind of transform as a single native Rust pass,
wrapped in py.detach (PyO3's GIL-release call) for the entire computation,
so the GIL is free for the rest of your program while it runs.
That covers the functions with a stdlib equivalent (normalize's pipeline
mirrors unicodedata.normalize + a few re.sub calls; quote/unquote
mirror urllib.parse; b64_encode_bytes/b64_decode mirror base64). Where
the stdlib has no equivalent at all (Unicode text segmentation: grapheme
clusters, word and sentence boundaries; leftmost-longest multi-pattern search;
edit-distance and phonetic matching; content-defined chunking; SimHash
near-duplicate detection; Merkle tree integrity; encoding detection), tors
supplies it over maintained Rust crates, GIL-released the same way, so async
services and threaded pipelines don't pay a blocking tax for text work.
Install
pip install tors
Building from source (a Rust toolchain and maturin):
pip install maturin
maturin develop --release
The document-extraction surface is a second wheel behind an extra:
pip install "tors[documents]". From a checkout,
uv sync --locked --extra documents builds and installs the payload wheel
from this tree. See Documents.
The underlying Rust crate is also on crates.io, published separately as
tors-core (the plain tors name belongs to an unrelated, dormant crate).
cargo add tors-core, then use tors::... in code: [lib] name in
Cargo.toml keeps the importable crate name tors regardless of the
published package name.
Pyodide / WebAssembly
The Rust cores are OS-free, so tors also builds for
Pyodide as a PEP 783 pyemscripten wheel: the
abi3-py310 story carries over unchanged, so one wheel covers every Pyodide
Python >= 3.10. The WASM workflow builds it on every push (artifact only;
publishing to PyPI's Emscripten platform is not wired up). To build one
locally:
rustup target add wasm32-unknown-emscripten
uvx --python 3.14 --from pyodide-build pyodide build . -o dist
The driving interpreter matters: Python 3.14 targets Pyodide 314.x /
pyemscripten_2026_0 with a stable Rust toolchain; driving from 3.13 pins a
Rust nightly older than this crate's MSRV. SIMD-dependent dependencies
(base64, simdutf8, memchr, aho-corasick) compile their scalar fallbacks for
wasm; the extension has been smoke-tested (import + representative calls
across every core family) in Pyodide under node.
Quickstart
import tors
tors.normalize("line one \n\n\n\nline two\r\n")
# 'line one\n\nline two'
tors.finalize("line one \n\n\n\nline two\r\n")
# ('line one\n\nline two', 'e986ba083c7c1a9361143d2d8ccd8477d1d5eeef8b94b67c6ad4693f8f7b942a')
tors.diff_opcodes_lines("l1\nl2\nl3\n", "l1\nX\nl3\nl4\n")
# [("equal", 0, 1, 0, 1), ("replace", 1, 2, 1, 2), ("equal", 2, 3, 2, 3),
# ("insert", 3, 3, 3, 4)]
thread = (
"Ana: kickoff at nine.\n"
"Ben: We briefed the U.S. team on the numbers. They asked for a follow-up.\n"
"Ana: done."
)
chunks = tors.chunk_hierarchical(thread, 40, ["\n", None])
# [(0, 21), (22, 59), (59, 95), (96, 106)]
[thread[s:e] for s, e in chunks]
# ['Ana: kickoff at nine.',
# 'Ben: We briefed the U.S. team on the ',
# 'numbers. They asked for a follow-up.',
# 'Ana: done.']
corpus = [
"the quick brown fox jumps over the lazy dog",
"a lazy cat sleeps all day",
"the fox and the dog are friends",
]
tors.bm25_rank("quick fox", corpus)
# [(0, 1.3162195220480066), (2, 0.4798180901812613), (1, 0.0)]
All of these run against the built extension; the output above is what they actually return.
What's inside
73 functions plus two small helper classes, grouped by what they do; the
documents extra adds seven document-extraction functions and its own helper
types. Full signatures, argument contracts, and edge cases are in the
API reference.
| family | functions |
|---|---|
| Unicode normalization & forms | normalize, finalize, nfc/nfd/nfkc/nfkd, html_unescape, strip_controls |
| UTF-8 / UTF-16 / base64 codecs | decode_utf8, finalize_utf8, utf8_is_valid, decode_utf16, utf16_is_valid, b64_encode_bytes, b64_decode, detect_encoding |
| Text segmentation (UAX #29) | grapheme_count, word_bounds(+_iter), word_count, sentence_bounds(+_iter), sentence_count |
Diffing (difflib-compatible) |
diff_opcodes, diff_opcodes_lines |
| Fuzzy & phonetic matching | similarity_ratio, get_close_matches, levenshtein, jaro, jaro_winkler, soundex, metaphone, double_metaphone, nysiis, daitch_mokotoff, refined_soundex |
| Multi-pattern search & redaction | find_patterns(+_iter), count_matches, replace_many, replace_many_masked, CompiledPatterns |
| Markdown / code-fence extraction | extract_code_blocks, strip_code_fences, dedent |
| JSON repair (json_repair port) | repair_json, repair_json_loads, repair_json_diagnostics |
| Truncation & lexical grounding | truncate_to_bounds, truncate_ellipsis, is_grounded |
| URL encoding | quote, quote_plus, unquote, unquote_plus |
| Text chunking | chunk_cdc, chunk_text(+_iter), chunk_by_words/_sentences/_paragraphs/_lines(+_iter), chunk_hierarchical |
| Information retrieval & integrity | tf_idf, bm25_rank, simhash64, simhash128, merkle_root, merkle_diff |
| Text-processing pipelines | apply_pipeline, CompiledLemmaDict |
Document-format extraction (tors.documents) |
to_markdown, to_text, sniff, pdf_extract, pdf_page_count, pdf_classify, pdf_link_uris |
Highlights
- GIL release on every call. 12 MiB of prose through
tors.finalizein a background thread holds the event loop's worst heartbeat gap to 10-14 ms; the pure-Python equivalent holds it for 92-108 ms. Full measured tables: Performance. - Async where it matters.
tors.aiowraps exactly the large-input functions inasyncio.to_thread, unconditionally, with no size-based branch; everything else keeps one sync spelling. Details: Async use. - Stateless by design. No pipeline objects, no caches, no handles to
manage; the build-once
CompiledLemmaDictandCompiledPatternshandles are the measured exceptions, and the scope cuts (no regex engine, no bundled lemma data, no search index) are decisions, not oversights. Details: Design and scope. - Permissive-only dependency tree, machine-checked by
cargo denyon every push. The full table and license accounting: Dependencies and licensing.
Contributing
See CONTRIBUTING.md for setup, the pre-PR checklist
(tests, clippy, fmt, ruff, the license gate), fuzzing, release process, and
commit-message conventions. For security vulnerabilities, see
SECURITY.md instead of filing a public issue.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tors-0.6.0.tar.gz.
File metadata
- Download URL: tors-0.6.0.tar.gz
- Upload date:
- Size: 1.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
caf1da3f18703b5802fe93534fbd9cb65b7c48f8c32d2367885715b1af20a7da
|
|
| MD5 |
169c5d4cf6356c97347fa810a2867292
|
|
| BLAKE2b-256 |
f3f8cdf585b1a26b8c188eb13ff4e12488414caea9796b45933710d329312184
|
File details
Details for the file tors-0.6.0-cp310-abi3-win_arm64.whl.
File metadata
- Download URL: tors-0.6.0-cp310-abi3-win_arm64.whl
- Upload date:
- Size: 2.7 MB
- Tags: CPython 3.10+, Windows ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
02678ed365f01b06a0a60c035575969498580d6d827bcba2ea7db858a8e89fb3
|
|
| MD5 |
500af26258f696d42e884bd525bcbdba
|
|
| BLAKE2b-256 |
6489c4fdafd1ddffeed0842d9553b90de72d3c8e9b5d583e00f3a2f7a2016a13
|
File details
Details for the file tors-0.6.0-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: tors-0.6.0-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 2.9 MB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
adf03cee720d9f8a06c59df195b6e3d2a7beed220d7f7cb23b5557d471a0957f
|
|
| MD5 |
eb69c591e24e4fe23acb732f45c14757
|
|
| BLAKE2b-256 |
f7366cb1d971e604b08b05465a18df254fcdb2f2bf7a7080b99efba45c7b2284
|
File details
Details for the file tors-0.6.0-cp310-abi3-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: tors-0.6.0-cp310-abi3-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 3.7 MB
- Tags: CPython 3.10+, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bda790c6617688a011dcf153c1c3db4db25020841e58dd408203a0b022669472
|
|
| MD5 |
11492791aa8fff1c7c3288e4d43d7a7d
|
|
| BLAKE2b-256 |
f3e9281bf69bc1e220d8219b0eff051f619d9448883ece61c29ce6dd77ada138
|
File details
Details for the file tors-0.6.0-cp310-abi3-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: tors-0.6.0-cp310-abi3-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 3.5 MB
- Tags: CPython 3.10+, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b202dcd32cf37de056499d596c4621934a8c9e25e840290331069907a629e02
|
|
| MD5 |
00224df5c11b7a25f0543b4e3c0d794c
|
|
| BLAKE2b-256 |
ff310be4902ad11825322e8bab1f9a082a297639eeeba9ede7eba01cdd591b07
|
File details
Details for the file tors-0.6.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: tors-0.6.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 3.4 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae18080c187c5d4fa08efe2fd95927cc97ae8805181809cf24d96b3b96ab1c30
|
|
| MD5 |
3c919d70733d5758ed61c66ea2dce1b0
|
|
| BLAKE2b-256 |
f4bf6efedfdbc1cb2b47bce5ec862a49b28cae5620ba57d2f1474c60a7977eeb
|
File details
Details for the file tors-0.6.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: tors-0.6.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 3.3 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e38f148d4d281e917eca55cf9ce1189ba98d89b24e9abd987a7326b2f1075c60
|
|
| MD5 |
af9ed20c144a4147131e1fb74f4caad9
|
|
| BLAKE2b-256 |
85aef29d523018e035c265a2cc66ccb3cd8fd5dc1daca2fa0aee292036edc2ce
|
File details
Details for the file tors-0.6.0-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: tors-0.6.0-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 3.0 MB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e684ce24594dab578ef4502d1aa9dfc7ca4c2423cde2d9c27886d3e295c64688
|
|
| MD5 |
8a05ce5ba5405d4cf03c7266042c8f87
|
|
| BLAKE2b-256 |
3438e4aff46774af7636825dd95c933aa1c5737893e026cd521c6f7add3846b5
|
File details
Details for the file tors-0.6.0-cp310-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: tors-0.6.0-cp310-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.10+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d39f925f5cf8866a9371a446bc6d3074595e9669ed7e42537a5aa4b77631582b
|
|
| MD5 |
e9704fd26c8f762fa532dd7eb36bdf3b
|
|
| BLAKE2b-256 |
3242e2b6d0ea5c13f9d687a61425aef2b88ba670fafb3bec942b3a934d91e411
|