Simplify public disclosure reader
Browse files- app.py +135 -78
- space_bundle_manifest.json +3 -3
app.py
CHANGED
|
@@ -240,9 +240,9 @@ def public_filer_cards_html(
|
|
| 240 |
lag = str(row.get("Typical filing lag (days)", "")).strip()
|
| 241 |
lag_text = f"{lag} day typical filing lag" if lag else "filing lag not available"
|
| 242 |
counts = [
|
| 243 |
-
f"{fmt_int(row.get('
|
| 244 |
-
f"{fmt_int(row.get('
|
| 245 |
-
f"{fmt_int(row.get('
|
| 246 |
lag_text,
|
| 247 |
]
|
| 248 |
count_html = "".join(f"<span>{html_escape(item)}</span>" for item in counts)
|
|
@@ -252,7 +252,7 @@ def public_filer_cards_html(
|
|
| 252 |
why_html = f"<p>{html_escape(why)}</p>" if why else ""
|
| 253 |
cards.append(
|
| 254 |
"<article class='filer-match-card'>"
|
| 255 |
-
f"<div><h3>{html_escape(row.get('
|
| 256 |
f"<strong class='filer-status'>{html_escape(status_text)}</strong>"
|
| 257 |
f"<div class='filer-counts'>{count_html}</div>"
|
| 258 |
f"{why_html}"
|
|
@@ -263,8 +263,8 @@ def public_filer_cards_html(
|
|
| 263 |
more = f"<p class='table-note'>Showing {fmt_int(max_rows)} of {fmt_int(len(frame))} matching public records.</p>"
|
| 264 |
return (
|
| 265 |
"<div class='result-card filer-results-card'>"
|
| 266 |
-
"<h2>Matching
|
| 267 |
-
"<p>
|
| 268 |
f"<div class='filer-card-list'>{''.join(cards)}</div>"
|
| 269 |
f"{more}"
|
| 270 |
"</div>"
|
|
@@ -386,12 +386,12 @@ def name_for_id(senator_id: str) -> str:
|
|
| 386 |
|
| 387 |
|
| 388 |
PUBLIC_FILER_COLUMNS = [
|
| 389 |
-
"
|
| 390 |
"State",
|
| 391 |
"Party",
|
| 392 |
-
"
|
| 393 |
-
"
|
| 394 |
-
"
|
| 395 |
"Typical filing lag (days)",
|
| 396 |
"Record status",
|
| 397 |
"Why shown",
|
|
@@ -500,15 +500,20 @@ def empty_state_figure(message: str):
|
|
| 500 |
def hero_html() -> str:
|
| 501 |
return """
|
| 502 |
<section class="hero-panel">
|
| 503 |
-
<div class="eyebrow">Official Senate disclosure
|
| 504 |
-
<h1>
|
| 505 |
<p class="hero-answer">
|
| 506 |
-
|
| 507 |
</p>
|
| 508 |
<p class="hero-why">
|
| 509 |
-
|
| 510 |
</p>
|
| 511 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
</section>
|
| 513 |
"""
|
| 514 |
|
|
@@ -516,20 +521,20 @@ def hero_html() -> str:
|
|
| 516 |
def plain_english_cards_html() -> str:
|
| 517 |
cards = [
|
| 518 |
(
|
| 519 |
-
"
|
| 520 |
-
"
|
| 521 |
"#e8f7f3",
|
| 522 |
"#0f766e",
|
| 523 |
),
|
| 524 |
(
|
| 525 |
-
"
|
| 526 |
-
"
|
| 527 |
"#eef4ff",
|
| 528 |
"#3155b7",
|
| 529 |
),
|
| 530 |
(
|
| 531 |
-
"
|
| 532 |
-
"
|
| 533 |
"#fff7e8",
|
| 534 |
"#8a4b00",
|
| 535 |
),
|
|
@@ -574,11 +579,11 @@ def overview_intro_html() -> str:
|
|
| 574 |
return """
|
| 575 |
<div class="simple-start">
|
| 576 |
<h2>Understand it in five seconds</h2>
|
| 577 |
-
<p>Use this like a reader for public Senate forms.</p>
|
| 578 |
<div class="question-grid">
|
| 579 |
-
<div><strong>1.
|
| 580 |
-
<div><strong>2.
|
| 581 |
-
<div><strong>3.
|
| 582 |
</div>
|
| 583 |
</div>
|
| 584 |
"""
|
|
@@ -835,12 +840,12 @@ def filer_search(query: str, max_rows: int = 12) -> pd.DataFrame:
|
|
| 835 |
display["record_status"] = frame.apply(data_status, axis=1).values
|
| 836 |
return display.rename(
|
| 837 |
columns={
|
| 838 |
-
"display_name": "
|
| 839 |
"state": "State",
|
| 840 |
"party": "Party",
|
| 841 |
-
"filings": "
|
| 842 |
-
"transactions": "
|
| 843 |
-
"wealth_ranges": "
|
| 844 |
"median_filing_lag_days": "Typical filing lag (days)",
|
| 845 |
"record_status": "Record status",
|
| 846 |
"why_shown": "Why shown",
|
|
@@ -869,7 +874,7 @@ def filer_search_html(query: str) -> str:
|
|
| 869 |
)
|
| 870 |
return note + public_filer_cards_html(
|
| 871 |
results,
|
| 872 |
-
"No matching public
|
| 873 |
"Try a last name, state abbreviation, party, filer ID, or common/legal-name variant.",
|
| 874 |
max_rows=12,
|
| 875 |
)
|
|
@@ -910,9 +915,9 @@ def filer_profile_html(query: str) -> str:
|
|
| 910 |
<h2>{name_for_id(senator_id)}</h2>
|
| 911 |
{status_note}
|
| 912 |
<div class='profile-grid'>
|
| 913 |
-
<span><strong>{fmt_int(row.get('filings', 0))}</strong>
|
| 914 |
-
<span><strong>{fmt_int(row.get('transactions', 0))}</strong>
|
| 915 |
-
<span><strong>{fmt_int(row.get('wealth_ranges', 0))}</strong>
|
| 916 |
<span><strong>{lag_text}</strong> typical filing lag</span>
|
| 917 |
</div>
|
| 918 |
</div>
|
|
@@ -957,7 +962,7 @@ def floor_money_text(value: float) -> str:
|
|
| 957 |
|
| 958 |
|
| 959 |
def disclosure_math_plain_text() -> str:
|
| 960 |
-
return "
|
| 961 |
|
| 962 |
|
| 963 |
def wealth_plain_result(row) -> dict[str, str]:
|
|
@@ -966,20 +971,20 @@ def wealth_plain_result(row) -> dict[str, str]:
|
|
| 966 |
year = present_text(row.get("report_year", "latest"), "latest")
|
| 967 |
if low_num is not None and high_num is not None:
|
| 968 |
short = f"{fmt_money(low_num)} to {fmt_money(high_num)}"
|
| 969 |
-
sentence = f"
|
| 970 |
missing = "Exact net worth is not reported."
|
| 971 |
-
note = "
|
| 972 |
elif low_num is not None:
|
| 973 |
short = floor_money_text(low_num)
|
| 974 |
sentence_value = short[:1].lower() + short[1:]
|
| 975 |
-
sentence = f"
|
| 976 |
-
missing = "No maximum
|
| 977 |
-
note = "The exact amount is not public here, and the
|
| 978 |
elif high_num is not None:
|
| 979 |
short = f"No more than {fmt_money(high_num)}"
|
| 980 |
-
sentence = f"
|
| 981 |
-
missing = "No minimum
|
| 982 |
-
note = "The exact amount is not public here, and the
|
| 983 |
else:
|
| 984 |
short = "Not enough dollar detail"
|
| 985 |
sentence = f"The {year} public form does not give enough dollar detail for a wealth summary."
|
|
@@ -1005,26 +1010,25 @@ def wealth_year_explanation(row) -> str:
|
|
| 1005 |
year = present_text(row.get("report_year", "this year"), "this year")
|
| 1006 |
low_num = numeric_or_none(row.get("net_worth_min"))
|
| 1007 |
high_num = numeric_or_none(row.get("net_worth_max"))
|
| 1008 |
-
math_text = disclosure_math_plain_text()
|
| 1009 |
if low_num is not None and high_num is not None:
|
| 1010 |
return (
|
| 1011 |
-
f"For {year},
|
| 1012 |
-
|
| 1013 |
)
|
| 1014 |
if low_num is not None:
|
| 1015 |
sentence = (
|
| 1016 |
-
f"For {year},
|
| 1017 |
-
"
|
| 1018 |
)
|
| 1019 |
if low_num < 0:
|
| 1020 |
-
sentence += " A negative
|
| 1021 |
return sentence
|
| 1022 |
if high_num is not None:
|
| 1023 |
return (
|
| 1024 |
-
f"For {year},
|
| 1025 |
-
"
|
| 1026 |
)
|
| 1027 |
-
return f"For {year},
|
| 1028 |
|
| 1029 |
|
| 1030 |
def wealth_movement_sentence(ordered: pd.DataFrame) -> str:
|
|
@@ -1037,7 +1041,7 @@ def wealth_movement_sentence(ordered: pd.DataFrame) -> str:
|
|
| 1037 |
previous_low = numeric_or_none(previous.get("net_worth_min"))
|
| 1038 |
previous_high = numeric_or_none(previous.get("net_worth_max"))
|
| 1039 |
if None in {latest_low, latest_high, previous_low, previous_high}:
|
| 1040 |
-
return "The
|
| 1041 |
latest_year = present_text(latest.get("report_year", "latest"))
|
| 1042 |
previous_year = present_text(previous.get("report_year", "previous"))
|
| 1043 |
if latest_low > previous_high:
|
|
@@ -1045,17 +1049,17 @@ def wealth_movement_sentence(ordered: pd.DataFrame) -> str:
|
|
| 1045 |
elif latest_high < previous_low:
|
| 1046 |
direction = "is below"
|
| 1047 |
else:
|
| 1048 |
-
return f"Compared with {previous_year}, the {latest_year}
|
| 1049 |
-
return f"Compared with {previous_year}, the {latest_year}
|
| 1050 |
|
| 1051 |
|
| 1052 |
def wealth_missing_number_text(row) -> str:
|
| 1053 |
low_num = numeric_or_none(row.get("net_worth_min"))
|
| 1054 |
high_num = numeric_or_none(row.get("net_worth_max"))
|
| 1055 |
if low_num is not None and high_num is None:
|
| 1056 |
-
return "No maximum
|
| 1057 |
if low_num is None and high_num is not None:
|
| 1058 |
-
return "No minimum
|
| 1059 |
if low_num is None and high_num is None:
|
| 1060 |
return "Low and high numbers not available."
|
| 1061 |
return "Low and high numbers available."
|
|
@@ -1092,11 +1096,14 @@ def filer_wealth_summary_html(query: str) -> str:
|
|
| 1092 |
else "<span class='source-missing'>Source link not available in this row</span>"
|
| 1093 |
)
|
| 1094 |
latest_plain = wealth_plain_result(latest)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
summary_cards = [
|
| 1096 |
-
("
|
| 1097 |
-
("
|
| 1098 |
-
("
|
| 1099 |
-
("Receipt", f"Filed {latest_filing_date}"),
|
| 1100 |
]
|
| 1101 |
card_html = "".join(
|
| 1102 |
f"<div><strong>{html_escape(label)}</strong><span>{html_escape(value)}</span></div>"
|
|
@@ -1126,27 +1133,25 @@ def filer_wealth_summary_html(query: str) -> str:
|
|
| 1126 |
more_note = f"<p class='table-note'>Showing 6 of {fmt_int(len(ordered))} past filing summaries. Download files and technical rows live in Advanced.</p>"
|
| 1127 |
return (
|
| 1128 |
"<div class='result-card wealth-result wealth-reader'>"
|
| 1129 |
-
f"<div class='profile-kicker'>
|
| 1130 |
-
f"<h2>{html_escape(name_for_id(senator_id))}
|
| 1131 |
-
|
|
|
|
| 1132 |
f"<div class='wealth-plain-grid'>{card_html}</div>"
|
| 1133 |
-
"<
|
| 1134 |
-
"<
|
| 1135 |
-
"<p>
|
| 1136 |
-
"</
|
| 1137 |
-
"<
|
| 1138 |
-
"<strong>Why not exact?</strong> Senate forms use broad dollar ranges, not exact account balances. This is not a bank balance, taxable income, trading profit, or proof of gains."
|
| 1139 |
-
f"<span>{html_escape(latest_plain['missing'])}</span>"
|
| 1140 |
-
"</div>"
|
| 1141 |
f"<p class='wealth-movement'>{html_escape(wealth_movement_sentence(ordered))}</p>"
|
| 1142 |
f"<div class='receipt-strip'><span>Latest filing date: <strong>{html_escape(latest_filing_date)}</strong></span>{source_link}</div>"
|
| 1143 |
"</div>"
|
| 1144 |
-
"<
|
| 1145 |
-
"<
|
| 1146 |
-
"<div class='wealth-year-guide'><strong>
|
| 1147 |
f"<ul class='wealth-year-list'>{''.join(year_items)}</ul>"
|
| 1148 |
f"{more_note}"
|
| 1149 |
-
"</
|
| 1150 |
)
|
| 1151 |
|
| 1152 |
|
|
@@ -1421,6 +1426,20 @@ body,
|
|
| 1421 |
margin: 10px 0 0 !important;
|
| 1422 |
font-weight: 700 !important;
|
| 1423 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1424 |
.not-verdict {
|
| 1425 |
display: inline-block;
|
| 1426 |
margin-top: 14px;
|
|
@@ -1707,7 +1726,7 @@ body,
|
|
| 1707 |
.wealth-answer-card strong {
|
| 1708 |
display: block;
|
| 1709 |
color: #061a18 !important;
|
| 1710 |
-
font-size:
|
| 1711 |
line-height: 1.08 !important;
|
| 1712 |
font-weight: 800 !important;
|
| 1713 |
margin-bottom: 8px;
|
|
@@ -1718,9 +1737,13 @@ body,
|
|
| 1718 |
line-height: 1.55 !important;
|
| 1719 |
margin: 0 !important;
|
| 1720 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1721 |
.wealth-plain-grid {
|
| 1722 |
display: grid;
|
| 1723 |
-
grid-template-columns: repeat(
|
| 1724 |
gap: 10px;
|
| 1725 |
margin-top: 12px;
|
| 1726 |
}
|
|
@@ -1763,9 +1786,20 @@ body,
|
|
| 1763 |
color: #2b1700 !important;
|
| 1764 |
margin: 0 !important;
|
| 1765 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1766 |
.wealth-reader h2 {
|
| 1767 |
max-width: 860px;
|
| 1768 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1769 |
.wealth-lede {
|
| 1770 |
font-size: 1.12rem !important;
|
| 1771 |
line-height: 1.5 !important;
|
|
@@ -1862,6 +1896,29 @@ body,
|
|
| 1862 |
color: #334155 !important;
|
| 1863 |
font-weight: 700 !important;
|
| 1864 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1865 |
.wealth-movement {
|
| 1866 |
margin-top: 12px !important;
|
| 1867 |
font-weight: 700 !important;
|
|
@@ -2142,7 +2199,7 @@ with gr.Blocks(title="Senate Disclosure Reader") as demo:
|
|
| 2142 |
|
| 2143 |
with gr.Tabs():
|
| 2144 |
with gr.Tab("Search Senator"):
|
| 2145 |
-
gr.HTML("<div class='section-intro'><strong>Search a senator.</strong> The first result
|
| 2146 |
filer_query = gr.Textbox(
|
| 2147 |
label="Name, state, or filer ID",
|
| 2148 |
value="",
|
|
|
|
| 240 |
lag = str(row.get("Typical filing lag (days)", "")).strip()
|
| 241 |
lag_text = f"{lag} day typical filing lag" if lag else "filing lag not available"
|
| 242 |
counts = [
|
| 243 |
+
f"{fmt_int(row.get('Public forms', 0))} public forms",
|
| 244 |
+
f"{fmt_int(row.get('Trade rows', 0))} trade rows",
|
| 245 |
+
f"{fmt_int(row.get('Wealth summaries', 0))} wealth summaries",
|
| 246 |
lag_text,
|
| 247 |
]
|
| 248 |
count_html = "".join(f"<span>{html_escape(item)}</span>" for item in counts)
|
|
|
|
| 252 |
why_html = f"<p>{html_escape(why)}</p>" if why else ""
|
| 253 |
cards.append(
|
| 254 |
"<article class='filer-match-card'>"
|
| 255 |
+
f"<div><h3>{html_escape(row.get('Matching record', ''))}</h3>{meta}</div>"
|
| 256 |
f"<strong class='filer-status'>{html_escape(status_text)}</strong>"
|
| 257 |
f"<div class='filer-counts'>{count_html}</div>"
|
| 258 |
f"{why_html}"
|
|
|
|
| 263 |
more = f"<p class='table-note'>Showing {fmt_int(max_rows)} of {fmt_int(len(frame))} matching public records.</p>"
|
| 264 |
return (
|
| 265 |
"<div class='result-card filer-results-card'>"
|
| 266 |
+
"<h2>Matching records</h2>"
|
| 267 |
+
"<p>If more than one record appears, use the one that says disclosure rows were found.</p>"
|
| 268 |
f"<div class='filer-card-list'>{''.join(cards)}</div>"
|
| 269 |
f"{more}"
|
| 270 |
"</div>"
|
|
|
|
| 386 |
|
| 387 |
|
| 388 |
PUBLIC_FILER_COLUMNS = [
|
| 389 |
+
"Matching record",
|
| 390 |
"State",
|
| 391 |
"Party",
|
| 392 |
+
"Public forms",
|
| 393 |
+
"Trade rows",
|
| 394 |
+
"Wealth summaries",
|
| 395 |
"Typical filing lag (days)",
|
| 396 |
"Record status",
|
| 397 |
"Why shown",
|
|
|
|
| 500 |
def hero_html() -> str:
|
| 501 |
return """
|
| 502 |
<section class="hero-panel">
|
| 503 |
+
<div class="eyebrow">Official Senate disclosure forms</div>
|
| 504 |
+
<h1>See what senators reported</h1>
|
| 505 |
<p class="hero-answer">
|
| 506 |
+
Search a name. The app turns public Senate financial disclosure forms into plain answers: reported wealth range, disclosed transaction rows, filing dates, and the source filing.
|
| 507 |
</p>
|
| 508 |
<p class="hero-why">
|
| 509 |
+
People might care because lawmakers file public financial disclosures. This makes those official records easier to read without turning them into accusations.
|
| 510 |
</p>
|
| 511 |
+
<div class="reader-model">
|
| 512 |
+
<span>Forms, not accusations</span>
|
| 513 |
+
<span>Ranges, not exact net worth</span>
|
| 514 |
+
<span>Receipts, not guesses</span>
|
| 515 |
+
</div>
|
| 516 |
+
<div class="not-verdict">No claim of wrongdoing or market-beating performance.</div>
|
| 517 |
</section>
|
| 518 |
"""
|
| 519 |
|
|
|
|
| 521 |
def plain_english_cards_html() -> str:
|
| 522 |
cards = [
|
| 523 |
(
|
| 524 |
+
"Start with a person",
|
| 525 |
+
"Search a senator and read the latest public form summary before opening charts or tables.",
|
| 526 |
"#e8f7f3",
|
| 527 |
"#0f766e",
|
| 528 |
),
|
| 529 |
(
|
| 530 |
+
"Read money as a range",
|
| 531 |
+
"Forms report dollar ranges. A result like 'at least $363K' is a disclosure floor, not a precise fortune.",
|
| 532 |
"#eef4ff",
|
| 533 |
"#3155b7",
|
| 534 |
),
|
| 535 |
(
|
| 536 |
+
"Open the receipt",
|
| 537 |
+
"Every useful row points back to the public filing so readers can check the source record.",
|
| 538 |
"#fff7e8",
|
| 539 |
"#8a4b00",
|
| 540 |
),
|
|
|
|
| 579 |
return """
|
| 580 |
<div class="simple-start">
|
| 581 |
<h2>Understand it in five seconds</h2>
|
| 582 |
+
<p>Use this like a receipt-backed reader for public Senate financial forms.</p>
|
| 583 |
<div class="question-grid">
|
| 584 |
+
<div><strong>1. Who?</strong><span>Search the senator or filer name.</span></div>
|
| 585 |
+
<div><strong>2. What was reported?</strong><span>Read the latest wealth range and disclosed transaction rows.</span></div>
|
| 586 |
+
<div><strong>3. Where is the proof?</strong><span>Open the Senate filing linked beside the result.</span></div>
|
| 587 |
</div>
|
| 588 |
</div>
|
| 589 |
"""
|
|
|
|
| 840 |
display["record_status"] = frame.apply(data_status, axis=1).values
|
| 841 |
return display.rename(
|
| 842 |
columns={
|
| 843 |
+
"display_name": "Matching record",
|
| 844 |
"state": "State",
|
| 845 |
"party": "Party",
|
| 846 |
+
"filings": "Public forms",
|
| 847 |
+
"transactions": "Trade rows",
|
| 848 |
+
"wealth_ranges": "Wealth summaries",
|
| 849 |
"median_filing_lag_days": "Typical filing lag (days)",
|
| 850 |
"record_status": "Record status",
|
| 851 |
"why_shown": "Why shown",
|
|
|
|
| 874 |
)
|
| 875 |
return note + public_filer_cards_html(
|
| 876 |
results,
|
| 877 |
+
"No matching public records found",
|
| 878 |
"Try a last name, state abbreviation, party, filer ID, or common/legal-name variant.",
|
| 879 |
max_rows=12,
|
| 880 |
)
|
|
|
|
| 915 |
<h2>{name_for_id(senator_id)}</h2>
|
| 916 |
{status_note}
|
| 917 |
<div class='profile-grid'>
|
| 918 |
+
<span><strong>{fmt_int(row.get('filings', 0))}</strong> public forms</span>
|
| 919 |
+
<span><strong>{fmt_int(row.get('transactions', 0))}</strong> trade rows</span>
|
| 920 |
+
<span><strong>{fmt_int(row.get('wealth_ranges', 0))}</strong> wealth summaries</span>
|
| 921 |
<span><strong>{lag_text}</strong> typical filing lag</span>
|
| 922 |
</div>
|
| 923 |
</div>
|
|
|
|
| 962 |
|
| 963 |
|
| 964 |
def disclosure_math_plain_text() -> str:
|
| 965 |
+
return "subtracting debts from assets reported on the public form"
|
| 966 |
|
| 967 |
|
| 968 |
def wealth_plain_result(row) -> dict[str, str]:
|
|
|
|
| 971 |
year = present_text(row.get("report_year", "latest"), "latest")
|
| 972 |
if low_num is not None and high_num is not None:
|
| 973 |
short = f"{fmt_money(low_num)} to {fmt_money(high_num)}"
|
| 974 |
+
sentence = f"For {year}, the public form puts {disclosure_math_plain_text()} between {short}."
|
| 975 |
missing = "Exact net worth is not reported."
|
| 976 |
+
note = "That is a reported range, not an exact private valuation."
|
| 977 |
elif low_num is not None:
|
| 978 |
short = floor_money_text(low_num)
|
| 979 |
sentence_value = short[:1].lower() + short[1:]
|
| 980 |
+
sentence = f"For {year}, the public form shows {sentence_value} after {disclosure_math_plain_text()}."
|
| 981 |
+
missing = "No calculable maximum from this filing."
|
| 982 |
+
note = "The exact amount is not public here, and the real number may be higher."
|
| 983 |
elif high_num is not None:
|
| 984 |
short = f"No more than {fmt_money(high_num)}"
|
| 985 |
+
sentence = f"For {year}, the public form shows no more than {fmt_money(high_num)} after {disclosure_math_plain_text()}."
|
| 986 |
+
missing = "No calculable minimum from this filing."
|
| 987 |
+
note = "The exact amount is not public here, and the real number may be lower."
|
| 988 |
else:
|
| 989 |
short = "Not enough dollar detail"
|
| 990 |
sentence = f"The {year} public form does not give enough dollar detail for a wealth summary."
|
|
|
|
| 1010 |
year = present_text(row.get("report_year", "this year"), "this year")
|
| 1011 |
low_num = numeric_or_none(row.get("net_worth_min"))
|
| 1012 |
high_num = numeric_or_none(row.get("net_worth_max"))
|
|
|
|
| 1013 |
if low_num is not None and high_num is not None:
|
| 1014 |
return (
|
| 1015 |
+
f"For {year}, the reported assets-minus-debts number lands somewhere from {fmt_money(low_num)} to {fmt_money(high_num)}. "
|
| 1016 |
+
"It is a public disclosure range, not an appraisal or exact net worth."
|
| 1017 |
)
|
| 1018 |
if low_num is not None:
|
| 1019 |
sentence = (
|
| 1020 |
+
f"For {year}, the public form supports at least {fmt_money(low_num)}. "
|
| 1021 |
+
"This bundle cannot calculate a top end from that filing, so the real number could be higher."
|
| 1022 |
)
|
| 1023 |
if low_num < 0:
|
| 1024 |
+
sentence += " A negative floor can happen when reported debt ranges can exceed reported asset ranges."
|
| 1025 |
return sentence
|
| 1026 |
if high_num is not None:
|
| 1027 |
return (
|
| 1028 |
+
f"For {year}, the public form supports no more than {fmt_money(high_num)}. "
|
| 1029 |
+
"This bundle cannot calculate a bottom end from that filing, so the real number could be lower."
|
| 1030 |
)
|
| 1031 |
+
return f"For {year}, the filing rows do not have enough dollar detail here to summarize assets minus debts."
|
| 1032 |
|
| 1033 |
|
| 1034 |
def wealth_movement_sentence(ordered: pd.DataFrame) -> str:
|
|
|
|
| 1041 |
previous_low = numeric_or_none(previous.get("net_worth_min"))
|
| 1042 |
previous_high = numeric_or_none(previous.get("net_worth_max"))
|
| 1043 |
if None in {latest_low, latest_high, previous_low, previous_high}:
|
| 1044 |
+
return "The forms are too broad for a clean year-to-year comparison."
|
| 1045 |
latest_year = present_text(latest.get("report_year", "latest"))
|
| 1046 |
previous_year = present_text(previous.get("report_year", "previous"))
|
| 1047 |
if latest_low > previous_high:
|
|
|
|
| 1049 |
elif latest_high < previous_low:
|
| 1050 |
direction = "is below"
|
| 1051 |
else:
|
| 1052 |
+
return f"Compared with {previous_year}, the {latest_year} reported range overlaps the earlier one, so the forms do not show a clear move."
|
| 1053 |
+
return f"Compared with {previous_year}, the {latest_year} reported range {direction} the earlier reported range."
|
| 1054 |
|
| 1055 |
|
| 1056 |
def wealth_missing_number_text(row) -> str:
|
| 1057 |
low_num = numeric_or_none(row.get("net_worth_min"))
|
| 1058 |
high_num = numeric_or_none(row.get("net_worth_max"))
|
| 1059 |
if low_num is not None and high_num is None:
|
| 1060 |
+
return "No calculable maximum from this filing."
|
| 1061 |
if low_num is None and high_num is not None:
|
| 1062 |
+
return "No calculable minimum from this filing."
|
| 1063 |
if low_num is None and high_num is None:
|
| 1064 |
return "Low and high numbers not available."
|
| 1065 |
return "Low and high numbers available."
|
|
|
|
| 1096 |
else "<span class='source-missing'>Source link not available in this row</span>"
|
| 1097 |
)
|
| 1098 |
latest_plain = wealth_plain_result(latest)
|
| 1099 |
+
index_match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id]
|
| 1100 |
+
index_row = index_match.iloc[0] if not index_match.empty else {}
|
| 1101 |
+
filing_count = int(index_row.get("filings", 0) or 0) if hasattr(index_row, "get") else 0
|
| 1102 |
+
transaction_count = int(index_row.get("transactions", 0) or 0) if hasattr(index_row, "get") else 0
|
| 1103 |
summary_cards = [
|
| 1104 |
+
("Year covered", latest_year),
|
| 1105 |
+
("Filed with Senate", latest_filing_date),
|
| 1106 |
+
("Records found", f"{fmt_int(filing_count)} public forms; {fmt_int(transaction_count)} trade rows"),
|
|
|
|
| 1107 |
]
|
| 1108 |
card_html = "".join(
|
| 1109 |
f"<div><strong>{html_escape(label)}</strong><span>{html_escape(value)}</span></div>"
|
|
|
|
| 1133 |
more_note = f"<p class='table-note'>Showing 6 of {fmt_int(len(ordered))} past filing summaries. Download files and technical rows live in Advanced.</p>"
|
| 1134 |
return (
|
| 1135 |
"<div class='result-card wealth-result wealth-reader'>"
|
| 1136 |
+
f"<div class='profile-kicker'>Latest filing in plain English</div>"
|
| 1137 |
+
f"<h2>{html_escape(name_for_id(senator_id))}</h2>"
|
| 1138 |
+
"<p class='profile-lens'>This is a summary of public Senate disclosure forms, not a judgment about the person.</p>"
|
| 1139 |
+
f"<div class='wealth-answer-card'><span>What the latest financial form says</span><strong>{html_escape(latest_plain['short'])}</strong><p>{html_escape(latest_plain['sentence'])} {html_escape(latest_plain['note'])}</p><p class='why-care-line'>Why someone might care: it shows the financial picture this official publicly reported for that year, and lets readers compare filings over time with source receipts.</p></div>"
|
| 1140 |
f"<div class='wealth-plain-grid'>{card_html}</div>"
|
| 1141 |
+
"<details class='reader-details'>"
|
| 1142 |
+
"<summary>How to read this number</summary>"
|
| 1143 |
+
"<p>Senate forms report assets and debts in dollar ranges. This app reads those ranges and summarizes the public form as a low-to-high range, or as a floor when the top end cannot be calculated.</p>"
|
| 1144 |
+
f"<p>{html_escape(latest_plain['missing'])} The number cannot show exact net worth, taxable income, trading profit, intent, wrongdoing, or whether any trade made money.</p>"
|
| 1145 |
+
"</details>"
|
|
|
|
|
|
|
|
|
|
| 1146 |
f"<p class='wealth-movement'>{html_escape(wealth_movement_sentence(ordered))}</p>"
|
| 1147 |
f"<div class='receipt-strip'><span>Latest filing date: <strong>{html_escape(latest_filing_date)}</strong></span>{source_link}</div>"
|
| 1148 |
"</div>"
|
| 1149 |
+
"<details class='result-card wealth-years-card history-drawer'>"
|
| 1150 |
+
"<summary>Show year-by-year filings</summary>"
|
| 1151 |
+
"<div class='wealth-year-guide'><strong>What each year means</strong><span>The year is the report year named by the public filing. The dollar phrase is what that filing supports for reported assets minus reported debts. A floor is not a ranking, and changes over time can reflect broad form ranges, purchases, sales, debts, filing differences, or parser limits.</span></div>"
|
| 1152 |
f"<ul class='wealth-year-list'>{''.join(year_items)}</ul>"
|
| 1153 |
f"{more_note}"
|
| 1154 |
+
"</details>"
|
| 1155 |
)
|
| 1156 |
|
| 1157 |
|
|
|
|
| 1426 |
margin: 10px 0 0 !important;
|
| 1427 |
font-weight: 700 !important;
|
| 1428 |
}
|
| 1429 |
+
.reader-model {
|
| 1430 |
+
display: flex;
|
| 1431 |
+
flex-wrap: wrap;
|
| 1432 |
+
gap: 8px;
|
| 1433 |
+
margin-top: 14px;
|
| 1434 |
+
}
|
| 1435 |
+
.reader-model span {
|
| 1436 |
+
background: #eef8f6 !important;
|
| 1437 |
+
border: 1px solid #99d4cb !important;
|
| 1438 |
+
border-radius: 999px;
|
| 1439 |
+
color: #073d38 !important;
|
| 1440 |
+
font-weight: 800 !important;
|
| 1441 |
+
padding: 8px 11px;
|
| 1442 |
+
}
|
| 1443 |
.not-verdict {
|
| 1444 |
display: inline-block;
|
| 1445 |
margin-top: 14px;
|
|
|
|
| 1726 |
.wealth-answer-card strong {
|
| 1727 |
display: block;
|
| 1728 |
color: #061a18 !important;
|
| 1729 |
+
font-size: 2rem !important;
|
| 1730 |
line-height: 1.08 !important;
|
| 1731 |
font-weight: 800 !important;
|
| 1732 |
margin-bottom: 8px;
|
|
|
|
| 1737 |
line-height: 1.55 !important;
|
| 1738 |
margin: 0 !important;
|
| 1739 |
}
|
| 1740 |
+
.wealth-answer-card .why-care-line {
|
| 1741 |
+
margin-top: 10px !important;
|
| 1742 |
+
font-weight: 800 !important;
|
| 1743 |
+
}
|
| 1744 |
.wealth-plain-grid {
|
| 1745 |
display: grid;
|
| 1746 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 1747 |
gap: 10px;
|
| 1748 |
margin-top: 12px;
|
| 1749 |
}
|
|
|
|
| 1786 |
color: #2b1700 !important;
|
| 1787 |
margin: 0 !important;
|
| 1788 |
}
|
| 1789 |
+
.wealth-year-guide span {
|
| 1790 |
+
color: #2b1700 !important;
|
| 1791 |
+
display: block;
|
| 1792 |
+
font-weight: 700 !important;
|
| 1793 |
+
line-height: 1.45 !important;
|
| 1794 |
+
}
|
| 1795 |
.wealth-reader h2 {
|
| 1796 |
max-width: 860px;
|
| 1797 |
}
|
| 1798 |
+
.profile-lens {
|
| 1799 |
+
color: #334155 !important;
|
| 1800 |
+
font-weight: 800 !important;
|
| 1801 |
+
margin: 0 0 10px !important;
|
| 1802 |
+
}
|
| 1803 |
.wealth-lede {
|
| 1804 |
font-size: 1.12rem !important;
|
| 1805 |
line-height: 1.5 !important;
|
|
|
|
| 1896 |
color: #334155 !important;
|
| 1897 |
font-weight: 700 !important;
|
| 1898 |
}
|
| 1899 |
+
.reader-details,
|
| 1900 |
+
.history-drawer {
|
| 1901 |
+
color: #111827 !important;
|
| 1902 |
+
}
|
| 1903 |
+
.reader-details {
|
| 1904 |
+
background: #f8fafc !important;
|
| 1905 |
+
border: 1px solid #d7dee8 !important;
|
| 1906 |
+
border-radius: 10px;
|
| 1907 |
+
padding: 12px;
|
| 1908 |
+
margin-top: 12px;
|
| 1909 |
+
}
|
| 1910 |
+
.reader-details summary,
|
| 1911 |
+
.history-drawer summary {
|
| 1912 |
+
color: #0f172a !important;
|
| 1913 |
+
cursor: pointer;
|
| 1914 |
+
font-size: 1.05rem !important;
|
| 1915 |
+
font-weight: 800 !important;
|
| 1916 |
+
list-style-position: inside;
|
| 1917 |
+
}
|
| 1918 |
+
.reader-details p {
|
| 1919 |
+
color: #111827 !important;
|
| 1920 |
+
margin: 8px 0 0 !important;
|
| 1921 |
+
}
|
| 1922 |
.wealth-movement {
|
| 1923 |
margin-top: 12px !important;
|
| 1924 |
font-weight: 700 !important;
|
|
|
|
| 2199 |
|
| 2200 |
with gr.Tabs():
|
| 2201 |
with gr.Tab("Search Senator"):
|
| 2202 |
+
gr.HTML("<div class='section-intro'><strong>Search a senator.</strong> The first result gives the latest filing in plain English. If this bundle has no usable rows for that exact record, it says that directly.</div>")
|
| 2203 |
filer_query = gr.Textbox(
|
| 2204 |
label="Name, state, or filer ID",
|
| 2205 |
value="",
|
space_bundle_manifest.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
| 8 |
"files": [
|
| 9 |
{
|
| 10 |
"path": "app.py",
|
| 11 |
-
"sha256": "
|
| 12 |
-
"byte_count":
|
| 13 |
},
|
| 14 |
{
|
| 15 |
"path": "data/audit/claim_boundary.json",
|
|
@@ -107,5 +107,5 @@
|
|
| 107 |
"byte_count": 47
|
| 108 |
}
|
| 109 |
],
|
| 110 |
-
"manifest_hash": "
|
| 111 |
}
|
|
|
|
| 8 |
"files": [
|
| 9 |
{
|
| 10 |
"path": "app.py",
|
| 11 |
+
"sha256": "019bf360828c2c49427393ec612e0f39ed360ca87edb543431f47c9a6a3ed007",
|
| 12 |
+
"byte_count": 81117
|
| 13 |
},
|
| 14 |
{
|
| 15 |
"path": "data/audit/claim_boundary.json",
|
|
|
|
| 107 |
"byte_count": 47
|
| 108 |
}
|
| 109 |
],
|
| 110 |
+
"manifest_hash": "36f44dce48fedfbbf0a99d93baeb2dd2c8a1d9b3625bd53c5e5c418eec76a953"
|
| 111 |
}
|