-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathrender_examples.py
More file actions
261 lines (226 loc) · 10.3 KB
/
Copy pathrender_examples.py
File metadata and controls
261 lines (226 loc) · 10.3 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
"""Regenerate the published example HTML diffs and their landing page under `examples/`.
Run from anywhere:
uv run python scripts/render_examples.py
Each example is a rendered diff between two versions of one bill in the committed
corpus, checked into the repo so reviewers can see real output without running the
pipeline themselves, and deployed to GitHub Pages as the project's public demo by
`.github/workflows/update-examples.yml`. Re-run after any change that affects diff
output (parser, diff classifier, renderer). The output HTML is marked
`linguist-generated=true` in `.gitattributes` so it doesn't pollute git blame or PR
diff views by default.
**Every example goes through the same pipeline the web app and CLI use** — the XML
ones via `compare.xml`, the PDF ones via `compare.pdf` (#42). This
script contributes no rendering of its own, so a published example cannot show a
reader something the tool would not produce for them. Two of these reports were
previously generated by a separate CI invocation and fell fifteenfold behind the
renderer without anything turning red; `tests/test_committed_examples.py` now
re-renders every spec below and compares.
`EXAMPLES_TO_RENDER` is the single source of truth for the published set: adding a
spec renders the report *and* lists it on `index.html`. The blurbs live here rather
than in the page so the two cannot disagree.
"""
from __future__ import annotations
import html
import sys
from dataclasses import dataclass
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
# `scripts/` is not an import root: running this file directly puts only its own directory
# on `sys.path`, so the checkout root has to be added by hand for `tests.corpus_paths` to
# resolve. Same bootstrap as its siblings here (#401 moved this module in beside them).
sys.path.insert(0, str(PROJECT_ROOT))
from deltatrack.compare.pdf import compare_pdfs_html # noqa: E402
from deltatrack.compare.xml import compare_xml_files_html # noqa: E402
from deltatrack.version_stems import label_from_stem, version_number_from_stem # noqa: E402
from tests.corpus_paths import FIXTURES_DIR # noqa: E402
BILLS = FIXTURES_DIR
EXAMPLES = PROJECT_ROOT / "examples"
@dataclass(frozen=True)
class ExampleSpec:
"""One bill version-pair to publish. Filenames follow `<n>_<label>.{xml,pdf}`."""
bill_dir: str # under the corpus, e.g. "118-hr-8752"
bill_type: str # "hr", "s", etc.
bill_number: int
v1_filename_stem: str # e.g. "1_reported-in-house"
v2_filename_stem: str # e.g. "2_engrossed-in-house"
formats: tuple[str, ...] # which pipelines to render: "xml", "pdf", or both
title: str # heading on the landing page
blurb: str # one-line description on the landing page
slug: str = "" # disambiguates two pairs from the same bill; omit for the first
def output_name(self, fmt: str) -> str:
"""Filename for one rendered report. Stable — README and Pages URLs point here."""
stem = f"{self.bill_type}{self.bill_number}"
if self.slug:
stem = f"{stem}_{self.slug}"
return f"{stem}_{fmt}_diff.html"
# Rendering both pipelines for one pair (HR 8752) is deliberate: it is the only way a
# reader can see that the PDF and XML paths produce the same report for the same bill.
EXAMPLES_TO_RENDER: list[ExampleSpec] = [
ExampleSpec(
bill_dir="118-hr-8752",
bill_type="hr",
bill_number=8752,
v1_filename_stem="1_reported-in-house",
v2_filename_stem="2_engrossed-in-house",
formats=("xml", "pdf"),
title="HR 8752 — Committee vs. Floor",
blurb=(
"Reported in House vs. engrossed in House: floor-amendment changes with "
"account-level dollar amounts. Rendered from both source formats — compare "
"the two to see the pipelines agree."
),
),
ExampleSpec(
bill_dir="118-hr-4366",
bill_type="hr",
bill_number=4366,
v1_filename_stem="1_reported-in-house",
v2_filename_stem="2_engrossed-in-house",
formats=("xml",),
slug="committee_vs_floor",
title="HR 4366 — Committee vs. Floor",
blurb=(
"Military Construction / Veterans Affairs, FY2024. Committee-reported vs. "
"floor-passed: a moderate diff with amendment-annotated amount changes."
),
),
ExampleSpec(
bill_dir="118-hr-4366",
bill_type="hr",
bill_number=4366,
v1_filename_stem="2_engrossed-in-house",
v2_filename_stem="4_engrossed-amendment-senate",
formats=("xml",),
slug="house_vs_senate",
title="HR 4366 — House vs. Senate",
blurb=(
"The same bill after the Senate substantially rewrites it. A large diff, "
"and the best look at how the report holds up at scale."
),
),
]
def render_xml_diff(spec: ExampleSpec) -> Path:
# Delegate to the same pipeline the web app and CLI use, exactly as the PDF
# renderer below does, so a published example is byte-for-byte the report a
# reader gets by running DeltaTrack on the same two files.
bill_dir = BILLS / spec.bill_dir
html_out = compare_xml_files_html(
bill_dir / f"{spec.v1_filename_stem}.xml",
bill_dir / f"{spec.v2_filename_stem}.xml",
)
out = EXAMPLES / spec.output_name("xml")
out.write_text(html_out)
return out
def render_pdf_diff(spec: ExampleSpec) -> Path:
# Delegate to the same pipeline the web app and CLI use, so the committed
# example carries the full-bill text view, section TOC, and embedded export
# rather than the thin per-change-only report.
bill_dir = BILLS / spec.bill_dir
html_out = compare_pdfs_html(
(bill_dir / f"{spec.v1_filename_stem}.pdf").read_bytes(),
(bill_dir / f"{spec.v2_filename_stem}.pdf").read_bytes(),
start_label=label_from_stem(spec.v1_filename_stem),
end_label=label_from_stem(spec.v2_filename_stem),
# Known here because the corpus filenames are numbered; an upload has no
# equivalent, which is why the parameter exists rather than being derived.
start_version_number=version_number_from_stem(spec.v1_filename_stem),
end_version_number=version_number_from_stem(spec.v2_filename_stem),
)
out = EXAMPLES / spec.output_name("pdf")
out.write_text(html_out)
return out
RENDERERS = {"xml": render_xml_diff, "pdf": render_pdf_diff}
#: Brand tokens, kept to the subset the landing page uses. Copied from the report
#: stylesheet in `formatters/diff_html.py` so the page a visitor lands on and the
#: reports it links look like one product. `tests/test_committed_examples.py` fails if
#: they drift apart. (Sharing the values with BillTrax upstream is epic #37.)
INDEX_TOKENS = """ --background: #f9f7f5; --foreground: #1c1c3a;
--card: #ffffff; --primary: #2c2c5c; --muted-foreground: #686881;
--border: #e3ddd7; --radius: 0.625rem;
--font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--font-serif: ui-serif, Georgia, 'Times New Roman', serif;
--shadow-soft: 0 1px 2px 0 rgba(28,28,58,0.04), 0 1px 3px 0 rgba(28,28,58,0.06);"""
INDEX_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DeltaTrack — Example Reports</title>
<style>
:root {{
{tokens}
}}
* {{ box-sizing: border-box; }}
body {{ font-family: var(--font-sans); color: var(--foreground); background: var(--background);
line-height: 1.6; margin: 0; padding: 48px 20px; }}
main {{ max-width: 760px; margin: 0 auto; }}
h1, h2 {{ font-family: var(--font-serif); letter-spacing: -0.02em; margin: 0; }}
h1 {{ font-size: 28px; }}
.lead {{ color: var(--muted-foreground); margin: 12px 0 32px; }}
.report {{ display: block; background: var(--card); border: 1px solid var(--border);
border-radius: var(--radius); box-shadow: var(--shadow-soft); padding: 20px 22px;
margin-bottom: 16px; text-decoration: none; color: inherit; }}
.report:hover {{ border-color: var(--primary); }}
.report h2 {{ font-size: 18px; color: var(--primary); }}
.report p {{ margin: 6px 0 0; color: var(--muted-foreground); font-size: 14px; }}
.report .meta {{ margin-top: 10px; font-size: 12px; letter-spacing: 0.04em;
text-transform: uppercase; color: var(--muted-foreground); }}
footer {{ margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--border);
font-size: 13px; color: var(--muted-foreground); }}
a {{ color: var(--primary); }}
</style>
</head>
<body>
<main>
<h1>DeltaTrack — Example Reports</h1>
<p class="lead">Real output from the DeltaTrack pipeline, rendered from official GPO
bill text. Each report is self-contained: open it and use the sidebar navigation, the
full-bill view, and the export button exactly as you would on your own comparison.</p>
{cards}
<footer>
Generated by <code>scripts/render_examples.py</code> from the
<a href="https://github.com/AgoraDMV/DeltaTrack">DeltaTrack</a> corpus, through the same
diff pipeline the web app serves. Do not edit these files by hand.
</footer>
</main>
</body>
</html>
"""
CARD_TEMPLATE = """<a class="report" href="{href}">
<h2>{title}</h2>
<p>{blurb}</p>
<div class="meta">{meta}</div>
</a>"""
FORMAT_META = {
"xml": "Bill XML pipeline",
"pdf": "PDF pipeline",
}
def render_index() -> Path:
"""Write the landing page listing every rendered report.
Generated rather than hand-maintained: the previous page was written by hand, and
listed two reports that no longer reflected the renderer while omitting the ones
that did.
"""
cards = []
for spec in EXAMPLES_TO_RENDER:
for fmt in spec.formats:
cards.append(
CARD_TEMPLATE.format(
href=html.escape(spec.output_name(fmt)),
title=html.escape(spec.title),
blurb=html.escape(spec.blurb),
meta=html.escape(FORMAT_META[fmt]),
)
)
out = EXAMPLES / "index.html"
out.write_text(INDEX_TEMPLATE.format(tokens=INDEX_TOKENS, cards="\n\n".join(cards)))
return out
def main() -> None:
for spec in EXAMPLES_TO_RENDER:
for fmt in spec.formats:
out = RENDERERS[fmt](spec)
print(f"Wrote {out} ({out.stat().st_size:,} bytes)")
out = render_index()
print(f"Wrote {out} ({out.stat().st_size:,} bytes)")
if __name__ == "__main__":
main()