144 lines
7.2 KiB
Python
144 lines
7.2 KiB
Python
"""add Meet Kevin tables
|
|
|
|
Revision ID: c3d4e5f6a7b8
|
|
Revises: b2c3d4e5f6a7
|
|
Create Date: 2026-05-21 10:00:00.000000
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = "c3d4e5f6a7b8"
|
|
down_revision: Union[str, Sequence[str], None] = "b2c3d4e5f6a7"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Create the 5 Meet Kevin tables with enums, indexes, and seed data."""
|
|
|
|
# Create kevin_channels table
|
|
op.create_table(
|
|
"kevin_channels",
|
|
sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True),
|
|
sa.Column("youtube_channel_id", sa.String(255), nullable=False, unique=True, index=True),
|
|
sa.Column("title", sa.String(255), nullable=False),
|
|
sa.Column("poll_enabled", sa.Boolean, nullable=False, server_default="true"),
|
|
sa.Column("poll_interval_seconds", sa.Integer, nullable=False, server_default="10800"),
|
|
sa.Column("daily_cost_cap_usd", sa.Numeric(8, 2), nullable=False, server_default="5.00"),
|
|
sa.Column("last_polled_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
)
|
|
|
|
# Create kevin_videos table
|
|
op.create_table(
|
|
"kevin_videos",
|
|
sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True),
|
|
sa.Column("channel_id", sa.BigInteger, sa.ForeignKey("kevin_channels.id"), nullable=False, index=True),
|
|
sa.Column("youtube_video_id", sa.String(255), nullable=False, unique=True, index=True),
|
|
sa.Column("title", sa.String(500), nullable=False),
|
|
sa.Column("description", sa.Text, nullable=True),
|
|
sa.Column("published_at", sa.DateTime(timezone=True), nullable=True, index=True),
|
|
sa.Column("duration_seconds", sa.Integer, nullable=True),
|
|
sa.Column("thumbnail_url", sa.Text, nullable=True),
|
|
sa.Column("status", sa.Enum("discovered", "captioned", "analyzed", "failed", "skipped", name="kevin_video_status"), nullable=False, server_default="discovered", index=True),
|
|
sa.Column("failure_reason", sa.String(500), nullable=True),
|
|
sa.Column("retry_count", sa.Integer, nullable=False, server_default="0"),
|
|
sa.Column("processed_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
)
|
|
|
|
# Create kevin_transcripts table
|
|
op.create_table(
|
|
"kevin_transcripts",
|
|
sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True),
|
|
sa.Column("video_id", sa.BigInteger, sa.ForeignKey("kevin_videos.id"), nullable=False, unique=True),
|
|
sa.Column("source", sa.Enum("captions_manual", "captions_auto", "none", name="kevin_transcript_source"), nullable=False),
|
|
sa.Column("language", sa.String(8), nullable=False),
|
|
sa.Column("raw_text", sa.Text, nullable=False),
|
|
sa.Column("segments_json", postgresql.JSONB, nullable=True),
|
|
sa.Column("word_count", sa.Integer, nullable=False),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
)
|
|
|
|
# Create kevin_analyses table
|
|
op.create_table(
|
|
"kevin_analyses",
|
|
sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True),
|
|
sa.Column("video_id", sa.BigInteger, sa.ForeignKey("kevin_videos.id"), nullable=False, index=True),
|
|
sa.Column("model", sa.String(100), nullable=False),
|
|
sa.Column("prompt_version", sa.String(50), nullable=False),
|
|
sa.Column("market_outlook_direction", sa.Enum("bullish", "neutral", "bearish", "mixed", name="kevin_market_outlook"), nullable=False),
|
|
sa.Column("market_outlook_reasoning", sa.Text, nullable=False),
|
|
sa.Column("macro_themes_json", postgresql.JSONB, nullable=True),
|
|
sa.Column("key_risks_json", postgresql.JSONB, nullable=True),
|
|
sa.Column("summary", sa.Text, nullable=False),
|
|
sa.Column("raw_response_json", postgresql.JSONB, nullable=True),
|
|
sa.Column("prompt_tokens", sa.Integer, nullable=False),
|
|
sa.Column("completion_tokens", sa.Integer, nullable=False),
|
|
sa.Column("cost_usd", sa.Numeric(10, 4), nullable=False),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
)
|
|
|
|
# Create kevin_stock_mentions table
|
|
op.create_table(
|
|
"kevin_stock_mentions",
|
|
sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True),
|
|
sa.Column("video_id", sa.BigInteger, sa.ForeignKey("kevin_videos.id"), nullable=False, index=True),
|
|
sa.Column("analysis_id", sa.BigInteger, sa.ForeignKey("kevin_analyses.id"), nullable=False),
|
|
sa.Column("symbol", sa.String(16), nullable=False, index=True),
|
|
sa.Column("action", sa.Enum("buy", "sell", "hold", "watch", "avoid", name="kevin_ticker_action"), nullable=False),
|
|
sa.Column("conviction", sa.Numeric(4, 3), nullable=False),
|
|
sa.Column("time_horizon", sa.Enum("intraday", "days", "weeks", "months", "long_term", "unspecified", name="kevin_time_horizon"), nullable=False),
|
|
sa.Column("rationale_quote", sa.Text, nullable=False),
|
|
sa.Column("video_timestamp_seconds", sa.Integer, nullable=True),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
)
|
|
|
|
# Create composite index on kevin_stock_mentions (symbol, created_at DESC)
|
|
op.create_index(
|
|
"ix_kevin_stock_mentions_symbol_created_at",
|
|
"kevin_stock_mentions",
|
|
["symbol", sa.text("created_at DESC")],
|
|
)
|
|
|
|
# Create partial index on kevin_videos (status) for pending videos
|
|
op.create_index(
|
|
"ix_kevin_videos_status_pending",
|
|
"kevin_videos",
|
|
["status"],
|
|
postgresql_where=sa.text("status IN ('discovered', 'captioned')"),
|
|
)
|
|
|
|
# Seed the Meet Kevin channel
|
|
op.execute(
|
|
"""
|
|
INSERT INTO kevin_channels (youtube_channel_id, title, poll_enabled, poll_interval_seconds, daily_cost_cap_usd)
|
|
VALUES ('UCUvvj5lwue7PspotMDjk5UA', 'Meet Kevin', true, 10800, 5.00)
|
|
ON CONFLICT (youtube_channel_id) DO NOTHING;
|
|
"""
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Drop all Meet Kevin tables and enum types."""
|
|
|
|
# Drop tables in reverse dependency order
|
|
op.drop_table("kevin_stock_mentions")
|
|
op.drop_table("kevin_analyses")
|
|
op.drop_table("kevin_transcripts")
|
|
op.drop_table("kevin_videos")
|
|
op.drop_table("kevin_channels")
|
|
|
|
# Drop enum types
|
|
sa.Enum(name="kevin_time_horizon").drop(op.get_bind(), checkfirst=False)
|
|
sa.Enum(name="kevin_ticker_action").drop(op.get_bind(), checkfirst=False)
|
|
sa.Enum(name="kevin_market_outlook").drop(op.get_bind(), checkfirst=False)
|
|
sa.Enum(name="kevin_transcript_source").drop(op.get_bind(), checkfirst=False)
|
|
sa.Enum(name="kevin_video_status").drop(op.get_bind(), checkfirst=False)
|