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.
Files changed (41) hide show
  1. datasette_libfec/__init__.py +18 -2
  2. datasette_libfec/libfec_client.py +225 -11
  3. datasette_libfec/libfec_export_rpc_client.py +358 -0
  4. datasette_libfec/libfec_rpc_client.py +335 -0
  5. datasette_libfec/libfec_search_rpc_client.py +308 -0
  6. datasette_libfec/manifest.json +84 -2
  7. datasette_libfec/page_data.py +87 -0
  8. datasette_libfec/router.py +24 -0
  9. datasette_libfec/routes_export.py +128 -0
  10. datasette_libfec/routes_exports.py +222 -0
  11. datasette_libfec/routes_pages.py +341 -0
  12. datasette_libfec/routes_rss.py +416 -0
  13. datasette_libfec/routes_search.py +78 -0
  14. datasette_libfec/state.py +6 -0
  15. datasette_libfec/static/gen/candidate-BEqDafKu.css +1 -0
  16. datasette_libfec/static/gen/candidate-tqxa29G-.js +3 -0
  17. datasette_libfec/static/gen/class-C5DDKbJD.js +2 -0
  18. datasette_libfec/static/gen/committee-Bmki9iKb.css +1 -0
  19. datasette_libfec/static/gen/committee-DY1GmylW.js +2 -0
  20. datasette_libfec/static/gen/contest-BbYrzKRg.js +1 -0
  21. datasette_libfec/static/gen/contest-D4Fj7kGA.css +1 -0
  22. datasette_libfec/static/gen/each-DkfQbqzj.js +1 -0
  23. datasette_libfec/static/gen/filing_detail-Ba6_iQwV.css +1 -0
  24. datasette_libfec/static/gen/filing_detail-D2ib3OM6.js +26 -0
  25. datasette_libfec/static/gen/index-AHqus2fd.js +9 -0
  26. datasette_libfec/static/gen/index-client-CDwZ_Ixa.js +1 -0
  27. datasette_libfec/static/gen/index-jv9_YIKt.css +1 -0
  28. datasette_libfec/static/gen/load-AXKAVXVj.js +1 -0
  29. datasette_libfec/templates/libfec_base.html +12 -0
  30. {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/METADATA +2 -2
  31. datasette_libfec-0.0.1a6.dist-info/RECORD +37 -0
  32. {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/top_level.txt +1 -0
  33. scripts/typegen-pagedata.py +6 -0
  34. datasette_libfec/routes.py +0 -189
  35. datasette_libfec/static/gen/index-6cjSv2YC.css +0 -1
  36. datasette_libfec/static/gen/index-CaTQMY-X.js +0 -1
  37. datasette_libfec/templates/libfec.html +0 -14
  38. datasette_libfec-0.0.1a4.dist-info/RECORD +0 -14
  39. {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/WHEEL +0 -0
  40. {datasette_libfec-0.0.1a4.dist-info → datasette_libfec-0.0.1a6.dist-info}/entry_points.txt +0 -0
  41. {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
+ )