Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Deploy: Consolidated gold tables, fixed nginx docs routing
Browse files- api/routes/bills_neon.py +92 -9
- frontend/src/pages/PolicyMap.tsx +9 -2
api/routes/bills_neon.py
CHANGED
|
@@ -260,8 +260,15 @@ async def fetch_bills_from_parquet(
|
|
| 260 |
raise
|
| 261 |
|
| 262 |
|
| 263 |
-
async def fetch_sessions_from_parquet(
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
try:
|
| 266 |
# Use flat file structure (all states in one file)
|
| 267 |
bills_file = GOLD_DIR / "bills_bills.parquet"
|
|
@@ -272,8 +279,71 @@ async def fetch_sessions_from_parquet(state: str) -> Dict[str, Any]:
|
|
| 272 |
# Connect to DuckDB
|
| 273 |
conn = duckdb.connect()
|
| 274 |
|
| 275 |
-
#
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
SELECT
|
| 278 |
session,
|
| 279 |
MAX(session_name) as session_name,
|
|
@@ -281,12 +351,12 @@ async def fetch_sessions_from_parquet(state: str) -> Dict[str, Any]:
|
|
| 281 |
MAX(latest_action_date) as end_date,
|
| 282 |
COUNT(*) as bill_count
|
| 283 |
FROM read_parquet(?)
|
| 284 |
-
WHERE
|
| 285 |
GROUP BY session, session_name
|
| 286 |
ORDER BY session DESC
|
| 287 |
"""
|
| 288 |
|
| 289 |
-
rows = conn.execute(sql,
|
| 290 |
|
| 291 |
sessions = []
|
| 292 |
for row in rows:
|
|
@@ -513,16 +583,29 @@ async def get_bills(
|
|
| 513 |
|
| 514 |
@router.get("/sessions")
|
| 515 |
async def get_sessions(
|
| 516 |
-
state: str = Query(..., description="State abbreviation (e.g., MA, AL)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
):
|
| 518 |
"""
|
| 519 |
-
Get legislative sessions for a state using parquet files.
|
| 520 |
|
| 521 |
**Examples:**
|
| 522 |
- `/api/bills/sessions?state=MA` - Get all Massachusetts sessions
|
|
|
|
| 523 |
"""
|
| 524 |
try:
|
| 525 |
-
result = await fetch_sessions_from_parquet(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
|
| 527 |
return result
|
| 528 |
|
|
|
|
| 260 |
raise
|
| 261 |
|
| 262 |
|
| 263 |
+
async def fetch_sessions_from_parquet(
|
| 264 |
+
state: str,
|
| 265 |
+
topic: Optional[str] = None,
|
| 266 |
+
chamber: Optional[str] = None,
|
| 267 |
+
bill_type: Optional[str] = None,
|
| 268 |
+
status: Optional[str] = None,
|
| 269 |
+
q: Optional[str] = None
|
| 270 |
+
) -> Dict[str, Any]:
|
| 271 |
+
"""Fetch sessions from parquet files using DuckDB, filtered by active filters."""
|
| 272 |
try:
|
| 273 |
# Use flat file structure (all states in one file)
|
| 274 |
bills_file = GOLD_DIR / "bills_bills.parquet"
|
|
|
|
| 279 |
# Connect to DuckDB
|
| 280 |
conn = duckdb.connect()
|
| 281 |
|
| 282 |
+
# Build WHERE clause with all filters
|
| 283 |
+
where_conditions = ["state = ?"]
|
| 284 |
+
params = [data_source, state]
|
| 285 |
+
|
| 286 |
+
# Topic filter
|
| 287 |
+
if topic:
|
| 288 |
+
topic_keywords = {
|
| 289 |
+
'fluoride': 'fluorid',
|
| 290 |
+
'dental': 'dental',
|
| 291 |
+
'medicaid': 'medicaid',
|
| 292 |
+
'oral health': 'oral|dental|teeth',
|
| 293 |
+
'health': 'health',
|
| 294 |
+
'education': 'education|school'
|
| 295 |
+
}
|
| 296 |
+
keyword = topic_keywords.get(topic.lower(), topic)
|
| 297 |
+
where_conditions.append(f"REGEXP_MATCHES(LOWER(title), LOWER(?))")
|
| 298 |
+
params.append(keyword)
|
| 299 |
+
|
| 300 |
+
# Chamber filter
|
| 301 |
+
if chamber:
|
| 302 |
+
if chamber.lower() == 'house':
|
| 303 |
+
where_conditions.append("(bill_number LIKE 'HB%' OR bill_number LIKE 'HR%' OR bill_number LIKE 'HJR%' OR bill_number LIKE 'HCR%' OR bill_number LIKE 'HJM%')")
|
| 304 |
+
elif chamber.lower() == 'senate':
|
| 305 |
+
where_conditions.append("(bill_number LIKE 'SB%' OR bill_number LIKE 'SR%' OR bill_number LIKE 'SJR%' OR bill_number LIKE 'SCR%' OR bill_number LIKE 'SJM%')")
|
| 306 |
+
elif chamber.lower() == 'joint':
|
| 307 |
+
where_conditions.append("(bill_number LIKE '%JR%' OR bill_number LIKE '%JM%')")
|
| 308 |
+
|
| 309 |
+
# Bill type filter
|
| 310 |
+
if bill_type:
|
| 311 |
+
type_patterns = {
|
| 312 |
+
'bill': "(bill_number LIKE 'HB%' OR bill_number LIKE 'SB%' OR bill_number LIKE 'AB%')",
|
| 313 |
+
'resolution': "(bill_number LIKE 'HR%' OR bill_number LIKE 'SR%' OR bill_number LIKE 'AR%')",
|
| 314 |
+
'joint_resolution': "(bill_number LIKE 'HJR%' OR bill_number LIKE 'SJR%' OR bill_number LIKE 'AJR%')",
|
| 315 |
+
'concurrent_resolution': "(bill_number LIKE 'HCR%' OR bill_number LIKE 'SCR%')",
|
| 316 |
+
'memorial': "(bill_number LIKE 'HJM%' OR bill_number LIKE 'SJM%')"
|
| 317 |
+
}
|
| 318 |
+
if bill_type.lower() in type_patterns:
|
| 319 |
+
where_conditions.append(type_patterns[bill_type.lower()])
|
| 320 |
+
|
| 321 |
+
# Status filter
|
| 322 |
+
if status:
|
| 323 |
+
status_keywords = {
|
| 324 |
+
'enacted': 'Enacted',
|
| 325 |
+
'passed': 'passed|Passed',
|
| 326 |
+
'adopted': 'Adopted|adopted',
|
| 327 |
+
'failed': 'Failed|failed',
|
| 328 |
+
'introduced': 'Introduced|introduced',
|
| 329 |
+
'referred': 'referred|Referred',
|
| 330 |
+
'reported': 'reported|Reported'
|
| 331 |
+
}
|
| 332 |
+
keyword = status_keywords.get(status.lower(), status)
|
| 333 |
+
where_conditions.append(f"REGEXP_MATCHES(COALESCE(latest_action_description, ''), ?)")
|
| 334 |
+
params.append(keyword)
|
| 335 |
+
|
| 336 |
+
# Search query
|
| 337 |
+
if q:
|
| 338 |
+
where_conditions.append("(LOWER(title) LIKE ? OR LOWER(bill_number) LIKE ?)")
|
| 339 |
+
search_pattern = f"%{q.lower()}%"
|
| 340 |
+
params.append(search_pattern)
|
| 341 |
+
params.append(search_pattern)
|
| 342 |
+
|
| 343 |
+
where_clause = " AND ".join(where_conditions)
|
| 344 |
+
|
| 345 |
+
# Aggregate sessions - filter by state and other filters
|
| 346 |
+
sql = f"""
|
| 347 |
SELECT
|
| 348 |
session,
|
| 349 |
MAX(session_name) as session_name,
|
|
|
|
| 351 |
MAX(latest_action_date) as end_date,
|
| 352 |
COUNT(*) as bill_count
|
| 353 |
FROM read_parquet(?)
|
| 354 |
+
WHERE {where_clause}
|
| 355 |
GROUP BY session, session_name
|
| 356 |
ORDER BY session DESC
|
| 357 |
"""
|
| 358 |
|
| 359 |
+
rows = conn.execute(sql, params).fetchall()
|
| 360 |
|
| 361 |
sessions = []
|
| 362 |
for row in rows:
|
|
|
|
| 583 |
|
| 584 |
@router.get("/sessions")
|
| 585 |
async def get_sessions(
|
| 586 |
+
state: str = Query(..., description="State abbreviation (e.g., MA, AL)"),
|
| 587 |
+
topic: Optional[str] = Query(None, description="Topic filter (e.g., fluoride, dental)"),
|
| 588 |
+
chamber: Optional[str] = Query(None, description="Chamber filter (house, senate, joint)"),
|
| 589 |
+
bill_type: Optional[str] = Query(None, description="Bill type filter"),
|
| 590 |
+
status: Optional[str] = Query(None, description="Status filter"),
|
| 591 |
+
q: Optional[str] = Query(None, description="Search query")
|
| 592 |
):
|
| 593 |
"""
|
| 594 |
+
Get legislative sessions for a state using parquet files, filtered by active search criteria.
|
| 595 |
|
| 596 |
**Examples:**
|
| 597 |
- `/api/bills/sessions?state=MA` - Get all Massachusetts sessions
|
| 598 |
+
- `/api/bills/sessions?state=MA&topic=dental` - Get sessions with dental bills
|
| 599 |
"""
|
| 600 |
try:
|
| 601 |
+
result = await fetch_sessions_from_parquet(
|
| 602 |
+
state=state.upper(),
|
| 603 |
+
topic=topic,
|
| 604 |
+
chamber=chamber,
|
| 605 |
+
bill_type=bill_type,
|
| 606 |
+
status=status,
|
| 607 |
+
q=q
|
| 608 |
+
)
|
| 609 |
|
| 610 |
return result
|
| 611 |
|
frontend/src/pages/PolicyMap.tsx
CHANGED
|
@@ -103,9 +103,16 @@ export default function PolicyMap() {
|
|
| 103 |
|
| 104 |
// Fetch sessions
|
| 105 |
const { data: sessionsData } = useQuery({
|
| 106 |
-
queryKey: ['sessions', selectedState],
|
| 107 |
queryFn: async () => {
|
| 108 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
return response.data
|
| 110 |
},
|
| 111 |
enabled: viewMode === 'list', // Only fetch sessions in list view
|
|
|
|
| 103 |
|
| 104 |
// Fetch sessions
|
| 105 |
const { data: sessionsData } = useQuery({
|
| 106 |
+
queryKey: ['sessions', selectedState, selectedTopic, selectedChamber, selectedBillType, selectedStatus, searchQuery],
|
| 107 |
queryFn: async () => {
|
| 108 |
+
const params = new URLSearchParams({ state: selectedState })
|
| 109 |
+
if (selectedTopic) params.append('topic', selectedTopic)
|
| 110 |
+
if (selectedChamber) params.append('chamber', selectedChamber)
|
| 111 |
+
if (selectedBillType) params.append('bill_type', selectedBillType)
|
| 112 |
+
if (selectedStatus) params.append('status', selectedStatus)
|
| 113 |
+
if (searchQuery) params.append('q', searchQuery)
|
| 114 |
+
|
| 115 |
+
const response = await api.get(`/bills/sessions?${params}`)
|
| 116 |
return response.data
|
| 117 |
},
|
| 118 |
enabled: viewMode === 'list', // Only fetch sessions in list view
|