datafog 4.8.1__tar.gz → 4.9.0__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-4.8.1 → datafog-4.9.0}/PKG-INFO +35 -4
- {datafog-4.8.1 → datafog-4.9.0}/README.md +28 -3
- datafog-4.9.0/datafog/__about__.py +1 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/__init__.py +28 -65
- datafog-4.9.0/datafog/_core_capabilities.py +141 -0
- datafog-4.9.0/datafog/_legacy_retirement.py +17 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/client.py +5 -0
- datafog-4.9.0/datafog/compat/__init__.py +1 -0
- datafog-4.9.0/datafog/compat/v4.py +105 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/engine.py +68 -2
- {datafog-4.8.1 → datafog-4.9.0}/datafog/main.py +3 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/image_processing/donut_processor.py +7 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/image_processing/image_downloader.py +4 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/image_processing/pytesseract_processor.py +3 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/spark_processing/pyspark_udfs.py +4 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/services/image_service.py +10 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/services/spark_service.py +3 -0
- datafog-4.9.0/datafog/v5.py +84 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog.egg-info/PKG-INFO +35 -4
- {datafog-4.8.1 → datafog-4.9.0}/datafog.egg-info/SOURCES.txt +12 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog.egg-info/requires.txt +7 -2
- {datafog-4.8.1 → datafog-4.9.0}/setup.py +4 -1
- datafog-4.9.0/tests/test_api_bridge_49.py +141 -0
- datafog-4.9.0/tests/test_contract_481.py +56 -0
- datafog-4.9.0/tests/test_core04_integration.py +215 -0
- datafog-4.9.0/tests/test_core_capability_adapter.py +386 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_install_profiles.py +5 -0
- datafog-4.9.0/tests/test_legacy_retirement.py +268 -0
- datafog-4.9.0/tests/test_rust_backend.py +215 -0
- datafog-4.9.0/tests/test_rust_contract.py +33 -0
- datafog-4.8.1/datafog/__about__.py +0 -1
- {datafog-4.8.1 → datafog-4.9.0}/LICENSE +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/__init___lean.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/__init___original.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/agent.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/config.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/core.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/exceptions.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/integrations/__init__.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/integrations/claude_code.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/integrations/litellm_guardrail.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/main_lean.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/main_original.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/models/__init__.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/models/annotator.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/models/anonymizer.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/models/common.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/models/spacy_nlp.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/__init__.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/image_processing/__init__.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/spark_processing/__init__.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/text_processing/__init__.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/text_processing/gliner_annotator.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/text_processing/regex_annotator/__init__.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/text_processing/regex_annotator/regex_annotator.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/processing/text_processing/spacy_pii_annotator.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/py.typed +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/services/__init__.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/services/text_service.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/services/text_service_lean.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/services/text_service_original.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog/telemetry.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog.egg-info/dependency_links.txt +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog.egg-info/entry_points.txt +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/datafog.egg-info/top_level.txt +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/setup.cfg +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_agent_api.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_allowlist.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_anonymizer.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_claude_code_hook.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_cli_smoke.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_client.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_de_pii_regex.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_detection_accuracy.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_donut_lazy_import.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_engine_api.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_gliner_annotator.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_image_service.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_litellm_guardrail.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_main.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_no_network_core.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_ocr_integration.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_regex_annotator.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_runtime_dependency_safety.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_spark_integration.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_telemetry.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_text_service.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_text_service_integration.py +0 -0
- {datafog-4.8.1 → datafog-4.9.0}/tests/test_v44_bridge_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datafog
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.9.0
|
|
4
4
|
Summary: Lightning-fast PII detection and anonymization library, 100x+ faster than NER-based detection (see benchmarks/)
|
|
5
5
|
Author: Sid Mohan
|
|
6
6
|
Author-email: sid@datafog.ai
|
|
@@ -28,6 +28,8 @@ License-File: LICENSE
|
|
|
28
28
|
Requires-Dist: pydantic<3.0,>=2.0
|
|
29
29
|
Requires-Dist: pydantic-settings>=2.0.0
|
|
30
30
|
Requires-Dist: typing-extensions>=4.0
|
|
31
|
+
Provides-Extra: rust
|
|
32
|
+
Requires-Dist: datafog-core<0.5,>=0.4.0; extra == "rust"
|
|
31
33
|
Provides-Extra: nlp
|
|
32
34
|
Requires-Dist: click<9.0,>=8.0; extra == "nlp"
|
|
33
35
|
Requires-Dist: spacy<4.0,>=3.7.0; extra == "nlp"
|
|
@@ -36,6 +38,8 @@ Requires-Dist: gliner>=0.2.5; extra == "nlp-advanced"
|
|
|
36
38
|
Requires-Dist: torch<2.7,>=2.1.0; extra == "nlp-advanced"
|
|
37
39
|
Requires-Dist: transformers>=4.20.0; extra == "nlp-advanced"
|
|
38
40
|
Requires-Dist: huggingface-hub>=0.16.0; extra == "nlp-advanced"
|
|
41
|
+
Requires-Dist: sentencepiece>=0.2.0; extra == "nlp-advanced"
|
|
42
|
+
Requires-Dist: protobuf>=4.0.0; extra == "nlp-advanced"
|
|
39
43
|
Provides-Extra: ocr
|
|
40
44
|
Requires-Dist: numpy>=1.24.0; extra == "ocr"
|
|
41
45
|
Requires-Dist: pytesseract>=0.3.0; extra == "ocr"
|
|
@@ -77,6 +81,8 @@ Requires-Dist: gliner>=0.2.5; extra == "all"
|
|
|
77
81
|
Requires-Dist: torch<2.7,>=2.1.0; extra == "all"
|
|
78
82
|
Requires-Dist: transformers>=4.20.0; extra == "all"
|
|
79
83
|
Requires-Dist: huggingface-hub>=0.16.0; extra == "all"
|
|
84
|
+
Requires-Dist: sentencepiece>=0.2.0; extra == "all"
|
|
85
|
+
Requires-Dist: protobuf>=4.0.0; extra == "all"
|
|
80
86
|
Requires-Dist: numpy>=1.24.0; extra == "all"
|
|
81
87
|
Requires-Dist: pytesseract>=0.3.0; extra == "all"
|
|
82
88
|
Requires-Dist: Pillow>=12.2.0; extra == "all"
|
|
@@ -178,6 +184,9 @@ pip install datafog[distributed]
|
|
|
178
184
|
pip install datafog[all]
|
|
179
185
|
```
|
|
180
186
|
|
|
187
|
+
The `nlp-advanced` extra includes SentencePiece and protobuf for GLiNER's
|
|
188
|
+
multilingual tokenizer; installing the OCR extra is not required for GLiNER.
|
|
189
|
+
|
|
181
190
|
Python 3.13 support is certified for the core SDK, CLI, `nlp`,
|
|
182
191
|
`nlp-advanced`, and `ocr` install profiles. Donut OCR still requires a model
|
|
183
192
|
that is available locally before runtime use. `distributed` and `all` remain
|
|
@@ -293,9 +302,10 @@ scan/redact helpers, or guardrail helpers.
|
|
|
293
302
|
model.
|
|
294
303
|
- A Java runtime is required by PySpark.
|
|
295
304
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
the
|
|
305
|
+
DataFog 4.9.0 deprecates OCR and Spark with visible use-time
|
|
306
|
+
warnings; their APIs and extras will be removed in 5.0. They remain functional
|
|
307
|
+
in 4.9. Users who need these features can stay on the final 4.x release. See
|
|
308
|
+
the [4.9 migration guide](docs/migration-4.9.md) for the transition plan.
|
|
299
309
|
|
|
300
310
|
## Backward-Compatible APIs
|
|
301
311
|
|
|
@@ -361,6 +371,27 @@ Telemetry does not include input text or detected PII values.
|
|
|
361
371
|
|
|
362
372
|
## Development
|
|
363
373
|
|
|
374
|
+
The [4.9 migration guide](docs/migration-4.9.md) explains opt-in Rust detection,
|
|
375
|
+
the native `datafog.v5` preview, and the revised 5.0 retirement schedule for
|
|
376
|
+
`detect`/`process`, OCR, and Spark. The Python detector remains the default.
|
|
377
|
+
|
|
378
|
+
The experimental Rust adapter in 4.9.0 requires Core `>=0.4.0,<0.5` and capability
|
|
379
|
+
contract 1. Upgrade with `python -m pip install --upgrade "datafog[rust]==4.9.0"`
|
|
380
|
+
to evaluate it. Base-only users can install `datafog==4.9.0` without Core;
|
|
381
|
+
installing the Rust extra does not change the default backend. See the
|
|
382
|
+
[4.9.0 release notes](RELEASE_NOTES_4.9.0.md).
|
|
383
|
+
Entity labels, locales, and activation settings come from the installed
|
|
384
|
+
Core, allowing compatible releases to add detectors without a Python update.
|
|
385
|
+
German detection is opt-in through locale or entity selection; UUID is opt-in
|
|
386
|
+
through `entity_types=["UUID"]`. Core's structured-only `PERSON` is unavailable
|
|
387
|
+
for explicit Rust text selection. The legacy overlap policy can suppress NPI in
|
|
388
|
+
favor of PHONE; the migration guide explains native alternatives. Lock the Core
|
|
389
|
+
version if detection output must remain reproducible.
|
|
390
|
+
|
|
391
|
+
The [4.8.1 compatibility contract](docs/migration-4.8.1-contract.md) records
|
|
392
|
+
published Python behavior for the Rust migration, with frozen fixtures and
|
|
393
|
+
instructions for independently reproducing them from the release wheel.
|
|
394
|
+
|
|
364
395
|
```bash
|
|
365
396
|
git clone https://github.com/datafog/datafog-python
|
|
366
397
|
cd datafog-python
|
|
@@ -70,6 +70,9 @@ pip install datafog[distributed]
|
|
|
70
70
|
pip install datafog[all]
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
The `nlp-advanced` extra includes SentencePiece and protobuf for GLiNER's
|
|
74
|
+
multilingual tokenizer; installing the OCR extra is not required for GLiNER.
|
|
75
|
+
|
|
73
76
|
Python 3.13 support is certified for the core SDK, CLI, `nlp`,
|
|
74
77
|
`nlp-advanced`, and `ocr` install profiles. Donut OCR still requires a model
|
|
75
78
|
that is available locally before runtime use. `distributed` and `all` remain
|
|
@@ -185,9 +188,10 @@ scan/redact helpers, or guardrail helpers.
|
|
|
185
188
|
model.
|
|
186
189
|
- A Java runtime is required by PySpark.
|
|
187
190
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
the
|
|
191
|
+
DataFog 4.9.0 deprecates OCR and Spark with visible use-time
|
|
192
|
+
warnings; their APIs and extras will be removed in 5.0. They remain functional
|
|
193
|
+
in 4.9. Users who need these features can stay on the final 4.x release. See
|
|
194
|
+
the [4.9 migration guide](docs/migration-4.9.md) for the transition plan.
|
|
191
195
|
|
|
192
196
|
## Backward-Compatible APIs
|
|
193
197
|
|
|
@@ -253,6 +257,27 @@ Telemetry does not include input text or detected PII values.
|
|
|
253
257
|
|
|
254
258
|
## Development
|
|
255
259
|
|
|
260
|
+
The [4.9 migration guide](docs/migration-4.9.md) explains opt-in Rust detection,
|
|
261
|
+
the native `datafog.v5` preview, and the revised 5.0 retirement schedule for
|
|
262
|
+
`detect`/`process`, OCR, and Spark. The Python detector remains the default.
|
|
263
|
+
|
|
264
|
+
The experimental Rust adapter in 4.9.0 requires Core `>=0.4.0,<0.5` and capability
|
|
265
|
+
contract 1. Upgrade with `python -m pip install --upgrade "datafog[rust]==4.9.0"`
|
|
266
|
+
to evaluate it. Base-only users can install `datafog==4.9.0` without Core;
|
|
267
|
+
installing the Rust extra does not change the default backend. See the
|
|
268
|
+
[4.9.0 release notes](RELEASE_NOTES_4.9.0.md).
|
|
269
|
+
Entity labels, locales, and activation settings come from the installed
|
|
270
|
+
Core, allowing compatible releases to add detectors without a Python update.
|
|
271
|
+
German detection is opt-in through locale or entity selection; UUID is opt-in
|
|
272
|
+
through `entity_types=["UUID"]`. Core's structured-only `PERSON` is unavailable
|
|
273
|
+
for explicit Rust text selection. The legacy overlap policy can suppress NPI in
|
|
274
|
+
favor of PHONE; the migration guide explains native alternatives. Lock the Core
|
|
275
|
+
version if detection output must remain reproducible.
|
|
276
|
+
|
|
277
|
+
The [4.8.1 compatibility contract](docs/migration-4.8.1-contract.md) records
|
|
278
|
+
published Python behavior for the Rust migration, with frozen fixtures and
|
|
279
|
+
instructions for independently reproducing them from the release wheel.
|
|
280
|
+
|
|
256
281
|
```bash
|
|
257
282
|
git clone https://github.com/datafog/datafog-python
|
|
258
283
|
cd datafog-python
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.9.0"
|
|
@@ -15,11 +15,9 @@ from .__about__ import __version__
|
|
|
15
15
|
from .agent import create_guardrail, filter_output, sanitize, scan_prompt
|
|
16
16
|
|
|
17
17
|
# Core API functions - always available (lightweight)
|
|
18
|
+
from .compat import v4 as _compat_v4
|
|
18
19
|
from .core import anonymize_text, detect_pii, get_supported_entities, scan_text
|
|
19
20
|
from .engine import Entity, RedactResult, ScanResult
|
|
20
|
-
from .engine import redact as _redact_entities
|
|
21
|
-
from .engine import scan as _scan
|
|
22
|
-
from .engine import scan_and_redact as _scan_and_redact
|
|
23
21
|
|
|
24
22
|
# Essential models - always available
|
|
25
23
|
from .models.common import EntityTypes
|
|
@@ -130,20 +128,10 @@ _LAZY_EXPORTS = {
|
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
|
|
133
|
-
_REDACT_PRESETS = {
|
|
134
|
-
"default": "token",
|
|
135
|
-
"llm": "token",
|
|
136
|
-
"mask": "mask",
|
|
137
|
-
"hash": "hash",
|
|
138
|
-
"replace": "pseudonymize",
|
|
139
|
-
"pseudonymize": "pseudonymize",
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
131
|
def _warn_v5_replacement(old_api: str, replacement: str) -> None:
|
|
144
132
|
warnings.warn(
|
|
145
|
-
f"datafog.{old_api}() is deprecated
|
|
146
|
-
"
|
|
133
|
+
f"datafog.{old_api}() is deprecated and will be removed in 5.0. Use {replacement} instead. "
|
|
134
|
+
"The earlier promise to retain this shim through 5.x has been revised.",
|
|
147
135
|
FutureWarning,
|
|
148
136
|
stacklevel=3,
|
|
149
137
|
)
|
|
@@ -156,25 +144,18 @@ def scan(
|
|
|
156
144
|
locales: list[str] | None = None,
|
|
157
145
|
allowlist: list[str] | None = None,
|
|
158
146
|
allowlist_patterns: list[str] | None = None,
|
|
147
|
+
*,
|
|
148
|
+
backend: str = "python",
|
|
159
149
|
) -> ScanResult:
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
phone numbers).
|
|
170
|
-
"""
|
|
171
|
-
return _scan(
|
|
172
|
-
text=text,
|
|
173
|
-
engine=engine,
|
|
174
|
-
entity_types=entity_types,
|
|
175
|
-
locales=locales,
|
|
176
|
-
allowlist=allowlist,
|
|
177
|
-
allowlist_patterns=allowlist_patterns,
|
|
150
|
+
"""Scan using the 4.x compatibility API; Rust detection is opt-in."""
|
|
151
|
+
return _compat_v4.scan(
|
|
152
|
+
text,
|
|
153
|
+
engine,
|
|
154
|
+
entity_types,
|
|
155
|
+
locales,
|
|
156
|
+
allowlist,
|
|
157
|
+
allowlist_patterns,
|
|
158
|
+
backend=backend,
|
|
178
159
|
)
|
|
179
160
|
|
|
180
161
|
|
|
@@ -188,39 +169,21 @@ def redact(
|
|
|
188
169
|
locales: list[str] | None = None,
|
|
189
170
|
allowlist: list[str] | None = None,
|
|
190
171
|
allowlist_patterns: list[str] | None = None,
|
|
172
|
+
*,
|
|
173
|
+
backend: str = "python",
|
|
191
174
|
) -> RedactResult:
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
except KeyError as exc:
|
|
205
|
-
allowed = ", ".join(sorted(_REDACT_PRESETS))
|
|
206
|
-
raise ValueError(f"preset must be one of: {allowed}") from exc
|
|
207
|
-
|
|
208
|
-
if entities is not None:
|
|
209
|
-
if allowlist or allowlist_patterns:
|
|
210
|
-
raise ValueError(
|
|
211
|
-
"allowlist/allowlist_patterns cannot be combined with explicit "
|
|
212
|
-
"entities; filter the entities before calling redact"
|
|
213
|
-
)
|
|
214
|
-
return _redact_entities(text=text, entities=entities, strategy=strategy)
|
|
215
|
-
|
|
216
|
-
return _scan_and_redact(
|
|
217
|
-
text=text,
|
|
218
|
-
engine=engine,
|
|
219
|
-
entity_types=entity_types,
|
|
220
|
-
strategy=strategy,
|
|
221
|
-
locales=locales,
|
|
222
|
-
allowlist=allowlist,
|
|
223
|
-
allowlist_patterns=allowlist_patterns,
|
|
175
|
+
"""Redact using the 4.x compatibility API and legacy strategies."""
|
|
176
|
+
return _compat_v4.redact(
|
|
177
|
+
text,
|
|
178
|
+
entities,
|
|
179
|
+
engine,
|
|
180
|
+
entity_types,
|
|
181
|
+
strategy,
|
|
182
|
+
preset,
|
|
183
|
+
locales,
|
|
184
|
+
allowlist,
|
|
185
|
+
allowlist_patterns,
|
|
186
|
+
backend=backend,
|
|
224
187
|
)
|
|
225
188
|
|
|
226
189
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Translate installed Core capabilities into native text scan configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
_ASCII_LOWER = str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _locale_key(value: str) -> str:
|
|
13
|
+
return value.strip(" \t\n\r\v\f").translate(_ASCII_LOWER)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _invalid(detail: str) -> RuntimeError:
|
|
17
|
+
return RuntimeError(f"Incompatible datafog-core capabilities: {detail}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _labels(value: Any, field: str) -> set[str]:
|
|
21
|
+
if not isinstance(value, list) or any(
|
|
22
|
+
not isinstance(label, str) or not label for label in value
|
|
23
|
+
):
|
|
24
|
+
raise _invalid(f"{field} must be a list of entity labels")
|
|
25
|
+
return set(value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@lru_cache(maxsize=4)
|
|
29
|
+
def _read_capabilities(capabilities: Any) -> tuple[set[str], dict, dict]:
|
|
30
|
+
"""Snapshot one installed build; replacing its reader invalidates the cache.
|
|
31
|
+
|
|
32
|
+
The private snapshot never escapes into native scan configuration. Failed
|
|
33
|
+
capability reads or validation are not cached by lru_cache.
|
|
34
|
+
"""
|
|
35
|
+
payload = deepcopy(capabilities())
|
|
36
|
+
if not isinstance(payload, dict):
|
|
37
|
+
raise _invalid("capabilities() must return a dictionary")
|
|
38
|
+
version = payload.get("contract_version")
|
|
39
|
+
if not isinstance(version, int) or isinstance(version, bool) or version != 1:
|
|
40
|
+
raise _invalid(f"unsupported contract_version {version!r}; expected 1")
|
|
41
|
+
supported = _labels(payload.get("supported_entities"), "supported_entities")
|
|
42
|
+
defaults = _labels(payload.get("default_entities"), "default_entities")
|
|
43
|
+
if not defaults <= supported:
|
|
44
|
+
raise _invalid("default_entities contains unsupported labels")
|
|
45
|
+
advertised_locales = payload.get("locales")
|
|
46
|
+
metadata = payload.get("entities")
|
|
47
|
+
if not isinstance(advertised_locales, dict) or not isinstance(metadata, dict):
|
|
48
|
+
raise _invalid("locales and entities must be dictionaries")
|
|
49
|
+
|
|
50
|
+
return supported, _locale_lookup(advertised_locales, supported), metadata
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _locale_lookup(advertised_locales: dict, supported: set[str]) -> dict[str, str]:
|
|
54
|
+
locale_lookup: dict[str, str] = {}
|
|
55
|
+
for locale, details in advertised_locales.items():
|
|
56
|
+
if not isinstance(locale, str) or not _locale_key(locale):
|
|
57
|
+
raise _invalid("locale identifiers must be nonempty strings")
|
|
58
|
+
if (
|
|
59
|
+
not isinstance(details, dict)
|
|
60
|
+
or not _labels(
|
|
61
|
+
details.get("enabled_entities"), f"locales[{locale!r}].enabled_entities"
|
|
62
|
+
)
|
|
63
|
+
<= supported
|
|
64
|
+
):
|
|
65
|
+
raise _invalid(f"invalid locale metadata for {locale!r}")
|
|
66
|
+
locale_lookup[_locale_key(locale)] = locale
|
|
67
|
+
|
|
68
|
+
return locale_lookup
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_locale(value: Any, locale_lookup: dict[str, str]) -> str:
|
|
72
|
+
if not isinstance(value, str) or _locale_key(value) not in locale_lookup:
|
|
73
|
+
raise ValueError(f"Unsupported locale for the Rust backend: {value!r}")
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _requested_locales(locales: Any, lookup: dict[str, str]) -> set[str]:
|
|
78
|
+
if isinstance(locales, str):
|
|
79
|
+
locales = [locales]
|
|
80
|
+
if locales is not None and (
|
|
81
|
+
not isinstance(locales, (list, tuple))
|
|
82
|
+
or any(not isinstance(locale, str) for locale in locales)
|
|
83
|
+
):
|
|
84
|
+
raise ValueError("locales must be a list of locale identifiers")
|
|
85
|
+
return {_resolve_locale(locale, lookup) for locale in locales or []}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _activation_config(label: str, metadata: dict) -> dict:
|
|
89
|
+
details = metadata.get(label)
|
|
90
|
+
if not isinstance(details, dict):
|
|
91
|
+
raise _invalid(f"missing entity metadata for {label!r}")
|
|
92
|
+
scopes = details.get("scopes")
|
|
93
|
+
if not isinstance(scopes, list) or any(
|
|
94
|
+
not isinstance(scope, str) for scope in scopes
|
|
95
|
+
):
|
|
96
|
+
raise _invalid(f"invalid scopes for {label!r}")
|
|
97
|
+
if "text" not in scopes:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Entity {label!r} is not available for Rust text scanning "
|
|
100
|
+
"(structured-only entity)"
|
|
101
|
+
)
|
|
102
|
+
activation = details.get("activation")
|
|
103
|
+
if not isinstance(activation, dict):
|
|
104
|
+
raise _invalid(f"missing activation metadata for {label!r}")
|
|
105
|
+
kind = activation.get("kind")
|
|
106
|
+
if kind == "default":
|
|
107
|
+
return {}
|
|
108
|
+
if kind not in {"locale", "config"}:
|
|
109
|
+
raise _invalid(f"unsupported activation kind {kind!r} for {label!r}")
|
|
110
|
+
config = activation.get("scan_config")
|
|
111
|
+
if not isinstance(config, dict) or not config:
|
|
112
|
+
raise _invalid(f"missing scan_config for {label!r}")
|
|
113
|
+
if kind == "locale" and "locale" not in config:
|
|
114
|
+
raise _invalid(f"missing activation locale for {label!r}")
|
|
115
|
+
return deepcopy(config)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def scan_configs(core: Any, requested: set[str], locales: Any) -> list[dict]:
|
|
119
|
+
"""Build a union of singular-locale scans without duplicating inventories."""
|
|
120
|
+
capabilities = getattr(core, "capabilities", None)
|
|
121
|
+
if not callable(capabilities):
|
|
122
|
+
raise _invalid(
|
|
123
|
+
"contract version 1 is required; install a compatible Core 0.4.x"
|
|
124
|
+
)
|
|
125
|
+
supported, lookup, metadata = _read_capabilities(capabilities)
|
|
126
|
+
selected_locales = _requested_locales(locales, lookup)
|
|
127
|
+
common: dict[str, Any] = {}
|
|
128
|
+
for label in sorted(requested & supported):
|
|
129
|
+
config = _activation_config(label, metadata)
|
|
130
|
+
for key, value in config.items():
|
|
131
|
+
if key == "locale":
|
|
132
|
+
selected_locales.add(_resolve_locale(value, lookup))
|
|
133
|
+
elif key in common and common[key] != value:
|
|
134
|
+
raise _invalid(f"conflicting activation values for {key!r}")
|
|
135
|
+
else:
|
|
136
|
+
common[key] = value
|
|
137
|
+
if not selected_locales:
|
|
138
|
+
return [common]
|
|
139
|
+
return [
|
|
140
|
+
deepcopy(dict(common, locale=locale)) for locale in sorted(selected_locales)
|
|
141
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Dependency-free retirement notices for optional legacy functionality."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LegacySurfaceWarning(FutureWarning):
|
|
7
|
+
"""An optional DataFog surface scheduled for removal in 5.0."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def warn_legacy_surface(surface: str) -> None:
|
|
11
|
+
"""Warn at the caller's public API boundary, before optional imports."""
|
|
12
|
+
warnings.warn(
|
|
13
|
+
f"DataFog {surface} support is deprecated in 4.9 and will be removed in 5.0. "
|
|
14
|
+
"Remain on the final 4.x release if you need continued support.",
|
|
15
|
+
LegacySurfaceWarning,
|
|
16
|
+
stacklevel=3,
|
|
17
|
+
)
|
|
@@ -10,6 +10,8 @@ from typing import List, Optional
|
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
12
|
|
|
13
|
+
from datafog._legacy_retirement import LegacySurfaceWarning, warn_legacy_surface
|
|
14
|
+
|
|
13
15
|
from .config import OperationType, get_config
|
|
14
16
|
from .engine import scan_and_redact
|
|
15
17
|
from .main import DataFog
|
|
@@ -63,6 +65,7 @@ def scan_image(
|
|
|
63
65
|
|
|
64
66
|
Prints results or exits with error on failure.
|
|
65
67
|
"""
|
|
68
|
+
warn_legacy_surface("OCR")
|
|
66
69
|
if not image_urls:
|
|
67
70
|
typer.echo("No image URLs or file paths provided. Please provide at least one.")
|
|
68
71
|
raise typer.Exit(code=1)
|
|
@@ -87,6 +90,8 @@ def scan_image(
|
|
|
87
90
|
except Exception:
|
|
88
91
|
pass
|
|
89
92
|
except Exception as e:
|
|
93
|
+
if isinstance(e, LegacySurfaceWarning):
|
|
94
|
+
raise
|
|
90
95
|
logging.exception("Error in run_ocr_pipeline")
|
|
91
96
|
try:
|
|
92
97
|
from .telemetry import track_error
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Compatibility APIs for migrations between DataFog major versions."""
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Legacy scan/redact facade, preserving the 4.x Python result schema.
|
|
2
|
+
|
|
3
|
+
The result classes retain their original engine identity. This namespace does
|
|
4
|
+
not include the deprecated detect/process shims.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..engine import Entity, RedactResult, ScanResult
|
|
8
|
+
from ..engine import redact as _redact_entities
|
|
9
|
+
from ..engine import scan as _scan
|
|
10
|
+
from ..engine import scan_and_redact as _scan_and_redact
|
|
11
|
+
|
|
12
|
+
__all__ = ["Entity", "ScanResult", "RedactResult", "scan", "redact"]
|
|
13
|
+
|
|
14
|
+
_REDACT_PRESETS = {
|
|
15
|
+
"default": "token",
|
|
16
|
+
"llm": "token",
|
|
17
|
+
"mask": "mask",
|
|
18
|
+
"hash": "hash",
|
|
19
|
+
"replace": "pseudonymize",
|
|
20
|
+
"pseudonymize": "pseudonymize",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def scan(
|
|
25
|
+
text: str,
|
|
26
|
+
engine: str = "regex",
|
|
27
|
+
entity_types: list[str] | None = None,
|
|
28
|
+
locales: list[str] | None = None,
|
|
29
|
+
allowlist: list[str] | None = None,
|
|
30
|
+
allowlist_patterns: list[str] | None = None,
|
|
31
|
+
*,
|
|
32
|
+
backend: str = "python",
|
|
33
|
+
) -> ScanResult:
|
|
34
|
+
"""
|
|
35
|
+
Scan with the legacy result schema; Rust detection is opt-in.
|
|
36
|
+
|
|
37
|
+
Defaults to the lightweight regex engine so the core install works without
|
|
38
|
+
optional dependency fallback warnings.
|
|
39
|
+
|
|
40
|
+
``allowlist`` exempts exact entity texts (your own support address, doc
|
|
41
|
+
placeholders); ``allowlist_patterns`` exempts entities whose full text
|
|
42
|
+
matches a regex (e.g. ``^\\d{10}$`` so unix timestamps stop matching as
|
|
43
|
+
phone numbers).
|
|
44
|
+
"""
|
|
45
|
+
return _scan(
|
|
46
|
+
text=text,
|
|
47
|
+
engine=engine,
|
|
48
|
+
entity_types=entity_types,
|
|
49
|
+
locales=locales,
|
|
50
|
+
allowlist=allowlist,
|
|
51
|
+
allowlist_patterns=allowlist_patterns,
|
|
52
|
+
backend=backend,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def redact(
|
|
57
|
+
text: str,
|
|
58
|
+
entities: list[Entity] | None = None,
|
|
59
|
+
engine: str = "regex",
|
|
60
|
+
entity_types: list[str] | None = None,
|
|
61
|
+
strategy: str = "token",
|
|
62
|
+
preset: str | None = None,
|
|
63
|
+
locales: list[str] | None = None,
|
|
64
|
+
allowlist: list[str] | None = None,
|
|
65
|
+
allowlist_patterns: list[str] | None = None,
|
|
66
|
+
*,
|
|
67
|
+
backend: str = "python",
|
|
68
|
+
) -> RedactResult:
|
|
69
|
+
"""
|
|
70
|
+
Redact with legacy strategies and result schema.
|
|
71
|
+
|
|
72
|
+
If entities are provided, redact those spans. Otherwise, scan text first
|
|
73
|
+
using the selected engine and redact the detected entities. ``allowlist``
|
|
74
|
+
and ``allowlist_patterns`` exempt findings from redaction (exact text and
|
|
75
|
+
full-text regex match respectively); they apply to the scan path and are
|
|
76
|
+
rejected when explicit ``entities`` are supplied.
|
|
77
|
+
"""
|
|
78
|
+
if backend not in ("python", "rust"):
|
|
79
|
+
raise ValueError("backend must be one of: python, rust")
|
|
80
|
+
|
|
81
|
+
if preset is not None:
|
|
82
|
+
try:
|
|
83
|
+
strategy = _REDACT_PRESETS[preset]
|
|
84
|
+
except KeyError as exc:
|
|
85
|
+
allowed = ", ".join(sorted(_REDACT_PRESETS))
|
|
86
|
+
raise ValueError(f"preset must be one of: {allowed}") from exc
|
|
87
|
+
|
|
88
|
+
if entities is not None:
|
|
89
|
+
if allowlist or allowlist_patterns:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"allowlist/allowlist_patterns cannot be combined with explicit "
|
|
92
|
+
"entities; filter the entities before calling redact"
|
|
93
|
+
)
|
|
94
|
+
return _redact_entities(text=text, entities=entities, strategy=strategy)
|
|
95
|
+
|
|
96
|
+
return _scan_and_redact(
|
|
97
|
+
text=text,
|
|
98
|
+
engine=engine,
|
|
99
|
+
entity_types=entity_types,
|
|
100
|
+
strategy=strategy,
|
|
101
|
+
locales=locales,
|
|
102
|
+
allowlist=allowlist,
|
|
103
|
+
allowlist_patterns=allowlist_patterns,
|
|
104
|
+
backend=backend,
|
|
105
|
+
)
|
|
@@ -209,6 +209,53 @@ def _regex_entities(
|
|
|
209
209
|
return _suppress_overlapping_entities(entities)
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
def _rust_entities(
|
|
213
|
+
text: str,
|
|
214
|
+
entity_types: Optional[list[str]] = None,
|
|
215
|
+
locales: Optional[list[str]] = None,
|
|
216
|
+
) -> list[Entity]:
|
|
217
|
+
"""Adapt native findings to the legacy regex result contract.
|
|
218
|
+
|
|
219
|
+
Core supplies candidate findings. Python applies the legacy overlap
|
|
220
|
+
suppression before entity selection and allowlists, preserving that order
|
|
221
|
+
without duplicating the native detectors.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
import datafog_core
|
|
225
|
+
except ImportError as exc:
|
|
226
|
+
raise ImportError(
|
|
227
|
+
"The Rust backend requires datafog-core. "
|
|
228
|
+
'Install with: pip install "datafog[rust]"'
|
|
229
|
+
) from exc
|
|
230
|
+
|
|
231
|
+
from ._core_capabilities import scan_configs
|
|
232
|
+
|
|
233
|
+
requested = {_canonical_type(value) for value in entity_types or []}
|
|
234
|
+
configs = scan_configs(datafog_core, requested, locales)
|
|
235
|
+
entities: list[Entity] = []
|
|
236
|
+
seen: set[tuple[str, int, int]] = set()
|
|
237
|
+
for config in configs:
|
|
238
|
+
for finding in datafog_core.scan(text, config=config):
|
|
239
|
+
canonical_type = _canonical_type(finding.entity_type)
|
|
240
|
+
start = finding.codepoint_range.start
|
|
241
|
+
end = finding.codepoint_range.end
|
|
242
|
+
key = (canonical_type, start, end)
|
|
243
|
+
if key in seen or not finding.matched_text.strip():
|
|
244
|
+
continue
|
|
245
|
+
seen.add(key)
|
|
246
|
+
entities.append(
|
|
247
|
+
Entity(
|
|
248
|
+
type=canonical_type,
|
|
249
|
+
text=finding.matched_text,
|
|
250
|
+
start=start,
|
|
251
|
+
end=end,
|
|
252
|
+
confidence=1.0,
|
|
253
|
+
engine="regex",
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
return _suppress_overlapping_entities(entities)
|
|
257
|
+
|
|
258
|
+
|
|
212
259
|
def _spacy_entities(text: str) -> list[Entity]:
|
|
213
260
|
annotator = _get_spacy_annotator()
|
|
214
261
|
if isinstance(annotator, _UnavailableAnnotator):
|
|
@@ -357,6 +404,13 @@ def _needs_ner(entity_types: Optional[list[str]]) -> bool:
|
|
|
357
404
|
return bool(requested & NER_ENTITY_TYPES)
|
|
358
405
|
|
|
359
406
|
|
|
407
|
+
def _validate_backend(backend: str, engine: str) -> None:
|
|
408
|
+
if backend not in {"python", "rust"}:
|
|
409
|
+
raise ValueError("backend must be one of: python, rust")
|
|
410
|
+
if backend == "rust" and engine != "regex":
|
|
411
|
+
raise ValueError("backend='rust' supports only engine='regex'")
|
|
412
|
+
|
|
413
|
+
|
|
360
414
|
def scan(
|
|
361
415
|
text: str,
|
|
362
416
|
engine: str = "smart",
|
|
@@ -364,9 +418,15 @@ def scan(
|
|
|
364
418
|
locales: Optional[list[str]] = None,
|
|
365
419
|
allowlist: Optional[list[str]] = None,
|
|
366
420
|
allowlist_patterns: Optional[list[str]] = None,
|
|
421
|
+
*,
|
|
422
|
+
backend: str = "python",
|
|
367
423
|
) -> ScanResult:
|
|
368
424
|
"""Scan text for PII entities.
|
|
369
425
|
|
|
426
|
+
``backend="rust"`` opts into experimental native detection for
|
|
427
|
+
``engine="regex"``. Results retain legacy regex provenance and confidence;
|
|
428
|
+
these are compatibility values, not native confidence estimates.
|
|
429
|
+
|
|
370
430
|
``allowlist`` exempts exact entity texts (e.g. your own support email);
|
|
371
431
|
``allowlist_patterns`` exempts entities whose full text matches a regex
|
|
372
432
|
(e.g. ``^\\d{10}$`` to stop unix timestamps matching as phone numbers).
|
|
@@ -377,11 +437,14 @@ def scan(
|
|
|
377
437
|
if engine not in {"regex", "spacy", "gliner", "smart"}:
|
|
378
438
|
raise ValueError("engine must be one of: regex, spacy, gliner, smart")
|
|
379
439
|
|
|
440
|
+
_validate_backend(backend, engine)
|
|
441
|
+
|
|
380
442
|
# Validate patterns up front so config errors fail fast even when the
|
|
381
443
|
# text contains no entities.
|
|
382
444
|
_compile_allowlist_patterns(allowlist_patterns)
|
|
383
445
|
|
|
384
|
-
|
|
446
|
+
detector = _rust_entities if backend == "rust" else _regex_entities
|
|
447
|
+
regex_entities = detector(
|
|
385
448
|
text,
|
|
386
449
|
entity_types=entity_types,
|
|
387
450
|
locales=locales,
|
|
@@ -547,8 +610,10 @@ def scan_and_redact(
|
|
|
547
610
|
locales: Optional[list[str]] = None,
|
|
548
611
|
allowlist: Optional[list[str]] = None,
|
|
549
612
|
allowlist_patterns: Optional[list[str]] = None,
|
|
613
|
+
*,
|
|
614
|
+
backend: str = "python",
|
|
550
615
|
) -> RedactResult:
|
|
551
|
-
"""
|
|
616
|
+
"""Scan with the selected backend, then apply legacy Python redaction."""
|
|
552
617
|
scan_result = scan(
|
|
553
618
|
text=text,
|
|
554
619
|
engine=engine,
|
|
@@ -556,5 +621,6 @@ def scan_and_redact(
|
|
|
556
621
|
locales=locales,
|
|
557
622
|
allowlist=allowlist,
|
|
558
623
|
allowlist_patterns=allowlist_patterns,
|
|
624
|
+
backend=backend,
|
|
559
625
|
)
|
|
560
626
|
return redact(text=text, entities=scan_result.entities, strategy=strategy)
|