Skip to content

Commit 479fedd

Browse files
sidmohan0claude
andcommitted
Initial commit: DataFog 2.0 Rust monorepo
Rust-powered PII detection and anonymization with Python (PyO3) and WASM bindings. - datafog-core: regex engine (7 patterns), anonymizer (redact/replace/hash), cascade orchestrator, chunking, optional NER via gline-rs + ONNX Runtime - datafog-python: PyO3 bindings with maturin build, feature-gated NER support, model auto-download, runtime feature detection - datafog-wasm: wasm-bindgen bindings (regex-only) with demo page - 106 Rust tests, 40 Python tests, criterion benchmarks, GitHub Actions CI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
0 parents  commit 479fedd

28 files changed

Lines changed: 3334 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, dev]
6+
pull_request:
7+
branches: [main, dev]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
rust-test:
14+
name: Rust Tests
15+
runs-on: ${{ matrix.os }}
16+
strategy:
17+
matrix:
18+
os: [ubuntu-latest, macos-latest, windows-latest]
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: dtolnay/rust-toolchain@stable
22+
with:
23+
components: clippy, rustfmt
24+
- uses: Swatinem/rust-cache@v2
25+
26+
- name: Check formatting
27+
run: cargo fmt --all -- --check
28+
29+
- name: Clippy
30+
run: cargo clippy --workspace -- -D warnings
31+
32+
- name: Test (default features)
33+
run: cargo test --workspace
34+
35+
python-build:
36+
name: Python Build & Test
37+
runs-on: ${{ matrix.os }}
38+
strategy:
39+
matrix:
40+
os: [ubuntu-latest, macos-latest]
41+
python-version: ["3.10", "3.11", "3.12", "3.13"]
42+
steps:
43+
- uses: actions/checkout@v4
44+
- uses: dtolnay/rust-toolchain@stable
45+
- uses: actions/setup-python@v5
46+
with:
47+
python-version: ${{ matrix.python-version }}
48+
49+
- name: Install dependencies
50+
run: pip install maturin pytest
51+
52+
- name: Build and install (editable)
53+
run: maturin develop
54+
55+
- name: Run Python tests
56+
run: pytest -v
57+
58+
wasm-build:
59+
name: WASM Build
60+
runs-on: ubuntu-latest
61+
steps:
62+
- uses: actions/checkout@v4
63+
- uses: dtolnay/rust-toolchain@stable
64+
with:
65+
targets: wasm32-unknown-unknown
66+
67+
- name: Install wasm-pack
68+
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
69+
70+
- name: Build WASM package
71+
run: wasm-pack build crates/datafog-wasm --target web
72+
73+
benchmarks:
74+
name: Benchmarks
75+
runs-on: ubuntu-latest
76+
steps:
77+
- uses: actions/checkout@v4
78+
- uses: dtolnay/rust-toolchain@stable
79+
- uses: Swatinem/rust-cache@v2
80+
81+
- name: Run benchmarks
82+
run: cargo bench --package datafog-core -- --output-format bencher

.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Rust
2+
target/
3+
Cargo.lock
4+
5+
# Python
6+
.venv/
7+
*.pyc
8+
__pycache__/
9+
*.egg-info/
10+
.eggs/
11+
dist/
12+
build/
13+
*.so
14+
*.dylib
15+
*.pyd
16+
*.whl
17+
18+
# WASM
19+
pkg/
20+
21+
# OS
22+
.DS_Store
23+
Thumbs.db
24+
25+
# IDE
26+
.idea/
27+
.vscode/
28+
*.swp
29+
*.swo
30+
31+
# Testing / benchmarks
32+
.pytest_cache/
33+
htmlcov/
34+
*.prof

Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[workspace]
2+
resolver = "2"
3+
members = [
4+
"crates/datafog-core",
5+
"crates/datafog-python",
6+
"crates/datafog-wasm",
7+
]
8+
9+
[workspace.package]
10+
version = "0.1.0"
11+
edition = "2021"
12+
license = "Apache-2.0"
13+
authors = ["DataFog <hello@datafog.ai>"]
14+
repository = "https://github.com/DataFog/datafog"
15+
16+
[workspace.dependencies]
17+
datafog-core = { path = "crates/datafog-core" }
18+
regex = "1"
19+
serde = { version = "1", features = ["derive"] }
20+
serde_json = "1"
21+
thiserror = "2"

README.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# DataFog
2+
3+
Fast PII detection and anonymization, built in Rust with Python and WASM bindings.
4+
5+
DataFog detects structured PII (emails, phone numbers, SSNs, credit cards, IPs, dates of birth, ZIP codes) using compiled regex patterns, and optionally detects soft PII (names, organizations, addresses) using a GLiNER ONNX model via NER. The regex engine runs in microseconds per kilobyte with zero external dependencies.
6+
7+
## Prerequisites
8+
9+
- **Rust** (stable) &mdash; install via [rustup](https://rustup.rs/)
10+
- **Python 3.9+** &mdash; for the Python bindings
11+
- **maturin** &mdash; for building the Rust-to-Python bridge
12+
13+
## Quick start (Python)
14+
15+
```bash
16+
git clone https://github.com/DataFog/datafog.git
17+
cd datafog
18+
19+
python3 -m venv .venv
20+
source .venv/bin/activate # Windows: .venv\Scripts\activate
21+
pip install maturin pytest
22+
23+
maturin develop # builds Rust, installs editable Python package
24+
pytest -v # runs the test suite
25+
```
26+
27+
Then in Python:
28+
29+
```python
30+
from datafog import DataFog, detect, anonymize_text
31+
32+
# Detect PII
33+
entities = detect("Contact john@example.com or call 555-123-4567")
34+
# [{"type": "EMAIL", "value": "john@example.com", "start": 8, "end": 24, "score": 1.0},
35+
# {"type": "PHONE", "value": "555-123-4567", "start": 33, "end": 45, "score": 1.0}]
36+
37+
# Anonymize
38+
clean = anonymize_text("SSN is 123-45-6789", method="redact")
39+
# "SSN is [REDACTED]"
40+
41+
# Class API with batch support
42+
fog = DataFog()
43+
results = fog.detect_batch(["john@test.com", "555-123-4567", "no pii here"])
44+
```
45+
46+
## Quick start (Rust)
47+
48+
Add to your `Cargo.toml`:
49+
50+
```toml
51+
[dependencies]
52+
datafog-core = "0.1"
53+
```
54+
55+
```rust
56+
use datafog_core::DataFog;
57+
use datafog_core::anonymizer::AnonymizeMethod;
58+
59+
let fog = DataFog::new();
60+
let result = fog.detect("Contact john@example.com");
61+
println!("{:?}", result.spans);
62+
63+
let anon = fog.anonymize("SSN is 123-45-6789", AnonymizeMethod::Redact);
64+
assert_eq!(anon.text, "SSN is [REDACTED]");
65+
```
66+
67+
## NER engine (optional)
68+
69+
The NER engine uses a [GLiNER](https://github.com/urchade/GLiNER) ONNX model to detect soft PII that regex can't catch: person names, organizations, locations, addresses, and more. It runs both regex and NER, then merges the results.
70+
71+
Building with NER pulls in `gline-rs` + ONNX Runtime (~50 MB binary size increase).
72+
73+
```bash
74+
# Build the Python package with NER + model auto-download
75+
maturin develop --features full
76+
```
77+
78+
```python
79+
from datafog import DataFog, has_ner_support
80+
81+
if has_ner_support():
82+
fog = DataFog(engine="ner") # downloads ~50 MB model on first use
83+
entities = fog.detect("John Smith works at Acme Corp in Paris")
84+
# detects PERSON, ORGANIZATION, LOCATION in addition to regex entities
85+
```
86+
87+
You can also point to a local model directory:
88+
89+
```python
90+
fog = DataFog(engine="ner", model="/path/to/model/dir")
91+
```
92+
93+
The model directory must contain `tokenizer.json` and `model.onnx`.
94+
95+
### Entity types
96+
97+
| Engine | Entity types |
98+
|--------|-------------|
99+
| Regex | `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DOB`, `ZIP` |
100+
| NER | `PERSON`, `ORGANIZATION`, `LOCATION`, `ADDRESS`, `MEDICAL_RECORD_NUMBER`, `ACCOUNT_NUMBER`, `LICENSE_NUMBER`, `PASSPORT_NUMBER`, `URL` |
101+
102+
When both engines are active, all entity types are available.
103+
104+
## Anonymization methods
105+
106+
```python
107+
from datafog import anonymize_text
108+
109+
anonymize_text("Email: john@test.com", method="redact") # "Email: [REDACTED]"
110+
anonymize_text("Email: john@test.com", method="replace") # "Email: [EMAIL_1]"
111+
anonymize_text("Email: john@test.com", method="hash") # "Email: a1b2c3d4..."
112+
```
113+
114+
Available methods: `redact`, `replace`, `hash` (SHA-256), `hash_md5`, `hash_sha3`.
115+
116+
## WASM
117+
118+
The WASM target provides regex-only detection for browser and edge environments.
119+
120+
```bash
121+
# Requires wasm-pack: https://rustwasm.github.io/wasm-pack/installer/
122+
wasm-pack build crates/datafog-wasm --target web
123+
```
124+
125+
A demo page is included at `crates/datafog-wasm/demo/index.html`.
126+
127+
## Project structure
128+
129+
```
130+
datafog/
131+
├── crates/
132+
│ ├── datafog-core/ # Pure Rust library (regex, NER, anonymizer, cascade)
133+
│ ├── datafog-python/ # PyO3 bindings
134+
│ └── datafog-wasm/ # wasm-bindgen bindings (regex-only)
135+
├── python/datafog/ # Python package source + type stubs
136+
├── tests/ # Python integration tests
137+
├── pyproject.toml # maturin build config
138+
└── Cargo.toml # Workspace root
139+
```
140+
141+
## Feature flags (Rust)
142+
143+
| Flag | What it adds | Dependencies |
144+
|------|-------------|-------------|
145+
| `default` | Regex-only detection | None beyond `regex`, `serde` |
146+
| `ner` | GLiNER NER engine | `gline-rs`, `ort` |
147+
| `model-download` | Auto-download models from HuggingFace | `reqwest`, `dirs` |
148+
| `parallel` | Rayon-based batch parallelism | `rayon` |
149+
| `ner-cuda` | CUDA GPU acceleration for NER | (implies `ner`) |
150+
| `ner-coreml` | Apple CoreML acceleration for NER | (implies `ner`) |
151+
152+
## Development
153+
154+
```bash
155+
# Run Rust tests
156+
cargo test --workspace
157+
158+
# Run clippy and check formatting
159+
cargo clippy --workspace -- -D warnings
160+
cargo fmt --all -- --check
161+
162+
# Run benchmarks
163+
cargo bench --package datafog-core
164+
165+
# Build and test Python
166+
source .venv/bin/activate
167+
maturin develop
168+
pytest -v
169+
```
170+
171+
## License
172+
173+
Apache 2.0

crates/datafog-core/Cargo.toml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
[package]
2+
name = "datafog-core"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
authors.workspace = true
7+
repository.workspace = true
8+
description = "Fast PII detection and anonymization engine"
9+
keywords = ["pii", "privacy", "anonymization", "ner", "regex"]
10+
categories = ["text-processing"]
11+
12+
[features]
13+
default = []
14+
ner = ["dep:gline-rs", "dep:ort"]
15+
ner-cuda = ["ner"]
16+
ner-coreml = ["ner"]
17+
model-download = ["dep:reqwest", "dep:tokio", "dep:dirs"]
18+
parallel = ["dep:rayon"]
19+
20+
[dependencies]
21+
regex = { workspace = true }
22+
serde = { workspace = true }
23+
serde_json = { workspace = true }
24+
thiserror = { workspace = true }
25+
md-5 = "0.10"
26+
sha2 = "0.10"
27+
sha3 = "0.10"
28+
rand = "0.8"
29+
30+
# Optional: NER
31+
gline-rs = { version = "1", optional = true }
32+
ort = { version = "2.0.0-rc.9", optional = true }
33+
34+
# Optional: Model download
35+
reqwest = { version = "0.12", features = ["blocking"], optional = true }
36+
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
37+
dirs = { version = "5", optional = true }
38+
39+
# Optional: Parallel processing
40+
rayon = { version = "1", optional = true }
41+
42+
[dev-dependencies]
43+
criterion = { version = "0.5", features = ["html_reports"] }
44+
45+
[[bench]]
46+
name = "regex_bench"
47+
harness = false
48+
49+
[[bench]]
50+
name = "anonymizer_bench"
51+
harness = false
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2+
use datafog_core::anonymizer::{AnonymizeMethod, HashType};
3+
use datafog_core::cascade::DataFog;
4+
5+
fn bench_anonymizer(c: &mut Criterion) {
6+
let fog = DataFog::new();
7+
8+
let sample_text = "Contact John Doe at john.doe@example.com or call (555) 123-4567. \
9+
His SSN is 123-45-6789 and credit card 4111111111111111. \
10+
He lives at 123 Main St, New York, NY 10001. \
11+
His IP address is 192.168.1.1 and his birthday is 01/01/1980.";
12+
13+
c.bench_function("anonymize_redact", |b| {
14+
b.iter(|| fog.anonymize(black_box(sample_text), AnonymizeMethod::Redact))
15+
});
16+
17+
c.bench_function("anonymize_replace", |b| {
18+
b.iter(|| fog.anonymize(black_box(sample_text), AnonymizeMethod::Replace))
19+
});
20+
21+
c.bench_function("anonymize_hash_sha256", |b| {
22+
b.iter(|| {
23+
fog.anonymize(
24+
black_box(sample_text),
25+
AnonymizeMethod::Hash(HashType::Sha256),
26+
)
27+
})
28+
});
29+
}
30+
31+
criterion_group!(benches, bench_anonymizer);
32+
criterion_main!(benches);

0 commit comments

Comments
 (0)