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