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

1""" 

2Database models for news subscriptions and related functionality. 

3These tables are created in per-user encrypted databases. 

4""" 

5 

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 

18 

19from .base import Base 

20 

21 

22class CardType(enum.Enum): 

23 """Types of cards in the system""" 

24 

25 NEWS = "news" 

26 RESEARCH = "research" 

27 UPDATE = "update" 

28 OVERVIEW = "overview" 

29 

30 

31class RatingType(enum.Enum): 

32 """Types of ratings""" 

33 

34 RELEVANCE = "relevance" # Thumbs up/down 

35 QUALITY = "quality" # 1-5 stars 

36 

37 

38class SubscriptionType(enum.Enum): 

39 """Types of subscriptions""" 

40 

41 SEARCH = "search" 

42 TOPIC = "topic" 

43 

44 

45class SubscriptionStatus(enum.Enum): 

46 """Status of subscriptions""" 

47 

48 ACTIVE = "active" 

49 PAUSED = "paused" 

50 EXPIRED = "expired" 

51 ERROR = "error" 

52 

53 

54class NewsSubscription(Base): 

55 """User's news subscriptions""" 

56 

57 __tablename__ = "news_subscriptions" 

58 

59 id = Column(String(50), primary_key=True) 

60 

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. 

73 

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 

84 

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 

89 

90 # Organization 

91 folder = Column(String(100)) # Folder name 

92 folder_id = Column(String(36)) # Folder ID 

93 notes = Column(Text) # User notes 

94 

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 

100 

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 

109 

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) 

115 

116 # Additional data 

117 extra_data = Column(JSON) # Additional flexible data 

118 

119 

120class SubscriptionFolder(Base): 

121 """Folders for organizing subscriptions""" 

122 

123 __tablename__ = "subscription_folders" 

124 

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 

130 

131 # Timestamps 

132 created_at = Column(UtcDateTime, default=utcnow()) 

133 updated_at = Column( 

134 UtcDateTime, 

135 default=utcnow(), 

136 onupdate=utcnow(), 

137 ) 

138 

139 # Settings 

140 is_default = Column(Boolean, default=False) 

141 sort_order = Column(Integer, default=0) 

142 

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 } 

160 

161 

162class NewsCard(Base): 

163 """Individual news cards/items""" 

164 

165 __tablename__ = "news_cards" 

166 

167 id = Column(String(50), primary_key=True) 

168 

169 # Content 

170 title = Column(String(500), nullable=False) 

171 summary = Column(Text) 

172 content = Column(Text) 

173 url = Column(String(1000)) 

174 

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 

179 

180 # Categorization 

181 category = Column(String(100)) 

182 tags = Column(JSON) # List of tags 

183 card_type = Column(Enum(CardType), default=CardType.NEWS) 

184 

185 # Timing 

186 published_at = Column(UtcDateTime) 

187 discovered_at = Column(UtcDateTime, default=utcnow()) 

188 

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) 

194 

195 # Metadata 

196 extra_data = Column(JSON) # Flexible additional data 

197 

198 # Subscription link 

199 subscription_id = Column(String(50), ForeignKey("news_subscriptions.id")) 

200 

201 

202class UserRating(Base): 

203 """User ratings/feedback on news items""" 

204 

205 __tablename__ = "news_user_ratings" 

206 

207 id = Column(Integer, primary_key=True) 

208 

209 # What was rated 

210 card_id = Column(String(50), ForeignKey("news_cards.id"), nullable=False) 

211 rating_type = Column(Enum(RatingType), nullable=False) 

212 

213 # Rating value 

214 rating_value = Column(String(20)) # 'up', 'down', or numeric 

215 

216 # When 

217 created_at = Column(UtcDateTime, default=utcnow()) 

218 

219 # Optional feedback 

220 comment = Column(Text) 

221 tags = Column(JSON) # User-applied tags 

222 

223 

224class UserPreference(Base): 

225 """User preferences for news""" 

226 

227 __tablename__ = "news_user_preferences" 

228 

229 id = Column(Integer, primary_key=True) 

230 

231 # Preference key-value pairs 

232 key = Column(String(100), nullable=False, unique=True) 

233 value = Column(JSON) 

234 

235 # Metadata 

236 created_at = Column(UtcDateTime, default=utcnow()) 

237 updated_at = Column( 

238 UtcDateTime, 

239 default=utcnow(), 

240 onupdate=utcnow(), 

241 ) 

242 

243 

244class NewsInterest(Base): 

245 """User's declared interests for news""" 

246 

247 __tablename__ = "news_interests" 

248 

249 id = Column(Integer, primary_key=True) 

250 

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 

255 

256 # Timing 

257 created_at = Column(UtcDateTime, default=utcnow()) 

258 expires_at = Column(UtcDateTime) # Optional expiration 

259 

260 # Source 

261 source = Column(String(50)) # 'manual', 'inferred', 'imported' 

262 source_id = Column(String(100))