forked from ChelseaKR/ca-tariff-parse
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaudit.py
More file actions
105 lines (79 loc) · 3.5 KB
/
Copy pathaudit.py
File metadata and controls
105 lines (79 loc) · 3.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""Independent check that nothing reached the output without a citation.
:class:`~ca_tariff_parse.model.Cited` already makes an uncited value impossible
to construct. This module is the second, independent guard: it walks the
serialised result and fails if it can reach a value-bearing leaf that is not
inside a ``Cited`` envelope.
The two mechanisms are deliberately unrelated. If a future recognizer bypasses
the model, or a new field is added to a dataclass and forgotten, this walk still
catches it before the JSON is written.
"""
from __future__ import annotations
from typing import Any
__all__ = ["UncitedValueError", "assert_fully_cited"]
class UncitedValueError(AssertionError):
"""Raised when the output contains a value with no citation behind it."""
#: Keys whose values are structural metadata about the parse rather than facts
#: read out of the source document. Each is restricted to a closed vocabulary
#: or a self describing block, checked below.
STRUCTURAL_KEYS = frozenset(
{"schema", "parser_version", "disclaimer", "source", "coverage", "unparsed"}
)
#: Controlled vocabularies. A structural field may only hold one of these.
VOCABULARIES: dict[str, frozenset[str]] = {
"kind": frozenset({"energy_usage", "fixed_charge", "credit"}),
"currency": frozenset({"USD"}),
"disposition": frozenset({"included", "excluded", "required"}),
}
#: Keys inside a serialised Cited envelope.
CITED_KEYS = frozenset({"value", "provenance"})
def _is_cited(node: Any) -> bool:
return (
isinstance(node, dict)
and set(node) >= CITED_KEYS
and isinstance(node.get("provenance"), dict)
)
def _check_provenance(node: dict[str, Any], path: str) -> None:
required = ("document_id", "document_sha256", "page", "section", "line", "snippet")
for field in required:
if not node.get(field):
raise UncitedValueError(f"{path}.provenance.{field} is empty or missing")
def _check_structural(key: str, value: Any, path: str) -> None:
"""Validate a field that describes the parse rather than quoting the source."""
if key in VOCABULARIES:
if value not in VOCABULARIES[key]:
raise UncitedValueError(
f"{path} holds {value!r}, which is outside the controlled "
f"vocabulary {sorted(VOCABULARIES[key])}"
)
return
if not isinstance(value, bool):
raise UncitedValueError(f"{path} must be a bool, got {type(value).__name__}")
def _walk_mapping(node: dict[str, Any], path: str) -> None:
if _is_cited(node):
_check_provenance(node["provenance"], path)
return
for key, value in node.items():
child = f"{path}.{key}" if path else key
if key in STRUCTURAL_KEYS:
continue
if key in VOCABULARIES or key == "residual":
_check_structural(key, value, child)
continue
_walk(value, child)
def _walk(node: Any, path: str) -> None:
if isinstance(node, dict):
_walk_mapping(node, path)
return
if isinstance(node, list):
for index, item in enumerate(node):
_walk(item, f"{path}[{index}]")
return
if node is None:
return
raise UncitedValueError(
f"{path} is a bare {type(node).__name__} ({node!r}) with no citation. "
"Every emitted value must be wrapped in a Cited envelope."
)
def assert_fully_cited(payload: dict[str, Any]) -> None:
"""Raise :class:`UncitedValueError` unless every value carries provenance."""
_walk(payload, "")