maskflow-cli 0.7.0__tar.gz → 0.8.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.
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/PKG-INFO +4 -2
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/pyproject.toml +14 -2
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/app.py +2 -0
- maskflow_cli-0.8.0/src/maskflow_cli/bench_render.py +88 -0
- maskflow_cli-0.8.0/src/maskflow_cli/commands/bench_cmd.py +68 -0
- maskflow_cli-0.8.0/tests/test_cli_bench.py +76 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/.dockerignore +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/.gitignore +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/Dockerfile +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/README.md +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/examples/README.md +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/examples/generate_sample.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/examples/sample-llm-traffic.jsonl +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/packaging/README.md +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/packaging/_entry.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/packaging/maskflow.spec +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/__init__.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/commands/__init__.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/commands/config_cmd.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/commands/doctor_cmd.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/commands/explain_cmd.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/doctor.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/doctor_render.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/evidence.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/explain.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/explain_render.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/render.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/__init__.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/aggregate.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/checkpoint.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/cmd.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/errors.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/fieldsel.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/pipeline.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/report/__init__.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/report/assets.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/report/build.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/report/csv_out.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/report/html.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/report/json_out.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/report/summary.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/severity.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/__init__.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/_api_common.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/_files.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/_http.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/_meta.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/base.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/csv.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/dir.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/helicone.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/jsonl.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/langfuse.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/langsmith.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/postgres.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/sources/s3.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/spec.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/src/maskflow_cli/scan/worker.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/conftest.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/fixtures/partial.toml +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/fixtures/typo.toml +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/fixtures/valid.toml +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/__init__.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/_fuzz_corpus.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/conftest.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/test_aggregate_and_severity.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/test_example_file.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/test_fieldsel.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/test_pipeline_and_report.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/test_report_no_pii_leak.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/test_sources_api.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/scan/test_sources_files.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/test_cli_doctor.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/test_cli_explain.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/test_cli_show.py +0 -0
- {maskflow_cli-0.7.0 → maskflow_cli-0.8.0}/tests/test_cli_validate.py +0 -0
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: maskflow-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Command-line interface for MaskFlow: .maskflowrc config validation and inspection
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Requires-Dist: httpx>=0.27
|
|
8
|
-
Requires-Dist: maskflow-core[yaml]<0.
|
|
8
|
+
Requires-Dist: maskflow-core[yaml]<0.9,>=0.6.0
|
|
9
9
|
Requires-Dist: maskflow-pack-india<0.6,>=0.1.0
|
|
10
10
|
Requires-Dist: maskflow-pack-intl<0.4,>=0.3.0
|
|
11
11
|
Requires-Dist: rich>=13.0
|
|
12
12
|
Requires-Dist: tomli-w>=1.0
|
|
13
13
|
Requires-Dist: typer>=0.12
|
|
14
|
+
Provides-Extra: bench
|
|
15
|
+
Requires-Dist: maskflow-bench<0.2,>=0.1.0; extra == 'bench'
|
|
14
16
|
Provides-Extra: dev
|
|
15
17
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
16
18
|
Provides-Extra: evidence
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "maskflow-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Command-line interface for MaskFlow: .maskflowrc config validation and inspection"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ dependencies = [
|
|
|
14
14
|
# -- pack-intl/pack-india now require it, so the floor moves with theirs
|
|
15
15
|
# even though this package doesn't import maskflow_core.recognizer
|
|
16
16
|
# directly.
|
|
17
|
-
"maskflow-core[yaml]>=0.6.0,<0.
|
|
17
|
+
"maskflow-core[yaml]>=0.6.0,<0.9",
|
|
18
18
|
# maskflow_pack_intl is imported (side-effect only, to populate the
|
|
19
19
|
# PIIType registry) for `maskflow config validate`'s soft entity-name
|
|
20
20
|
# cross-check -- see app.py. pack-intl 0.1.0 itself requires core<0.3,
|
|
@@ -71,12 +71,24 @@ s3 = [
|
|
|
71
71
|
postgres = [
|
|
72
72
|
"psycopg[binary]>=3.1",
|
|
73
73
|
]
|
|
74
|
+
# `maskflow bench --my-data` (issue #36): the corpus-agnostic scoring core
|
|
75
|
+
# (JSONL loading, label canonicalization, strict/partial-overlap matching,
|
|
76
|
+
# report writers). Same policy as [evidence] above -- opt-in so a bare CLI
|
|
77
|
+
# install (and the standalone PyInstaller binary) doesn't require it;
|
|
78
|
+
# bench_cmd.py imports it lazily and every other command runs without it.
|
|
79
|
+
# maskflow-bench itself depends only on maskflow-core, not on the heavy
|
|
80
|
+
# competitor-comparison adapters (presidio/mask-privacy/anthropic) -- those
|
|
81
|
+
# stay dev-only in bench/, never installed here either way.
|
|
82
|
+
bench = [
|
|
83
|
+
"maskflow-bench>=0.1.0,<0.2",
|
|
84
|
+
]
|
|
74
85
|
|
|
75
86
|
[tool.uv.sources]
|
|
76
87
|
maskflow-core = { workspace = true }
|
|
77
88
|
maskflow-pack-intl = { workspace = true }
|
|
78
89
|
maskflow-pack-india = { workspace = true }
|
|
79
90
|
maskflow-evidence = { workspace = true }
|
|
91
|
+
maskflow-bench = { workspace = true }
|
|
80
92
|
|
|
81
93
|
[tool.uv]
|
|
82
94
|
package = true
|
|
@@ -6,6 +6,7 @@ import maskflow_pack_india # noqa: F401 -- import side effect registers pack-in
|
|
|
6
6
|
import maskflow_pack_intl # noqa: F401 -- import side effect registers pack-intl's entity types
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
+
from .commands.bench_cmd import bench
|
|
9
10
|
from .commands.config_cmd import app as config_app
|
|
10
11
|
from .commands.doctor_cmd import doctor
|
|
11
12
|
from .commands.explain_cmd import explain
|
|
@@ -16,6 +17,7 @@ app.add_typer(config_app, name="config")
|
|
|
16
17
|
app.command("doctor")(doctor)
|
|
17
18
|
app.command("explain")(explain)
|
|
18
19
|
app.command("scan")(scan)
|
|
20
|
+
app.command("bench")(bench)
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def main() -> None:
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Rich rendering for `maskflow bench --my-data`. Kept separate from
|
|
2
|
+
bench_cmd.py so the command body stays thin, same split as
|
|
3
|
+
doctor.py/doctor_render.py.
|
|
4
|
+
|
|
5
|
+
Only type-checked against maskflow-bench (TYPE_CHECKING guard below,
|
|
6
|
+
`from __future__ import annotations` keeps every annotation an
|
|
7
|
+
unevaluated string at runtime) -- this module never constructs a
|
|
8
|
+
PRFResult/AdapterRunResult itself, only duck-types over one handed in by
|
|
9
|
+
bench_cmd.py, so it never needs the optional [bench] extra installed just
|
|
10
|
+
to be imported (bench_cmd.py's own lazy import guards the actual need for
|
|
11
|
+
it)."""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from rich import box
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from maskflow_bench.matching import PRFResult
|
|
23
|
+
from maskflow_bench.runner import AdapterRunResult
|
|
24
|
+
|
|
25
|
+
# Same rationale as doctor_render.py's: a fixed width so output doesn't
|
|
26
|
+
# wrap unpredictably under a non-tty (CI logs, CliRunner in tests).
|
|
27
|
+
CONSOLE_WIDTH = 100
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cell(prf: PRFResult | None, field: str) -> str:
|
|
31
|
+
"""'—' when the metric is undefined (matching.PRFResult.f1's own
|
|
32
|
+
convention, see PR #97): no predictions and no gold for this type.
|
|
33
|
+
A real measured zero renders as "0.0%", not "—"."""
|
|
34
|
+
if prf is None:
|
|
35
|
+
return "—"
|
|
36
|
+
value = getattr(prf, field)
|
|
37
|
+
return "—" if value is None else f"{value:.1%}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _results_table(canonical_labels: tuple[str, ...], result: AdapterRunResult) -> Table:
|
|
41
|
+
table = Table(box=box.SIMPLE_HEAD, show_edge=False, pad_edge=False)
|
|
42
|
+
table.add_column("Entity")
|
|
43
|
+
table.add_column("Strict P", justify="right")
|
|
44
|
+
table.add_column("Strict R", justify="right")
|
|
45
|
+
table.add_column("Strict F1", justify="right")
|
|
46
|
+
table.add_column("Partial P", justify="right")
|
|
47
|
+
table.add_column("Partial R", justify="right")
|
|
48
|
+
table.add_column("Partial F1", justify="right")
|
|
49
|
+
for label in canonical_labels:
|
|
50
|
+
strict = result.strict.get(label)
|
|
51
|
+
partial = result.partial.get(label)
|
|
52
|
+
table.add_row(
|
|
53
|
+
label,
|
|
54
|
+
_cell(strict, "precision"),
|
|
55
|
+
_cell(strict, "recall"),
|
|
56
|
+
_cell(strict, "f1"),
|
|
57
|
+
_cell(partial, "precision"),
|
|
58
|
+
_cell(partial, "recall"),
|
|
59
|
+
_cell(partial, "f1"),
|
|
60
|
+
)
|
|
61
|
+
return table
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def render_my_data_result(
|
|
65
|
+
console: Console,
|
|
66
|
+
num_docs: int,
|
|
67
|
+
canonical_labels: tuple[str, ...],
|
|
68
|
+
result: AdapterRunResult,
|
|
69
|
+
) -> None:
|
|
70
|
+
console.print("[bold]MaskFlow bench --my-data[/bold]")
|
|
71
|
+
console.print(f"{num_docs} document(s), {len(canonical_labels)} entity type(s) in your labels.")
|
|
72
|
+
console.print()
|
|
73
|
+
if not canonical_labels:
|
|
74
|
+
console.print(
|
|
75
|
+
'[yellow]No gold (value_class="positive") entities found -- nothing to '
|
|
76
|
+
"score. Check your file against docs/bench.md's schema.[/yellow]"
|
|
77
|
+
)
|
|
78
|
+
return
|
|
79
|
+
console.print(_results_table(canonical_labels, result))
|
|
80
|
+
console.print()
|
|
81
|
+
console.print(
|
|
82
|
+
'[dim]"—" means undefined (no predictions and no gold for that type); '
|
|
83
|
+
'"0.0%" is a measured zero. Strict = exact span match, partial = any overlap.[/dim]'
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def make_console() -> Console:
|
|
88
|
+
return Console(width=CONSOLE_WIDTH)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""`maskflow bench --my-data <path>` -- scores MaskFlow's own detector
|
|
2
|
+
against a user's own labelled documents and prints per-entity
|
|
3
|
+
precision/recall/F1. Not a comparison tool (see docs/bench.md); the
|
|
4
|
+
multi-adapter comparison against Presidio/mask-privacy/etc. that produced
|
|
5
|
+
the published IndiaPII-Bench figures lives in bench/ (repo dev tooling).
|
|
6
|
+
|
|
7
|
+
`maskflow-bench` is an **optional** dependency (`pip install
|
|
8
|
+
'maskflow-cli[bench]'`) -- imported lazily here, same policy as
|
|
9
|
+
`maskflow_cli.evidence`, so a bare CLI install (and the standalone
|
|
10
|
+
PyInstaller binary) doesn't require it and every other command runs
|
|
11
|
+
without it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
|
|
20
|
+
from ..bench_render import make_console, render_my_data_result
|
|
21
|
+
|
|
22
|
+
_MISSING = (
|
|
23
|
+
"`maskflow bench` needs the bench extra -- install it with: pip install 'maskflow-cli[bench]'"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_MY_DATA_OPTION = typer.Option(
|
|
27
|
+
..., "--my-data", exists=True, dir_okay=False, help="Path to your labelled JSONL file."
|
|
28
|
+
)
|
|
29
|
+
_OUT_OPTION = typer.Option(
|
|
30
|
+
None, "--out", help="Directory to also write results.json and results.md into."
|
|
31
|
+
)
|
|
32
|
+
_LIMIT_OPTION = typer.Option(
|
|
33
|
+
None, "--limit", help="Score only the first N documents (useful to smoke-test a large file)."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def bench(
|
|
38
|
+
my_data: Path = _MY_DATA_OPTION,
|
|
39
|
+
out: Path | None = _OUT_OPTION,
|
|
40
|
+
limit: int | None = _LIMIT_OPTION,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Runs MaskFlow's detector over your own labelled JSONL file and
|
|
43
|
+
reports per-entity precision/recall/F1 under both strict-span and
|
|
44
|
+
partial-overlap matching. See docs/bench.md for the file schema."""
|
|
45
|
+
try:
|
|
46
|
+
from maskflow_bench.loader import LoaderError
|
|
47
|
+
from maskflow_bench.report import write_report
|
|
48
|
+
from maskflow_bench.scorer import score_my_data
|
|
49
|
+
except ModuleNotFoundError as exc:
|
|
50
|
+
typer.echo(_MISSING, err=True)
|
|
51
|
+
raise typer.Exit(code=1) from exc
|
|
52
|
+
|
|
53
|
+
console = make_console()
|
|
54
|
+
try:
|
|
55
|
+
num_docs, labels, result = score_my_data(my_data, limit=limit)
|
|
56
|
+
except LoaderError as exc:
|
|
57
|
+
typer.echo(f"{my_data}:{exc}", err=True)
|
|
58
|
+
raise typer.Exit(code=1) from exc
|
|
59
|
+
|
|
60
|
+
if num_docs == 0:
|
|
61
|
+
typer.echo(f"{my_data}: no documents found -- is the file empty?", err=True)
|
|
62
|
+
raise typer.Exit(code=1)
|
|
63
|
+
|
|
64
|
+
render_my_data_result(console, num_docs, labels, result)
|
|
65
|
+
|
|
66
|
+
if out is not None:
|
|
67
|
+
write_report(out, "my-data", num_docs, labels, {"maskflow": result})
|
|
68
|
+
console.print(f"\nwrote {out / 'results.json'} and {out / 'results.md'}")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from maskflow_bench.scorer import score_my_data
|
|
7
|
+
from maskflow_cli.app import app
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
runner = CliRunner()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _write_fixture(path: Path) -> None:
|
|
14
|
+
# PAN below is structurally well-formed (5 letters + 4 digits + 1
|
|
15
|
+
# letter) but fabricated for this test, not a real allotted number --
|
|
16
|
+
# synthetic fixture data only, per project policy.
|
|
17
|
+
rows = [
|
|
18
|
+
{
|
|
19
|
+
"id": "fixture-1",
|
|
20
|
+
"text": "Please update my PAN ABCDE1234F on file.",
|
|
21
|
+
"entities": [{"start": 22, "end": 32, "label": "PAN"}],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "fixture-2",
|
|
25
|
+
"text": "No PII in this line at all.",
|
|
26
|
+
"entities": [],
|
|
27
|
+
},
|
|
28
|
+
]
|
|
29
|
+
path.write_text("\n".join(json.dumps(r) for r in rows) + "\n", encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_my_data_prints_a_per_entity_table(tmp_path: Path) -> None:
|
|
33
|
+
fixture = tmp_path / "my.jsonl"
|
|
34
|
+
_write_fixture(fixture)
|
|
35
|
+
|
|
36
|
+
result = runner.invoke(app, ["bench", "--my-data", str(fixture)])
|
|
37
|
+
|
|
38
|
+
assert result.exit_code == 0
|
|
39
|
+
assert "PAN" in result.stdout
|
|
40
|
+
assert "2 document(s)" in result.stdout
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_my_data_out_writes_results_matching_the_scorer_directly(tmp_path: Path) -> None:
|
|
44
|
+
fixture = tmp_path / "my.jsonl"
|
|
45
|
+
_write_fixture(fixture)
|
|
46
|
+
out_dir = tmp_path / "out"
|
|
47
|
+
|
|
48
|
+
result = runner.invoke(app, ["bench", "--my-data", str(fixture), "--out", str(out_dir)])
|
|
49
|
+
|
|
50
|
+
assert result.exit_code == 0
|
|
51
|
+
data = json.loads((out_dir / "results.json").read_text(encoding="utf-8"))
|
|
52
|
+
assert (out_dir / "results.md").exists()
|
|
53
|
+
|
|
54
|
+
num_docs, labels, expected = score_my_data(fixture)
|
|
55
|
+
assert data["num_docs"] == num_docs
|
|
56
|
+
assert data["canonical_labels"] == list(labels)
|
|
57
|
+
assert data["adapters"]["maskflow"]["available"] is True
|
|
58
|
+
assert data["adapters"]["maskflow"]["strict"]["PAN"]["tp"] == expected.strict["PAN"].tp
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_my_data_malformed_line_reports_line_number_and_exits_nonzero(tmp_path: Path) -> None:
|
|
62
|
+
fixture = tmp_path / "bad.jsonl"
|
|
63
|
+
fixture.write_text(
|
|
64
|
+
json.dumps({"text": "ok", "entities": []}) + "\n" + json.dumps({"text": "missing ents"}),
|
|
65
|
+
encoding="utf-8",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
result = runner.invoke(app, ["bench", "--my-data", str(fixture)])
|
|
69
|
+
|
|
70
|
+
assert result.exit_code != 0
|
|
71
|
+
assert "line 2" in result.stdout + result.stderr
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_my_data_missing_file_exits_nonzero(tmp_path: Path) -> None:
|
|
75
|
+
result = runner.invoke(app, ["bench", "--my-data", str(tmp_path / "does-not-exist.jsonl")])
|
|
76
|
+
assert result.exit_code != 0
|
|
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
|
|
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
|