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

214 statements  

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

1from __future__ import annotations 

2 

3from dataclasses import dataclass, field 

4from datetime import datetime 

5from enum import StrEnum 

6 

7 

8class Registry(StrEnum): 

9 PYPI = "pypi" 

10 CRATES = "crates.io" 

11 NPM = "npm" 

12 HOMEBREW = "homebrew" 

13 GO = "go" 

14 CRAN = "cran" 

15 PUB = "pub" 

16 

17 @property 

18 def display_name(self) -> str: 

19 return { 

20 self.PYPI: "PyPI", 

21 self.CRATES: "crates.io", 

22 self.NPM: "npm", 

23 self.HOMEBREW: "Homebrew", 

24 self.GO: "Go Modules", 

25 self.CRAN: "CRAN", 

26 self.PUB: "pub.dev", 

27 }[self] 

28 

29 @property 

30 def icon(self) -> str: 

31 return { 

32 self.PYPI: "📦", 

33 self.CRATES: "🦀", 

34 self.NPM: "📜", 

35 self.HOMEBREW: "🍺", 

36 self.GO: "🐹", 

37 self.CRAN: "📈", 

38 self.PUB: "🎯", 

39 }[self] 

40 

41 @property 

42 def language(self) -> str: 

43 return { 

44 self.PYPI: "Python", 

45 self.CRATES: "Rust", 

46 self.NPM: "JavaScript", 

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

48 self.GO: "Go", 

49 self.CRAN: "R", 

50 self.PUB: "Dart/Flutter", 

51 }[self] 

52 

53 @property 

54 def install_command(self) -> str: 

55 return { 

56 self.PYPI: "pip install", 

57 self.CRATES: "cargo install", 

58 self.NPM: "npm install", 

59 self.HOMEBREW: "brew install", 

60 self.GO: "go get", 

61 self.CRAN: "install.packages", 

62 self.PUB: "dart pub add", 

63 }[self] 

64 

65 

66@dataclass 

67class PackageRef: 

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

69 

70 name: str 

71 registry: Registry 

72 favorite: bool = False 

73 project_name: str = "" 

74 

75 

76@dataclass 

77class Project: 

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

79 

80 name: str 

81 description: str = "" 

82 favorite: bool = False 

83 repository_url: str = "" 

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

85 title: str = "" 

86 

87 

88@dataclass 

89class Version: 

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

91 

92 version: str 

93 release_date: datetime | None = None 

94 downloads: int = 0 

95 is_yanked: bool = False 

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

97 external_id: int | str | None = None 

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

99 size_bytes: int | None = None 

100 

101 

102@dataclass 

103class DownloadTrendPoint: 

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

105 

106 date: str 

107 count: int 

108 

109 

110@dataclass 

111class DownloadCounts: 

112 """Breakdown of downloads by period.""" 

113 

114 today: int = 0 

115 week: int = 0 

116 month: int = 0 

117 

118 

119@dataclass 

120class GitHubStats: 

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

122 

123 stars: int = 0 

124 forks: int = 0 

125 open_issues: int = 0 

126 created_at: datetime | None = None 

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

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

129 has_readme: bool = False 

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

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

132 stars_delta_7d: int | None = None 

133 open_issues_delta_7d: int | None = None 

134 

135 

136@dataclass 

137class Dependency: 

138 """A dependency of a package.""" 

139 

140 name: str 

141 requirement: str = "" 

142 optional: bool = False 

143 

144 

145@dataclass 

146class ReleaseFile: 

147 """One published artifact for a version. 

148 

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

150 """ 

151 

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

153 size: int = 0 

154 filename: str = "" 

155 

156 

157@dataclass 

158class ReverseDependency: 

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

160 

161 name: str 

162 downloads: int = 0 

163 

164 

165@dataclass 

166class AdvisoryReference: 

167 """An external reference attached to a security advisory.""" 

168 

169 type: str 

170 url: str 

171 

172 

173@dataclass 

174class SecurityAdvisory: 

175 """A vulnerability affecting the inspected package version.""" 

176 

177 id: str 

178 summary: str = "" 

179 details: str = "" 

180 aliases: list[str] = field(default_factory=list) 

181 severity: str = "" 

182 published: datetime | None = None 

183 modified: datetime | None = None 

184 fixed_versions: list[str] = field(default_factory=list) 

185 references: list[AdvisoryReference] = field(default_factory=list) 

186 url: str = "" 

187 

188 

189@dataclass 

190class GitHubIssueEvent: 

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

192 

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

194 """ 

195 

196 number: int 

197 title: str 

198 is_pull_request: bool 

199 created_at: datetime 

200 closed_at: datetime | None 

201 url: str = "" 

202 

203 

204@dataclass 

205class HistorySnapshot: 

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

207 

208 timestamp: datetime 

209 stars: int 

210 open_issues: int 

211 health_score: int | None = None 

212 reverse_dependency_count: int | None = None 

213 

214 

215@dataclass 

216class MetricTimelinePoint: 

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

218 

219 label: str 

220 value: int 

221 

222 

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

224 

225 

226@dataclass 

227class InstallMethod: 

228 label: str 

229 count: int 

230 percent: float 

231 

232 

233@dataclass 

234class InstallBreakdown: 

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

236 caption: str = "" 

237 is_estimate: bool = False 

238 

239 

240@dataclass 

241class ReverseDependencySummary: 

242 count: int | None = None 

243 monthly_growth: int | None = None 

244 caption: str = "" 

245 

246 

247class ActivityEventKind(StrEnum): 

248 RELEASE = "release" 

249 ISSUE_OPENED = "issue_opened" 

250 ISSUE_CLOSED = "issue_closed" 

251 PR_OPENED = "pr_opened" 

252 PR_CLOSED = "pr_closed" 

253 

254 @property 

255 def icon(self) -> str: 

256 return { 

257 self.RELEASE: "◉", 

258 self.ISSUE_OPENED: "◈", 

259 self.ISSUE_CLOSED: "✓", 

260 self.PR_OPENED: "⇄", 

261 self.PR_CLOSED: "⇥", 

262 }[self] 

263 

264 @property 

265 def label(self) -> str: 

266 return { 

267 self.RELEASE: "New Release", 

268 self.ISSUE_OPENED: "New Issue", 

269 self.ISSUE_CLOSED: "Issue Closed", 

270 self.PR_OPENED: "New PR", 

271 self.PR_CLOSED: "PR Closed", 

272 }[self] 

273 

274 

275@dataclass 

276class ActivityEvent: 

277 kind: ActivityEventKind 

278 timestamp: datetime 

279 title: str 

280 ref: str = "" 

281 url: str = "" 

282 

283 

284@dataclass 

285class HealthSubScore: 

286 label: str 

287 score: int 

288 max_score: int = 20 

289 

290 

291@dataclass 

292class HealthScore: 

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

294 total: int = 0 # 0-100 

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

296 

297 

298@dataclass 

299class DerivedPackageData: 

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

301 

302 health_score: HealthScore = field(default_factory=HealthScore) 

303 install_breakdown: InstallBreakdown = field(default_factory=InstallBreakdown) 

304 reverse_dependency_summary: ReverseDependencySummary = field( 

305 default_factory=ReverseDependencySummary 

306 ) 

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

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

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

310 adoption_caption: str = "" 

311 downloads_30d_total: int = 0 

312 downloads_30d_pct_change: float | None = None 

313 

314 

315@dataclass 

316class PackageInfo: 

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

318 

319 name: str 

320 registry: Registry 

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

322 description: str = "" 

323 author: str = "" 

324 license: str = "" 

325 homepage: str = "" 

326 repository_url: str = "" 

327 documentation_url: str = "" 

328 latest_version: str = "" 

329 latest_release_date: datetime | None = None 

330 total_downloads: int = 0 

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

332 download_counts: DownloadCounts = field(default_factory=DownloadCounts) 

333 github_stats: GitHubStats = field(default_factory=GitHubStats) 

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

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

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

337 release_notes: str = "" 

338 # raw extras feeding derivation 

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

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

341 default_factory=dict 

342 ) # crates.io only 

343 reverse_dependencies: list[ReverseDependency] = field( 

344 default_factory=list 

345 ) # crates.io only 

346 reverse_dependency_count: int | None = None 

347 reverse_dependency_monthly_growth: int | None = None 

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

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

350 security_advisories: list[SecurityAdvisory] = field(default_factory=list) 

351 

352 

353@dataclass 

354class FetchError: 

355 """Error when fetching package data.""" 

356 

357 package_name: str 

358 registry: Registry 

359 message: str 

360 

361 

362@dataclass 

363class SearchResult: 

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

365 

366 name: str 

367 registry: Registry 

368 version: str = "" 

369 description: str = "" 

370 url: str = "" 

371 score: float = 0.0 

372 exact: bool = False