Spaces:
Sleeping
Sleeping
| from datetime import datetime, timedelta | |
| from typing import List, Optional | |
| from sqlalchemy import create_engine, Column, Integer, String, Text, Float, DateTime, Boolean, ForeignKey, JSON, Index | |
| from sqlalchemy.ext.declarative import declarative_base | |
| from sqlalchemy.orm import relationship, sessionmaker | |
| from sqlalchemy import event | |
| from src.config.settings import DATABASE_URL | |
| Base = declarative_base() | |
| class RawNews(Base): | |
| __tablename__ = "raw_news" | |
| id = Column(Integer, primary_key=True, index=True) | |
| source_id = Column(String, index=True) | |
| source_name = Column(String) | |
| author = Column(String, nullable=True) | |
| title = Column(String) | |
| description = Column(Text, nullable=True) | |
| url = Column(String, unique=True, index=True) | |
| url_to_image = Column(String, nullable=True) | |
| published_at = Column(DateTime) | |
| content = Column(Text, nullable=True) | |
| collected_at = Column(DateTime, default=datetime.utcnow) | |
| # Metadata for processing status | |
| is_verified = Column(Boolean, default=False) | |
| verification_score = Column(Float, default=0.0) | |
| processed = Column(Boolean, default=False) | |
| country = Column(String, nullable=True, index=True) | |
| class VerifiedNews(Base): | |
| __tablename__ = "verified_news" | |
| __table_args__ = ( | |
| Index('idx_verified_news_query', 'country', 'category', 'created_at'), | |
| Index('idx_verified_news_impact', 'country', 'impact_score', 'created_at'), | |
| ) | |
| id = Column(Integer, primary_key=True, index=True) | |
| raw_news_id = Column(Integer, ForeignKey("raw_news.id")) | |
| title = Column(String) | |
| content = Column(Text) | |
| summary_bullets = Column(JSON) # List of strings | |
| # Analysis Fields | |
| analysis = Column(JSON, nullable=True) # Flexible storage for extra metadata | |
| impact_tags = Column(JSON) # e.g. ["Jobs", "Market"] | |
| bias_rating = Column(String) # e.g. "Neutral", "Slightly Biased" | |
| category = Column(String, index=True) | |
| sub_category = Column(String, nullable=True, index=True) # e.g. "Scholarships", "Exams" | |
| country = Column(String, nullable=True, index=True) | |
| credibility_score = Column(Float) | |
| impact_score = Column(Integer) # 1-10 | |
| why_it_matters = Column(Text) | |
| who_is_affected = Column(Text, nullable=True) | |
| short_term_impact = Column(Text, nullable=True) | |
| long_term_impact = Column(Text, nullable=True) | |
| sentiment = Column(String) | |
| lang = Column(String, default='english', index=True) # Source language | |
| is_fake = Column(Boolean, default=False) | |
| flag_count = Column(Integer, default=0) | |
| published_at = Column(DateTime) | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| # New Perfection Fields | |
| translation_cache = Column(JSON, default=dict) # {lang: {title, why, impacted}} | |
| audio_url = Column(String, nullable=True) # Path to local TTS mp3 | |
| raw_news = relationship("RawNews") | |
| def image_url(self) -> Optional[str]: | |
| """Backward compatibility for templates and logic expecting image_url attribute.""" | |
| if self.raw_news and self.raw_news.url_to_image: | |
| return self.raw_news.url_to_image | |
| return None | |
| def url(self) -> str: | |
| """Helper to access the source URL.""" | |
| if self.raw_news: | |
| return self.raw_news.url | |
| return "#" | |
| def source_name(self) -> str: | |
| """Helper to access the source name.""" | |
| if self.raw_news: | |
| return self.raw_news.source_name | |
| return "Unknown" | |
| def to_dict(self): | |
| return { | |
| "id": self.id, | |
| "title": self.title, | |
| "content": self.content, | |
| "summary_bullets": self.summary_bullets, | |
| "analysis": self.analysis, | |
| "impact_tags": self.impact_tags, | |
| "bias_rating": self.bias_rating, | |
| "category": self.category, | |
| "sub_category": self.sub_category, | |
| "country": self.country, | |
| "credibility_score": self.credibility_score, | |
| "impact_score": self.impact_score, | |
| "why_it_matters": self.why_it_matters, | |
| "who_is_affected": self.who_is_affected, | |
| "short_term_impact": self.short_term_impact, | |
| "long_term_impact": self.long_term_impact, | |
| "sentiment": self.sentiment, | |
| "published_at": self.published_at.isoformat() if self.published_at else None, | |
| "created_at": self.created_at.isoformat() if self.created_at else None, | |
| "source_name": self.raw_news.source_name if self.raw_news else "Unknown", | |
| "url": self.url, | |
| "image_url": self.image_url | |
| } | |
| class DailyDigest(Base): | |
| __tablename__ = "daily_digests" | |
| id = Column(Integer, primary_key=True, index=True) | |
| date = Column(DateTime, default=datetime.utcnow) | |
| content_json = Column(JSON) # Full structured digest | |
| is_published = Column(Boolean, default=False) | |
| class TopicTracking(Base): | |
| __tablename__ = "topic_tracking" | |
| id = Column(Integer, primary_key=True, index=True) | |
| user_id = Column(Integer, ForeignKey("users.id")) | |
| news_id = Column(Integer, ForeignKey("verified_news.id"), nullable=True) | |
| topic_keywords = Column(JSON) # ["AI", "Nvidia"] | |
| language = Column(String, default="english") | |
| notify_sms = Column(Boolean, default=True) | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| expires_at = Column(DateTime, default=lambda: datetime.utcnow() + timedelta(days=30)) | |
| user = relationship("User", back_populates="tracked_topics") | |
| news = relationship("VerifiedNews") | |
| class TrackNotification(Base): | |
| __tablename__ = "track_notifications" | |
| id = Column(Integer, primary_key=True, index=True) | |
| user_id = Column(Integer, ForeignKey("users.id")) | |
| news_id = Column(Integer, ForeignKey("verified_news.id")) | |
| notified_at = Column(DateTime, default=datetime.utcnow) | |
| user = relationship("User") | |
| news = relationship("VerifiedNews") | |
| class OTPVerification(Base): | |
| __tablename__ = "otp_verifications" | |
| id = Column(Integer, primary_key=True, index=True) | |
| phone = Column(String, index=True) | |
| otp_code = Column(String) | |
| expires_at = Column(DateTime) | |
| is_verified = Column(Boolean, default=False) | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| class User(Base): | |
| __tablename__ = "users" | |
| id = Column(Integer, primary_key=True, index=True) | |
| firebase_uid = Column(String, unique=True, index=True) | |
| email = Column(String, nullable=True) | |
| phone = Column(String, nullable=True) | |
| push_token = Column(String, nullable=True) | |
| bounty_points = Column(Integer, default=0) | |
| preferred_language = Column(String, default="english") | |
| bio = Column(Text, nullable=True) | |
| profile_image_url = Column(String, nullable=True) | |
| # Premium Streak & Rewards | |
| current_streak = Column(Integer, default=0) | |
| streak_history = Column(JSON, default=dict) # e.g. {"2026-04-04": "success", "2026-04-03": "missed"} | |
| subscription_status = Column(String, default="free") # "free", "premium_eligible", "activated" | |
| last_active_date = Column(DateTime, nullable=True) | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| subscriptions = relationship("Subscription", back_populates="user") | |
| folders = relationship("Folder", back_populates="user") | |
| saved_articles = relationship("SavedArticle", back_populates="user") | |
| read_history = relationship("ReadHistory", back_populates="user") | |
| tracked_topics = relationship("TopicTracking", back_populates="user") | |
| class Subscription(Base): | |
| __tablename__ = "subscriptions" | |
| id = Column(Integer, primary_key=True, index=True) | |
| user_id = Column(Integer, ForeignKey("users.id")) | |
| category = Column(String) # e.g. "Technology", "All" | |
| user = relationship("User", back_populates="subscriptions") | |
| class Folder(Base): | |
| __tablename__ = "folders" | |
| id = Column(Integer, primary_key=True, index=True) | |
| user_id = Column(Integer, ForeignKey("users.id")) | |
| name = Column(String) | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| user = relationship("User", back_populates="folders") | |
| saved_articles = relationship("SavedArticle", back_populates="folder") | |
| class SavedArticle(Base): | |
| __tablename__ = "saved_articles" | |
| id = Column(Integer, primary_key=True, index=True) | |
| user_id = Column(Integer, ForeignKey("users.id")) | |
| folder_id = Column(Integer, ForeignKey("folders.id"), nullable=True) | |
| news_id = Column(Integer, ForeignKey("verified_news.id")) | |
| saved_at = Column(DateTime, default=datetime.utcnow) | |
| user = relationship("User", back_populates="saved_articles") | |
| folder = relationship("Folder", back_populates="saved_articles") | |
| news = relationship("VerifiedNews") | |
| class ReadHistory(Base): | |
| __tablename__ = "read_history" | |
| id = Column(Integer, primary_key=True, index=True) | |
| user_id = Column(Integer, ForeignKey("users.id")) | |
| news_id = Column(Integer, ForeignKey("verified_news.id")) | |
| read_at = Column(DateTime, default=datetime.utcnow) | |
| user = relationship("User", back_populates="read_history") | |
| news = relationship("VerifiedNews") | |
| class FlaggedArticle(Base): | |
| __tablename__ = "flagged_articles" | |
| id = Column(Integer, primary_key=True, index=True) | |
| user_id = Column(Integer, ForeignKey("users.id")) | |
| news_id = Column(Integer, ForeignKey("verified_news.id")) | |
| reason = Column(String, nullable=True) | |
| flagged_at = Column(DateTime, default=datetime.utcnow) | |
| user = relationship("User") | |
| news = relationship("VerifiedNews") | |
| class BreakingNews(Base): | |
| __tablename__ = "breaking_news" | |
| id = Column(Integer, primary_key=True, index=True) | |
| verified_news_id = Column(Integer, ForeignKey("verified_news.id")) | |
| classification = Column(String) # Breaking News, Developing News, Top Headline | |
| breaking_headline = Column(String) | |
| what_happened = Column(JSON) # List of bullet points | |
| why_matters = Column(Text) | |
| next_updates = Column(JSON) # List of possible next updates | |
| confidence_level = Column(String) # High, Medium, Low | |
| impact_score = Column(Integer) # 1-10 | |
| recency_minutes = Column(Integer) | |
| url = Column(String, nullable=True) # Direct access for performance | |
| image_url = Column(String, nullable=True) # Cached image link | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) | |
| verified_news = relationship("VerifiedNews") | |
| class Advertisement(Base): | |
| __tablename__ = "advertisements" | |
| id = Column(Integer, primary_key=True, index=True) | |
| image_url = Column(String, nullable=False) | |
| caption = Column(String, nullable=True) | |
| position = Column(String, default="both") # "left", "right", "both", "mobile" | |
| target_node = Column(String, default="Global") | |
| target_url = Column(String, nullable=True) | |
| target_platform = Column(String, default="both") # "main", "student", "both" | |
| is_active = Column(Boolean, default=True) | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| class Newspaper(Base): | |
| __tablename__ = "newspapers" | |
| id = Column(Integer, primary_key=True, index=True) | |
| name = Column(String, nullable=False) | |
| url = Column(String, nullable=False) | |
| logo_text = Column(String, nullable=True) # e.g. "NYT" | |
| logo_color = Column(String, nullable=True) # e.g. "#000000" | |
| country = Column(String, default="Global") | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| class ProtocolHistory(Base): | |
| __tablename__ = "protocol_history" | |
| id = Column(Integer, primary_key=True, index=True) | |
| action = Column(String, nullable=False) # e.g. 'deploy', 'delete', 'register' | |
| target_type = Column(String, nullable=False) # e.g. 'article', 'source', 'ad' | |
| target_id = Column(String, nullable=True) | |
| admin_user = Column(String, nullable=False) | |
| details = Column(Text, nullable=True) | |
| timestamp = Column(DateTime, default=datetime.utcnow) | |
| class SystemConfig(Base): | |
| __tablename__ = "system_config" | |
| id = Column(Integer, primary_key=True, index=True) | |
| config_key = Column(String, unique=True, index=True) # e.g. 'show_exams_section' | |
| config_value = Column(String) # 'true' or 'false' or JSON | |
| updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) | |
| if DATABASE_URL.startswith("sqlite"): | |
| engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) | |
| else: | |
| # Production Optimized Engine for PostgreSQL (Supabase/Railway with pgBouncer compatibility) | |
| engine = create_engine( | |
| DATABASE_URL, | |
| pool_pre_ping=True, # Checks if connection is alive before using it | |
| pool_recycle=600, # Recycles connections every 10 minutes to prevent stale pgBouncer links | |
| pool_size=15, # Cap baseline connection pool size | |
| max_overflow=25, # Maximum transient bursting connections | |
| connect_args={ | |
| "connect_timeout": 30, | |
| "application_name": "UniArcBackendPooling" | |
| } | |
| ) | |
| SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | |
| def init_db(): | |
| print(f"[DEBUG] Initializing database with engine: {engine.url}") | |
| Base.metadata.create_all(bind=engine) | |
| def set_sqlite_pragma(dbapi_connection, connection_record): | |
| if DATABASE_URL.startswith("sqlite"): | |
| cursor = dbapi_connection.cursor() | |
| cursor.execute("PRAGMA journal_mode=WAL") | |
| cursor.execute("PRAGMA synchronous=NORMAL") | |
| cursor.close() | |