datafog-core 0.4.0__tar.gz → 0.4.2__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 (59) hide show
  1. {datafog_core-0.4.0 → datafog_core-0.4.2}/Cargo.lock +4 -4
  2. {datafog_core-0.4.0 → datafog_core-0.4.2}/PKG-INFO +15 -9
  3. {datafog_core-0.4.0/crates/core → datafog_core-0.4.2}/README.md +14 -8
  4. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/Cargo.toml +1 -1
  5. datafog_core-0.4.2/bindings/python/tests/api_key_conformance.py +122 -0
  6. datafog_core-0.4.2/bindings/python/tests/bearer_token_conformance.py +134 -0
  7. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/capabilities_conformance.py +1 -1
  8. datafog_core-0.4.2/bindings/python/tests/credential_overlap_conformance.py +103 -0
  9. datafog_core-0.4.2/bindings/python/tests/credential_uri_conformance.py +127 -0
  10. datafog_core-0.4.2/bindings/python/tests/email_context_conformance.py +49 -0
  11. datafog_core-0.4.2/bindings/python/tests/synthetic_fixtures.py +24 -0
  12. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/test_installed.py +13 -0
  13. datafog_core-0.4.2/bindings/python/tests/test_python49_integration.py +204 -0
  14. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/test_typing.py +2 -1
  15. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/Cargo.toml +1 -1
  16. {datafog_core-0.4.0 → datafog_core-0.4.2/crates/core}/README.md +14 -8
  17. datafog_core-0.4.2/crates/core/src/api_key.rs +66 -0
  18. datafog_core-0.4.2/crates/core/src/bearer_token.rs +87 -0
  19. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/capabilities.rs +8 -1
  20. datafog_core-0.4.2/crates/core/src/credential_uri.rs +200 -0
  21. datafog_core-0.4.2/crates/core/src/email_context.rs +172 -0
  22. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/lib.rs +151 -3
  23. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/structured.rs +13 -1
  24. datafog_core-0.4.2/crates/core/tests/api_key.rs +120 -0
  25. datafog_core-0.4.2/crates/core/tests/bearer_token.rs +113 -0
  26. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/tests/capabilities.rs +8 -3
  27. datafog_core-0.4.2/crates/core/tests/credential_overlaps.rs +113 -0
  28. datafog_core-0.4.2/crates/core/tests/credential_uri.rs +96 -0
  29. datafog_core-0.4.2/crates/core/tests/email_context.rs +107 -0
  30. datafog_core-0.4.2/crates/core/tests/support/synthetic_fixtures.rs +32 -0
  31. {datafog_core-0.4.0 → datafog_core-0.4.2}/pyproject.toml +1 -1
  32. {datafog_core-0.4.0 → datafog_core-0.4.2}/python/datafog_core/__init__.pyi +1 -0
  33. {datafog_core-0.4.0 → datafog_core-0.4.2}/Cargo.toml +0 -0
  34. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/src/lib.rs +0 -0
  35. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/german_conformance.py +0 -0
  36. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/jwt_conformance.py +0 -0
  37. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/npi_conformance.py +0 -0
  38. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/private_key_conformance.py +0 -0
  39. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/requirements-typing.txt +0 -0
  40. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/us_routing_number_conformance.py +0 -0
  41. {datafog_core-0.4.0 → datafog_core-0.4.2}/bindings/python/tests/uuid_conformance.py +0 -0
  42. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/examples/german_benchmark.rs +0 -0
  43. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/examples/scan_benchmark.rs +0 -0
  44. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/german.rs +0 -0
  45. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/jwt.rs +0 -0
  46. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/npi.rs +0 -0
  47. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/offsets.rs +0 -0
  48. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/private_key.rs +0 -0
  49. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/selection_tests.rs +0 -0
  50. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/us_routing_number.rs +0 -0
  51. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/src/uuid.rs +0 -0
  52. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/tests/german.rs +0 -0
  53. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/tests/jwt.rs +0 -0
  54. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/tests/npi.rs +0 -0
  55. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/tests/private_key.rs +0 -0
  56. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/tests/us_routing_number.rs +0 -0
  57. {datafog_core-0.4.0 → datafog_core-0.4.2}/crates/core/tests/uuid.rs +0 -0
  58. {datafog_core-0.4.0 → datafog_core-0.4.2}/python/datafog_core/__init__.py +0 -0
  59. {datafog_core-0.4.0 → datafog_core-0.4.2}/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.0"
83
+ version = "0.4.2"
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.0"
97
+ version = "0.4.2"
98
98
  dependencies = [
99
99
  "datafog-core",
100
100
  "pyo3",
@@ -104,7 +104,7 @@ dependencies = [
104
104
 
105
105
  [[package]]
106
106
  name = "datafog-node"
107
- version = "0.4.0"
107
+ version = "0.4.2"
108
108
  dependencies = [
109
109
  "datafog-core",
110
110
  "napi",
@@ -115,7 +115,7 @@ dependencies = [
115
115
 
116
116
  [[package]]
117
117
  name = "datafog-wasm"
118
- version = "0.4.0"
118
+ version = "0.4.2"
119
119
  dependencies = [
120
120
  "datafog-core",
121
121
  "serde",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datafog-core
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: License :: OSI Approved :: MIT License
6
6
  Classifier: Programming Language :: Python :: 3
@@ -25,7 +25,7 @@ Project-URL: Repository, https://github.com/DataFog/datafog-core
25
25
 
26
26
  Fast structured PII detection, implemented in Rust and exposed for Rust, Python, Node.js, and browsers.
27
27
 
28
- It detects `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DATE`, and `ZIP_CODE`. The next release also detects `JWT` tokens by default; see the [JWT reference](docs/reference/jwt.mdx). With an explicit German locale, it also detects `DE_IBAN`, `DE_VAT_ID`, `DE_TAX_ID`, `DE_SOCIAL_SECURITY_NUMBER`, `DE_POSTAL_CODE`, `DE_PASSPORT_NUMBER`, and `DE_RESIDENCE_PERMIT_NUMBER` (unreleased). Complete PEM private-key blocks are also detected as `PRIVATE_KEY` by default (unreleased); see the [private-key reference](docs/reference/private-keys.mdx). Context-labeled `US_ROUTING_NUMBER` detection is also available (unreleased); see [the detector rules](docs/reference/us-routing-number.mdx). Context-labeled `NPI` detection is also available (unreleased); see [the detector rules](docs/reference/npi.mdx). Every binding returns the same finding information:
28
+ It detects `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DATE`, and `ZIP_CODE`. Version 0.4.0 also detects `JWT` tokens by default; see the [JWT reference](docs/reference/jwt.mdx). With an explicit German locale, it also detects `DE_IBAN`, `DE_VAT_ID`, `DE_TAX_ID`, `DE_SOCIAL_SECURITY_NUMBER`, `DE_POSTAL_CODE`, `DE_PASSPORT_NUMBER`, and `DE_RESIDENCE_PERMIT_NUMBER`. Complete PEM private-key blocks are also detected as `PRIVATE_KEY` by default; see the [private-key reference](docs/reference/private-keys.mdx). Context-labeled `US_ROUTING_NUMBER` detection is also available; see [the detector rules](docs/reference/us-routing-number.mdx). Context-labeled `NPI` detection is also available; see [the detector rules](docs/reference/npi.mdx). Every binding returns the same finding information:
29
29
 
30
30
  ```text
31
31
  entity type, matched text, byte range, code-point range,
@@ -79,8 +79,12 @@ full-match regex values:
79
79
  }
80
80
  ```
81
81
 
82
- `scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean }, transform: ... }` so
82
+ `scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean, format?: "text" | "env" | "sql" }, transform: ... }` so
83
83
  detection settings remain separate from transformation policy.
84
+ For `.env` assignments or standard SQL quoted strings, select `format: "env"`
85
+ or `format: "sql"` to preserve email boundaries. See the
86
+ [email source-context policy](docs/guides/email-boundaries.mdx) for supported
87
+ syntax, raw escaped spans, and limitations. Default text scanning is unchanged.
84
88
 
85
89
  ## Packages
86
90
 
@@ -297,12 +301,12 @@ fixtures/ Shared conformance fixtures
297
301
 
298
302
  [MIT](LICENSE)
299
303
 
300
- ## German structured identifiers (unreleased)
304
+ ## German structured identifiers
301
305
 
302
306
  Pass `{"locale":"de"}` to text or structured scans. Trimmed, ASCII
303
307
  case-insensitive `de`, `de-DE`, and `de_DE` activate all seven German detectors;
304
308
  omitted locale and recognized `en-US`/`fr` aliases keep base detection only.
305
- The 0.4.0 candidate rejects unsupported explicit locales.
309
+ Version 0.4.0 rejects unsupported explicit locales.
306
310
 
307
311
  ```python
308
312
  from datafog_core import scan_and_transform
@@ -321,10 +325,10 @@ boundaries, never newlines. Returned text and offsets preserve the source.
321
325
  Passport and residence-permit patterns are legacy heuristics with limited
322
326
  coverage. See the [German entity reference](docs/reference/german-entities.mdx)
323
327
  and [migration differences](docs/guides/migrating-from-datafog-python.mdx).
324
- The Python 4.9 adapter requires a subsequently published compatible Core wheel;
325
- this source change does not update its extra pin or publish a release.
328
+ The higher-level Python adapter and its dependency update were merged in
329
+ [PR #179](https://github.com/DataFog/datafog-python/pull/179). Its package release is separate from Core.
326
330
 
327
- ## UUID identifiers (unreleased)
331
+ ## UUID identifiers
328
332
 
329
333
  Canonical UUID detection is opt-in: pass `{"detect_uuid":true}` to text or
330
334
  structured scans, independently of locale. It emits `UUID` findings for versions
@@ -332,5 +336,7 @@ structured scans, independently of locale. It emits `UUID` findings for versions
332
336
  sensitivity. See the [UUID reference](docs/reference/uuid.mdx) for boundaries,
333
337
  excluded sentinel forms and transformation examples.
334
338
 
335
- The source candidate targets **0.4.0**; publication and downstream Python integration are separate release gates. See the [candidate release checklist](docs/releases/0-4-0.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
339
+ **0.4.1 is published** for Rust, Python, Node.js, and WASM. The higher-level Python adapter is integrated in [merged PR #179](https://github.com/DataFog/datafog-python/pull/179); its package release is separate. See the [release notes](docs/releases/0-4-1.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
340
+
341
+ **Core 0.4.1** adds three default detectors: provider-prefixed [`API_KEY`](docs/reference/api-keys.mdx), context-required [`BEARER_TOKEN`](docs/reference/bearer-token.mdx), and whole PostgreSQL [`CREDENTIAL_URI`](docs/reference/credential-uri.mdx). The registry reports 23 supported entities and 14 defaults with capability contract 1. See the [release notes](docs/releases/0-4-1.mdx) for exact scopes, preserved legacy Python overlap behavior, and the passed local exact-wheel Python 4.9 integration gate. Python's default backend is unchanged.
336
342
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Fast structured PII detection, implemented in Rust and exposed for Rust, Python, Node.js, and browsers.
4
4
 
5
- It detects `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DATE`, and `ZIP_CODE`. The next release also detects `JWT` tokens by default; see the [JWT reference](docs/reference/jwt.mdx). With an explicit German locale, it also detects `DE_IBAN`, `DE_VAT_ID`, `DE_TAX_ID`, `DE_SOCIAL_SECURITY_NUMBER`, `DE_POSTAL_CODE`, `DE_PASSPORT_NUMBER`, and `DE_RESIDENCE_PERMIT_NUMBER` (unreleased). Complete PEM private-key blocks are also detected as `PRIVATE_KEY` by default (unreleased); see the [private-key reference](docs/reference/private-keys.mdx). Context-labeled `US_ROUTING_NUMBER` detection is also available (unreleased); see [the detector rules](docs/reference/us-routing-number.mdx). Context-labeled `NPI` detection is also available (unreleased); see [the detector rules](docs/reference/npi.mdx). Every binding returns the same finding information:
5
+ It detects `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DATE`, and `ZIP_CODE`. Version 0.4.0 also detects `JWT` tokens by default; see the [JWT reference](docs/reference/jwt.mdx). With an explicit German locale, it also detects `DE_IBAN`, `DE_VAT_ID`, `DE_TAX_ID`, `DE_SOCIAL_SECURITY_NUMBER`, `DE_POSTAL_CODE`, `DE_PASSPORT_NUMBER`, and `DE_RESIDENCE_PERMIT_NUMBER`. Complete PEM private-key blocks are also detected as `PRIVATE_KEY` by default; see the [private-key reference](docs/reference/private-keys.mdx). Context-labeled `US_ROUTING_NUMBER` detection is also available; see [the detector rules](docs/reference/us-routing-number.mdx). Context-labeled `NPI` detection is also available; see [the detector rules](docs/reference/npi.mdx). Every binding returns the same finding information:
6
6
 
7
7
  ```text
8
8
  entity type, matched text, byte range, code-point range,
@@ -56,8 +56,12 @@ full-match regex values:
56
56
  }
57
57
  ```
58
58
 
59
- `scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean }, transform: ... }` so
59
+ `scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean, format?: "text" | "env" | "sql" }, transform: ... }` so
60
60
  detection settings remain separate from transformation policy.
61
+ For `.env` assignments or standard SQL quoted strings, select `format: "env"`
62
+ or `format: "sql"` to preserve email boundaries. See the
63
+ [email source-context policy](docs/guides/email-boundaries.mdx) for supported
64
+ syntax, raw escaped spans, and limitations. Default text scanning is unchanged.
61
65
 
62
66
  ## Packages
63
67
 
@@ -274,12 +278,12 @@ fixtures/ Shared conformance fixtures
274
278
 
275
279
  [MIT](LICENSE)
276
280
 
277
- ## German structured identifiers (unreleased)
281
+ ## German structured identifiers
278
282
 
279
283
  Pass `{"locale":"de"}` to text or structured scans. Trimmed, ASCII
280
284
  case-insensitive `de`, `de-DE`, and `de_DE` activate all seven German detectors;
281
285
  omitted locale and recognized `en-US`/`fr` aliases keep base detection only.
282
- The 0.4.0 candidate rejects unsupported explicit locales.
286
+ Version 0.4.0 rejects unsupported explicit locales.
283
287
 
284
288
  ```python
285
289
  from datafog_core import scan_and_transform
@@ -298,10 +302,10 @@ boundaries, never newlines. Returned text and offsets preserve the source.
298
302
  Passport and residence-permit patterns are legacy heuristics with limited
299
303
  coverage. See the [German entity reference](docs/reference/german-entities.mdx)
300
304
  and [migration differences](docs/guides/migrating-from-datafog-python.mdx).
301
- The Python 4.9 adapter requires a subsequently published compatible Core wheel;
302
- this source change does not update its extra pin or publish a release.
305
+ The higher-level Python adapter and its dependency update were merged in
306
+ [PR #179](https://github.com/DataFog/datafog-python/pull/179). Its package release is separate from Core.
303
307
 
304
- ## UUID identifiers (unreleased)
308
+ ## UUID identifiers
305
309
 
306
310
  Canonical UUID detection is opt-in: pass `{"detect_uuid":true}` to text or
307
311
  structured scans, independently of locale. It emits `UUID` findings for versions
@@ -309,4 +313,6 @@ structured scans, independently of locale. It emits `UUID` findings for versions
309
313
  sensitivity. See the [UUID reference](docs/reference/uuid.mdx) for boundaries,
310
314
  excluded sentinel forms and transformation examples.
311
315
 
312
- The source candidate targets **0.4.0**; publication and downstream Python integration are separate release gates. See the [candidate release checklist](docs/releases/0-4-0.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
316
+ **0.4.1 is published** for Rust, Python, Node.js, and WASM. The higher-level Python adapter is integrated in [merged PR #179](https://github.com/DataFog/datafog-python/pull/179); its package release is separate. See the [release notes](docs/releases/0-4-1.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
317
+
318
+ **Core 0.4.1** adds three default detectors: provider-prefixed [`API_KEY`](docs/reference/api-keys.mdx), context-required [`BEARER_TOKEN`](docs/reference/bearer-token.mdx), and whole PostgreSQL [`CREDENTIAL_URI`](docs/reference/credential-uri.mdx). The registry reports 23 supported entities and 14 defaults with capability contract 1. See the [release notes](docs/releases/0-4-1.mdx) for exact scopes, preserved legacy Python overlap behavior, and the passed local exact-wheel Python 4.9 integration gate. Python's default backend is unchanged.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "datafog-core-python"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  description = "Python bindings for datafog-core"
@@ -0,0 +1,122 @@
1
+ """Shared API-key fixtures against the installed wheel, including provider round trips."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import datafog_core as api
6
+ from synthetic_fixtures import expand_fixture
7
+
8
+ RECORDS = [
9
+ expand_fixture(json.loads(line))
10
+ for line in (Path(__file__).resolve().parents[3] / "fixtures/api-key.jsonl")
11
+ .read_text()
12
+ .splitlines()
13
+ ]
14
+
15
+
16
+ def project(text, findings):
17
+ result = []
18
+ for f in findings:
19
+ if f.entity_type != "API_KEY":
20
+ continue
21
+ assert (
22
+ text.encode()[f.byte_range.start : f.byte_range.end].decode()
23
+ == f.matched_text
24
+ )
25
+ assert text[f.codepoint_range.start : f.codepoint_range.end] == f.matched_text
26
+ assert f.confidence is None
27
+ assert f.detector_name == "datafog-core/" + f.entity_type.lower().replace(
28
+ "_", "-"
29
+ )
30
+ assert f.detector_version
31
+ result.append(
32
+ dict(
33
+ label=f.entity_type,
34
+ text=f.matched_text,
35
+ start=f.codepoint_range.start,
36
+ end=f.codepoint_range.end,
37
+ )
38
+ )
39
+ return result
40
+
41
+
42
+ def verify():
43
+ for row in RECORDS:
44
+ findings = api.scan(row["text"], row["config"])
45
+ assert project(row["text"], findings) == row["entities"], row["id"]
46
+ data = {"a/b": row["text"], "array": [row["text"]], "z": row["text"]}
47
+ located = api.scan_structured(data, row["config"]).findings
48
+ for path in ["/a~1b", "/array/0", "/z"]:
49
+ assert (
50
+ project(row["text"], [f.finding for f in located if f.path == path])
51
+ == row["entities"]
52
+ ), row["id"]
53
+ for case in row.get("transforms", []):
54
+ result = api.scan_and_transform(
55
+ row["text"], {"scan": row["config"], "transform": case["config"]}
56
+ )
57
+ assert result.text == case["text"], row["id"]
58
+ assert api.transform(row["text"], findings, case["config"]) == result
59
+ for t in result.transformations:
60
+ assert (
61
+ result.text.encode()[
62
+ t.output_byte_range.start : t.output_byte_range.end
63
+ ].decode()
64
+ == t.replacement
65
+ )
66
+ assert (
67
+ result.text[
68
+ t.output_codepoint_range.start : t.output_codepoint_range.end
69
+ ]
70
+ == t.replacement
71
+ )
72
+ assert any(
73
+ f.byte_range == t.source_byte_range
74
+ and f.entity_type == t.entity_type
75
+ for f in findings
76
+ )
77
+ assert not hasattr(t, "matched_text") and not hasattr(t, "finding")
78
+ structured = api.scan_and_transform_structured(
79
+ data, {"scan": row["config"], "transform": case["config"]}
80
+ )
81
+ assert structured.data == {
82
+ "a/b": case["text"],
83
+ "array": [case["text"]],
84
+ "z": case["text"],
85
+ }
86
+ explicit = api.transform_structured(data, located, case["config"])
87
+ assert explicit.data == structured.data
88
+ assert [(t.path, t.transformation) for t in explicit.transformations] == [
89
+ (t.path, t.transformation) for t in structured.transformations
90
+ ]
91
+ async def verify_providers(manager, token_manager, context):
92
+ for row in RECORDS:
93
+ if not row.get("sample"):
94
+ continue
95
+ entities = list(dict.fromkeys(e["label"] for e in row["entities"]))
96
+ pseudonyms = await manager.scan_and_transform(
97
+ row["text"],
98
+ {
99
+ "scan": row["config"],
100
+ "transform": {
101
+ "default": {"strategy": "pseudonymize", "key_ref": "german"},
102
+ "entities": entities,
103
+ },
104
+ },
105
+ )
106
+ assert (
107
+ len(pseudonyms.transformations) == len(row["entities"])
108
+ and pseudonyms.text != row["text"]
109
+ )
110
+ tokens = await token_manager.scan_and_transform(
111
+ row["text"],
112
+ {
113
+ "scan": row["config"],
114
+ "transform": {
115
+ "default": {"strategy": "tokenize", "token_ref": "german"},
116
+ "entities": entities,
117
+ },
118
+ },
119
+ context,
120
+ )
121
+ assert len(tokens.transformations) == len(row["entities"])
122
+ assert (await token_manager.restore(tokens.text, context)).text == row["text"]
@@ -0,0 +1,134 @@
1
+ """Shared BEARER_TOKEN fixtures against the installed wheel, including provider round trips."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import datafog_core as api
6
+
7
+ RECORDS = [
8
+ json.loads(line)
9
+ for line in (Path(__file__).resolve().parents[3] / "fixtures/bearer-token.jsonl")
10
+ .read_text()
11
+ .splitlines()
12
+ ]
13
+
14
+
15
+ def project(text, findings):
16
+ result = []
17
+ for f in findings:
18
+ if not f.entity_type == "BEARER_TOKEN":
19
+ continue
20
+ assert (
21
+ text.encode()[f.byte_range.start : f.byte_range.end].decode()
22
+ == f.matched_text
23
+ )
24
+ assert text[f.codepoint_range.start : f.codepoint_range.end] == f.matched_text
25
+ assert f.confidence is None
26
+ assert f.detector_name == "datafog-core/" + f.entity_type.lower().replace(
27
+ "_", "-"
28
+ )
29
+ assert f.detector_version
30
+ result.append(
31
+ dict(
32
+ label=f.entity_type,
33
+ text=f.matched_text,
34
+ start=f.codepoint_range.start,
35
+ end=f.codepoint_range.end,
36
+ )
37
+ )
38
+ return result
39
+
40
+
41
+ def verify():
42
+ for row in RECORDS:
43
+ findings = api.scan(row["text"], row["config"])
44
+ if "overlap" in row:
45
+ assert any(f.entity_type == row["overlap"] for f in findings)
46
+ assert project(row["text"], findings) == row["entities"], row["id"]
47
+ data = {"a/b": row["text"], "array": [row["text"]], "z": row["text"]}
48
+ located = api.scan_structured(data, row["config"]).findings
49
+ for path in ["/a~1b", "/array/0", "/z"]:
50
+ assert (
51
+ project(row["text"], [f.finding for f in located if f.path == path])
52
+ == row["entities"]
53
+ ), row["id"]
54
+ for case in row.get("transforms", []):
55
+ result = api.scan_and_transform(
56
+ row["text"], {"scan": row["config"], "transform": case["config"]}
57
+ )
58
+ assert result.text == case["text"], row["id"]
59
+ assert api.transform(row["text"], findings, case["config"]) == result
60
+ for t in result.transformations:
61
+ assert (
62
+ result.text.encode()[
63
+ t.output_byte_range.start : t.output_byte_range.end
64
+ ].decode()
65
+ == t.replacement
66
+ )
67
+ assert (
68
+ result.text[
69
+ t.output_codepoint_range.start : t.output_codepoint_range.end
70
+ ]
71
+ == t.replacement
72
+ )
73
+ assert any(
74
+ f.byte_range == t.source_byte_range
75
+ and f.entity_type == t.entity_type
76
+ for f in findings
77
+ )
78
+ assert not hasattr(t, "matched_text") and not hasattr(t, "finding")
79
+ structured = api.scan_and_transform_structured(
80
+ data, {"scan": row["config"], "transform": case["config"]}
81
+ )
82
+ assert structured.data == {
83
+ "a/b": case["text"],
84
+ "array": [case["text"]],
85
+ "z": case["text"],
86
+ }
87
+ explicit = api.transform_structured(data, located, case["config"])
88
+ assert explicit.data == structured.data
89
+ assert [(t.path, t.transformation) for t in explicit.transformations] == [
90
+ (t.path, t.transformation) for t in structured.transformations
91
+ ]
92
+
93
+ data = {"👋/~":{"aUtHoRiZaTiOn":" bEaReR abc._~+/==\t"}, "Authorization":["Bearer hidden"], "X-Authorization":"Bearer hidden", "a":"Authorization:", "b":"Bearer hidden", "invalid":{"Authorization":"Bearer bad!value"}}
94
+ found = [f for f in api.scan_structured(data).findings if f.finding.entity_type == "BEARER_TOKEN"]
95
+ assert len(found) == 1
96
+ assert found[0].path == "/👋~1~0/aUtHoRiZaTiOn"
97
+ assert project(data["👋/~"]["aUtHoRiZaTiOn"], [found[0].finding]) == [dict(label="BEARER_TOKEN",text="abc._~+/==",start=9,end=19)]
98
+ expected = {**data,"👋/~":{"aUtHoRiZaTiOn":" bEaReR [BEARER_TOKEN]\t"}}
99
+ assert api.scan_and_transform_structured(data,{"transform":{"default":{"strategy":"redact"},"entities":["BEARER_TOKEN"]}}).data == expected
100
+ assert not any(f.entity_type == "BEARER_TOKEN" for f in api.scan("Bearer abc123"))
101
+
102
+
103
+ async def verify_providers(manager, token_manager, context):
104
+ for row in RECORDS:
105
+ if not row.get("sample"):
106
+ continue
107
+ entities = list(dict.fromkeys(e["label"] for e in row["entities"]))
108
+ pseudonyms = await manager.scan_and_transform(
109
+ row["text"],
110
+ {
111
+ "scan": row["config"],
112
+ "transform": {
113
+ "default": {"strategy": "pseudonymize", "key_ref": "jwt"},
114
+ "entities": entities,
115
+ },
116
+ },
117
+ )
118
+ assert (
119
+ len(pseudonyms.transformations) == len(row["entities"])
120
+ and pseudonyms.text != row["text"]
121
+ )
122
+ tokens = await token_manager.scan_and_transform(
123
+ row["text"],
124
+ {
125
+ "scan": row["config"],
126
+ "transform": {
127
+ "default": {"strategy": "tokenize", "token_ref": "jwt"},
128
+ "entities": entities,
129
+ },
130
+ },
131
+ context,
132
+ )
133
+ assert len(tokens.transformations) == len(row["entities"])
134
+ assert (await token_manager.restore(tokens.text, context)).text == row["text"]
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  import datafog_core as api
6
6
 
7
7
  SUPPORTED = sorted([
8
- "CREDIT_CARD", "DATE", "DE_IBAN", "DE_PASSPORT_NUMBER", "DE_POSTAL_CODE",
8
+ "BEARER_TOKEN", "CREDIT_CARD", "DATE", "DE_IBAN", "DE_PASSPORT_NUMBER", "DE_POSTAL_CODE", "API_KEY", "CREDENTIAL_URI",
9
9
  "DE_RESIDENCE_PERMIT_NUMBER", "DE_SOCIAL_SECURITY_NUMBER", "DE_TAX_ID",
10
10
  "DE_VAT_ID", "EMAIL", "IP_ADDRESS", "JWT", "NPI", "PERSON", "PHONE",
11
11
  "PRIVATE_KEY", "SSN", "US_ROUTING_NUMBER", "UUID", "ZIP_CODE",
@@ -0,0 +1,103 @@
1
+ """Cross-detector overlap selection against the installed wheel."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import datafog_core as api
6
+ from synthetic_fixtures import expand_fixture
7
+
8
+ RECORDS = [
9
+ expand_fixture(json.loads(line))
10
+ for line in (Path(__file__).resolve().parents[3] / "fixtures/credential-overlaps.jsonl")
11
+ .read_text()
12
+ .splitlines()
13
+ ]
14
+
15
+
16
+ def project(text, findings):
17
+ result = []
18
+ for f in findings:
19
+ if f.entity_type not in {"API_KEY", "BEARER_TOKEN", "CREDENTIAL_URI", "JWT"}:
20
+ continue
21
+ assert (
22
+ text.encode()[f.byte_range.start : f.byte_range.end].decode()
23
+ == f.matched_text
24
+ )
25
+ assert text[f.codepoint_range.start : f.codepoint_range.end] == f.matched_text
26
+ assert f.confidence is None
27
+ assert f.detector_name == "datafog-core/" + f.entity_type.lower().replace(
28
+ "_", "-"
29
+ )
30
+ assert f.detector_version
31
+ result.append(
32
+ dict(
33
+ label=f.entity_type,
34
+ text=f.matched_text,
35
+ start=f.codepoint_range.start,
36
+ end=f.codepoint_range.end,
37
+ )
38
+ )
39
+ return result
40
+
41
+
42
+ def verify():
43
+ for row in RECORDS:
44
+ findings = api.scan(row["text"], row["config"])
45
+ if "authorization_value" in row:
46
+ value = row["authorization_value"]
47
+ data = {"👋/headers": {"aUtHoRiZaTiOn": value}}
48
+ located = api.scan_structured(data).findings
49
+ assert all(item.path == "/👋~1headers/aUtHoRiZaTiOn" for item in located)
50
+ expected = [dict(entity, start=7, end=len(value)) for entity in row["entities"]]
51
+ assert project(value, [item.finding for item in located]) == expected
52
+ for entity in row["entities"]:
53
+ label = entity["label"]
54
+ result = api.scan_and_transform_structured(data, {"transform": {"default": {"strategy": "redact"}, "entities": [label]}})
55
+ assert result.data == {"👋/headers": {"aUtHoRiZaTiOn": f"Bearer [{label}]"}}
56
+ if "overlap" in row:
57
+ assert any(f.entity_type == row["overlap"] for f in findings)
58
+ assert project(row["text"], findings) == row["entities"], row["id"]
59
+ data = {"a/b": row["text"], "array": [row["text"]], "z": row["text"]}
60
+ located = api.scan_structured(data, row["config"]).findings
61
+ for path in ["/a~1b", "/array/0", "/z"]:
62
+ assert (
63
+ project(row["text"], [f.finding for f in located if f.path == path])
64
+ == row["entities"]
65
+ ), row["id"]
66
+ for case in row.get("transforms", []):
67
+ result = api.scan_and_transform(
68
+ row["text"], {"scan": row["config"], "transform": case["config"]}
69
+ )
70
+ assert result.text == case["text"], row["id"]
71
+ assert api.transform(row["text"], findings, case["config"]) == result
72
+ for t in result.transformations:
73
+ assert (
74
+ result.text.encode()[
75
+ t.output_byte_range.start : t.output_byte_range.end
76
+ ].decode()
77
+ == t.replacement
78
+ )
79
+ assert (
80
+ result.text[
81
+ t.output_codepoint_range.start : t.output_codepoint_range.end
82
+ ]
83
+ == t.replacement
84
+ )
85
+ assert any(
86
+ f.byte_range == t.source_byte_range
87
+ and f.entity_type == t.entity_type
88
+ for f in findings
89
+ )
90
+ assert not hasattr(t, "matched_text") and not hasattr(t, "finding")
91
+ structured = api.scan_and_transform_structured(
92
+ data, {"scan": row["config"], "transform": case["config"]}
93
+ )
94
+ assert structured.data == {
95
+ "a/b": case["text"],
96
+ "array": [case["text"]],
97
+ "z": case["text"],
98
+ }
99
+ explicit = api.transform_structured(data, located, case["config"])
100
+ assert explicit.data == structured.data
101
+ assert [(t.path, t.transformation) for t in explicit.transformations] == [
102
+ (t.path, t.transformation) for t in structured.transformations
103
+ ]