feat: add Alembic migration for Meet Kevin tables
This commit is contained in:
parent
a49e46f787
commit
61adf63c7d
1 changed files with 144 additions and 0 deletions
144
alembic/versions/c3d4e5f6a7b8_add_meet_kevin_tables.py
Normal file
144
alembic/versions/c3d4e5f6a7b8_add_meet_kevin_tables.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue