faultgate 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.
- {faultgate-0.4.0 → faultgate-0.4.2}/CHANGELOG.md +4 -0
- {faultgate-0.4.0/src/faultgate.egg-info → faultgate-0.4.2}/PKG-INFO +9 -3
- {faultgate-0.4.0 → faultgate-0.4.2}/README.md +4 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/pyproject.toml +5 -3
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/__init__.py +1 -1
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/redact.py +64 -13
- {faultgate-0.4.0 → faultgate-0.4.2/src/faultgate.egg-info}/PKG-INFO +9 -3
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate.egg-info/SOURCES.txt +2 -0
- faultgate-0.4.2/tests/test_integrations.py +34 -0
- faultgate-0.4.2/tests/test_redact_find.py +129 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/LICENSE +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/MANIFEST.in +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/formats/braintrust.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/formats/langfuse.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/formats/langsmith.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/formats/openinference.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/gate/band.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/gate/baseline_glm_run1.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/gate/baseline_glm_run2.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/gate/candidate_gpt.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/gate/candidate_qwen.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/gate/golden.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/gate/policy.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/golden.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/policy_retail.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/policy_retail_v031.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/policy_support.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/examples/traces.json +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/setup.cfg +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/band.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/checks.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/cli.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/formats.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/judge.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/precheck.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/report.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/runner.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate/trace.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate.egg-info/dependency_links.txt +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate.egg-info/entry_points.txt +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate.egg-info/requires.txt +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/src/faultgate.egg-info/top_level.txt +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/__init__.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/conftest.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/fixtures/make_retail_gaps.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_band.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_checks.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_cli.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_formats.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_golden.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_judge.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_policy.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_redact.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_report.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_runner.py +0 -0
- {faultgate-0.4.0 → faultgate-0.4.2}/tests/test_trace.py +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 — 2026-10-26
|
|
4
|
+
|
|
5
|
+
- Update package metadata and bundled README with the FAULTLINE product page and current source repository. Runtime behavior is unchanged.
|
|
6
|
+
|
|
3
7
|
## 0.4.0 — 2026-10-14
|
|
4
8
|
|
|
5
9
|
Closes the six policy gaps an outside client found in 0.3.1 (17 retail probes: 6 caught, 6 missed) and adds an outcome input.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: faultgate
|
|
3
|
-
Version: 0.4.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: FAULTLINE faultgate: open-source release gate for AI agents, trace checks, and regression decisions.
|
|
5
5
|
Author: Samir Sawarkar
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://www.faultline-labs.com/products/faultgate/
|
|
8
|
+
Project-URL: Repository, https://github.com/Faultline-lab/faultgate
|
|
9
|
+
Project-URL: Issues, https://github.com/Faultline-lab/faultgate/issues
|
|
8
10
|
Project-URL: Evidence, https://github.com/samirsawarkar/faultline-ai-reliability
|
|
9
11
|
Keywords: ai,agents,evaluation,reliability,opentelemetry,llm
|
|
10
12
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -34,6 +36,10 @@ Dynamic: license-file
|
|
|
34
36
|
|
|
35
37
|
**A local-first release gate for AI agents.** Hand it the traces your agent recorded, it checks every run for known failure modes, measures your run-to-run noise, and exits non-zero only when a candidate is a real regression. Free, offline, no model required for the default checks.
|
|
36
38
|
|
|
39
|
+
**Product and examples:** [FAULTLINE faultgate](https://www.faultline-labs.com/products/faultgate/) · **Documentation:** [faultgate docs](https://www.faultline-labs.com/docs/) · **PyPI:** [`faultgate`](https://pypi.org/project/faultgate/). Use faultgate to test recorded agent runs against business rules and measured baseline variation before a CI release.
|
|
40
|
+
|
|
41
|
+
**Why measure a baseline first?** [Regression or noise? A release gate that measures first](https://www.faultline-labs.com/blog/faultgate-regression-or-noise/) explains run-to-run variation and the current PASS, FAIL, and INCONCLUSIVE verdicts.
|
|
42
|
+
|
|
37
43
|
```
|
|
38
44
|
$ faultgate check examples/traces.json
|
|
39
45
|
trace search_loop abstention policy repeated_call unrecovered_tool_error context_overflow
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
**A local-first release gate for AI agents.** Hand it the traces your agent recorded, it checks every run for known failure modes, measures your run-to-run noise, and exits non-zero only when a candidate is a real regression. Free, offline, no model required for the default checks.
|
|
4
4
|
|
|
5
|
+
**Product and examples:** [FAULTLINE faultgate](https://www.faultline-labs.com/products/faultgate/) · **Documentation:** [faultgate docs](https://www.faultline-labs.com/docs/) · **PyPI:** [`faultgate`](https://pypi.org/project/faultgate/). Use faultgate to test recorded agent runs against business rules and measured baseline variation before a CI release.
|
|
6
|
+
|
|
7
|
+
**Why measure a baseline first?** [Regression or noise? A release gate that measures first](https://www.faultline-labs.com/blog/faultgate-regression-or-noise/) explains run-to-run variation and the current PASS, FAIL, and INCONCLUSIVE verdicts.
|
|
8
|
+
|
|
5
9
|
```
|
|
6
10
|
$ faultgate check examples/traces.json
|
|
7
11
|
trace search_loop abstention policy repeated_call unrecovered_tool_error context_overflow
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "faultgate"
|
|
7
|
-
version = "0.4.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.4.2"
|
|
8
|
+
description = "FAULTLINE faultgate: open-source release gate for AI agents, trace checks, and regression decisions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
@@ -33,7 +33,9 @@ demo = ["pyarrow", "huggingface_hub"]
|
|
|
33
33
|
faultgate = "faultgate.cli:main"
|
|
34
34
|
|
|
35
35
|
[project.urls]
|
|
36
|
-
Homepage = "https://
|
|
36
|
+
Homepage = "https://www.faultline-labs.com/products/faultgate/"
|
|
37
|
+
Repository = "https://github.com/Faultline-lab/faultgate"
|
|
38
|
+
Issues = "https://github.com/Faultline-lab/faultgate/issues"
|
|
37
39
|
Evidence = "https://github.com/samirsawarkar/faultline-ai-reliability"
|
|
38
40
|
|
|
39
41
|
[tool.setuptools.packages.find]
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""faultgate — local-first release gate for AI agents."""
|
|
2
|
-
__version__ = "0.4.
|
|
2
|
+
__version__ = "0.4.2"
|
|
@@ -31,29 +31,52 @@ def _luhn(digits: str) -> bool:
|
|
|
31
31
|
return total % 10 == 0
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
The run can carry more than the card: an SSN or an order number written next to it
|
|
38
|
-
("4111 1111 1111 1111 123-45-6789"). Testing only the whole run let the card through.
|
|
39
|
-
"""
|
|
34
|
+
def _card_spans(m: re.Match[str]) -> list[tuple[int, int]]:
|
|
35
|
+
"""Return every Luhn-valid 13-19 digit window of whole digit groups in m as (start, end)."""
|
|
40
36
|
text = m.group(0)
|
|
37
|
+
base = m.start()
|
|
41
38
|
groups = [(g.start(), g.end()) for g in re.finditer(r"\d+", text)]
|
|
42
|
-
|
|
39
|
+
spans: list[tuple[int, int]] = []
|
|
40
|
+
i = 0
|
|
43
41
|
while i < len(groups):
|
|
44
42
|
for j in range(len(groups), i, -1):
|
|
45
43
|
digits = "".join(text[a:b] for a, b in groups[i:j])
|
|
46
44
|
if 13 <= len(digits) <= 19 and _luhn(digits):
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
spans.append((base + groups[i][0], base + groups[j - 1][1]))
|
|
46
|
+
i = j
|
|
49
47
|
break
|
|
50
48
|
else:
|
|
51
49
|
i += 1
|
|
50
|
+
return spans
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _card(m: re.Match[str], tag: str) -> str:
|
|
54
|
+
"""Mask every Luhn-valid 13-19 digit window of whole digit groups in a run of groups.
|
|
55
|
+
|
|
56
|
+
The run can carry more than the card: an SSN or an order number written next to it
|
|
57
|
+
("4111 1111 1111 1111 123-45-6789"). Testing only the whole run let the card through.
|
|
58
|
+
"""
|
|
59
|
+
text = m.group(0)
|
|
60
|
+
base = m.start()
|
|
61
|
+
out, done = [], 0
|
|
62
|
+
for start, end in _card_spans(m):
|
|
63
|
+
out += [text[done:start - base], tag]
|
|
64
|
+
done = end - base
|
|
52
65
|
return "".join(out) + text[done:]
|
|
53
66
|
|
|
54
67
|
|
|
68
|
+
def _phone_spans(m: re.Match[str]) -> list[tuple[int, int]]:
|
|
69
|
+
return [(m.start(), m.end())] if 9 <= len(re.sub(r"\D", "", m.group(0))) <= 15 else []
|
|
70
|
+
|
|
71
|
+
|
|
55
72
|
def _phone(m: re.Match[str], tag: str) -> str:
|
|
56
|
-
return tag if
|
|
73
|
+
return tag if _phone_spans(m) else m.group(0)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_SPANS_FN: dict[Callable[[re.Match[str], str], str], Callable[[re.Match[str]], list[tuple[int, int]]]] = {
|
|
77
|
+
_card: _card_spans,
|
|
78
|
+
_phone: _phone_spans,
|
|
79
|
+
}
|
|
57
80
|
|
|
58
81
|
|
|
59
82
|
# Order matters: secrets, SSNs and cards before the looser phone / IP patterns.
|
|
@@ -88,6 +111,35 @@ PATTERNS: list[tuple[str, re.Pattern[str], Callable[[re.Match[str], str], str] |
|
|
|
88
111
|
]
|
|
89
112
|
|
|
90
113
|
|
|
114
|
+
def find(text: str) -> list[tuple[int, int, str]]:
|
|
115
|
+
"""Return every private value in text as (start, end, kind), in PATTERNS order with overlaps removed."""
|
|
116
|
+
if not text:
|
|
117
|
+
return []
|
|
118
|
+
taken: list[tuple[int, int, str]] = []
|
|
119
|
+
work = text
|
|
120
|
+
for kind, rx, sub in PATTERNS:
|
|
121
|
+
found: list[tuple[int, int]] = []
|
|
122
|
+
for m in rx.finditer(work):
|
|
123
|
+
if sub is None:
|
|
124
|
+
found.append((m.start(), m.end()))
|
|
125
|
+
elif sub in _SPANS_FN:
|
|
126
|
+
found.extend(_SPANS_FN[sub](m))
|
|
127
|
+
else:
|
|
128
|
+
found.append((m.start(), m.end()))
|
|
129
|
+
found = [(s, e) for s, e in found if e > s]
|
|
130
|
+
if not found:
|
|
131
|
+
continue
|
|
132
|
+
taken.extend((s, e, kind) for s, e in found)
|
|
133
|
+
# Later patterns see earlier matches as masked (same length, so offsets stay valid): an SSN written
|
|
134
|
+
# beside a card must not join the card's digit run, exactly as sequential replacement behaved.
|
|
135
|
+
chars = list(work)
|
|
136
|
+
for s, e in found:
|
|
137
|
+
chars[s:e] = "\x00" * (e - s)
|
|
138
|
+
work = "".join(chars)
|
|
139
|
+
taken.sort(key=lambda s: s[0])
|
|
140
|
+
return taken
|
|
141
|
+
|
|
142
|
+
|
|
91
143
|
@dataclass(frozen=True)
|
|
92
144
|
class Redactor:
|
|
93
145
|
name: str
|
|
@@ -98,9 +150,8 @@ class Redactor:
|
|
|
98
150
|
|
|
99
151
|
|
|
100
152
|
def _builtin(text: str) -> str:
|
|
101
|
-
for
|
|
102
|
-
|
|
103
|
-
text = rx.sub(lambda m: tag if sub is None else sub(m, tag), text)
|
|
153
|
+
for start, end, kind in reversed(find(text)):
|
|
154
|
+
text = text[:start] + f"<{kind}>" + text[end:]
|
|
104
155
|
return text
|
|
105
156
|
|
|
106
157
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: faultgate
|
|
3
|
-
Version: 0.4.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: FAULTLINE faultgate: open-source release gate for AI agents, trace checks, and regression decisions.
|
|
5
5
|
Author: Samir Sawarkar
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://www.faultline-labs.com/products/faultgate/
|
|
8
|
+
Project-URL: Repository, https://github.com/Faultline-lab/faultgate
|
|
9
|
+
Project-URL: Issues, https://github.com/Faultline-lab/faultgate/issues
|
|
8
10
|
Project-URL: Evidence, https://github.com/samirsawarkar/faultline-ai-reliability
|
|
9
11
|
Keywords: ai,agents,evaluation,reliability,opentelemetry,llm
|
|
10
12
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -34,6 +36,10 @@ Dynamic: license-file
|
|
|
34
36
|
|
|
35
37
|
**A local-first release gate for AI agents.** Hand it the traces your agent recorded, it checks every run for known failure modes, measures your run-to-run noise, and exits non-zero only when a candidate is a real regression. Free, offline, no model required for the default checks.
|
|
36
38
|
|
|
39
|
+
**Product and examples:** [FAULTLINE faultgate](https://www.faultline-labs.com/products/faultgate/) · **Documentation:** [faultgate docs](https://www.faultline-labs.com/docs/) · **PyPI:** [`faultgate`](https://pypi.org/project/faultgate/). Use faultgate to test recorded agent runs against business rules and measured baseline variation before a CI release.
|
|
40
|
+
|
|
41
|
+
**Why measure a baseline first?** [Regression or noise? A release gate that measures first](https://www.faultline-labs.com/blog/faultgate-regression-or-noise/) explains run-to-run variation and the current PASS, FAIL, and INCONCLUSIVE verdicts.
|
|
42
|
+
|
|
37
43
|
```
|
|
38
44
|
$ faultgate check examples/traces.json
|
|
39
45
|
trace search_loop abstention policy repeated_call unrecovered_tool_error context_overflow
|
|
@@ -43,9 +43,11 @@ tests/test_checks.py
|
|
|
43
43
|
tests/test_cli.py
|
|
44
44
|
tests/test_formats.py
|
|
45
45
|
tests/test_golden.py
|
|
46
|
+
tests/test_integrations.py
|
|
46
47
|
tests/test_judge.py
|
|
47
48
|
tests/test_policy.py
|
|
48
49
|
tests/test_redact.py
|
|
50
|
+
tests/test_redact_find.py
|
|
49
51
|
tests/test_report.py
|
|
50
52
|
tests/test_runner.py
|
|
51
53
|
tests/test_trace.py
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Check every integrations/*/expected.json against faultgate's real output.
|
|
2
|
+
|
|
3
|
+
Reads only the saved trace files. Needs no internet, API key or framework.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
13
|
+
CASES = [
|
|
14
|
+
(path.parent, trace)
|
|
15
|
+
for path in sorted((ROOT / "integrations").glob("*/expected.json"))
|
|
16
|
+
for trace in json.loads(path.read_text())
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize(
|
|
21
|
+
"recipe,trace", CASES, ids=[f"{r.name}/{t}" for r, t in CASES]
|
|
22
|
+
)
|
|
23
|
+
def test_recipe_trace(recipe, trace, tmp_path):
|
|
24
|
+
expected = json.loads((recipe / "expected.json").read_text())[trace]
|
|
25
|
+
report = tmp_path / "report.json"
|
|
26
|
+
run = subprocess.run(
|
|
27
|
+
[shutil.which("faultgate"), "check", str(recipe / "traces" / trace),
|
|
28
|
+
"--policy", str(recipe / "policy.json"), "--json", str(report)],
|
|
29
|
+
capture_output=True, text=True,
|
|
30
|
+
)
|
|
31
|
+
assert run.returncode == expected["exit_code"], run.stdout + run.stderr
|
|
32
|
+
data = json.loads(report.read_text())
|
|
33
|
+
fired = sorted(name for name, count in data["fired"].items() if count)
|
|
34
|
+
assert fired == sorted(expected["fired"])
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from faultgate.redact import builtin, find
|
|
4
|
+
|
|
5
|
+
R = builtin()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.parametrize("text, expected_kind, expected_val", [
|
|
9
|
+
("jwt token eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U in header",
|
|
10
|
+
"SECRET", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"),
|
|
11
|
+
("Authorization: Bearer abcdefghijklmnopqrstuvwxyz012345",
|
|
12
|
+
"SECRET", "abcdefghijklmnopqrstuvwxyz012345"),
|
|
13
|
+
("api key sk-proj-abcdefghijklmnopqrstuvwx",
|
|
14
|
+
"SECRET", "sk-proj-abcdefghijklmnopqrstuvwx"),
|
|
15
|
+
("aws key AKIAIOSFODNN7EXAMPLE",
|
|
16
|
+
"SECRET", "AKIAIOSFODNN7EXAMPLE"),
|
|
17
|
+
("contact user at jane.doe+x@corp.example.com today",
|
|
18
|
+
"EMAIL", "jane.doe+x@corp.example.com"),
|
|
19
|
+
("ssn is 123-45-6789 here",
|
|
20
|
+
"US_SSN", "123-45-6789"),
|
|
21
|
+
("card 4111 1111 1111 1111 on file",
|
|
22
|
+
"CREDIT_CARD", "4111 1111 1111 1111"),
|
|
23
|
+
("card 4111-1111-1111-1111",
|
|
24
|
+
"CREDIT_CARD", "4111-1111-1111-1111"),
|
|
25
|
+
("amex 3782 822463 10005 charged",
|
|
26
|
+
"CREDIT_CARD", "3782 822463 10005"),
|
|
27
|
+
("connect to 192.168.10.24 now",
|
|
28
|
+
"IP_ADDRESS", "192.168.10.24"),
|
|
29
|
+
("connect to 2001:0db8:85a3:0000:0000:8a2e:0370:7334 now",
|
|
30
|
+
"IP_ADDRESS", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
|
31
|
+
("call +1 (415) 555-0132 today",
|
|
32
|
+
"PHONE", "+1 (415) 555-0132"),
|
|
33
|
+
("call 020 7946 0958",
|
|
34
|
+
"PHONE", "020 7946 0958"),
|
|
35
|
+
])
|
|
36
|
+
def test_find_each_kind(text, expected_kind, expected_val):
|
|
37
|
+
spans = find(text)
|
|
38
|
+
assert len(spans) == 1
|
|
39
|
+
start, end, kind = spans[0]
|
|
40
|
+
assert kind == expected_kind
|
|
41
|
+
assert text[start:end] == expected_val
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_find_luhn_invalid_card_not_returned():
|
|
45
|
+
assert find("card 4111 1111 1111 1112") == []
|
|
46
|
+
assert find("card 4111 1111 1111 1112 rejected by gateway") == []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_find_card_plus_ssn_in_one_run():
|
|
50
|
+
text = "4111 1111 1111 1111 123-45-6789"
|
|
51
|
+
spans = find(text)
|
|
52
|
+
assert len(spans) == 2
|
|
53
|
+
assert spans[0] == (0, 19, "CREDIT_CARD")
|
|
54
|
+
assert spans[1] == (20, 31, "US_SSN")
|
|
55
|
+
assert text[spans[0][0]:spans[0][1]] == "4111 1111 1111 1111"
|
|
56
|
+
assert text[spans[1][0]:spans[1][1]] == "123-45-6789"
|
|
57
|
+
|
|
58
|
+
# Non-card digits in a run are not included in the card span
|
|
59
|
+
text_extra = "4111 1111 1111 1111 1234"
|
|
60
|
+
spans_extra = find(text_extra)
|
|
61
|
+
assert spans_extra == [(0, 19, "CREDIT_CARD")]
|
|
62
|
+
assert text_extra[spans_extra[0][0]:spans_extra[0][1]] == "4111 1111 1111 1111"
|
|
63
|
+
|
|
64
|
+
# Multiple Luhn-valid cards in one digit run
|
|
65
|
+
text_multi = "4111 1111 1111 1111 5555 4444 3333 1111"
|
|
66
|
+
spans_multi = find(text_multi)
|
|
67
|
+
assert len(spans_multi) == 2
|
|
68
|
+
assert spans_multi[0] == (0, 19, "CREDIT_CARD")
|
|
69
|
+
assert spans_multi[1] == (20, 39, "CREDIT_CARD")
|
|
70
|
+
assert text_multi[spans_multi[0][0]:spans_multi[0][1]] == "4111 1111 1111 1111"
|
|
71
|
+
assert text_multi[spans_multi[1][0]:spans_multi[1][1]] == "5555 4444 3333 1111"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_find_overlap_removed():
|
|
75
|
+
# Bearer secret containing an SSN pattern: Bearer (SECRET) precedes US_SSN in PATTERNS
|
|
76
|
+
text = "Bearer 123-45-6789-abcdef0123456789"
|
|
77
|
+
spans = find(text)
|
|
78
|
+
assert len(spans) == 1
|
|
79
|
+
start, end, kind = spans[0]
|
|
80
|
+
assert kind == "SECRET"
|
|
81
|
+
assert text[start:end] == "123-45-6789-abcdef0123456789"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_find_positions_slice_back_to_value():
|
|
85
|
+
text = (
|
|
86
|
+
"Refund for jane@corp.example.com of card 4111 1111 1111 1111 "
|
|
87
|
+
"verified with ssn 123-45-6789 on server 192.168.1.50 now. "
|
|
88
|
+
"Call +1 (415) 555-0132 or use token sk-proj-abcdefghijklmnopqrstuvwx."
|
|
89
|
+
)
|
|
90
|
+
spans = find(text)
|
|
91
|
+
assert len(spans) == 6
|
|
92
|
+
|
|
93
|
+
# Verify sorted strictly by start and non-overlapping
|
|
94
|
+
for i in range(len(spans) - 1):
|
|
95
|
+
assert spans[i][0] < spans[i + 1][0]
|
|
96
|
+
assert spans[i][1] <= spans[i + 1][0]
|
|
97
|
+
|
|
98
|
+
# Verify each position slices back to the exact value
|
|
99
|
+
expected_values = [
|
|
100
|
+
"jane@corp.example.com",
|
|
101
|
+
"4111 1111 1111 1111",
|
|
102
|
+
"123-45-6789",
|
|
103
|
+
"192.168.1.50",
|
|
104
|
+
"+1 (415) 555-0132",
|
|
105
|
+
"sk-proj-abcdefghijklmnopqrstuvwx",
|
|
106
|
+
]
|
|
107
|
+
for (start, end, _), expected in zip(spans, expected_values):
|
|
108
|
+
assert text[start:end] == expected
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_find_empty_string():
|
|
112
|
+
assert find("") == []
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_find_phone_digit_count_filter():
|
|
116
|
+
# 8 digits: not kept
|
|
117
|
+
assert find("call 123-45678") == []
|
|
118
|
+
# 9 digits: kept
|
|
119
|
+
assert len(find("call 123-456-789")) == 1
|
|
120
|
+
# 11 digits: kept
|
|
121
|
+
assert len(find("call +1 (415) 555-0132")) == 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_builtin_replaces_spans_right_to_left():
|
|
125
|
+
text = "4111 1111 1111 1111 123-45-6789"
|
|
126
|
+
assert R(text) == "<CREDIT_CARD> <US_SSN>"
|
|
127
|
+
|
|
128
|
+
text2 = "call +1 (415) 555-0132 and mail test@example.com"
|
|
129
|
+
assert R(text2) == "call <PHONE> and mail <EMAIL>"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|