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

213 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-14 23:28 +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 

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 AdvisoryReference: 

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

163 

164 type: str 

165 url: str 

166 

167 

168@dataclass 

169class SecurityAdvisory: 

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

171 

172 id: str 

173 summary: str = "" 

174 details: str = "" 

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

176 severity: str = "" 

177 published: datetime | None = None 

178 modified: datetime | None = None 

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

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

181 url: str = "" 

182 

183 

184@dataclass 

185class GitHubIssueEvent: 

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

187 

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

189 """ 

190 

191 number: int 

192 title: str 

193 is_pull_request: bool 

194 created_at: datetime 

195 closed_at: datetime | None 

196 url: str = "" 

197 

198 

199@dataclass 

200class HistorySnapshot: 

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

202 

203 timestamp: datetime 

204 stars: int 

205 open_issues: int 

206 health_score: int | None = None 

207 reverse_dependency_count: int | None = None 

208 

209 

210@dataclass 

211class MetricTimelinePoint: 

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

213 

214 label: str 

215 value: int 

216 

217 

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

219 

220 

221@dataclass 

222class InstallMethod: 

223 label: str 

224 count: int 

225 percent: float 

226 

227 

228@dataclass 

229class InstallBreakdown: 

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

231 caption: str = "" 

232 is_estimate: bool = False 

233 

234 

235@dataclass 

236class ReverseDependencySummary: 

237 count: int | None = None 

238 monthly_growth: int | None = None 

239 caption: str = "" 

240 

241 

242class ActivityEventKind(StrEnum): 

243 RELEASE = "release" 

244 ISSUE_OPENED = "issue_opened" 

245 ISSUE_CLOSED = "issue_closed" 

246 PR_OPENED = "pr_opened" 

247 PR_CLOSED = "pr_closed" 

248 

249 @property 

250 def icon(self) -> str: 

251 return { 

252 self.RELEASE: "◉", 

253 self.ISSUE_OPENED: "◈", 

254 self.ISSUE_CLOSED: "✓", 

255 self.PR_OPENED: "⇄", 

256 self.PR_CLOSED: "⇥", 

257 }[self] 

258 

259 @property 

260 def label(self) -> str: 

261 return { 

262 self.RELEASE: "New Release", 

263 self.ISSUE_OPENED: "New Issue", 

264 self.ISSUE_CLOSED: "Issue Closed", 

265 self.PR_OPENED: "New PR", 

266 self.PR_CLOSED: "PR Closed", 

267 }[self] 

268 

269 

270@dataclass 

271class ActivityEvent: 

272 kind: ActivityEventKind 

273 timestamp: datetime 

274 title: str 

275 ref: str = "" 

276 url: str = "" 

277 

278 

279@dataclass 

280class HealthSubScore: 

281 label: str 

282 score: int 

283 max_score: int = 20 

284 

285 

286@dataclass 

287class HealthScore: 

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

289 total: int = 0 # 0-100 

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

291 

292 

293@dataclass 

294class DerivedPackageData: 

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

296 

297 health_score: HealthScore = field(default_factory=HealthScore) 

298 install_breakdown: InstallBreakdown = field(default_factory=InstallBreakdown) 

299 reverse_dependency_summary: ReverseDependencySummary = field( 

300 default_factory=ReverseDependencySummary 

301 ) 

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

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

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

305 adoption_caption: str = "" 

306 downloads_30d_total: int = 0 

307 downloads_30d_pct_change: float | None = None 

308 

309 

310@dataclass 

311class PackageInfo: 

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

313 

314 name: str 

315 registry: Registry 

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

317 description: str = "" 

318 author: str = "" 

319 license: str = "" 

320 homepage: str = "" 

321 repository_url: str = "" 

322 documentation_url: str = "" 

323 latest_version: str = "" 

324 latest_release_date: datetime | None = None 

325 total_downloads: int = 0 

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

327 download_counts: DownloadCounts = field(default_factory=DownloadCounts) 

328 github_stats: GitHubStats = field(default_factory=GitHubStats) 

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

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

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

332 release_notes: str = "" 

333 # raw extras feeding derivation 

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

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

336 default_factory=dict 

337 ) # crates.io only 

338 reverse_dependencies: list[ReverseDependency] = field( 

339 default_factory=list 

340 ) # crates.io only 

341 reverse_dependency_count: int | None = None 

342 reverse_dependency_monthly_growth: int | None = None 

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

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

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

346 

347 

348@dataclass 

349class FetchError: 

350 """Error when fetching package data.""" 

351 

352 package_name: str 

353 registry: Registry 

354 message: str 

355 

356 

357@dataclass 

358class SearchResult: 

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

360 

361 name: str 

362 registry: Registry 

363 version: str = "" 

364 description: str = "" 

365 url: str = "" 

366 score: float = 0.0 

367 exact: bool = False