forked from ChelseaKR/power-content-check
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodel.py
More file actions
313 lines (254 loc) · 10.5 KB
/
Copy pathmodel.py
File metadata and controls
313 lines (254 loc) · 10.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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
"""Core data types.
Nothing in this module knows how to read a document or how to enforce a rule.
It only describes what a check is, what a result is, and what a report is.
"""
from __future__ import annotations
import dataclasses
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any
class Status(StrEnum):
"""Outcome of a single check against a single document.
There is deliberately no "unknown but probably fine" state. Anything the
tool could not measure is NOT_EVALUATED, which is never treated as a pass.
"""
CONFORMS = "conforms"
DOES_NOT_CONFORM = "does_not_conform"
NOT_EVALUATED = "not_evaluated"
class Basis(StrEnum):
"""Where the requirement a check enforces actually comes from.
REGULATION_TEXT
The regulation enumerates the element in words. The check enforces
what the text says.
TEMPLATE_FORMAT
The element is part of the label format the California Energy
Commission itself issues. Title 20 CCR section 1393.1(i) provides that
the Energy Commission generates the label or supplies the template and
that a retail supplier may not alter the format. A check on this basis
enforces the issued format, not a sentence of regulatory text.
"""
REGULATION_TEXT = "regulation_text"
TEMPLATE_FORMAT = "template_format"
class Blocker(StrEnum):
"""Why a registered check enforces nothing, and whether that can change.
Registering a requirement the tool does not measure keeps the gap visible.
It also invites the same question to be reopened every time someone reads
the catalog. This enum answers the question once.
PERMANENT
No version of this tool that reads the document it is handed can decide
the requirement, because the fact it turns on is not in the document,
or because deciding it would mean inventing a rule the published
sources do not supply. Reopening it needs a different tool or a changed
regulation, not more effort here.
CONDITIONAL
Blocked on something nameable that could change: a capability this tool
has chosen not to build, or a document that does not exist yet. The
reason says what would unblock it.
"""
PERMANENT = "permanent"
CONDITIONAL = "conditional"
class Readability(StrEnum):
READABLE = "readable"
UNREADABLE = "unreadable"
@dataclass(frozen=True)
class Source:
"""A published document that a citation points at."""
key: str
title: str
publisher: str
url: str
retrieved: str
effective: str | None = None
def to_dict(self) -> dict[str, Any]:
return dataclasses.asdict(self)
@dataclass(frozen=True)
class Citation:
"""The published requirement a check enforces.
Every check must carry one. A check with no citation cannot be registered.
"""
source: Source
locator: str
quote: str
def to_dict(self) -> dict[str, Any]:
return {
"source_key": self.source.key,
"source_title": self.source.title,
"source_url": self.source.url,
"source_effective": self.source.effective,
"source_retrieved": self.source.retrieved,
"locator": self.locator,
"quote": self.quote,
}
@dataclass(frozen=True)
class CheckSpec:
"""A registered check.
``id`` is stable for the life of the project. It is never renumbered and
never reused for a different requirement. See tests/test_registry.py, which
pins the full identifier set.
"""
id: str
title: str
citation: Citation
basis: Basis
implemented: bool
what_it_looks_for: str
unimplemented_reason: str | None = None
blocker: Blocker | None = None
def __post_init__(self) -> None:
if self.implemented and self.unimplemented_reason is not None:
raise ValueError(f"{self.id}: implemented check carries an unimplemented reason")
if self.implemented and self.blocker is not None:
raise ValueError(f"{self.id}: implemented check carries a blocker")
if not self.implemented and not self.unimplemented_reason:
raise ValueError(f"{self.id}: unimplemented check must say why")
if not self.implemented and self.blocker is None:
raise ValueError(f"{self.id}: unimplemented check must say whether that is permanent")
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"title": self.title,
"basis": self.basis.value,
"implemented": self.implemented,
"what_it_looks_for": self.what_it_looks_for,
"unimplemented_reason": self.unimplemented_reason,
"blocker": self.blocker.value if self.blocker else None,
"citation": self.citation.to_dict(),
}
@dataclass(frozen=True)
class CheckResult:
"""The outcome of one check against one document."""
check_id: str
status: Status
finding: str
detail: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"check_id": self.check_id,
"status": self.status.value,
"finding": self.finding,
"detail": self.detail,
}
@dataclass(frozen=True)
class DocumentReport:
"""Everything the tool concluded about one document."""
path: str
readability: Readability
unreadable_reason: str | None
sha256: str | None
page_count: int | None
results: list[CheckResult] = field(default_factory=list)
#: Raster images the document declares, or None when that is unknown or the
#: input is plain text. Not a regulatory quantity; it qualifies what an
#: absence finding is entitled to mean.
image_count: int | None = None
#: Times the document paints a vector path, or None when unknown or the
#: input is plain text. The companion to ``image_count``: what the page
#: declares beside what the page does. No check reads either. See ADR 0012.
vector_shape_count: int | None = None
#: The sentence describing what the tool was able to look at. Reproduced on
#: the report and appended to every deviation.
extraction_basis: str | None = None
@property
def counts(self) -> dict[str, int]:
tally = {status.value: 0 for status in Status}
for result in self.results:
tally[result.status.value] += 1
return tally
@property
def has_nonconformance(self) -> bool:
return any(r.status is Status.DOES_NOT_CONFORM for r in self.results)
@property
def has_unevaluated(self) -> bool:
return any(r.status is Status.NOT_EVALUATED for r in self.results)
def to_dict(self) -> dict[str, Any]:
return {
"path": self.path,
"readability": self.readability.value,
"unreadable_reason": self.unreadable_reason,
"sha256": self.sha256,
"page_count": self.page_count,
"image_count": self.image_count,
"vector_shape_count": self.vector_shape_count,
"extraction_basis": self.extraction_basis,
"counts": self.counts,
"results": [r.to_dict() for r in self.results],
}
#: Version of the JSON report's shape, not of the tool and not of the
#: ruleset. Within one version every key is append only; removing a key,
#: renaming one, or changing what a key holds is a breaking change and moves
#: this number. See docs/adr/0010. Pinned by tests/test_report.py, which
#: asserts the exact key sets, so a shape change fails here before it fails
#: for a consumer.
SCHEMA_VERSION = 1
class ExitCode:
"""Process exit codes.
Precedence runs highest first, so a run that both checked nothing and found
a deviation cannot report the quieter of the two.
3 Nothing was checked. An empty denominator is never a pass.
2 At least one check could not be evaluated, including any document the
tool could not read.
1 At least one check found a deviation from the prescribed format.
0 Every document was readable and every implemented check conformed.
"""
OK = 0
NONCONFORMANCE = 1
NOT_EVALUATED = 2
NOTHING_CHECKED = 3
USAGE_ERROR = 64
@dataclass(frozen=True)
class RunReport:
"""The result of one invocation."""
tool: str
tool_version: str
ruleset_id: str
ruleset_effective: str
generated_at: str
documents: list[DocumentReport]
notice: str
#: Files found inside a directory the caller named that are not a supported
#: label format, so were not checked. Named rather than dropped in silence,
#: because the Energy Commission publishes a second rendering of each label
#: beside it and a reader should be told which file was read. These are not
#: documents and they are not in any count.
skipped: list[str] = field(default_factory=list)
@property
def exit_code(self) -> int:
if not self.documents:
return ExitCode.NOTHING_CHECKED
if any(d.readability is Readability.UNREADABLE for d in self.documents):
return ExitCode.NOT_EVALUATED
if any(d.has_unevaluated for d in self.documents):
return ExitCode.NOT_EVALUATED
if any(d.has_nonconformance for d in self.documents):
return ExitCode.NONCONFORMANCE
return ExitCode.OK
@property
def summary(self) -> dict[str, Any]:
return {
"documents_checked": len(self.documents),
"documents_readable": sum(
1 for d in self.documents if d.readability is Readability.READABLE
),
"documents_unreadable": sum(
1 for d in self.documents if d.readability is Readability.UNREADABLE
),
"conforms": sum(d.counts[Status.CONFORMS.value] for d in self.documents),
"does_not_conform": sum(
d.counts[Status.DOES_NOT_CONFORM.value] for d in self.documents
),
"not_evaluated": sum(d.counts[Status.NOT_EVALUATED.value] for d in self.documents),
}
def to_dict(self) -> dict[str, Any]:
return {
"schema_version": SCHEMA_VERSION,
"tool": self.tool,
"tool_version": self.tool_version,
"ruleset_id": self.ruleset_id,
"ruleset_effective": self.ruleset_effective,
"generated_at": self.generated_at,
"notice": self.notice,
"skipped": list(self.skipped),
"summary": self.summary,
"exit_code": self.exit_code,
"documents": [d.to_dict() for d in self.documents],
}