feat: PocketVeto v1.0.0 — initial public release
Self-hosted US Congress monitoring platform with AI policy briefs, bill/member/topic follows, ntfy + RSS + email notifications, alignment scoring, collections, and draft-letter generator. Authored by: Jack Levy
This commit is contained in:
381
backend/app/schemas/schemas.py
Normal file
381
backend/app/schemas/schemas.py
Normal file
@@ -0,0 +1,381 @@
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Generic, Optional, TypeVar
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
# ── Notifications ──────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Bill Notes ────────────────────────────────────────────────────────────────
|
||||
|
||||
class BillNoteSchema(BaseModel):
|
||||
id: int
|
||||
bill_id: str
|
||||
content: str
|
||||
pinned: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class BillNoteUpsert(BaseModel):
|
||||
content: str
|
||||
pinned: bool = False
|
||||
|
||||
|
||||
# ── Notifications ──────────────────────────────────────────────────────────────
|
||||
|
||||
class NotificationSettingsResponse(BaseModel):
|
||||
ntfy_topic_url: str = ""
|
||||
ntfy_auth_method: str = "none" # none | token | basic
|
||||
ntfy_token: str = ""
|
||||
ntfy_username: str = ""
|
||||
ntfy_password_set: bool = False
|
||||
ntfy_enabled: bool = False
|
||||
rss_enabled: bool = False
|
||||
rss_token: Optional[str] = None
|
||||
email_enabled: bool = False
|
||||
email_address: str = ""
|
||||
# Digest
|
||||
digest_enabled: bool = False
|
||||
digest_frequency: str = "daily" # daily | weekly
|
||||
# Quiet hours — stored as local-time hour integers (0-23); timezone is IANA name
|
||||
quiet_hours_start: Optional[int] = None
|
||||
quiet_hours_end: Optional[int] = None
|
||||
timezone: Optional[str] = None # IANA name, e.g. "America/New_York"
|
||||
alert_filters: Optional[dict] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class NotificationSettingsUpdate(BaseModel):
|
||||
ntfy_topic_url: Optional[str] = None
|
||||
ntfy_auth_method: Optional[str] = None
|
||||
ntfy_token: Optional[str] = None
|
||||
ntfy_username: Optional[str] = None
|
||||
ntfy_password: Optional[str] = None
|
||||
ntfy_enabled: Optional[bool] = None
|
||||
rss_enabled: Optional[bool] = None
|
||||
email_enabled: Optional[bool] = None
|
||||
email_address: Optional[str] = None
|
||||
digest_enabled: Optional[bool] = None
|
||||
digest_frequency: Optional[str] = None
|
||||
quiet_hours_start: Optional[int] = None
|
||||
quiet_hours_end: Optional[int] = None
|
||||
timezone: Optional[str] = None # IANA name sent by the browser on save
|
||||
alert_filters: Optional[dict] = None
|
||||
|
||||
|
||||
class NotificationEventSchema(BaseModel):
|
||||
id: int
|
||||
bill_id: str
|
||||
event_type: str
|
||||
payload: Optional[Any] = None
|
||||
dispatched_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class NtfyTestRequest(BaseModel):
|
||||
ntfy_topic_url: str
|
||||
ntfy_auth_method: str = "none"
|
||||
ntfy_token: str = ""
|
||||
ntfy_username: str = ""
|
||||
ntfy_password: str = ""
|
||||
|
||||
|
||||
class FollowModeTestRequest(BaseModel):
|
||||
mode: str # pocket_veto | pocket_boost
|
||||
event_type: str # new_document | new_amendment | bill_updated
|
||||
|
||||
|
||||
class NotificationTestResult(BaseModel):
|
||||
status: str # "ok" | "error"
|
||||
detail: str
|
||||
event_count: Optional[int] = None # RSS only
|
||||
|
||||
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
|
||||
effectiveness_score: Optional[float] = None
|
||||
effectiveness_percentile: Optional[float] = None
|
||||
effectiveness_tier: Optional[str] = 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
|
||||
share_token: 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
|
||||
bill_category: Optional[str] = None
|
||||
has_document: bool = False
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class BillDetailSchema(BillSchema):
|
||||
actions: list[BillActionSchema] = []
|
||||
news_articles: list[NewsArticleSchema] = []
|
||||
trend_scores: list[TrendScoreSchema] = []
|
||||
briefs: list[BriefSchema] = []
|
||||
has_document: bool = False
|
||||
|
||||
|
||||
# ── 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
|
||||
follow_mode: str = "neutral"
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class FollowModeUpdate(BaseModel):
|
||||
follow_mode: str
|
||||
|
||||
|
||||
# ── 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
|
||||
api_keys_configured: dict[str, bool]
|
||||
|
||||
|
||||
# ── Collections ────────────────────────────────────────────────────────────────
|
||||
|
||||
class CollectionCreate(BaseModel):
|
||||
name: str
|
||||
is_public: bool = False
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not 1 <= len(v) <= 100:
|
||||
raise ValueError("name must be 1–100 characters")
|
||||
return v
|
||||
|
||||
|
||||
class CollectionUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
is_public: Optional[bool] = None
|
||||
|
||||
|
||||
class CollectionSchema(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
is_public: bool
|
||||
share_token: str
|
||||
bill_count: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class CollectionDetailSchema(CollectionSchema):
|
||||
bills: list[BillSchema]
|
||||
|
||||
|
||||
class BriefShareResponse(BaseModel):
|
||||
brief: BriefSchema
|
||||
bill: BillSchema
|
||||
|
||||
|
||||
# ── Votes ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
class MemberVotePositionSchema(BaseModel):
|
||||
bioguide_id: Optional[str] = None
|
||||
member_name: Optional[str] = None
|
||||
party: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
position: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class BillVoteSchema(BaseModel):
|
||||
id: int
|
||||
congress: int
|
||||
chamber: str
|
||||
session: int
|
||||
roll_number: int
|
||||
question: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
vote_date: Optional[date] = None
|
||||
yeas: Optional[int] = None
|
||||
nays: Optional[int] = None
|
||||
not_voting: Optional[int] = None
|
||||
result: Optional[str] = None
|
||||
source_url: Optional[str] = None
|
||||
positions: list[MemberVotePositionSchema] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
Reference in New Issue
Block a user