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
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-04 22:15 +0000
1from __future__ import annotations
3from dataclasses import dataclass, field
4from datetime import datetime
5from enum import Enum
8class Registry(str, Enum):
9 PYPI = "pypi"
10 CRATES = "crates.io"
11 NPM = "npm"
12 HOMEBREW = "homebrew"
13 GO = "go"
14 CRAN = "cran"
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]
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]
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]
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]
61@dataclass
62class PackageRef:
63 """A reference to a package from config."""
65 name: str
66 registry: Registry
67 favorite: bool = False
68 project_name: str = ""
71@dataclass
72class Project:
73 """A named collection of packages to monitor."""
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 = ""
83@dataclass
84class Version:
85 """A specific version of a package."""
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
97@dataclass
98class DownloadTrendPoint:
99 """A single data point for download trend charts."""
101 date: str
102 count: int
105@dataclass
106class DownloadCounts:
107 """Breakdown of downloads by period."""
109 today: int = 0
110 week: int = 0
111 month: int = 0
114@dataclass
115class GitHubStats:
116 """GitHub repository stats (raw, from the repo + signals APIs)."""
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
131@dataclass
132class Dependency:
133 """A dependency of a package."""
135 name: str
136 requirement: str = ""
137 optional: bool = False
140@dataclass
141class ReleaseFile:
142 """One published artifact for a version.
144 PyPI has several per version; npm/crates get one synthetic entry.
145 """
147 packagetype: str # "bdist_wheel" | "sdist" | "npm-package" | "crate"
148 size: int = 0
149 filename: str = ""
152@dataclass
153class ReverseDependency:
154 """A package that depends on this one, ranked by its own real total downloads."""
156 name: str
157 downloads: int = 0
160@dataclass
161class GitHubIssueEvent:
162 """Raw GitHub issue OR pull request record.
164 Feeds both health-score issue counts and the Activity timeline.
165 """
167 number: int
168 title: str
169 is_pull_request: bool
170 created_at: datetime
171 closed_at: datetime | None
172 url: str = ""
175@dataclass
176class HistorySnapshot:
177 """A point-in-time snapshot cached locally to derive week-over-week deltas."""
179 timestamp: datetime
180 stars: int
181 open_issues: int
182 health_score: int | None = None
183 reverse_dependency_count: int | None = None
186@dataclass
187class MetricTimelinePoint:
188 """A compact labeled metric point for dashboard sparklines."""
190 label: str
191 value: int
194# ── Derived-display dataclasses (produced by derived.py, consumed by widgets) ──
197@dataclass
198class InstallMethod:
199 label: str
200 count: int
201 percent: float
204@dataclass
205class InstallBreakdown:
206 methods: list[InstallMethod] = field(default_factory=list)
207 caption: str = ""
208 is_estimate: bool = False
211@dataclass
212class ReverseDependencySummary:
213 count: int | None = None
214 monthly_growth: int | None = None
215 caption: str = ""
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"
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]
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]
246@dataclass
247class ActivityEvent:
248 kind: ActivityEventKind
249 timestamp: datetime
250 title: str
251 ref: str = ""
252 url: str = ""
255@dataclass
256class HealthSubScore:
257 label: str
258 score: int
259 max_score: int = 20
262@dataclass
263class HealthScore:
264 sub_scores: list[HealthSubScore] = field(default_factory=list)
265 total: int = 0 # 0-100
266 grade: str = "—" # "A".."F"
269@dataclass
270class DerivedPackageData:
271 """All display-ready metrics derived (via arithmetic, no I/O) from a PackageInfo."""
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
286@dataclass
287class PackageInfo:
288 """Full package information fetched from the registry."""
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)
323@dataclass
324class FetchError:
325 """Error when fetching package data."""
327 package_name: str
328 registry: Registry
329 message: str
332@dataclass
333class SearchResult:
334 """A normalized package discovery result from a registry search."""
336 name: str
337 registry: Registry
338 version: str = ""
339 description: str = ""
340 url: str = ""
341 score: float = 0.0
342 exact: bool = False