Coverage for src / local_deep_research / database / models / news.py: 99%
111 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
1"""
2Database models for news subscriptions and related functionality.
3These tables are created in per-user encrypted databases.
4"""
6from sqlalchemy import (
7 Column,
8 Integer,
9 String,
10 JSON,
11 Text,
12 Boolean,
13 ForeignKey,
14 Enum,
15)
16from sqlalchemy_utc import UtcDateTime, utcnow
17import enum
19from .base import Base
22class CardType(enum.Enum):
23 """Types of cards in the system"""
25 NEWS = "news"
26 RESEARCH = "research"
27 UPDATE = "update"
28 OVERVIEW = "overview"
31class RatingType(enum.Enum):
32 """Types of ratings"""
34 RELEVANCE = "relevance" # Thumbs up/down
35 QUALITY = "quality" # 1-5 stars
38class SubscriptionType(enum.Enum):
39 """Types of subscriptions"""
41 SEARCH = "search"
42 TOPIC = "topic"
45class SubscriptionStatus(enum.Enum):
46 """Status of subscriptions"""
48 ACTIVE = "active"
49 PAUSED = "paused"
50 EXPIRED = "expired"
51 ERROR = "error"
54class NewsSubscription(Base):
55 """User's news subscriptions"""
57 __tablename__ = "news_subscriptions"
59 id = Column(String(50), primary_key=True)
61 # Subscription details
62 name = Column(String(255)) # Optional friendly name
63 subscription_type = Column(
64 String(20), nullable=False
65 ) # 'search' or 'topic'
66 query_or_topic = Column(Text, nullable=False)
67 refresh_interval_minutes = Column(
68 Integer, default=1440
69 ) # Default 24 hours = 1440 minutes
70 frequency = Column(
71 String(50), default="daily"
72 ) # daily, weekly, hourly, etc.
74 # Timing
75 created_at = Column(UtcDateTime, default=utcnow())
76 updated_at = Column(
77 UtcDateTime,
78 default=utcnow(),
79 onupdate=utcnow(),
80 )
81 last_refresh = Column(UtcDateTime)
82 next_refresh = Column(UtcDateTime)
83 expires_at = Column(UtcDateTime) # Optional expiration
85 # Source tracking
86 source_type = Column(String(50)) # 'manual', 'research', 'news_topic'
87 source_id = Column(String(100)) # ID of source (research_id, news_id)
88 created_from = Column(Text) # Description of source
90 # Organization
91 folder = Column(String(100)) # Folder name
92 folder_id = Column(String(36)) # Folder ID
93 notes = Column(Text) # User notes
95 # Model configuration
96 model_provider = Column(String(50)) # OLLAMA, OPENAI, ANTHROPIC, etc.
97 model = Column(String(100)) # Specific model name
98 search_strategy = Column(String(50)) # Strategy for searches
99 custom_endpoint = Column(String(255)) # Custom API endpoint if used
101 # Search configuration
102 search_engine = Column(String(50)) # Search engine to use
103 search_iterations = Column(
104 Integer, default=3
105 ) # Number of search iterations
106 questions_per_iteration = Column(
107 Integer, default=5
108 ) # Questions per iteration
110 # State
111 status = Column(String(20), default="active")
112 is_active = Column(Boolean, default=True) # Whether subscription is active
113 error_count = Column(Integer, default=0)
114 last_error = Column(Text)
116 # Additional data
117 extra_data = Column(JSON) # Additional flexible data
120class SubscriptionFolder(Base):
121 """Folders for organizing subscriptions"""
123 __tablename__ = "subscription_folders"
125 id = Column(String(36), primary_key=True) # UUID
126 name = Column(String(100), nullable=False)
127 description = Column(Text)
128 color = Column(String(7)) # Hex color
129 icon = Column(String(50)) # Icon identifier
131 # Timestamps
132 created_at = Column(UtcDateTime, default=utcnow())
133 updated_at = Column(
134 UtcDateTime,
135 default=utcnow(),
136 onupdate=utcnow(),
137 )
139 # Settings
140 is_default = Column(Boolean, default=False)
141 sort_order = Column(Integer, default=0)
143 def to_dict(self):
144 """Convert folder to dictionary."""
145 return {
146 "id": self.id,
147 "name": self.name,
148 "description": self.description,
149 "color": self.color,
150 "icon": self.icon,
151 "created_at": self.created_at.isoformat()
152 if self.created_at
153 else None,
154 "updated_at": self.updated_at.isoformat()
155 if self.updated_at
156 else None,
157 "is_default": self.is_default,
158 "sort_order": self.sort_order,
159 }
162class NewsCard(Base):
163 """Individual news cards/items"""
165 __tablename__ = "news_cards"
167 id = Column(String(50), primary_key=True)
169 # Content
170 title = Column(String(500), nullable=False)
171 summary = Column(Text)
172 content = Column(Text)
173 url = Column(String(1000))
175 # Source info
176 source_name = Column(String(200))
177 source_type = Column(String(50)) # 'research', 'rss', 'api', etc.
178 source_id = Column(String(100)) # ID in source system
180 # Categorization
181 category = Column(String(100))
182 tags = Column(JSON) # List of tags
183 card_type = Column(Enum(CardType), default=CardType.NEWS)
185 # Timing
186 published_at = Column(UtcDateTime)
187 discovered_at = Column(UtcDateTime, default=utcnow())
189 # Interaction tracking
190 is_read = Column(Boolean, default=False)
191 read_at = Column(UtcDateTime)
192 is_saved = Column(Boolean, default=False)
193 saved_at = Column(UtcDateTime)
195 # Metadata
196 extra_data = Column(JSON) # Flexible additional data
198 # Subscription link
199 subscription_id = Column(String(50), ForeignKey("news_subscriptions.id"))
202class UserRating(Base):
203 """User ratings/feedback on news items"""
205 __tablename__ = "news_user_ratings"
207 id = Column(Integer, primary_key=True)
209 # What was rated
210 card_id = Column(String(50), ForeignKey("news_cards.id"), nullable=False)
211 rating_type = Column(Enum(RatingType), nullable=False)
213 # Rating value
214 rating_value = Column(String(20)) # 'up', 'down', or numeric
216 # When
217 created_at = Column(UtcDateTime, default=utcnow())
219 # Optional feedback
220 comment = Column(Text)
221 tags = Column(JSON) # User-applied tags
224class UserPreference(Base):
225 """User preferences for news"""
227 __tablename__ = "news_user_preferences"
229 id = Column(Integer, primary_key=True)
231 # Preference key-value pairs
232 key = Column(String(100), nullable=False, unique=True)
233 value = Column(JSON)
235 # Metadata
236 created_at = Column(UtcDateTime, default=utcnow())
237 updated_at = Column(
238 UtcDateTime,
239 default=utcnow(),
240 onupdate=utcnow(),
241 )
244class NewsInterest(Base):
245 """User's declared interests for news"""
247 __tablename__ = "news_interests"
249 id = Column(Integer, primary_key=True)
251 # Interest details
252 topic = Column(String(200), nullable=False)
253 interest_type = Column(String(50)) # 'positive', 'negative', 'keyword'
254 strength = Column(Integer, default=5) # 1-10 scale
256 # Timing
257 created_at = Column(UtcDateTime, default=utcnow())
258 expires_at = Column(UtcDateTime) # Optional expiration
260 # Source
261 source = Column(String(50)) # 'manual', 'inferred', 'imported'
262 source_id = Column(String(100))