import datetime as dt from sqlalchemy import DateTime, Float, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from .db import Base class Article(Base): __tablename__ = "articles" __table_args__ = (UniqueConstraint("url", name="uq_article_url"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) source: Mapped[str] = mapped_column(String(128), index=True) source_bias: Mapped[str] = mapped_column(String(64), default="") title: Mapped[str] = mapped_column(Text) url: Mapped[str] = mapped_column(Text, index=True) summary: Mapped[str] = mapped_column(Text, default="") published_at: Mapped[dt.datetime] = mapped_column(DateTime, index=True) fetched_at: Mapped[dt.datetime] = mapped_column(DateTime, default=dt.datetime.utcnow) location_name: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) country: Mapped[str | None] = mapped_column(String(128), nullable=True) lat: Mapped[float | None] = mapped_column(Float, nullable=True) lon: Mapped[float | None] = mapped_column(Float, nullable=True) # Rounded lat/lon grid key articles are clustered on, e.g. "31.5:34.5" cluster_key: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) class MarketPrice(Base): __tablename__ = "market_prices" id: Mapped[int] = mapped_column(Integer, primary_key=True) symbol: Mapped[str] = mapped_column(String(32), index=True) label: Mapped[str] = mapped_column(String(128)) category: Mapped[str] = mapped_column(String(32)) # "index" | "commodity" price: Mapped[float] = mapped_column(Float) currency: Mapped[str] = mapped_column(String(8), default="USD") change_pct: Mapped[float | None] = mapped_column(Float, nullable=True) recorded_at: Mapped[dt.datetime] = mapped_column(DateTime, index=True, default=dt.datetime.utcnow) class MarketSpike(Base): __tablename__ = "market_spikes" id: Mapped[int] = mapped_column(Integer, primary_key=True) symbol: Mapped[str] = mapped_column(String(32), index=True) label: Mapped[str] = mapped_column(String(128)) from_price: Mapped[float] = mapped_column(Float) to_price: Mapped[float] = mapped_column(Float) pct_change: Mapped[float] = mapped_column(Float) # This instrument's recent typical (mean absolute) poll-to-poll move, # for comparison — null if there wasn't enough price history yet to # compute one (see market_spike_min_samples). Lets the UI show "Nx this # instrument's normal move" instead of just the raw percentage. baseline_volatility_pct: Mapped[float | None] = mapped_column(Float, nullable=True) window_start: Mapped[dt.datetime] = mapped_column(DateTime) window_end: Mapped[dt.datetime] = mapped_column(DateTime) detected_at: Mapped[dt.datetime] = mapped_column(DateTime, index=True, default=dt.datetime.utcnow) # JSON-encoded list of Article.id, ranked most-to-least likely relevant. article_ids_json: Mapped[str] = mapped_column(Text, default="[]") # JSON-encoded [[word, count], ...] — plain word-frequency across every # article in the window's titles, stopwords dropped. A heuristic hint at # "likely factors," not an explanation; see textutil.py. top_keywords_json: Mapped[str] = mapped_column(Text, default="[]") class ConflictEvent(Base): __tablename__ = "conflict_events" __table_args__ = (UniqueConstraint("source", "external_id", name="uq_conflict_event"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) source: Mapped[str] = mapped_column(String(32), default="acled") external_id: Mapped[str] = mapped_column(String(64)) event_type: Mapped[str] = mapped_column(String(128)) actor1: Mapped[str] = mapped_column(String(256), default="") actor2: Mapped[str] = mapped_column(String(256), default="") fatalities: Mapped[int | None] = mapped_column(Integer, nullable=True) notes: Mapped[str] = mapped_column(Text, default="") location_name: Mapped[str] = mapped_column(String(128), default="") country: Mapped[str] = mapped_column(String(128), default="") lat: Mapped[float] = mapped_column(Float) lon: Mapped[float] = mapped_column(Float) event_date: Mapped[dt.datetime] = mapped_column(DateTime, index=True)