Coverage for src/secchi/models.py: 97%

196 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-04 22:15 +0000

1from __future__ import annotations 

2 

3from dataclasses import dataclass, field 

4from datetime import datetime 

5from enum import Enum 

6 

7 

8class Registry(str, Enum): 

9 PYPI = "pypi" 

10 CRATES = "crates.io" 

11 NPM = "npm" 

12 HOMEBREW = "homebrew" 

13 GO = "go" 

14 CRAN = "cran" 

15 

16 @property 

17 def display_name(self) -> str: 

18 return { 

19 self.PYPI: "PyPI", 

20 self.CRATES: "crates.io", 

21 self.NPM: "npm", 

22 self.HOMEBREW: "Homebrew", 

23 self.GO: "Go Modules", 

24 self.CRAN: "CRAN", 

25 }[self] 

26 

27 @property 

28 def icon(self) -> str: 

29 return { 

30 self.PYPI: "📦", 

31 self.CRATES: "🦀", 

32 self.NPM: "📜", 

33 self.HOMEBREW: "🍺", 

34 self.GO: "🐹", 

35 self.CRAN: "📈", 

36 }[self] 

37 

38 @property 

39 def language(self) -> str: 

40 return { 

41 self.PYPI: "Python", 

42 self.CRATES: "Rust", 

43 self.NPM: "JavaScript", 

44 self.HOMEBREW: "Ruby/C++", 

45 self.GO: "Go", 

46 self.CRAN: "R", 

47 }[self] 

48 

49 @property 

50 def install_command(self) -> str: 

51 return { 

52 self.PYPI: "pip install", 

53 self.CRATES: "cargo install", 

54 self.NPM: "npm install", 

55 self.HOMEBREW: "brew install", 

56 self.GO: "go get", 

57 self.CRAN: "install.packages", 

58 }[self] 

59 

60 

61@dataclass 

62class PackageRef: 

63 """A reference to a package from config.""" 

64 

65 name: str 

66 registry: Registry 

67 favorite: bool = False 

68 project_name: str = "" 

69 

70 

71@dataclass 

72class Project: 

73 """A named collection of packages to monitor.""" 

74 

75 name: str 

76 description: str = "" 

77 favorite: bool = False 

78 repository_url: str = "" 

79 packages: list[PackageRef] = field(default_factory=list) 

80 title: str = "" 

81 

82 

83@dataclass 

84class Version: 

85 """A specific version of a package.""" 

86 

87 version: str 

88 release_date: datetime | None = None 

89 downloads: int = 0 

90 is_yanked: bool = False 

91 # crates.io numeric version id — joins to /downloads' `version` field. None for pypi/npm. 

92 external_id: int | str | None = None 

93 # pypi: sum of release-file sizes; npm: dist.unpackedSize; crates: crate_size. 

94 size_bytes: int | None = None 

95 

96 

97@dataclass 

98class DownloadTrendPoint: 

99 """A single data point for download trend charts.""" 

100 

101 date: str 

102 count: int 

103 

104 

105@dataclass 

106class DownloadCounts: 

107 """Breakdown of downloads by period.""" 

108 

109 today: int = 0 

110 week: int = 0 

111 month: int = 0 

112 

113 

114@dataclass 

115class GitHubStats: 

116 """GitHub repository stats (raw, from the repo + signals APIs).""" 

117 

118 stars: int = 0 

119 forks: int = 0 

120 open_issues: int = 0 

121 created_at: datetime | None = None 

122 pushed_at: datetime | None = None # proxy for "last commit" 

123 has_ci: bool = False # proxy for "Testing": .github/workflows non-empty 

124 has_readme: bool = False 

125 resolved: bool = False # True iff an owner/repo was derivable + fetched 

126 # week-over-week deltas from local history cache. None = no baseline yet ("—"). 

127 stars_delta_7d: int | None = None 

128 open_issues_delta_7d: int | None = None 

129 

130 

131@dataclass 

132class Dependency: 

133 """A dependency of a package.""" 

134 

135 name: str 

136 requirement: str = "" 

137 optional: bool = False 

138 

139 

140@dataclass 

141class ReleaseFile: 

142 """One published artifact for a version. 

143 

144 PyPI has several per version; npm/crates get one synthetic entry. 

145 """ 

146 

147 packagetype: str # "bdist_wheel" | "sdist" | "npm-package" | "crate" 

148 size: int = 0 

149 filename: str = "" 

150 

151 

152@dataclass 

153class ReverseDependency: 

154 """A package that depends on this one, ranked by its own real total downloads.""" 

155 

156 name: str 

157 downloads: int = 0 

158 

159 

160@dataclass 

161class GitHubIssueEvent: 

162 """Raw GitHub issue OR pull request record. 

163 

164 Feeds both health-score issue counts and the Activity timeline. 

165 """ 

166 

167 number: int 

168 title: str 

169 is_pull_request: bool 

170 created_at: datetime 

171 closed_at: datetime | None 

172 url: str = "" 

173 

174 

175@dataclass 

176class HistorySnapshot: 

177 """A point-in-time snapshot cached locally to derive week-over-week deltas.""" 

178 

179 timestamp: datetime 

180 stars: int 

181 open_issues: int 

182 health_score: int | None = None 

183 reverse_dependency_count: int | None = None 

184 

185 

186@dataclass 

187class MetricTimelinePoint: 

188 """A compact labeled metric point for dashboard sparklines.""" 

189 

190 label: str 

191 value: int 

192 

193 

194# ── Derived-display dataclasses (produced by derived.py, consumed by widgets) ── 

195 

196 

197@dataclass 

198class InstallMethod: 

199 label: str 

200 count: int 

201 percent: float 

202 

203 

204@dataclass 

205class InstallBreakdown: 

206 methods: list[InstallMethod] = field(default_factory=list) 

207 caption: str = "" 

208 is_estimate: bool = False 

209 

210 

211@dataclass 

212class ReverseDependencySummary: 

213 count: int | None = None 

214 monthly_growth: int | None = None 

215 caption: str = "" 

216 

217 

218class ActivityEventKind(str, Enum): 

219 RELEASE = "release" 

220 ISSUE_OPENED = "issue_opened" 

221 ISSUE_CLOSED = "issue_closed" 

222 PR_OPENED = "pr_opened" 

223 PR_CLOSED = "pr_closed" 

224 

225 @property 

226 def icon(self) -> str: 

227 return { 

228 self.RELEASE: "◉", 

229 self.ISSUE_OPENED: "◈", 

230 self.ISSUE_CLOSED: "✓", 

231 self.PR_OPENED: "⇄", 

232 self.PR_CLOSED: "⇥", 

233 }[self] 

234 

235 @property 

236 def label(self) -> str: 

237 return { 

238 self.RELEASE: "New Release", 

239 self.ISSUE_OPENED: "New Issue", 

240 self.ISSUE_CLOSED: "Issue Closed", 

241 self.PR_OPENED: "New PR", 

242 self.PR_CLOSED: "PR Closed", 

243 }[self] 

244 

245 

246@dataclass 

247class ActivityEvent: 

248 kind: ActivityEventKind 

249 timestamp: datetime 

250 title: str 

251 ref: str = "" 

252 url: str = "" 

253 

254 

255@dataclass 

256class HealthSubScore: 

257 label: str 

258 score: int 

259 max_score: int = 20 

260 

261 

262@dataclass 

263class HealthScore: 

264 sub_scores: list[HealthSubScore] = field(default_factory=list) 

265 total: int = 0 # 0-100 

266 grade: str = "—" # "A".."F" 

267 

268 

269@dataclass 

270class DerivedPackageData: 

271 """All display-ready metrics derived (via arithmetic, no I/O) from a PackageInfo.""" 

272 

273 health_score: HealthScore = field(default_factory=HealthScore) 

274 install_breakdown: InstallBreakdown = field(default_factory=InstallBreakdown) 

275 reverse_dependency_summary: ReverseDependencySummary = field( 

276 default_factory=ReverseDependencySummary 

277 ) 

278 health_timeline: list[MetricTimelinePoint] = field(default_factory=list) 

279 activity: list[ActivityEvent] = field(default_factory=list) 

280 release_adoption: dict[str, float] = field(default_factory=dict) 

281 adoption_caption: str = "" 

282 downloads_30d_total: int = 0 

283 downloads_30d_pct_change: float | None = None 

284 

285 

286@dataclass 

287class PackageInfo: 

288 """Full package information fetched from the registry.""" 

289 

290 name: str 

291 registry: Registry 

292 source_registries: list[Registry] = field(default_factory=list) 

293 description: str = "" 

294 author: str = "" 

295 license: str = "" 

296 homepage: str = "" 

297 repository_url: str = "" 

298 documentation_url: str = "" 

299 latest_version: str = "" 

300 latest_release_date: datetime | None = None 

301 total_downloads: int = 0 

302 package_kind: str = "" # best-effort "CLI" / "Library" / etc. 

303 download_counts: DownloadCounts = field(default_factory=DownloadCounts) 

304 github_stats: GitHubStats = field(default_factory=GitHubStats) 

305 versions: list[Version] = field(default_factory=list) 

306 dependencies: list[Dependency] = field(default_factory=list) 

307 download_trend: list[DownloadTrendPoint] = field(default_factory=list) 

308 release_notes: str = "" 

309 # raw extras feeding derivation 

310 latest_release_files: list[ReleaseFile] = field(default_factory=list) 

311 version_downloads_recent: dict[int | str, int] = field( 

312 default_factory=dict 

313 ) # crates.io only 

314 reverse_dependencies: list[ReverseDependency] = field( 

315 default_factory=list 

316 ) # crates.io only 

317 reverse_dependency_count: int | None = None 

318 reverse_dependency_monthly_growth: int | None = None 

319 health_history: list[MetricTimelinePoint] = field(default_factory=list) 

320 github_issue_events: list[GitHubIssueEvent] = field(default_factory=list) 

321 

322 

323@dataclass 

324class FetchError: 

325 """Error when fetching package data.""" 

326 

327 package_name: str 

328 registry: Registry 

329 message: str 

330 

331 

332@dataclass 

333class SearchResult: 

334 """A normalized package discovery result from a registry search.""" 

335 

336 name: str 

337 registry: Registry 

338 version: str = "" 

339 description: str = "" 

340 url: str = "" 

341 score: float = 0.0 

342 exact: bool = False