import logging from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import desc, func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.database import get_db from app.models import Bill, Member, MemberTrendScore, MemberNewsArticle from app.schemas.schemas import ( BillSchema, MemberSchema, MemberTrendScoreSchema, MemberNewsArticleSchema, PaginatedResponse, ) from app.services import congress_api logger = logging.getLogger(__name__) router = APIRouter() @router.get("", response_model=PaginatedResponse[MemberSchema]) async def list_members( chamber: Optional[str] = Query(None), party: Optional[str] = Query(None), state: Optional[str] = Query(None), q: Optional[str] = Query(None), page: int = Query(1, ge=1), per_page: int = Query(50, ge=1, le=250), db: AsyncSession = Depends(get_db), ): query = select(Member) if chamber: query = query.where(Member.chamber == chamber) if party: query = query.where(Member.party == party) if state: query = query.where(Member.state == state) if q: # name is stored as "Last, First" — also match "First Last" order first_last = func.concat( func.split_part(Member.name, ", ", 2), " ", func.split_part(Member.name, ", ", 1), ) query = query.where(or_( Member.name.ilike(f"%{q}%"), first_last.ilike(f"%{q}%"), )) total = await db.scalar(select(func.count()).select_from(query.subquery())) or 0 query = query.order_by(Member.last_name, Member.first_name).offset((page - 1) * per_page).limit(per_page) result = await db.execute(query) members = result.scalars().all() return PaginatedResponse( items=members, total=total, page=page, per_page=per_page, pages=max(1, (total + per_page - 1) // per_page), ) @router.get("/{bioguide_id}", response_model=MemberSchema) async def get_member(bioguide_id: str, db: AsyncSession = Depends(get_db)): member = await db.get(Member, bioguide_id) if not member: raise HTTPException(status_code=404, detail="Member not found") # Kick off member interest scoring on first view (non-blocking) if member.detail_fetched is None: try: from app.workers.member_interest import fetch_member_news, calculate_member_trend_score fetch_member_news.delay(bioguide_id) calculate_member_trend_score.delay(bioguide_id) except Exception: pass # Lazy-enrich with detail data from Congress.gov on first view if member.detail_fetched is None: try: detail_raw = congress_api.get_member_detail(bioguide_id) enriched = congress_api.parse_member_detail_from_api(detail_raw) for field, value in enriched.items(): if value is not None: setattr(member, field, value) member.detail_fetched = datetime.now(timezone.utc) await db.commit() await db.refresh(member) except Exception as e: logger.warning(f"Could not enrich member detail for {bioguide_id}: {e}") # Attach latest trend score result_schema = MemberSchema.model_validate(member) latest_trend = ( await db.execute( select(MemberTrendScore) .where(MemberTrendScore.member_id == bioguide_id) .order_by(desc(MemberTrendScore.score_date)) .limit(1) ) ) trend = latest_trend.scalar_one_or_none() if trend: result_schema.latest_trend = MemberTrendScoreSchema.model_validate(trend) return result_schema @router.get("/{bioguide_id}/trend", response_model=list[MemberTrendScoreSchema]) async def get_member_trend( bioguide_id: str, days: int = Query(30, ge=7, le=365), db: AsyncSession = Depends(get_db), ): from datetime import date, timedelta cutoff = date.today() - timedelta(days=days) result = await db.execute( select(MemberTrendScore) .where(MemberTrendScore.member_id == bioguide_id, MemberTrendScore.score_date >= cutoff) .order_by(MemberTrendScore.score_date) ) return result.scalars().all() @router.get("/{bioguide_id}/news", response_model=list[MemberNewsArticleSchema]) async def get_member_news(bioguide_id: str, db: AsyncSession = Depends(get_db)): result = await db.execute( select(MemberNewsArticle) .where(MemberNewsArticle.member_id == bioguide_id) .order_by(desc(MemberNewsArticle.published_at)) .limit(20) ) return result.scalars().all() @router.get("/{bioguide_id}/bills", response_model=PaginatedResponse[BillSchema]) async def get_member_bills( bioguide_id: str, page: int = Query(1, ge=1), per_page: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), ): query = select(Bill).options(selectinload(Bill.briefs), selectinload(Bill.sponsor)).where(Bill.sponsor_id == bioguide_id) total = await db.scalar(select(func.count()).select_from(query.subquery())) or 0 query = query.order_by(desc(Bill.introduced_date)).offset((page - 1) * per_page).limit(per_page) result = await db.execute(query) bills = result.scalars().all() items = [] for bill in bills: b = BillSchema.model_validate(bill) if bill.briefs: b.latest_brief = bill.briefs[0] items.append(b) return PaginatedResponse( items=items, total=total, page=page, per_page=per_page, pages=max(1, (total + per_page - 1) // per_page), )