fix(meet-kevin): API smoke-test bugs from Task 17 QA
Three issues caught during end-to-end manual QA against docker-compose:
1. SAEnum field columns serialized to Python enum NAMES ('DISCOVERED')
but the DB enum had VALUES ('discovered'). Added `values_callable`
to all 5 SAEnum() declarations in shared/models/meet_kevin.py so they
emit values, matching the migration's enum literals.
2. /dashboard's "last 7 days" / "last 14 days" filters used
`func.cast("7 days", type_=None)` which produced NullType DDL.
Replaced with `text("now() - interval '7 days'")`.
3. /dashboard's outlook trend query repeated `func.date_trunc("day", col)`
in SELECT, GROUP BY and ORDER BY — Postgres treats each as a separate
parameterized expression. Hoisted into a single `day_trunc` variable
so all three clauses reference the same SQL fragment.
All 11 /api/meet-kevin/* endpoints now return valid JSON against a
docker-compose Postgres seeded with one analyzed video + NVDA mention.
This commit is contained in:
parent
01856bab9f
commit
7b81980c66
2 changed files with 14 additions and 14 deletions
|
|
@ -612,14 +612,15 @@ async def get_dashboard(
|
||||||
}
|
}
|
||||||
|
|
||||||
# Top conviction last 7 days
|
# Top conviction last 7 days
|
||||||
seven_days_ago = func.now() - func.cast("7 days", type_=None)
|
from sqlalchemy import text
|
||||||
|
seven_days_ago = text("now() - interval '7 days'")
|
||||||
top_conviction_result = await session.execute(
|
top_conviction_result = await session.execute(
|
||||||
select(
|
select(
|
||||||
KevinStockMention.symbol,
|
KevinStockMention.symbol,
|
||||||
func.max(KevinStockMention.conviction).label("max_conviction"),
|
func.max(KevinStockMention.conviction).label("max_conviction"),
|
||||||
func.count().label("mention_count"),
|
func.count().label("mention_count"),
|
||||||
)
|
)
|
||||||
.where(KevinStockMention.created_at >= func.now() - func.cast("7 days", type_=None))
|
.where(KevinStockMention.created_at >= text("now() - interval '7 days'"))
|
||||||
.group_by(KevinStockMention.symbol)
|
.group_by(KevinStockMention.symbol)
|
||||||
.order_by(desc(func.max(KevinStockMention.conviction)))
|
.order_by(desc(func.max(KevinStockMention.conviction)))
|
||||||
.limit(10)
|
.limit(10)
|
||||||
|
|
@ -634,18 +635,17 @@ async def get_dashboard(
|
||||||
]
|
]
|
||||||
|
|
||||||
# 14-day outlook trend: date × direction → count
|
# 14-day outlook trend: date × direction → count
|
||||||
|
# Use a single trunc expression so SELECT/GROUP BY/ORDER BY reference the same SQL fragment
|
||||||
|
day_trunc = func.date_trunc("day", KevinAnalysis.created_at)
|
||||||
outlook_result = await session.execute(
|
outlook_result = await session.execute(
|
||||||
select(
|
select(
|
||||||
func.date_trunc("day", KevinAnalysis.created_at).label("day"),
|
day_trunc.label("day"),
|
||||||
KevinAnalysis.market_outlook_direction,
|
KevinAnalysis.market_outlook_direction,
|
||||||
func.count().label("count"),
|
func.count().label("count"),
|
||||||
)
|
)
|
||||||
.where(KevinAnalysis.created_at >= func.now() - func.cast("14 days", type_=None))
|
.where(KevinAnalysis.created_at >= text("now() - interval '14 days'"))
|
||||||
.group_by(
|
.group_by(day_trunc, KevinAnalysis.market_outlook_direction)
|
||||||
func.date_trunc("day", KevinAnalysis.created_at),
|
.order_by(day_trunc)
|
||||||
KevinAnalysis.market_outlook_direction,
|
|
||||||
)
|
|
||||||
.order_by(func.date_trunc("day", KevinAnalysis.created_at))
|
|
||||||
)
|
)
|
||||||
outlook_trend = [
|
outlook_trend = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ class KevinVideo(TimestampMixin, Base):
|
||||||
duration_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
duration_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||||
thumbnail_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
thumbnail_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
status: Mapped[KevinVideoStatus] = mapped_column(
|
status: Mapped[KevinVideoStatus] = mapped_column(
|
||||||
SAEnum(KevinVideoStatus, name="kevin_video_status"),
|
SAEnum(KevinVideoStatus, name="kevin_video_status", values_callable=lambda x: [e.value for e in x]),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
default=KevinVideoStatus.DISCOVERED,
|
default=KevinVideoStatus.DISCOVERED,
|
||||||
index=True,
|
index=True,
|
||||||
|
|
@ -159,7 +159,7 @@ class KevinTranscript(Base):
|
||||||
BigInteger, ForeignKey("kevin_videos.id"), unique=True, nullable=False
|
BigInteger, ForeignKey("kevin_videos.id"), unique=True, nullable=False
|
||||||
)
|
)
|
||||||
source: Mapped[KevinTranscriptSource] = mapped_column(
|
source: Mapped[KevinTranscriptSource] = mapped_column(
|
||||||
SAEnum(KevinTranscriptSource, name="kevin_transcript_source"), nullable=False
|
SAEnum(KevinTranscriptSource, name="kevin_transcript_source", values_callable=lambda x: [e.value for e in x]), nullable=False
|
||||||
)
|
)
|
||||||
language: Mapped[str] = mapped_column(String(8), nullable=False)
|
language: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
raw_text: Mapped[str] = mapped_column(Text, nullable=False)
|
raw_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
|
@ -185,7 +185,7 @@ class KevinAnalysis(Base):
|
||||||
model: Mapped[str] = mapped_column(String(100), nullable=False)
|
model: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
prompt_version: Mapped[str] = mapped_column(String(50), nullable=False)
|
prompt_version: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
market_outlook_direction: Mapped[KevinMarketOutlook] = mapped_column(
|
market_outlook_direction: Mapped[KevinMarketOutlook] = mapped_column(
|
||||||
SAEnum(KevinMarketOutlook, name="kevin_market_outlook"), nullable=False
|
SAEnum(KevinMarketOutlook, name="kevin_market_outlook", values_callable=lambda x: [e.value for e in x]), nullable=False
|
||||||
)
|
)
|
||||||
market_outlook_reasoning: Mapped[str] = mapped_column(Text, nullable=False)
|
market_outlook_reasoning: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
macro_themes_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
macro_themes_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||||
|
|
@ -222,11 +222,11 @@ class KevinStockMention(Base):
|
||||||
String(16), nullable=False, index=True
|
String(16), nullable=False, index=True
|
||||||
)
|
)
|
||||||
action: Mapped[KevinTickerAction] = mapped_column(
|
action: Mapped[KevinTickerAction] = mapped_column(
|
||||||
SAEnum(KevinTickerAction, name="kevin_ticker_action"), nullable=False
|
SAEnum(KevinTickerAction, name="kevin_ticker_action", values_callable=lambda x: [e.value for e in x]), nullable=False
|
||||||
)
|
)
|
||||||
conviction: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False)
|
conviction: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False)
|
||||||
time_horizon: Mapped[KevinTimeHorizon] = mapped_column(
|
time_horizon: Mapped[KevinTimeHorizon] = mapped_column(
|
||||||
SAEnum(KevinTimeHorizon, name="kevin_time_horizon"), nullable=False
|
SAEnum(KevinTimeHorizon, name="kevin_time_horizon", values_callable=lambda x: [e.value for e in x]), nullable=False
|
||||||
)
|
)
|
||||||
rationale_quote: Mapped[str] = mapped_column(Text, nullable=False)
|
rationale_quote: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
video_timestamp_seconds: Mapped[int | None] = mapped_column(
|
video_timestamp_seconds: Mapped[int | None] = mapped_column(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue