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
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 23:28 +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"
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 AdvisoryReference:
162 """An external reference attached to a security advisory."""
164 type: str
165 url: str
168@dataclass
169class SecurityAdvisory:
170 """A vulnerability affecting the inspected package version."""
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 = ""
184@dataclass
185class GitHubIssueEvent:
186 """Raw GitHub issue OR pull request record.
188 Feeds both health-score issue counts and the Activity timeline.
189 """
191 number: int
192 title: str
193 is_pull_request: bool
194 created_at: datetime
195 closed_at: datetime | None
196 url: str = ""
199@dataclass
200class HistorySnapshot:
201 """A point-in-time snapshot cached locally to derive week-over-week deltas."""
203 timestamp: datetime
204 stars: int
205 open_issues: int
206 health_score: int | None = None
207 reverse_dependency_count: int | None = None
210@dataclass
211class MetricTimelinePoint:
212 """A compact labeled metric point for dashboard sparklines."""
214 label: str
215 value: int
218# ── Derived-display dataclasses (produced by derived.py, consumed by widgets) ──
221@dataclass
222class InstallMethod:
223 label: str
224 count: int
225 percent: float
228@dataclass
229class InstallBreakdown:
230 methods: list[InstallMethod] = field(default_factory=list)
231 caption: str = ""
232 is_estimate: bool = False
235@dataclass
236class ReverseDependencySummary:
237 count: int | None = None
238 monthly_growth: int | None = None
239 caption: str = ""
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"
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]
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]
270@dataclass
271class ActivityEvent:
272 kind: ActivityEventKind
273 timestamp: datetime
274 title: str
275 ref: str = ""
276 url: str = ""
279@dataclass
280class HealthSubScore:
281 label: str
282 score: int
283 max_score: int = 20
286@dataclass
287class HealthScore:
288 sub_scores: list[HealthSubScore] = field(default_factory=list)
289 total: int = 0 # 0-100
290 grade: str = "—" # "A".."F"
293@dataclass
294class DerivedPackageData:
295 """All display-ready metrics derived (via arithmetic, no I/O) from a PackageInfo."""
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
310@dataclass
311class PackageInfo:
312 """Full package information fetched from the registry."""
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)
348@dataclass
349class FetchError:
350 """Error when fetching package data."""
352 package_name: str
353 registry: Registry
354 message: str
357@dataclass
358class SearchResult:
359 """A normalized package discovery result from a registry search."""
361 name: str
362 registry: Registry
363 version: str = ""
364 description: str = ""
365 url: str = ""
366 score: float = 0.0
367 exact: bool = False