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.
Files changed (69) hide show
  1. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/Cargo.lock +4 -4
  2. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/PKG-INFO +6 -6
  3. {datafog_core-0.4.5b2/crates/core → datafog_core-0.4.5b3}/README.md +5 -5
  4. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/Cargo.toml +2 -2
  5. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/src/models.rs +48 -4
  6. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/Cargo.toml +1 -1
  7. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3/crates/core}/README.md +5 -5
  8. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models/gliner2.rs +3 -3
  9. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models/tests.rs +201 -3
  10. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models.rs +217 -6
  11. datafog_core-0.4.5b3/crates/core/tests/model_datafog.rs +118 -0
  12. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/pyproject.toml +1 -1
  13. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/python/datafog_core/__init__.pyi +2 -1
  14. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/python/datafog_core/_gliner2_worker.py +1 -1
  15. datafog_core-0.4.5b2/crates/core/tests/model_datafog.rs +0 -44
  16. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/Cargo.toml +0 -0
  17. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/src/lib.rs +0 -0
  18. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/api_key_conformance.py +0 -0
  19. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/bearer_token_conformance.py +0 -0
  20. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/capabilities_conformance.py +0 -0
  21. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/credential_overlap_conformance.py +0 -0
  22. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/credential_uri_conformance.py +0 -0
  23. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/email_context_conformance.py +0 -0
  24. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/german_conformance.py +0 -0
  25. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/jwt_conformance.py +0 -0
  26. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/npi_conformance.py +0 -0
  27. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/private_key_conformance.py +0 -0
  28. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/requirements-typing.txt +0 -0
  29. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/synthetic_fixtures.py +0 -0
  30. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/test_installed.py +0 -0
  31. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/test_python49_integration.py +0 -0
  32. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/test_typing.py +0 -0
  33. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/us_routing_number_conformance.py +0 -0
  34. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/bindings/python/tests/uuid_conformance.py +0 -0
  35. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/examples/german_benchmark.rs +0 -0
  36. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/examples/scan_benchmark.rs +0 -0
  37. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/api_key.rs +0 -0
  38. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/bearer_token.rs +0 -0
  39. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/capabilities.rs +0 -0
  40. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/credential_uri.rs +0 -0
  41. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/email_context.rs +0 -0
  42. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/german.rs +0 -0
  43. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/jwt.rs +0 -0
  44. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/lib.rs +0 -0
  45. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models/gliner2-lock.json +0 -0
  46. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/models/process.rs +0 -0
  47. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/npi.rs +0 -0
  48. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/offsets.rs +0 -0
  49. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/private_key.rs +0 -0
  50. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/scan_selection_tests.rs +0 -0
  51. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/selection_tests.rs +0 -0
  52. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/structured.rs +0 -0
  53. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/us_routing_number.rs +0 -0
  54. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/src/uuid.rs +0 -0
  55. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/api_key.rs +0 -0
  56. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/bearer_token.rs +0 -0
  57. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/capabilities.rs +0 -0
  58. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/credential_overlaps.rs +0 -0
  59. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/credential_uri.rs +0 -0
  60. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/email_context.rs +0 -0
  61. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/german.rs +0 -0
  62. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/jwt.rs +0 -0
  63. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/npi.rs +0 -0
  64. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/private_key.rs +0 -0
  65. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/support/synthetic_fixtures.rs +0 -0
  66. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/us_routing_number.rs +0 -0
  67. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/crates/core/tests/uuid.rs +0 -0
  68. {datafog_core-0.4.5b2 → datafog_core-0.4.5b3}/python/datafog_core/__init__.py +0 -0
  69. {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.2"
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.2"
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.2"
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.2"
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.5b2
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.1](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.1)
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.1](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.1) adds local sessions for the DataFog PII model and GLiNER2 in Rust, Python, and Node.js on all five native platforms.
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.5b1'
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.1 --features models
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.5b1'
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.1](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.1)
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.1](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.1) adds local sessions for the DataFog PII model and GLiNER2 in Rust, Python, and Node.js on all five native platforms.
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.5b1'
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.1 --features models
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.5b1'
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.2"
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.2", path = "../../crates/core", features = ["models"] }
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
- #[pyo3(signature = (text, *, cancellation=None))]
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(|| self.session.scan_request(&request, &text, &cancellation))
109
- .map(ModelScanResult::from)
110
- .map_err(|error| model_error(py, error))
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())
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "datafog-core"
3
- version = "0.4.5-beta.2"
3
+ version = "0.4.5-beta.3"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  description = "Fast structured PII detection library"
@@ -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.1](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.1)
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.1](https://github.com/DataFog/datafog-core/releases/tag/v0.4.5-beta.1) adds local sessions for the DataFog PII model and GLiNER2 in Rust, Python, and Node.js on all five native platforms.
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.5b1'
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.1 --features models
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.5b1'
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.5b2"
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.5b2","runtime":runtime(),
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.5b2","runtime":{},"findings":[]}).to_string();
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
- .scan_request(&request, "secret", &Cancellation::default())
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
- .scan_request(&request, "secret", &cancellation)
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
- .scan_request(&next, "", &Cancellation::default())
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.scan_request(&request, text, cancellation)
529
+ self.scan_model_request(&request, text, cancellation)
482
530
  }
483
531
 
484
- pub fn scan_request(
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
+ }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "datafog-core"
7
- version = "0.4.5b2"
7
+ version = "0.4.5b3"
8
8
  description = "Fast structured PII detection implemented in Rust"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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.5b2"
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
- }