datafog-core 0.4.5b2__tar.gz → 0.4.5b3__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.
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/Cargo.lock +4 -4
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/PKG-INFO +6 -6
- {datafog_core-0.4.5b2/crates/core → datafog_core-0.4.5b3}/README.md +5 -5
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/Cargo.toml +2 -2
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/src/models.rs +48 -4
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/Cargo.toml +1 -1
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3/crates/core}/README.md +5 -5
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models/gliner2.rs +3 -3
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models/tests.rs +201 -3
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models.rs +217 -6
- datafog_core-0.4.5b3/crates/core/tests/model_datafog.rs +118 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/pyproject.toml +1 -1
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/python/datafog_core/__init__.pyi +2 -1
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/python/datafog_core/_gliner2_worker.py +1 -1
- datafog_core-0.4.5b2/crates/core/tests/model_datafog.rs +0 -44
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/Cargo.toml +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/src/lib.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/api_key_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/bearer_token_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/capabilities_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/credential_overlap_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/credential_uri_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/email_context_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/german_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/jwt_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/npi_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/private_key_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/requirements-typing.txt +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/synthetic_fixtures.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/test_installed.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/test_python49_integration.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/test_typing.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/us_routing_number_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/uuid_conformance.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/examples/german_benchmark.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/examples/scan_benchmark.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/api_key.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/bearer_token.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/capabilities.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/credential_uri.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/email_context.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/german.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/jwt.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/lib.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models/gliner2-lock.json +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models/process.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/npi.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/offsets.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/private_key.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/scan_selection_tests.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/selection_tests.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/structured.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/us_routing_number.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/uuid.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/api_key.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/bearer_token.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/capabilities.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/credential_overlaps.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/credential_uri.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/email_context.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/german.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/jwt.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/npi.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/private_key.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/support/synthetic_fixtures.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/us_routing_number.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/uuid.rs +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/python/datafog_core/__init__.py +0 -0
- {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/python/datafog_core/py.typed +0 -0
|
@@ -80,7 +80,7 @@ checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d"
|
|
|
80
80
|
|
|
81
81
|
[[package]]
|
|
82
82
|
name = "datafog-core"
|
|
83
|
-
version = "0.4.5-beta.
|
|
83
|
+
version = "0.4.5-beta.3"
|
|
84
84
|
dependencies = [
|
|
85
85
|
"base64",
|
|
86
86
|
"futures",
|
|
@@ -94,7 +94,7 @@ dependencies = [
|
|
|
94
94
|
|
|
95
95
|
[[package]]
|
|
96
96
|
name = "datafog-core-python"
|
|
97
|
-
version = "0.4.5-beta.
|
|
97
|
+
version = "0.4.5-beta.3"
|
|
98
98
|
dependencies = [
|
|
99
99
|
"datafog-core",
|
|
100
100
|
"pyo3",
|
|
@@ -105,7 +105,7 @@ dependencies = [
|
|
|
105
105
|
|
|
106
106
|
[[package]]
|
|
107
107
|
name = "datafog-node"
|
|
108
|
-
version = "0.4.5-beta.
|
|
108
|
+
version = "0.4.5-beta.3"
|
|
109
109
|
dependencies = [
|
|
110
110
|
"datafog-core",
|
|
111
111
|
"napi",
|
|
@@ -116,7 +116,7 @@ dependencies = [
|
|
|
116
116
|
|
|
117
117
|
[[package]]
|
|
118
118
|
name = "datafog-wasm"
|
|
119
|
-
version = "0.4.5-beta.
|
|
119
|
+
version = "0.4.5-beta.3"
|
|
120
120
|
dependencies = [
|
|
121
121
|
"datafog-core",
|
|
122
122
|
"serde",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datafog-core
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5b3
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: License :: OSI Approved :: MIT License
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -59,7 +59,7 @@ DataFog Core is a Rust library with Python, Node.js, and browser WebAssembly bin
|
|
|
59
59
|
|
|
60
60
|
Rule-based scanning runs locally without model weights or downloads. Optional local model sessions add DataFog and GLiNER2 support in Rust, Python, and Node.js.
|
|
61
61
|
|
|
62
|
-
**Stable:** [0.4.4](https://github.com/DataFog/datafog-core/releases/tag/v0.4.4) · **Model beta:** [0.4.5-beta.
|
|
62
|
+
**Stable:** [0.4.4](https://github.com/DataFog/datafog-core/releases/tag/v0.4.4) · **Model beta:** [0.4.5-beta.3](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.3)
|
|
63
63
|
|
|
64
64
|
## Quickstart
|
|
65
65
|
|
|
@@ -198,20 +198,20 @@ Keep scan settings under `scan` and transformation policy under `transform`. A d
|
|
|
198
198
|
|
|
199
199
|
## Local models (beta)
|
|
200
200
|
|
|
201
|
-
[0.4.5-beta.
|
|
201
|
+
[0.4.5-beta.3](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.3) adds local sessions for the DataFog PII model and GLiNER2 in Rust, Python, and Node.js on all five native platforms.
|
|
202
202
|
|
|
203
203
|
Install the beta for your runtime:
|
|
204
204
|
|
|
205
205
|
```bash
|
|
206
|
-
python -m pip install 'datafog-core==0.4.
|
|
206
|
+
python -m pip install 'datafog-core==0.4.5b3'
|
|
207
207
|
npm install @datafog/node@beta
|
|
208
|
-
cargo add datafog-core@0.4.5-beta.
|
|
208
|
+
cargo add datafog-core@0.4.5-beta.3 --features models
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
For GLiNER2, install its optional dependencies in the Python environment used by the runner:
|
|
212
212
|
|
|
213
213
|
```bash
|
|
214
|
-
python -m pip install 'datafog-core[gliner2]==0.4.
|
|
214
|
+
python -m pip install 'datafog-core[gliner2]==0.4.5b3'
|
|
215
215
|
```
|
|
216
216
|
|
|
217
217
|
The GLiNER2 runner ships in the Python package and also serves Node.js and Rust callers. Download and verify model files separately, then use `ModelSession.open`, text `scan`, and `close`. Reuse sessions across calls. Both backends return consistent results and errors, with cancellation and cleanup support.
|
|
@@ -26,7 +26,7 @@ DataFog Core is a Rust library with Python, Node.js, and browser WebAssembly bin
|
|
|
26
26
|
|
|
27
27
|
Rule-based scanning runs locally without model weights or downloads. Optional local model sessions add DataFog and GLiNER2 support in Rust, Python, and Node.js.
|
|
28
28
|
|
|
29
|
-
**Stable:** [0.4.4](https://github.com/DataFog/datafog-core/releases/tag/v0.4.4) · **Model beta:** [0.4.5-beta.
|
|
29
|
+
**Stable:** [0.4.4](https://github.com/DataFog/datafog-core/releases/tag/v0.4.4) · **Model beta:** [0.4.5-beta.3](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.3)
|
|
30
30
|
|
|
31
31
|
## Quickstart
|
|
32
32
|
|
|
@@ -165,20 +165,20 @@ Keep scan settings under `scan` and transformation policy under `transform`. A d
|
|
|
165
165
|
|
|
166
166
|
## Local models (beta)
|
|
167
167
|
|
|
168
|
-
[0.4.5-beta.
|
|
168
|
+
[0.4.5-beta.3](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.3) adds local sessions for the DataFog PII model and GLiNER2 in Rust, Python, and Node.js on all five native platforms.
|
|
169
169
|
|
|
170
170
|
Install the beta for your runtime:
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
|
-
python -m pip install 'datafog-core==0.4.
|
|
173
|
+
python -m pip install 'datafog-core==0.4.5b3'
|
|
174
174
|
npm install @datafog/node@beta
|
|
175
|
-
cargo add datafog-core@0.4.5-beta.
|
|
175
|
+
cargo add datafog-core@0.4.5-beta.3 --features models
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
For GLiNER2, install its optional dependencies in the Python environment used by the runner:
|
|
179
179
|
|
|
180
180
|
```bash
|
|
181
|
-
python -m pip install 'datafog-core[gliner2]==0.4.
|
|
181
|
+
python -m pip install 'datafog-core[gliner2]==0.4.5b3'
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
The GLiNER2 runner ships in the Python package and also serves Node.js and Rust callers. Download and verify model files separately, then use `ModelSession.open`, text `scan`, and `close`. Reuse sessions across calls. Both backends return consistent results and errors, with cancellation and cleanup support.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "datafog-core-python"
|
|
3
|
-
version = "0.4.5-beta.
|
|
3
|
+
version = "0.4.5-beta.3"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.88"
|
|
6
6
|
description = "Python bindings for datafog-core"
|
|
@@ -12,7 +12,7 @@ name = "datafog_core"
|
|
|
12
12
|
crate-type = ["cdylib"]
|
|
13
13
|
|
|
14
14
|
[dependencies]
|
|
15
|
-
datafog-core = { version = "0.4.5-beta.
|
|
15
|
+
datafog-core = { version = "0.4.5-beta.3", path = "../../crates/core", features = ["models"] }
|
|
16
16
|
pyo3 = { version = "0.29", features = ["abi3-py310"] }
|
|
17
17
|
pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
|
|
18
18
|
serde_json = "1"
|
|
@@ -91,8 +91,49 @@ impl ModelSession {
|
|
|
91
91
|
.map(|session| Self { session })
|
|
92
92
|
.map_err(|error| model_error(py, error))
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
/// Rule detectors handle the entity types they support and the model runs
|
|
95
|
+
/// only for requested types it alone provides. Omitting `config` equals
|
|
96
|
+
/// `{}`: every enabled rule detector plus the model's remaining labels.
|
|
97
|
+
#[pyo3(signature = (text, config=None, *, cancellation=None))]
|
|
95
98
|
fn scan(
|
|
99
|
+
&self,
|
|
100
|
+
py: Python<'_>,
|
|
101
|
+
text: String,
|
|
102
|
+
config: Option<&Bound<'_, PyAny>>,
|
|
103
|
+
cancellation: Option<PyRef<'_, ModelCancellation>>,
|
|
104
|
+
) -> PyResult<ModelScanResult> {
|
|
105
|
+
let routed = match config {
|
|
106
|
+
Some(config) => {
|
|
107
|
+
let value = py_to_json(py, config, "")?;
|
|
108
|
+
self.session
|
|
109
|
+
.routed_config(&value)
|
|
110
|
+
.map_err(|error| model_error(py, error))?
|
|
111
|
+
}
|
|
112
|
+
None => self.session.default_routed_config(),
|
|
113
|
+
};
|
|
114
|
+
// Rule-only routing never reserves the model worker.
|
|
115
|
+
let request = if routed.needs_model() {
|
|
116
|
+
Some(
|
|
117
|
+
self.session
|
|
118
|
+
.prepare_request()
|
|
119
|
+
.map_err(|error| model_error(py, error))?,
|
|
120
|
+
)
|
|
121
|
+
} else {
|
|
122
|
+
None
|
|
123
|
+
};
|
|
124
|
+
let cancellation = cancellation
|
|
125
|
+
.map(|v| Arc::clone(&v.cancellation))
|
|
126
|
+
.unwrap_or_default();
|
|
127
|
+
py.detach(|| {
|
|
128
|
+
self.session
|
|
129
|
+
.scan_routed_request(request.as_ref(), &text, &routed, &cancellation)
|
|
130
|
+
})
|
|
131
|
+
.map(ModelScanResult::from)
|
|
132
|
+
.map_err(|error| model_error(py, error))
|
|
133
|
+
}
|
|
134
|
+
/// Raw model output: every model label, with no rule detectors.
|
|
135
|
+
#[pyo3(signature = (text, *, cancellation=None))]
|
|
136
|
+
fn scan_model(
|
|
96
137
|
&self,
|
|
97
138
|
py: Python<'_>,
|
|
98
139
|
text: String,
|
|
@@ -105,9 +146,12 @@ impl ModelSession {
|
|
|
105
146
|
let cancellation = cancellation
|
|
106
147
|
.map(|v| Arc::clone(&v.cancellation))
|
|
107
148
|
.unwrap_or_default();
|
|
108
|
-
py.detach(||
|
|
109
|
-
.
|
|
110
|
-
|
|
149
|
+
py.detach(|| {
|
|
150
|
+
self.session
|
|
151
|
+
.scan_model_request(&request, &text, &cancellation)
|
|
152
|
+
})
|
|
153
|
+
.map(ModelScanResult::from)
|
|
154
|
+
.map_err(|error| model_error(py, error))
|
|
111
155
|
}
|
|
112
156
|
fn close(&self, py: Python<'_>) -> PyResult<()> {
|
|
113
157
|
py.detach(|| self.session.close())
|
|
@@ -26,7 +26,7 @@ DataFog Core is a Rust library with Python, Node.js, and browser WebAssembly bin
|
|
|
26
26
|
|
|
27
27
|
Rule-based scanning runs locally without model weights or downloads. Optional local model sessions add DataFog and GLiNER2 support in Rust, Python, and Node.js.
|
|
28
28
|
|
|
29
|
-
**Stable:** [0.4.4](https://github.com/DataFog/datafog-core/releases/tag/v0.4.4) · **Model beta:** [0.4.5-beta.
|
|
29
|
+
**Stable:** [0.4.4](https://github.com/DataFog/datafog-core/releases/tag/v0.4.4) · **Model beta:** [0.4.5-beta.3](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.3)
|
|
30
30
|
|
|
31
31
|
## Quickstart
|
|
32
32
|
|
|
@@ -165,20 +165,20 @@ Keep scan settings under `scan` and transformation policy under `transform`. A d
|
|
|
165
165
|
|
|
166
166
|
## Local models (beta)
|
|
167
167
|
|
|
168
|
-
[0.4.5-beta.
|
|
168
|
+
[0.4.5-beta.3](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.3) adds local sessions for the DataFog PII model and GLiNER2 in Rust, Python, and Node.js on all five native platforms.
|
|
169
169
|
|
|
170
170
|
Install the beta for your runtime:
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
|
-
python -m pip install 'datafog-core==0.4.
|
|
173
|
+
python -m pip install 'datafog-core==0.4.5b3'
|
|
174
174
|
npm install @datafog/node@beta
|
|
175
|
-
cargo add datafog-core@0.4.5-beta.
|
|
175
|
+
cargo add datafog-core@0.4.5-beta.3 --features models
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
For GLiNER2, install its optional dependencies in the Python environment used by the runner:
|
|
179
179
|
|
|
180
180
|
```bash
|
|
181
|
-
python -m pip install 'datafog-core[gliner2]==0.4.
|
|
181
|
+
python -m pip install 'datafog-core[gliner2]==0.4.5b3'
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
The GLiNER2 runner ships in the Python package and also serves Node.js and Rust callers. Download and verify model files separately, then use `ModelSession.open`, text `scan`, and `close`. Reuse sessions across calls. Both backends return consistent results and errors, with cancellation and cleanup support.
|
|
@@ -255,7 +255,7 @@ pub(super) fn decode(bytes: &[u8], max_findings: usize) -> Result<BackendResult,
|
|
|
255
255
|
serde_json::from_value(value).map_err(|_| ModelError::invalid_output())?;
|
|
256
256
|
let expected = runtime();
|
|
257
257
|
if output.runtime != expected
|
|
258
|
-
|| output.worker_version != "0.4.
|
|
258
|
+
|| output.worker_version != "0.4.5b3"
|
|
259
259
|
|| output.findings.len() > max_findings
|
|
260
260
|
{
|
|
261
261
|
return Err(ModelError::invalid_output());
|
|
@@ -318,7 +318,7 @@ mod tests {
|
|
|
318
318
|
descriptor.thresholds = [("street_address".into(), 0.5)].into();
|
|
319
319
|
descriptor.score_scale = SCALE.into();
|
|
320
320
|
let response = |score: f64| {
|
|
321
|
-
serde_json::json!({"worker_version":"0.4.
|
|
321
|
+
serde_json::json!({"worker_version":"0.4.5b3","runtime":runtime(),
|
|
322
322
|
"findings":[{"label":"street_address","start":2,"end":17,"text":"123 Main Street","raw_score":score}]}).to_string()
|
|
323
323
|
};
|
|
324
324
|
let valid = decode(response(0.9).as_bytes(), 100).unwrap();
|
|
@@ -346,7 +346,7 @@ mod tests {
|
|
|
346
346
|
.is_empty()
|
|
347
347
|
);
|
|
348
348
|
let corrupt =
|
|
349
|
-
serde_json::json!({"worker_version":"0.4.
|
|
349
|
+
serde_json::json!({"worker_version":"0.4.5b3","runtime":{},"findings":[]}).to_string();
|
|
350
350
|
assert!(decode(corrupt.as_bytes(), 100).is_err());
|
|
351
351
|
}
|
|
352
352
|
}
|
|
@@ -277,7 +277,7 @@ fn admission_precedes_scheduling_and_expired_or_cancelled_requests_do_not_infer(
|
|
|
277
277
|
request.deadline = Instant::now();
|
|
278
278
|
assert_eq!(
|
|
279
279
|
session
|
|
280
|
-
.
|
|
280
|
+
.scan_model_request(&request, "secret", &Cancellation::default())
|
|
281
281
|
.unwrap_err()
|
|
282
282
|
.code,
|
|
283
283
|
"model_timeout"
|
|
@@ -290,7 +290,7 @@ fn admission_precedes_scheduling_and_expired_or_cancelled_requests_do_not_infer(
|
|
|
290
290
|
cancellation.cancel();
|
|
291
291
|
assert_eq!(
|
|
292
292
|
session
|
|
293
|
-
.
|
|
293
|
+
.scan_model_request(&request, "secret", &cancellation)
|
|
294
294
|
.unwrap_err()
|
|
295
295
|
.code,
|
|
296
296
|
"model_cancelled"
|
|
@@ -300,7 +300,7 @@ fn admission_precedes_scheduling_and_expired_or_cancelled_requests_do_not_infer(
|
|
|
300
300
|
session.request_close();
|
|
301
301
|
assert_eq!(
|
|
302
302
|
session
|
|
303
|
-
.
|
|
303
|
+
.scan_model_request(&next, "", &Cancellation::default())
|
|
304
304
|
.unwrap_err()
|
|
305
305
|
.code,
|
|
306
306
|
"model_cancelled"
|
|
@@ -334,3 +334,201 @@ fn native_seven_label_mapping_preserves_names_and_identifier_meaning() {
|
|
|
334
334
|
assert_eq!(result[0].1.native_label.as_deref(), Some(native));
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
|
+
|
|
338
|
+
fn idle_session() -> ModelSession {
|
|
339
|
+
ModelSession {
|
|
340
|
+
descriptor: descriptor(),
|
|
341
|
+
inner: Mutex::new(SessionInner { worker: None }),
|
|
342
|
+
closing: AtomicBool::new(false),
|
|
343
|
+
state: AtomicU8::new(1),
|
|
344
|
+
cleanup_failed: Arc::new(AtomicBool::new(false)),
|
|
345
|
+
admitted: Arc::new(AtomicBool::new(false)),
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn routed_rule_entities_never_touch_the_model_worker() {
|
|
351
|
+
let session = idle_session();
|
|
352
|
+
let config = session
|
|
353
|
+
.routed_config(&serde_json::json!({"entities": ["CREDIT_CARD", "EMAIL"]}))
|
|
354
|
+
.unwrap();
|
|
355
|
+
assert!(!config.needs_model());
|
|
356
|
+
let text = "Card 4111 1111 1111 1111, mail may@example.test, call (212) 555-0100.";
|
|
357
|
+
// Another request holds the worker and the admission flag; rule-only
|
|
358
|
+
// routing neither waits for them nor reports `model_busy`.
|
|
359
|
+
let held = session.inner.lock().unwrap();
|
|
360
|
+
let request = session.prepare_request().unwrap();
|
|
361
|
+
let result = session
|
|
362
|
+
.scan_routed(text, &config, &Cancellation::default())
|
|
363
|
+
.unwrap();
|
|
364
|
+
let found: Vec<_> = result
|
|
365
|
+
.findings
|
|
366
|
+
.iter()
|
|
367
|
+
.map(|finding| (finding.entity_type.as_str(), finding.matched_text.as_str()))
|
|
368
|
+
.collect();
|
|
369
|
+
assert_eq!(
|
|
370
|
+
found,
|
|
371
|
+
[
|
|
372
|
+
("CREDIT_CARD", "4111 1111 1111 1111"),
|
|
373
|
+
("EMAIL", "may@example.test")
|
|
374
|
+
]
|
|
375
|
+
);
|
|
376
|
+
assert_eq!(result.evidence.len(), 2);
|
|
377
|
+
for (index, evidence) in result.evidence.iter().enumerate() {
|
|
378
|
+
assert_eq!(evidence.finding_index, index);
|
|
379
|
+
assert!(
|
|
380
|
+
evidence.sources[0]
|
|
381
|
+
.detector_name
|
|
382
|
+
.starts_with("datafog-core/")
|
|
383
|
+
);
|
|
384
|
+
assert!(evidence.sources[0].raw_score.is_none());
|
|
385
|
+
}
|
|
386
|
+
assert_eq!(result.model, "DataFog/pii-en-65m");
|
|
387
|
+
drop(request);
|
|
388
|
+
drop(held);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#[test]
|
|
392
|
+
fn routed_scans_keep_the_session_lifecycle_and_limits() {
|
|
393
|
+
let session = idle_session();
|
|
394
|
+
let config = session
|
|
395
|
+
.routed_config(&serde_json::json!({"entities": ["EMAIL"]}))
|
|
396
|
+
.unwrap();
|
|
397
|
+
let cancelled = Cancellation::default();
|
|
398
|
+
cancelled.cancel();
|
|
399
|
+
assert_eq!(
|
|
400
|
+
session
|
|
401
|
+
.scan_routed("may@example.test", &config, &cancelled)
|
|
402
|
+
.unwrap_err()
|
|
403
|
+
.code,
|
|
404
|
+
"model_cancelled"
|
|
405
|
+
);
|
|
406
|
+
let oversized = "x".repeat(session.descriptor.limits.max_input_bytes + 1);
|
|
407
|
+
assert_eq!(
|
|
408
|
+
session
|
|
409
|
+
.scan_routed(&oversized, &config, &Cancellation::default())
|
|
410
|
+
.unwrap_err()
|
|
411
|
+
.reason,
|
|
412
|
+
"limit_exceeded"
|
|
413
|
+
);
|
|
414
|
+
session.state.store(2, Ordering::Release);
|
|
415
|
+
assert_eq!(
|
|
416
|
+
session
|
|
417
|
+
.scan_routed("may@example.test", &config, &Cancellation::default())
|
|
418
|
+
.unwrap_err()
|
|
419
|
+
.code,
|
|
420
|
+
"model_session_failed"
|
|
421
|
+
);
|
|
422
|
+
let session = idle_session();
|
|
423
|
+
session.close().unwrap();
|
|
424
|
+
assert_eq!(
|
|
425
|
+
session
|
|
426
|
+
.scan_routed("may@example.test", &config, &Cancellation::default())
|
|
427
|
+
.unwrap_err()
|
|
428
|
+
.code,
|
|
429
|
+
"model_session_closed"
|
|
430
|
+
);
|
|
431
|
+
// A model-routed configuration must arrive with its reserved request.
|
|
432
|
+
let session = idle_session();
|
|
433
|
+
let model = session
|
|
434
|
+
.routed_config(&serde_json::json!({"entities": ["FIRST_NAME"]}))
|
|
435
|
+
.unwrap();
|
|
436
|
+
assert_eq!(
|
|
437
|
+
session
|
|
438
|
+
.scan_routed_request(None, "Jane", &model, &Cancellation::default())
|
|
439
|
+
.unwrap_err()
|
|
440
|
+
.path,
|
|
441
|
+
"/request"
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[test]
|
|
446
|
+
fn routed_configuration_splits_entities_between_rules_and_model() {
|
|
447
|
+
let session = idle_session();
|
|
448
|
+
let mixed = session
|
|
449
|
+
.routed_config(&serde_json::json!({"entities": ["FIRST_NAME", "EMAIL", "CREDIT_CARD"]}))
|
|
450
|
+
.unwrap();
|
|
451
|
+
assert!(mixed.needs_model());
|
|
452
|
+
assert_eq!(
|
|
453
|
+
mixed.model_entities,
|
|
454
|
+
BTreeSet::from(["FIRST_NAME".to_owned()])
|
|
455
|
+
);
|
|
456
|
+
assert_eq!(
|
|
457
|
+
mixed.rules.as_ref().and_then(ScanConfig::entities),
|
|
458
|
+
Some(&BTreeSet::from([
|
|
459
|
+
"CREDIT_CARD".to_owned(),
|
|
460
|
+
"EMAIL".to_owned()
|
|
461
|
+
]))
|
|
462
|
+
);
|
|
463
|
+
// Shared labels such as EMAIL and PHONE go to rules; without a selection
|
|
464
|
+
// the model returns only the labels rules cannot produce.
|
|
465
|
+
let everything = session.routed_config(&serde_json::json!({})).unwrap();
|
|
466
|
+
assert!(everything.rules.as_ref().unwrap().entities().is_none());
|
|
467
|
+
assert_eq!(
|
|
468
|
+
everything.model_entities,
|
|
469
|
+
BTreeSet::from(
|
|
470
|
+
[
|
|
471
|
+
"ACCOUNT_NUMBER",
|
|
472
|
+
"CUSTOMER_ID",
|
|
473
|
+
"FIRST_NAME",
|
|
474
|
+
"LAST_NAME",
|
|
475
|
+
"STREET_ADDRESS"
|
|
476
|
+
]
|
|
477
|
+
.map(String::from)
|
|
478
|
+
)
|
|
479
|
+
);
|
|
480
|
+
// Omitting the configuration routes exactly like an empty configuration.
|
|
481
|
+
let default = session.default_routed_config();
|
|
482
|
+
assert_eq!(default.model_entities, everything.model_entities);
|
|
483
|
+
assert_eq!(default.rules, everything.rules);
|
|
484
|
+
let model_only = session
|
|
485
|
+
.routed_config(&serde_json::json!({"entities": ["STREET_ADDRESS"], "locale": "de"}))
|
|
486
|
+
.unwrap();
|
|
487
|
+
assert!(model_only.rules.is_none());
|
|
488
|
+
for (config, reason, path) in [
|
|
489
|
+
(serde_json::json!([]), "invalid_type", ""),
|
|
490
|
+
(
|
|
491
|
+
serde_json::json!({"entities": []}),
|
|
492
|
+
"empty_value",
|
|
493
|
+
"/entities",
|
|
494
|
+
),
|
|
495
|
+
(
|
|
496
|
+
serde_json::json!({"entities": "EMAIL"}),
|
|
497
|
+
"invalid_type",
|
|
498
|
+
"/entities",
|
|
499
|
+
),
|
|
500
|
+
(
|
|
501
|
+
serde_json::json!({"entities": ["EMAIL", 1]}),
|
|
502
|
+
"invalid_type",
|
|
503
|
+
"/entities/1",
|
|
504
|
+
),
|
|
505
|
+
(
|
|
506
|
+
serde_json::json!({"entities": ["EMAIL", "EMAIL"]}),
|
|
507
|
+
"duplicate_value",
|
|
508
|
+
"/entities/1",
|
|
509
|
+
),
|
|
510
|
+
(
|
|
511
|
+
serde_json::json!({"entities": ["EMAIL", "CARD_NUMBER"]}),
|
|
512
|
+
"invalid_value",
|
|
513
|
+
"/entities/1",
|
|
514
|
+
),
|
|
515
|
+
(
|
|
516
|
+
serde_json::json!({"entities": ["STREET_ADDRESS"], "locale": "xx"}),
|
|
517
|
+
"invalid_value",
|
|
518
|
+
"/locale",
|
|
519
|
+
),
|
|
520
|
+
(
|
|
521
|
+
serde_json::json!({"entities": ["EMAIL"], "unknown": true}),
|
|
522
|
+
"unknown_field",
|
|
523
|
+
"/unknown",
|
|
524
|
+
),
|
|
525
|
+
] {
|
|
526
|
+
let error = session.routed_config(&config).unwrap_err();
|
|
527
|
+
assert_eq!(error.code, "invalid_configuration", "{config}");
|
|
528
|
+
assert_eq!(
|
|
529
|
+
(error.reason.as_str(), error.path.as_str()),
|
|
530
|
+
(reason, path),
|
|
531
|
+
"{config}"
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
@@ -6,10 +6,10 @@ pub use gliner2::{Gliner2Config, Gliner2Label};
|
|
|
6
6
|
#[cfg(test)]
|
|
7
7
|
mod tests;
|
|
8
8
|
|
|
9
|
-
use crate::{Finding, PrivacyError, TextIndex, TextRange};
|
|
9
|
+
use crate::{Finding, PrivacyError, PrivacyErrorReason, ScanConfig, TextIndex, TextRange};
|
|
10
10
|
use serde::{Deserialize, Serialize};
|
|
11
11
|
use sha2::{Digest, Sha256};
|
|
12
|
-
use std::collections::BTreeMap;
|
|
12
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
13
13
|
use std::fs::File;
|
|
14
14
|
use std::io::Read;
|
|
15
15
|
use std::path::{Component, Path, PathBuf};
|
|
@@ -233,6 +233,42 @@ pub struct FindingEvidence {
|
|
|
233
233
|
pub sources: Vec<SourceEvidence>,
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/// Entity routing for a model session scan. Rule detectors handle every
|
|
237
|
+
/// requested entity type they support; the model runs only for requested
|
|
238
|
+
/// types that it alone provides. Validated against one session's labels.
|
|
239
|
+
#[derive(Clone, Debug)]
|
|
240
|
+
pub struct RoutedScanConfig {
|
|
241
|
+
rules: Option<ScanConfig>,
|
|
242
|
+
model_entities: BTreeSet<String>,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
impl RoutedScanConfig {
|
|
246
|
+
/// Whether a scan with this configuration runs model inference.
|
|
247
|
+
pub fn needs_model(&self) -> bool {
|
|
248
|
+
!self.model_entities.is_empty()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn rule_entry(finding: Finding) -> (Finding, SourceEvidence) {
|
|
253
|
+
let source = SourceEvidence {
|
|
254
|
+
detector_name: finding.detector_name.clone(),
|
|
255
|
+
detector_version: finding.detector_version.clone(),
|
|
256
|
+
native_label: None,
|
|
257
|
+
raw_score: None,
|
|
258
|
+
calibrated_confidence: finding.confidence.map(f64::from),
|
|
259
|
+
};
|
|
260
|
+
(finding, source)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn rule_entries(text: &str, config: &RoutedScanConfig) -> Vec<(Finding, SourceEvidence)> {
|
|
264
|
+
config.rules.as_ref().map_or_else(Vec::new, |rules| {
|
|
265
|
+
crate::scan_with_config(text, rules)
|
|
266
|
+
.into_iter()
|
|
267
|
+
.map(rule_entry)
|
|
268
|
+
.collect()
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
236
272
|
#[derive(Debug, Serialize)]
|
|
237
273
|
pub struct ModelScanResult {
|
|
238
274
|
pub contract_version: u32,
|
|
@@ -472,16 +508,28 @@ impl ModelSession {
|
|
|
472
508
|
Ok(request.deadline)
|
|
473
509
|
}
|
|
474
510
|
|
|
511
|
+
/// Scan with default routing: every enabled rule detector, plus the model
|
|
512
|
+
/// for the labels rules do not produce. Equivalent to an empty routed
|
|
513
|
+
/// configuration, as an omitted rule scan configuration equals `{}`.
|
|
475
514
|
pub fn scan(
|
|
476
515
|
&self,
|
|
477
516
|
text: &str,
|
|
478
517
|
cancellation: &Cancellation,
|
|
518
|
+
) -> Result<ModelScanResult, ModelError> {
|
|
519
|
+
self.scan_routed(text, &self.default_routed_config(), cancellation)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/// Raw model output: every model label, with no rule detectors.
|
|
523
|
+
pub fn scan_model(
|
|
524
|
+
&self,
|
|
525
|
+
text: &str,
|
|
526
|
+
cancellation: &Cancellation,
|
|
479
527
|
) -> Result<ModelScanResult, ModelError> {
|
|
480
528
|
let request = self.prepare_request()?;
|
|
481
|
-
self.
|
|
529
|
+
self.scan_model_request(&request, text, cancellation)
|
|
482
530
|
}
|
|
483
531
|
|
|
484
|
-
pub fn
|
|
532
|
+
pub fn scan_model_request(
|
|
485
533
|
&self,
|
|
486
534
|
request: &ModelRequest,
|
|
487
535
|
text: &str,
|
|
@@ -490,25 +538,188 @@ impl ModelSession {
|
|
|
490
538
|
let deadline = self.request_deadline(request)?;
|
|
491
539
|
self.check(text, deadline, cancellation)?;
|
|
492
540
|
let mut inner = self.lock()?;
|
|
493
|
-
self.scan_locked(&mut inner, text, deadline, cancellation)
|
|
541
|
+
self.scan_locked(&mut inner, text, None, deadline, cancellation)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/// Validate a routed scan configuration. It accepts the rule scan fields
|
|
545
|
+
/// (`entities`, `locale`, `detect_uuid`, `format`); `entities` may also
|
|
546
|
+
/// name this session's model labels. Without `entities`, every enabled
|
|
547
|
+
/// rule detector runs and the model returns the labels rules do not cover.
|
|
548
|
+
pub fn routed_config(&self, value: &serde_json::Value) -> Result<RoutedScanConfig, ModelError> {
|
|
549
|
+
let invalid = |reason, path: &str, message: &str| {
|
|
550
|
+
ModelError::from(PrivacyError::invalid_configuration(reason, path, message))
|
|
551
|
+
};
|
|
552
|
+
let object = value.as_object().ok_or_else(|| {
|
|
553
|
+
invalid(
|
|
554
|
+
PrivacyErrorReason::InvalidType,
|
|
555
|
+
"",
|
|
556
|
+
"scan configuration must be an object",
|
|
557
|
+
)
|
|
558
|
+
})?;
|
|
559
|
+
let model_labels: BTreeSet<&str> = self
|
|
560
|
+
.descriptor
|
|
561
|
+
.labels
|
|
562
|
+
.values()
|
|
563
|
+
.map(String::as_str)
|
|
564
|
+
.collect();
|
|
565
|
+
let mut rule_object = object.clone();
|
|
566
|
+
let (rule_entities, model_entities) = match object.get("entities") {
|
|
567
|
+
None => (None, self.model_only_labels()),
|
|
568
|
+
Some(entities) => {
|
|
569
|
+
let entities = entities.as_array().ok_or_else(|| {
|
|
570
|
+
invalid(
|
|
571
|
+
PrivacyErrorReason::InvalidType,
|
|
572
|
+
"/entities",
|
|
573
|
+
"entities must be an array",
|
|
574
|
+
)
|
|
575
|
+
})?;
|
|
576
|
+
if entities.is_empty() {
|
|
577
|
+
return Err(invalid(
|
|
578
|
+
PrivacyErrorReason::EmptyValue,
|
|
579
|
+
"/entities",
|
|
580
|
+
"entities must contain at least one entity type",
|
|
581
|
+
));
|
|
582
|
+
}
|
|
583
|
+
let mut seen = BTreeSet::new();
|
|
584
|
+
let mut rules = Vec::new();
|
|
585
|
+
let mut model = BTreeSet::new();
|
|
586
|
+
for (index, entity) in entities.iter().enumerate() {
|
|
587
|
+
let path = format!("/entities/{index}");
|
|
588
|
+
let entity = entity.as_str().ok_or_else(|| {
|
|
589
|
+
invalid(
|
|
590
|
+
PrivacyErrorReason::InvalidType,
|
|
591
|
+
&path,
|
|
592
|
+
"entity type must be a string",
|
|
593
|
+
)
|
|
594
|
+
})?;
|
|
595
|
+
if !seen.insert(entity) {
|
|
596
|
+
return Err(invalid(
|
|
597
|
+
PrivacyErrorReason::DuplicateValue,
|
|
598
|
+
&path,
|
|
599
|
+
"entity selection contains a duplicate entity type",
|
|
600
|
+
));
|
|
601
|
+
}
|
|
602
|
+
if crate::capabilities::text_entity_supported(entity) {
|
|
603
|
+
rules.push(serde_json::Value::String(entity.to_owned()));
|
|
604
|
+
} else if model_labels.contains(entity) {
|
|
605
|
+
model.insert(entity.to_owned());
|
|
606
|
+
} else {
|
|
607
|
+
return Err(invalid(
|
|
608
|
+
PrivacyErrorReason::InvalidValue,
|
|
609
|
+
&path,
|
|
610
|
+
"entity type is not supported by rules or this model",
|
|
611
|
+
));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
(Some(rules), model)
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
// Rule-routed entities were validated above, so remaining errors name
|
|
618
|
+
// the shared locale, UUID and format fields at their own paths.
|
|
619
|
+
let run_rules = match rule_entities {
|
|
620
|
+
None => true,
|
|
621
|
+
Some(rules) if rules.is_empty() => {
|
|
622
|
+
rule_object.remove("entities");
|
|
623
|
+
false
|
|
624
|
+
}
|
|
625
|
+
Some(rules) => {
|
|
626
|
+
rule_object.insert("entities".into(), serde_json::Value::Array(rules));
|
|
627
|
+
true
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
let rules = crate::parse_scan_config(&serde_json::Value::Object(rule_object))?;
|
|
631
|
+
Ok(RoutedScanConfig {
|
|
632
|
+
rules: run_rules.then_some(rules),
|
|
633
|
+
model_entities,
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/// The routing used without a configuration: all enabled rule detectors
|
|
638
|
+
/// and the model labels that rules do not produce.
|
|
639
|
+
pub fn default_routed_config(&self) -> RoutedScanConfig {
|
|
640
|
+
RoutedScanConfig {
|
|
641
|
+
rules: Some(ScanConfig::new()),
|
|
642
|
+
model_entities: self.model_only_labels(),
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
fn model_only_labels(&self) -> BTreeSet<String> {
|
|
647
|
+
self.descriptor
|
|
648
|
+
.labels
|
|
649
|
+
.values()
|
|
650
|
+
.filter(|label| !crate::capabilities::text_entity_supported(label))
|
|
651
|
+
.cloned()
|
|
652
|
+
.collect()
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/// Scan with entity routing. A configuration that needs no model labels
|
|
656
|
+
/// runs only rule detectors, without reserving or waiting for the worker.
|
|
657
|
+
pub fn scan_routed(
|
|
658
|
+
&self,
|
|
659
|
+
text: &str,
|
|
660
|
+
config: &RoutedScanConfig,
|
|
661
|
+
cancellation: &Cancellation,
|
|
662
|
+
) -> Result<ModelScanResult, ModelError> {
|
|
663
|
+
let request = if config.needs_model() {
|
|
664
|
+
Some(self.prepare_request()?)
|
|
665
|
+
} else {
|
|
666
|
+
None
|
|
667
|
+
};
|
|
668
|
+
self.scan_routed_request(request.as_ref(), text, config, cancellation)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/// Bindings reserve the session before dispatch only when the routed
|
|
672
|
+
/// configuration needs the model; otherwise `request` is `None`.
|
|
673
|
+
pub fn scan_routed_request(
|
|
674
|
+
&self,
|
|
675
|
+
request: Option<&ModelRequest>,
|
|
676
|
+
text: &str,
|
|
677
|
+
config: &RoutedScanConfig,
|
|
678
|
+
cancellation: &Cancellation,
|
|
679
|
+
) -> Result<ModelScanResult, ModelError> {
|
|
680
|
+
if !config.needs_model() {
|
|
681
|
+
// Lifecycle, input limits and cancellation apply without the worker.
|
|
682
|
+
let deadline = deadline(self.descriptor.limits.timeout_ms)?;
|
|
683
|
+
self.check(text, deadline, cancellation)?;
|
|
684
|
+
if self.state.load(Ordering::Acquire) == 2 {
|
|
685
|
+
return Err(ModelError::new(
|
|
686
|
+
"model_session_failed",
|
|
687
|
+
"failed",
|
|
688
|
+
"/backend",
|
|
689
|
+
));
|
|
690
|
+
}
|
|
691
|
+
let result = self.compose(rule_entries(text, config))?;
|
|
692
|
+
bounded_json(&result, self.descriptor.limits.max_response_bytes, "/data")?;
|
|
693
|
+
return Ok(result);
|
|
694
|
+
}
|
|
695
|
+
let request = request.ok_or_else(|| ModelError::config("invalid_value", "/request"))?;
|
|
696
|
+
let deadline = self.request_deadline(request)?;
|
|
697
|
+
self.check(text, deadline, cancellation)?;
|
|
698
|
+
let mut inner = self.lock()?;
|
|
699
|
+
self.scan_locked(&mut inner, text, Some(config), deadline, cancellation)
|
|
494
700
|
}
|
|
495
701
|
|
|
496
702
|
fn scan_locked(
|
|
497
703
|
&self,
|
|
498
704
|
inner: &mut SessionInner,
|
|
499
705
|
text: &str,
|
|
706
|
+
routing: Option<&RoutedScanConfig>,
|
|
500
707
|
deadline: Instant,
|
|
501
708
|
cancellation: &Cancellation,
|
|
502
709
|
) -> Result<ModelScanResult, ModelError> {
|
|
503
710
|
let native = self.native(inner, text, deadline, cancellation)?;
|
|
504
711
|
let converted = convert(text, native, &self.descriptor);
|
|
505
|
-
let entries = match converted {
|
|
712
|
+
let mut entries = match converted {
|
|
506
713
|
Ok(entries) => entries,
|
|
507
714
|
Err(error) => {
|
|
508
715
|
self.retire(inner)?;
|
|
509
716
|
return Err(error);
|
|
510
717
|
}
|
|
511
718
|
};
|
|
719
|
+
if let Some(config) = routing {
|
|
720
|
+
entries.retain(|(finding, _)| config.model_entities.contains(&finding.entity_type));
|
|
721
|
+
entries.splice(0..0, rule_entries(text, config));
|
|
722
|
+
}
|
|
512
723
|
self.active_check(inner, text, deadline, cancellation)?;
|
|
513
724
|
let result = self.compose(entries)?;
|
|
514
725
|
bounded_json(&result, self.descriptor.limits.max_response_bytes, "/data")?;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#![cfg(all(feature = "models", not(target_arch = "wasm32")))]
|
|
2
|
+
use datafog_core::models::{Cancellation, ModelSession, ModelSessionConfig};
|
|
3
|
+
|
|
4
|
+
/// Explicit real-model integration. Missing config or assets fail the test.
|
|
5
|
+
#[test]
|
|
6
|
+
#[ignore = "requires explicitly provisioned local model"]
|
|
7
|
+
fn pinned_model_execution() {
|
|
8
|
+
let config = std::env::var("DATAFOG_MODEL_CONFIG")
|
|
9
|
+
.expect("DATAFOG_MODEL_CONFIG must name a verified local session config");
|
|
10
|
+
let config: ModelSessionConfig =
|
|
11
|
+
serde_json::from_reader(std::fs::File::open(config).unwrap()).unwrap();
|
|
12
|
+
let session = ModelSession::open(config).unwrap();
|
|
13
|
+
let text = "👋 John Smith lives at 123 Main Street. Email john@example.com.";
|
|
14
|
+
let result = session.scan_model(text, &Cancellation::default()).unwrap();
|
|
15
|
+
assert!(!result.findings.is_empty());
|
|
16
|
+
assert_eq!(result.contract_version, 1);
|
|
17
|
+
assert_eq!(result.evidence.len(), result.findings.len());
|
|
18
|
+
for finding in &result.findings {
|
|
19
|
+
assert_eq!(
|
|
20
|
+
&text[finding.byte_range.start..finding.byte_range.end],
|
|
21
|
+
finding.matched_text
|
|
22
|
+
);
|
|
23
|
+
assert_eq!(
|
|
24
|
+
text.chars()
|
|
25
|
+
.skip(finding.codepoint_range.start)
|
|
26
|
+
.take(finding.codepoint_range.end - finding.codepoint_range.start)
|
|
27
|
+
.collect::<String>(),
|
|
28
|
+
finding.matched_text
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
assert!(
|
|
32
|
+
session
|
|
33
|
+
.scan("", &Cancellation::default())
|
|
34
|
+
.unwrap()
|
|
35
|
+
.findings
|
|
36
|
+
.is_empty()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Routing: rule entities come from rule detectors; model entities are the
|
|
40
|
+
// plain model scan's findings for those types, and nothing else is returned.
|
|
41
|
+
let text =
|
|
42
|
+
"John Smith lives at 123 Main Street. Card 4111 1111 1111 1111, email john@example.com.";
|
|
43
|
+
let rules_only = session
|
|
44
|
+
.routed_config(&serde_json::json!({"entities": ["CREDIT_CARD", "EMAIL"]}))
|
|
45
|
+
.unwrap();
|
|
46
|
+
assert!(!rules_only.needs_model());
|
|
47
|
+
let routed = session
|
|
48
|
+
.scan_routed(text, &rules_only, &Cancellation::default())
|
|
49
|
+
.unwrap();
|
|
50
|
+
let rule_findings = datafog_core::scan_with_config(
|
|
51
|
+
text,
|
|
52
|
+
&datafog_core::ScanConfig::new()
|
|
53
|
+
.with_entities(vec!["CREDIT_CARD".into(), "EMAIL".into()])
|
|
54
|
+
.unwrap(),
|
|
55
|
+
);
|
|
56
|
+
assert_eq!(routed.findings, rule_findings);
|
|
57
|
+
let mixed = session
|
|
58
|
+
.routed_config(&serde_json::json!({"entities": ["STREET_ADDRESS", "EMAIL"]}))
|
|
59
|
+
.unwrap();
|
|
60
|
+
assert!(mixed.needs_model());
|
|
61
|
+
let routed = session
|
|
62
|
+
.scan_routed(text, &mixed, &Cancellation::default())
|
|
63
|
+
.unwrap();
|
|
64
|
+
let model_addresses: Vec<_> = session
|
|
65
|
+
.scan_model(text, &Cancellation::default())
|
|
66
|
+
.unwrap()
|
|
67
|
+
.findings
|
|
68
|
+
.into_iter()
|
|
69
|
+
.filter(|finding| finding.entity_type == "STREET_ADDRESS")
|
|
70
|
+
.collect();
|
|
71
|
+
let routed_addresses: Vec<_> = routed
|
|
72
|
+
.findings
|
|
73
|
+
.iter()
|
|
74
|
+
.filter(|finding| finding.entity_type == "STREET_ADDRESS")
|
|
75
|
+
.cloned()
|
|
76
|
+
.collect();
|
|
77
|
+
assert_eq!(routed_addresses, model_addresses);
|
|
78
|
+
let emails: Vec<_> = routed
|
|
79
|
+
.findings
|
|
80
|
+
.iter()
|
|
81
|
+
.filter(|finding| finding.entity_type == "EMAIL")
|
|
82
|
+
.collect();
|
|
83
|
+
assert_eq!(emails.len(), 1);
|
|
84
|
+
assert_eq!(emails[0].detector_name, "datafog-core/email");
|
|
85
|
+
assert!(
|
|
86
|
+
routed
|
|
87
|
+
.findings
|
|
88
|
+
.iter()
|
|
89
|
+
.all(|finding| matches!(finding.entity_type.as_str(), "STREET_ADDRESS" | "EMAIL"))
|
|
90
|
+
);
|
|
91
|
+
assert_eq!(routed.evidence.len(), routed.findings.len());
|
|
92
|
+
|
|
93
|
+
// Without a configuration, scan routes like `{}`: rule findings as from
|
|
94
|
+
// the rule scanner, plus the model's labels that rules do not produce.
|
|
95
|
+
let default = session.scan(text, &Cancellation::default()).unwrap();
|
|
96
|
+
let empty = session
|
|
97
|
+
.scan_routed(
|
|
98
|
+
text,
|
|
99
|
+
&session.routed_config(&serde_json::json!({})).unwrap(),
|
|
100
|
+
&Cancellation::default(),
|
|
101
|
+
)
|
|
102
|
+
.unwrap();
|
|
103
|
+
assert_eq!(default.findings, empty.findings);
|
|
104
|
+
let rules: Vec<_> = default
|
|
105
|
+
.findings
|
|
106
|
+
.iter()
|
|
107
|
+
.filter(|finding| !finding.detector_name.starts_with("datafog-core/model/"))
|
|
108
|
+
.cloned()
|
|
109
|
+
.collect();
|
|
110
|
+
assert_eq!(rules, datafog_core::scan(text));
|
|
111
|
+
|
|
112
|
+
session.close().unwrap();
|
|
113
|
+
session.close().unwrap();
|
|
114
|
+
assert_eq!(
|
|
115
|
+
session.scan("", &Cancellation::default()).unwrap_err().code,
|
|
116
|
+
"model_session_closed"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -63,7 +63,8 @@ class ModelCancellation:
|
|
|
63
63
|
class ModelSession:
|
|
64
64
|
@staticmethod
|
|
65
65
|
def open(config: _ModelConfig) -> ModelSession: ...
|
|
66
|
-
def scan(self, text: str, *, cancellation: ModelCancellation | None = None) -> ModelScanResult: ...
|
|
66
|
+
def scan(self, text: str, config: _ScanConfig | None = None, *, cancellation: ModelCancellation | None = None) -> ModelScanResult: ...
|
|
67
|
+
def scan_model(self, text: str, *, cancellation: ModelCancellation | None = None) -> ModelScanResult: ...
|
|
67
68
|
def close(self) -> None: ...
|
|
68
69
|
def __enter__(self) -> ModelSession: ...
|
|
69
70
|
def __exit__(self, kind: object, value: object, traceback: object) -> bool: ...
|
|
@@ -11,7 +11,7 @@ import platform
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
import sys
|
|
13
13
|
|
|
14
|
-
__version__ = "0.4.
|
|
14
|
+
__version__ = "0.4.5b3"
|
|
15
15
|
RUNTIME = {"gliner2": "2.1.0", "transformers": "5.8.0", "torch": "2.12.0"}
|
|
16
16
|
if sys.platform == "darwin" and platform.machine() == "x86_64":
|
|
17
17
|
RUNTIME.update(transformers="4.57.6", torch="2.2.2")
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#![cfg(all(feature = "models", not(target_arch = "wasm32")))]
|
|
2
|
-
use datafog_core::models::{Cancellation, ModelSession, ModelSessionConfig};
|
|
3
|
-
|
|
4
|
-
/// Explicit real-model integration. Missing config or assets fail the test.
|
|
5
|
-
#[test]
|
|
6
|
-
#[ignore = "requires explicitly provisioned local model"]
|
|
7
|
-
fn pinned_model_execution() {
|
|
8
|
-
let config = std::env::var("DATAFOG_MODEL_CONFIG")
|
|
9
|
-
.expect("DATAFOG_MODEL_CONFIG must name a verified local session config");
|
|
10
|
-
let config: ModelSessionConfig =
|
|
11
|
-
serde_json::from_reader(std::fs::File::open(config).unwrap()).unwrap();
|
|
12
|
-
let session = ModelSession::open(config).unwrap();
|
|
13
|
-
let text = "👋 John Smith lives at 123 Main Street. Email john@example.com.";
|
|
14
|
-
let result = session.scan(text, &Cancellation::default()).unwrap();
|
|
15
|
-
assert!(!result.findings.is_empty());
|
|
16
|
-
assert_eq!(result.contract_version, 1);
|
|
17
|
-
assert_eq!(result.evidence.len(), result.findings.len());
|
|
18
|
-
for finding in &result.findings {
|
|
19
|
-
assert_eq!(
|
|
20
|
-
&text[finding.byte_range.start..finding.byte_range.end],
|
|
21
|
-
finding.matched_text
|
|
22
|
-
);
|
|
23
|
-
assert_eq!(
|
|
24
|
-
text.chars()
|
|
25
|
-
.skip(finding.codepoint_range.start)
|
|
26
|
-
.take(finding.codepoint_range.end - finding.codepoint_range.start)
|
|
27
|
-
.collect::<String>(),
|
|
28
|
-
finding.matched_text
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
assert!(
|
|
32
|
-
session
|
|
33
|
-
.scan("", &Cancellation::default())
|
|
34
|
-
.unwrap()
|
|
35
|
-
.findings
|
|
36
|
-
.is_empty()
|
|
37
|
-
);
|
|
38
|
-
session.close().unwrap();
|
|
39
|
-
session.close().unwrap();
|
|
40
|
-
assert_eq!(
|
|
41
|
-
session.scan("", &Cancellation::default()).unwrap_err().code,
|
|
42
|
-
"model_session_closed"
|
|
43
|
-
);
|
|
44
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/bearer_token_conformance.py
RENAMED
|
File without changes
|
{datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/capabilities_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
{datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/credential_uri_conformance.py
RENAMED
|
File without changes
|
{datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/email_context_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/private_key_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/test_python49_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
{datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/us_routing_number_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/support/synthetic_fixtures.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|