datasette-libfec 0.0.1a4__py3-none-any.whl → 0.0.1a6__py3-none-any.whl
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.
- datasette_libfec/__init__.py +18 -2
- datasette_libfec/libfec_client.py +225 -11
- datasette_libfec/libfec_export_rpc_client.py +358 -0
- datasette_libfec/libfec_rpc_client.py +335 -0
- datasette_libfec/libfec_search_rpc_client.py +308 -0
- datasette_libfec/manifest.json +84 -2
- datasette_libfec/page_data.py +87 -0
- datasette_libfec/router.py +24 -0
- datasette_libfec/routes_export.py +128 -0
- datasette_libfec/routes_exports.py +222 -0
- datasette_libfec/routes_pages.py +341 -0
- datasette_libfec/routes_rss.py +416 -0
- datasette_libfec/routes_search.py +78 -0
- datasette_libfec/state.py +6 -0
- datasette_libfec/static/gen/candidate-BEqDafKu.css +1 -0
- datasette_libfec/static/gen/candidate-tqxa29G-.js +3 -0
- datasette_libfec/static/gen/class-C5DDKbJD.js +2 -0
- datasette_libfec/static/gen/committee-Bmki9iKb.css +1 -0
- datasette_libfec/static/gen/committee-DY1GmylW.js +2 -0
- datasette_libfec/static/gen/contest-BbYrzKRg.js +1 -0
- datasette_libfec/static/gen/contest-D4Fj7kGA.css +1 -0
- datasette_libfec/static/gen/each-DkfQbqzj.js +1 -0
- datasette_libfec/static/gen/filing_detail-Ba6_iQwV.css +1 -0
- datasette_libfec/static/gen/filing_detail-D2ib3OM6.js +26 -0
- datasette_libfec/static/gen/index-AHqus2fd.js +9 -0
- datasette_libfec/static/gen/index-client-CDwZ_Ixa.js +1 -0
- datasette_libfec/static/gen/index-jv9_YIKt.css +1 -0
- datasette_libfec/static/gen/load-AXKAVXVj.js +1 -0
- datasette_libfec/templates/libfec_base.html +12 -0
- {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/METADATA +2 -2
- datasette_libfec-0.0.1a6.dist-info/RECORD +37 -0
- {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/top_level.txt +1 -0
- scripts/typegen-pagedata.py +6 -0
- datasette_libfec/routes.py +0 -189
- datasette_libfec/static/gen/index-6cjSv2YC.css +0 -1
- datasette_libfec/static/gen/index-CaTQMY-X.js +0 -1
- datasette_libfec/templates/libfec.html +0 -14
- datasette_libfec-0.0.1a4.dist-info/RECORD +0 -14
- {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/WHEEL +0 -0
- {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/entry_points.txt +0 -0
- {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routes for contest, candidate, and committee pages.
|
|
3
|
+
"""
|
|
4
|
+
from datasette import Response
|
|
5
|
+
|
|
6
|
+
from .router import router, check_permission
|
|
7
|
+
from .page_data import (
|
|
8
|
+
Candidate,
|
|
9
|
+
CandidatePageData,
|
|
10
|
+
Committee,
|
|
11
|
+
CommitteePageData,
|
|
12
|
+
ContestPageData,
|
|
13
|
+
Filing, FilingDetailPageData, IndexPageData
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.GET("/-/libfec$")
|
|
18
|
+
@check_permission()
|
|
19
|
+
async def libfec_page(datasette, request):
|
|
20
|
+
db = datasette.get_database()
|
|
21
|
+
page_data = IndexPageData(database_name=db.name)
|
|
22
|
+
return Response.html(
|
|
23
|
+
await datasette.render_template(
|
|
24
|
+
"libfec_base.html",
|
|
25
|
+
{
|
|
26
|
+
"page_title": "FEC Import",
|
|
27
|
+
"entrypoint": "src/index_view.ts",
|
|
28
|
+
"page_data": page_data.model_dump(),
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.GET("/-/libfec/filing/(?P<filing_id>[^/]+)")
|
|
35
|
+
@check_permission()
|
|
36
|
+
async def filing_detail_page(datasette, request, filing_id: str):
|
|
37
|
+
db = datasette.get_database()
|
|
38
|
+
filing = None
|
|
39
|
+
form_data = None
|
|
40
|
+
error = None
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
filing_row = await db.execute(
|
|
44
|
+
"SELECT * FROM libfec_filings WHERE filing_id = ?", [filing_id]
|
|
45
|
+
)
|
|
46
|
+
filing_result = filing_row.first()
|
|
47
|
+
|
|
48
|
+
if not filing_result:
|
|
49
|
+
return Response.html("<h1>Filing not found</h1>", status=404)
|
|
50
|
+
|
|
51
|
+
filing = Filing(**dict(filing_result))
|
|
52
|
+
|
|
53
|
+
# Fetch form-specific data based on cover_record_form
|
|
54
|
+
form_type = filing.cover_record_form
|
|
55
|
+
form_table_map = {
|
|
56
|
+
"F1": "libfec_F1",
|
|
57
|
+
"F1S": "libfec_F1S",
|
|
58
|
+
"F2": "libfec_F2",
|
|
59
|
+
"F3": "libfec_F3",
|
|
60
|
+
"F3P": "libfec_F3P",
|
|
61
|
+
"F3S": "libfec_F3S",
|
|
62
|
+
"F3X": "libfec_F3X",
|
|
63
|
+
"F24": "libfec_F24",
|
|
64
|
+
"F6": "libfec_F6",
|
|
65
|
+
"F99": "libfec_F99",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
table_name = form_table_map.get(form_type) if form_type else None
|
|
69
|
+
if table_name:
|
|
70
|
+
try:
|
|
71
|
+
form_row = await db.execute(
|
|
72
|
+
f"SELECT * FROM {table_name} WHERE filing_id = ?", [filing_id]
|
|
73
|
+
)
|
|
74
|
+
form_result = form_row.first()
|
|
75
|
+
if form_result:
|
|
76
|
+
form_data = dict(form_result)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
error = f"Error fetching form data: {e}"
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
error = str(e)
|
|
82
|
+
|
|
83
|
+
page_data = FilingDetailPageData(
|
|
84
|
+
filing_id=filing_id,
|
|
85
|
+
filing=filing,
|
|
86
|
+
form_data=form_data,
|
|
87
|
+
database_name=db.name,
|
|
88
|
+
error=error,
|
|
89
|
+
)
|
|
90
|
+
return Response.html(
|
|
91
|
+
await datasette.render_template(
|
|
92
|
+
"libfec_base.html",
|
|
93
|
+
{
|
|
94
|
+
"page_title": f"Filing {filing_id}",
|
|
95
|
+
"entrypoint": "src/filing_detail_view.ts",
|
|
96
|
+
"page_data": page_data.model_dump(),
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@router.GET("/-/libfec/contest$")
|
|
103
|
+
@check_permission()
|
|
104
|
+
async def contest_page(datasette, request):
|
|
105
|
+
"""
|
|
106
|
+
Contest page showing candidates for a specific race.
|
|
107
|
+
|
|
108
|
+
Query params:
|
|
109
|
+
- state: Two-letter state code (e.g., "CA")
|
|
110
|
+
- office: Office type - "H" (House), "S" (Senate), or "P" (President)
|
|
111
|
+
- district: District number for House races (optional for S/P)
|
|
112
|
+
- cycle: Election cycle year (default: 2026)
|
|
113
|
+
"""
|
|
114
|
+
state = request.args.get("state")
|
|
115
|
+
office = request.args.get("office")
|
|
116
|
+
district = request.args.get("district")
|
|
117
|
+
cycle = int(request.args.get("cycle", 2026))
|
|
118
|
+
|
|
119
|
+
if not state or not office:
|
|
120
|
+
return Response.html("<h1>Missing required parameters: state and office</h1>", status=400)
|
|
121
|
+
|
|
122
|
+
candidates = []
|
|
123
|
+
error = None
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
db = datasette.get_database()
|
|
127
|
+
|
|
128
|
+
if office == "H" and district:
|
|
129
|
+
candidates_result = await db.execute(
|
|
130
|
+
"""
|
|
131
|
+
SELECT * FROM libfec_candidates
|
|
132
|
+
WHERE state = ? AND office = ? AND district = ? AND cycle = ?
|
|
133
|
+
GROUP BY candidate_id
|
|
134
|
+
ORDER BY name
|
|
135
|
+
""",
|
|
136
|
+
[state, office, district, cycle]
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
candidates_result = await db.execute(
|
|
140
|
+
"""
|
|
141
|
+
SELECT * FROM libfec_candidates
|
|
142
|
+
WHERE state = ? AND office = ? AND cycle = ?
|
|
143
|
+
GROUP BY candidate_id
|
|
144
|
+
ORDER BY name
|
|
145
|
+
""",
|
|
146
|
+
[state, office, cycle]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
candidates = [Candidate(**dict(row)) for row in candidates_result.rows]
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
error = str(e)
|
|
153
|
+
|
|
154
|
+
# Build contest description
|
|
155
|
+
office_names = {"H": "House", "S": "Senate", "P": "President"}
|
|
156
|
+
office_name = office_names.get(office, office)
|
|
157
|
+
contest_description = f"{state} {office_name}"
|
|
158
|
+
if office == "H" and district:
|
|
159
|
+
contest_description = f"{state} Congressional District {district}"
|
|
160
|
+
elif office == "S":
|
|
161
|
+
contest_description = f"{state} Senate"
|
|
162
|
+
|
|
163
|
+
page_data = ContestPageData(
|
|
164
|
+
state=state,
|
|
165
|
+
office=office,
|
|
166
|
+
district=district,
|
|
167
|
+
cycle=cycle,
|
|
168
|
+
contest_description=contest_description,
|
|
169
|
+
candidates=candidates,
|
|
170
|
+
error=error,
|
|
171
|
+
)
|
|
172
|
+
return Response.html(
|
|
173
|
+
await datasette.render_template(
|
|
174
|
+
"libfec_base.html",
|
|
175
|
+
{
|
|
176
|
+
"page_title": f"{contest_description} - {cycle}",
|
|
177
|
+
"entrypoint": "src/contest_view.ts",
|
|
178
|
+
"page_data": page_data.model_dump(),
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@router.GET("/-/libfec/candidate/(?P<candidate_id>[^/]+)$")
|
|
185
|
+
@check_permission()
|
|
186
|
+
async def candidate_page(datasette, request, candidate_id: str):
|
|
187
|
+
"""
|
|
188
|
+
Candidate detail page.
|
|
189
|
+
|
|
190
|
+
Shows candidate information and their principal committee.
|
|
191
|
+
"""
|
|
192
|
+
cycle = int(request.args.get("cycle", 2026))
|
|
193
|
+
candidate = None
|
|
194
|
+
committee = None
|
|
195
|
+
filings = []
|
|
196
|
+
error = None
|
|
197
|
+
principal_committee_id = None
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
db = datasette.get_database()
|
|
201
|
+
|
|
202
|
+
# Fetch candidate from database
|
|
203
|
+
candidate_result = await db.execute(
|
|
204
|
+
"""
|
|
205
|
+
SELECT * FROM libfec_candidates
|
|
206
|
+
WHERE candidate_id = ? AND cycle = ?
|
|
207
|
+
""",
|
|
208
|
+
[candidate_id, cycle]
|
|
209
|
+
)
|
|
210
|
+
candidate_row = candidate_result.first()
|
|
211
|
+
if candidate_row:
|
|
212
|
+
candidate = Candidate(**dict(candidate_row))
|
|
213
|
+
principal_committee_id = candidate.principal_campaign_committee
|
|
214
|
+
|
|
215
|
+
# Get principal committee if available
|
|
216
|
+
if principal_committee_id:
|
|
217
|
+
committee_result = await db.execute(
|
|
218
|
+
"""
|
|
219
|
+
SELECT * FROM libfec_committees
|
|
220
|
+
WHERE committee_id = ? AND cycle = ?
|
|
221
|
+
""",
|
|
222
|
+
[principal_committee_id, cycle]
|
|
223
|
+
)
|
|
224
|
+
committee_row = committee_result.first()
|
|
225
|
+
if committee_row:
|
|
226
|
+
committee = Committee(**dict(committee_row))
|
|
227
|
+
|
|
228
|
+
# Fetch filings for this committee
|
|
229
|
+
filings_result = await db.execute(
|
|
230
|
+
"""
|
|
231
|
+
SELECT * FROM libfec_filings
|
|
232
|
+
WHERE filer_id = ?
|
|
233
|
+
ORDER BY filing_id DESC
|
|
234
|
+
LIMIT 50
|
|
235
|
+
""",
|
|
236
|
+
[principal_committee_id]
|
|
237
|
+
)
|
|
238
|
+
filings = [Filing(**dict(row)) for row in filings_result.rows]
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
error = str(e)
|
|
242
|
+
|
|
243
|
+
page_data = CandidatePageData(
|
|
244
|
+
candidate_id=candidate_id,
|
|
245
|
+
cycle=cycle,
|
|
246
|
+
candidate=candidate,
|
|
247
|
+
committee=committee,
|
|
248
|
+
filings=filings,
|
|
249
|
+
error=error,
|
|
250
|
+
)
|
|
251
|
+
candidate_name = candidate.name if candidate else candidate_id
|
|
252
|
+
return Response.html(
|
|
253
|
+
await datasette.render_template(
|
|
254
|
+
"libfec_base.html",
|
|
255
|
+
{
|
|
256
|
+
"page_title": f"{candidate_name} - Candidate",
|
|
257
|
+
"entrypoint": "src/candidate_view.ts",
|
|
258
|
+
"page_data": page_data.model_dump(),
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@router.GET("/-/libfec/committee/(?P<committee_id>[^/]+)$")
|
|
265
|
+
@check_permission()
|
|
266
|
+
async def committee_page(datasette, request, committee_id: str):
|
|
267
|
+
"""
|
|
268
|
+
Committee detail page.
|
|
269
|
+
|
|
270
|
+
Shows committee information and recent filings.
|
|
271
|
+
"""
|
|
272
|
+
cycle = int(request.args.get("cycle", 2026))
|
|
273
|
+
committee = None
|
|
274
|
+
candidate = None
|
|
275
|
+
filings = []
|
|
276
|
+
error = None
|
|
277
|
+
candidate_id = None
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
db = datasette.get_database()
|
|
281
|
+
|
|
282
|
+
# Fetch committee from database
|
|
283
|
+
committee_result = await db.execute(
|
|
284
|
+
"""
|
|
285
|
+
SELECT * FROM libfec_committees
|
|
286
|
+
WHERE committee_id = ? AND cycle = ?
|
|
287
|
+
""",
|
|
288
|
+
[committee_id, cycle]
|
|
289
|
+
)
|
|
290
|
+
committee_row = committee_result.first()
|
|
291
|
+
if committee_row:
|
|
292
|
+
committee = Committee(**dict(committee_row))
|
|
293
|
+
candidate_id = committee.candidate_id
|
|
294
|
+
|
|
295
|
+
# If this committee has a candidate_id, fetch the candidate
|
|
296
|
+
if candidate_id:
|
|
297
|
+
candidate_result = await db.execute(
|
|
298
|
+
"""
|
|
299
|
+
SELECT * FROM libfec_candidates
|
|
300
|
+
WHERE candidate_id = ? AND cycle = ?
|
|
301
|
+
""",
|
|
302
|
+
[candidate_id, cycle]
|
|
303
|
+
)
|
|
304
|
+
candidate_row = candidate_result.first()
|
|
305
|
+
if candidate_row:
|
|
306
|
+
candidate = Candidate(**dict(candidate_row))
|
|
307
|
+
|
|
308
|
+
# Fetch filings for this committee
|
|
309
|
+
filings_result = await db.execute(
|
|
310
|
+
"""
|
|
311
|
+
SELECT * FROM libfec_filings
|
|
312
|
+
WHERE filer_id = ?
|
|
313
|
+
ORDER BY filing_id DESC
|
|
314
|
+
LIMIT 50
|
|
315
|
+
""",
|
|
316
|
+
[committee_id]
|
|
317
|
+
)
|
|
318
|
+
filings = [Filing(**dict(row)) for row in filings_result.rows]
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
error = str(e)
|
|
322
|
+
|
|
323
|
+
page_data = CommitteePageData(
|
|
324
|
+
committee_id=committee_id,
|
|
325
|
+
cycle=cycle,
|
|
326
|
+
committee=committee,
|
|
327
|
+
candidate=candidate,
|
|
328
|
+
filings=filings,
|
|
329
|
+
error=error,
|
|
330
|
+
)
|
|
331
|
+
committee_name = committee.name if committee else committee_id
|
|
332
|
+
return Response.html(
|
|
333
|
+
await datasette.render_template(
|
|
334
|
+
"libfec_base.html",
|
|
335
|
+
{
|
|
336
|
+
"page_title": f"{committee_name} - Committee",
|
|
337
|
+
"entrypoint": "src/committee_view.ts",
|
|
338
|
+
"page_data": page_data.model_dump(),
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
)
|