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
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-16 12:16 +0000
1from __future__ import annotations
3from dataclasses import dataclass, field
4from datetime import datetime
5from enum import StrEnum
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"
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]
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]
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]
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]
66@dataclass
67class PackageRef:
68 """A reference to a package from config."""
70 name: str
71 registry: Registry
72 favorite: bool = False
73 project_name: str = ""
76@dataclass
77class Project:
78 """A named collection of packages to monitor."""
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 = ""
88@dataclass
89class Version:
90 """A specific version of a package."""
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
102@dataclass
103class DownloadTrendPoint:
104 """A single data point for download trend charts."""
106 date: str
107 count: int
110@dataclass
111class DownloadCounts:
112 """Breakdown of downloads by period."""
114 today: int = 0
115 week: int = 0
116 month: int = 0
119@dataclass
120class GitHubStats:
121 """GitHub repository stats (raw, from the repo + signals APIs)."""
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
136@dataclass
137class Dependency:
138 """A dependency of a package."""
140 name: str
141 requirement: str = ""
142 optional: bool = False
145@dataclass
146class ReleaseFile:
147 """One published artifact for a version.
149 PyPI has several per version; npm/crates get one synthetic entry.
150 """
152 packagetype: str # "bdist_wheel" | "sdist" | "npm-package" | "crate"
153 size: int = 0
154 filename: str = ""
157@dataclass
158class ReverseDependency:
159 """A package that depends on this one, ranked by its own real total downloads."""
161 name: str
162 downloads: int = 0
165@dataclass
166class AdvisoryReference:
167 """An external reference attached to a security advisory."""
169 type: str
170 url: str
173@dataclass
174class SecurityAdvisory:
175 """A vulnerability affecting the inspected package version."""
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 = ""
189@dataclass
190class GitHubIssueEvent:
191 """Raw GitHub issue OR pull request record.
193 Feeds both health-score issue counts and the Activity timeline.
194 """
196 number: int
197 title: str
198 is_pull_request: bool
199 created_at: datetime
200 closed_at: datetime | None
201 url: str = ""
204@dataclass
205class HistorySnapshot:
206 """A point-in-time snapshot cached locally to derive week-over-week deltas."""
208 timestamp: datetime
209 stars: int
210 open_issues: int
211 health_score: int | None = None
212 reverse_dependency_count: int | None = None
215@dataclass
216class MetricTimelinePoint:
217 """A compact labeled metric point for dashboard sparklines."""
219 label: str
220 value: int
223# ── Derived-display dataclasses (produced by derived.py, consumed by widgets) ──
226@dataclass
227class InstallMethod:
228 label: str
229 count: int
230 percent: float
233@dataclass
234class InstallBreakdown:
235 methods: list[InstallMethod] = field(default_factory=list)
236 caption: str = ""
237 is_estimate: bool = False
240@dataclass
241class ReverseDependencySummary:
242 count: int | None = None
243 monthly_growth: int | None = None
244 caption: str = ""
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"
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]
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]
275@dataclass
276class ActivityEvent:
277 kind: ActivityEventKind
278 timestamp: datetime
279 title: str
280 ref: str = ""
281 url: str = ""
284@dataclass
285class HealthSubScore:
286 label: str
287 score: int
288 max_score: int = 20
291@dataclass
292class HealthScore:
293 sub_scores: list[HealthSubScore] = field(default_factory=list)
294 total: int = 0 # 0-100
295 grade: str = "—" # "A".."F"
298@dataclass
299class DerivedPackageData:
300 """All display-ready metrics derived (via arithmetic, no I/O) from a PackageInfo."""
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
315@dataclass
316class PackageInfo:
317 """Full package information fetched from the registry."""
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)
353@dataclass
354class FetchError:
355 """Error when fetching package data."""
357 package_name: str
358 registry: Registry
359 message: str
362@dataclass
363class SearchResult:
364 """A normalized package discovery result from a registry search."""
366 name: str
367 registry: Registry
368 version: str = ""
369 description: str = ""
370 url: str = ""
371 score: float = 0.0
372 exact: bool = False