Adds Google Trends, NewsAPI, and Google News RSS scoring for members,
mirroring the existing bill interest pipeline. Member profiles now show
a Public Interest chart (with signal breakdown) and a Related News panel.
Key changes:
- New member_trend_scores + member_news_articles tables (migration 0008)
- fetch_gnews_articles() added to news_service for unlimited RSS article storage
- Bill news fetcher now combines NewsAPI + Google News RSS (more coverage)
- New member_interest Celery worker with scheduled news + trend tasks
- GET /members/{id}/trend and /news API endpoints
- TrendChart redesigned with signal breakdown badges and bar+line combo chart
- NewsPanel accepts generic article shape (bills and members)
Co-Authored-By: Jack Levy
204 lines
6.5 KiB
Python
204 lines
6.5 KiB
Python
from datetime import date, datetime
|
|
from typing import Any, Generic, Optional, TypeVar
|
|
|
|
from pydantic import BaseModel
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class PaginatedResponse(BaseModel, Generic[T]):
|
|
items: list[T]
|
|
total: int
|
|
page: int
|
|
per_page: int
|
|
pages: int
|
|
|
|
|
|
# ── Member ────────────────────────────────────────────────────────────────────
|
|
|
|
class MemberSchema(BaseModel):
|
|
bioguide_id: str
|
|
name: str
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
party: Optional[str] = None
|
|
state: Optional[str] = None
|
|
chamber: Optional[str] = None
|
|
district: Optional[str] = None
|
|
photo_url: Optional[str] = None
|
|
official_url: Optional[str] = None
|
|
congress_url: Optional[str] = None
|
|
birth_year: Optional[str] = None
|
|
address: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
terms_json: Optional[list[Any]] = None
|
|
leadership_json: Optional[list[Any]] = None
|
|
sponsored_count: Optional[int] = None
|
|
cosponsored_count: Optional[int] = None
|
|
latest_trend: Optional["MemberTrendScoreSchema"] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Bill Brief ────────────────────────────────────────────────────────────────
|
|
|
|
class BriefSchema(BaseModel):
|
|
id: int
|
|
brief_type: str = "full"
|
|
summary: Optional[str] = None
|
|
key_points: Optional[list[Any]] = None
|
|
risks: Optional[list[Any]] = None
|
|
deadlines: Optional[list[dict[str, Any]]] = None
|
|
topic_tags: Optional[list[str]] = None
|
|
llm_provider: Optional[str] = None
|
|
llm_model: Optional[str] = None
|
|
govinfo_url: Optional[str] = None
|
|
created_at: Optional[datetime] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Bill Action ───────────────────────────────────────────────────────────────
|
|
|
|
class BillActionSchema(BaseModel):
|
|
id: int
|
|
action_date: Optional[date] = None
|
|
action_text: Optional[str] = None
|
|
action_type: Optional[str] = None
|
|
chamber: Optional[str] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── News Article ──────────────────────────────────────────────────────────────
|
|
|
|
class NewsArticleSchema(BaseModel):
|
|
id: int
|
|
source: Optional[str] = None
|
|
headline: Optional[str] = None
|
|
url: Optional[str] = None
|
|
published_at: Optional[datetime] = None
|
|
relevance_score: Optional[float] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Trend Score ───────────────────────────────────────────────────────────────
|
|
|
|
class TrendScoreSchema(BaseModel):
|
|
score_date: date
|
|
newsapi_count: int
|
|
gnews_count: int
|
|
gtrends_score: float
|
|
composite_score: float
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class MemberTrendScoreSchema(BaseModel):
|
|
score_date: date
|
|
newsapi_count: int
|
|
gnews_count: int
|
|
gtrends_score: float
|
|
composite_score: float
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class MemberNewsArticleSchema(BaseModel):
|
|
id: int
|
|
source: Optional[str] = None
|
|
headline: Optional[str] = None
|
|
url: Optional[str] = None
|
|
published_at: Optional[datetime] = None
|
|
relevance_score: Optional[float] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Bill ──────────────────────────────────────────────────────────────────────
|
|
|
|
class BillSchema(BaseModel):
|
|
bill_id: str
|
|
congress_number: int
|
|
bill_type: str
|
|
bill_number: int
|
|
title: Optional[str] = None
|
|
short_title: Optional[str] = None
|
|
introduced_date: Optional[date] = None
|
|
latest_action_date: Optional[date] = None
|
|
latest_action_text: Optional[str] = None
|
|
status: Optional[str] = None
|
|
chamber: Optional[str] = None
|
|
congress_url: Optional[str] = None
|
|
sponsor: Optional[MemberSchema] = None
|
|
latest_brief: Optional[BriefSchema] = None
|
|
latest_trend: Optional[TrendScoreSchema] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class BillDetailSchema(BillSchema):
|
|
actions: list[BillActionSchema] = []
|
|
news_articles: list[NewsArticleSchema] = []
|
|
trend_scores: list[TrendScoreSchema] = []
|
|
briefs: list[BriefSchema] = []
|
|
|
|
|
|
# ── Follow ────────────────────────────────────────────────────────────────────
|
|
|
|
class FollowCreate(BaseModel):
|
|
follow_type: str # bill | member | topic
|
|
follow_value: str
|
|
|
|
|
|
class FollowSchema(BaseModel):
|
|
id: int
|
|
user_id: int
|
|
follow_type: str
|
|
follow_value: str
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Settings ──────────────────────────────────────────────────────────────────
|
|
|
|
# ── Auth ──────────────────────────────────────────────────────────────────────
|
|
|
|
class UserCreate(BaseModel):
|
|
email: str
|
|
password: str
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
id: int
|
|
email: str
|
|
is_admin: bool
|
|
notification_prefs: dict
|
|
created_at: Optional[datetime] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
user: "UserResponse"
|
|
|
|
|
|
# ── Settings ──────────────────────────────────────────────────────────────────
|
|
|
|
class SettingUpdate(BaseModel):
|
|
key: str
|
|
value: str
|
|
|
|
|
|
class SettingsResponse(BaseModel):
|
|
llm_provider: str
|
|
llm_model: str
|
|
congress_poll_interval_minutes: int
|
|
newsapi_enabled: bool
|
|
pytrends_enabled: bool
|