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; the
one-shot hashing family mirrors hashlib/hmac; the random-generation
family mirrors secrets/uuid). 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.
GIL-release is not the same as free-threaded-build support: tors wheels
target the standard CPython ABI (cp310-abi3), which a free-threaded
3.13t interpreter cannot load — free-threaded support starts at 3.14t
(source build; PyO3's floor), and GIL-release is what makes that floor
irrelevant for the standard build: the GIL is already free for the rest
of your program on every CPython tors installs into.
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
106 functions plus two small helper classes and six pinned constants (five
charset alphabets and the key-family tuple),
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 |
| Contact & credential scrub | scrub_pii(+_report), KEY_FAMILIES |
| 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 |
| Hashing & request signing | md5_hex, sha1_hex, sha256_hex, sha512_hex, hmac_sha256_hex + raw-digest _digest twins (md5/sha1: checksum/legacy-interop only, broken for security since the 2000s — never signatures, certificates, or passwords) |
| Random generation (keys, tokens, ids) | random_string, random_hex, random_b62, random_b64url, uuid4(+_bytes), uuid7(+_bytes) |
| 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, scrub_log_text, CompiledPatterns |
| Escape-parity byte scan | contains_unescaped, find_unescaped |
| JSON validity gate (parse-and-discard) | json_is_valid |
| Byte lengths without the encode copy | utf8_byte_len, utf16_byte_len |
| Batch charset validation | first_invalid_charset, first_invalid_offender (the same scan's offender detail — item index, codepoint position, character — for rejection messages), CHARSET_B62/_B64URL/_HEX_LOWER/_HEX_UPPER/_HEX_MIXED (pinned alphabets that pair with the validator as data) |
| 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, minhash_signature, merkle_root, merkle_diff, content_hash |
| UUIDv7 field operations | uuid7_timestamp_ms, uuid_version, uuid_parse |
| 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. (The one exception:utf8_byte_lendeliberately does not detach — its whole body is the borrow, and a detach bracketing no work starves a co-resident loop's heartbeat; see API reference.) 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.
Release files for tors 0.10.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tors-0.10.1.tar.gz | 2.2 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| tors-0.10.1-cp310-abi3-win_arm64.whl | CPython 3.10 | abi3 | Windows ARM64 | Details |
| tors-0.10.1-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| tors-0.10.1-cp310-abi3-musllinux_1_2_x86_64.whl | CPython 3.10 | abi3 | Linux musl 1.2+ x86-64 | Details |
| tors-0.10.1-cp310-abi3-musllinux_1_2_aarch64.whl | CPython 3.10 | abi3 | Linux musl 1.2+ ARM64 | Details |
| tors-0.10.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| tors-0.10.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| tors-0.10.1-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| tors-0.10.1-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 29.6 MB
Release files / tors-0.10.1.tar.gz
| Download URL | tors-0.10.1.tar.gz |
|---|---|
| Size | 2.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
88eb00ece8749d1b5e03b0528b0467136924ad9a2e16eb9f41eed31657e98d28
|
|
BLAKE2b-256 checksum How to use checksums |
c4d0ab447e8515bd4cca8a4507b51294df0acbd4b0a3aad30f4d737f1aa54640
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|
Release files / tors-0.10.1-cp310-abi3-win_arm64.whl
| Download URL | tors-0.10.1-cp310-abi3-win_arm64.whl |
|---|---|
| Size | 2.9 MB |
| Tags | CPython 3.10 Windows ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
dfdc27ac67bd5292766e1d7746f5cd3b52a929b7b2c8885f03e4a345b54d5d61
|
|
BLAKE2b-256 checksum How to use checksums |
777c3578bae86fc2b1636072d4672f8a724ea6908a038ace7cea9239118c5853
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|
Release files / tors-0.10.1-cp310-abi3-win_amd64.whl
| Download URL | tors-0.10.1-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 3.1 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
a1c2b977cd8d0589550401750865bc65c2e9105b47f0837d40c0ebb61bd1637e
|
|
BLAKE2b-256 checksum How to use checksums |
de3b80369ba62849d25aff9945c682889d2a39810fb647642d7743b0b27470cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|
Release files / tors-0.10.1-cp310-abi3-musllinux_1_2_x86_64.whl
| Download URL | tors-0.10.1-cp310-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 3.9 MB |
| Tags | CPython 3.10 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
8a6906e419b15b5eafe1cc15b223d20541ee3425f1e126b6bb1c1e56b825cea8
|
|
BLAKE2b-256 checksum How to use checksums |
ca6ceb125761812b55e0d236b494ca6d5899a379e1a316b841f3b479187f244b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|
Release files / tors-0.10.1-cp310-abi3-musllinux_1_2_aarch64.whl
| Download URL | tors-0.10.1-cp310-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 3.7 MB |
| Tags | CPython 3.10 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
bdff22102a53a21bcfb06daad1a3eabc9fa87f033342d114c5e1f698885a0c17
|
|
BLAKE2b-256 checksum How to use checksums |
b2fd01f995f9f8f7a8ec12e783407d397d4ccee475b53b9e878915fe41e6bfc0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|
Release files / tors-0.10.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | tors-0.10.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 3.6 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
259863d6cb2c156883263368a408abf5121a33f3c03f8b9a40197e18a1f77f7d
|
|
BLAKE2b-256 checksum How to use checksums |
e701291763c78b4d60da5069d13ac438e45776f84b766d5c61d559e32c32a91c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|
Release files / tors-0.10.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | tors-0.10.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 3.6 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
e0b57af44a4a1ceba5041aef7c5114eba529d9ac3300b226b75d0d6591841acf
|
|
BLAKE2b-256 checksum How to use checksums |
adfae9d2742cc9e48eec5ae9a570847dd4166da353c4fb93252040d350e22600
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|
Release files / tors-0.10.1-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | tors-0.10.1-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 3.2 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
5730d620125536a56d052921274f10ccbbac3e70f0df28144908e392b2015db2
|
|
BLAKE2b-256 checksum How to use checksums |
56b9f9e70e40a5b2f4a9f15c3b9403d0e0ad22105f2e6c90e96a8bab5cc76917
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|
Release files / tors-0.10.1-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | tors-0.10.1-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 3.4 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
5b720b7518b513eb6c6479be5ba1059b3ce7bde8a56964eea03b7666e63b1345
|
|
BLAKE2b-256 checksum How to use checksums |
0bd9b7839a73f79cc31cf8d2a6a4f6c73202687115eb94862613a0a95c55dc6f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}
|