Coverage for src/secchi/aggregate.py: 19%
74 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
1"""Pure helpers for presenting one logical package across registries."""
3from __future__ import annotations
5from collections.abc import Iterable
6from dataclasses import replace
8from secchi.models import (
9 DownloadTrendPoint,
10 InstallBreakdown,
11 InstallMethod,
12 PackageInfo,
13 PackageRef,
14 Registry,
15)
18def package_key(ref: PackageRef) -> str:
19 project = f"{ref.project_name}:" if ref.project_name else ""
20 return f"{project}{ref.registry.value}:{ref.name}"
23def logical_package_refs(refs: list[PackageRef]) -> list[PackageRef]:
24 """Collapse same-named registry variants into one navigable package."""
25 grouped: dict[str, PackageRef] = {}
26 for ref in refs:
27 key = ref.name.lower()
28 current = grouped.get(key)
29 if current is None:
30 grouped[key] = replace(ref)
31 elif ref.favorite and not current.favorite:
32 current.favorite = True
33 return list(grouped.values())
36def combine_package_infos(ref: PackageRef, infos: list[PackageInfo]) -> PackageInfo:
37 """Combine compatible activity signals while retaining a primary profile."""
38 primary = pick_primary_info(infos)
39 combined = replace(primary)
40 combined.name = ref.name
41 combined.source_registries = unique_registries(info.registry for info in infos)
42 combined.total_downloads = sum(info.total_downloads for info in infos)
43 combined.download_counts = replace(primary.download_counts)
44 combined.download_counts.today = sum(info.download_counts.today for info in infos)
45 combined.download_counts.week = sum(info.download_counts.week for info in infos)
46 combined.download_counts.month = sum(info.download_counts.month for info in infos)
47 combined.download_trend = combine_download_trends(infos)
49 best_github = next(
50 (info.github_stats for info in infos if info.github_stats.resolved), None
51 )
52 if best_github is not None:
53 combined.github_stats = best_github
55 crates_info = next(
56 (info for info in infos if info.registry is Registry.CRATES), None
57 )
58 if crates_info is not None:
59 combined.reverse_dependencies = crates_info.reverse_dependencies
60 combined.reverse_dependency_count = crates_info.reverse_dependency_count
61 combined.reverse_dependency_monthly_growth = (
62 crates_info.reverse_dependency_monthly_growth
63 )
64 combined.health_history = primary.health_history
65 combined.security_advisories = _combine_security_advisories(infos)
66 return combined
69def pick_primary_info(infos: list[PackageInfo]) -> PackageInfo:
70 for registry in (Registry.CRATES, Registry.PYPI, Registry.NPM):
71 for info in infos:
72 if info.registry is registry and info.latest_version:
73 return info
74 return infos[0]
77def unique_registries(registries: Iterable[Registry]) -> list[Registry]:
78 seen: set[Registry] = set()
79 return [
80 registry
81 for registry in registries
82 if not (registry in seen or seen.add(registry))
83 ]
86def combine_download_trends(infos: list[PackageInfo]) -> list[DownloadTrendPoint]:
87 counts: dict[str, int] = {}
88 for info in infos:
89 for point in info.download_trend:
90 counts[point.date] = counts.get(point.date, 0) + point.count
91 return [
92 DownloadTrendPoint(date=date, count=counts[date]) for date in sorted(counts)
93 ]
96def _combine_security_advisories(infos: list[PackageInfo]):
97 seen: set[str] = set()
98 advisories = []
99 for info in infos:
100 for advisory in info.security_advisories:
101 if advisory.id not in seen:
102 seen.add(advisory.id)
103 advisories.append(advisory)
104 return advisories
107def combine_install_breakdown(infos: list[PackageInfo]) -> InstallBreakdown:
108 totals: dict[str, int] = {}
109 for info in infos:
110 label = info.registry.display_name
111 count = info.download_counts.month or sum(
112 p.count for p in info.download_trend[-30:]
113 )
114 totals[label] = totals.get(label, 0) + (count or info.total_downloads)
115 total = sum(totals.values())
116 if total <= 0:
117 return InstallBreakdown(
118 caption="No 30-day download data available across ecosystems."
119 )
120 methods = [
121 InstallMethod(label=label, count=count, percent=count / total * 100)
122 for label, count in sorted(
123 totals.items(), key=lambda item: item[1], reverse=True
124 )
125 ]
126 return InstallBreakdown(
127 methods=methods, caption="Combined from registry 30-day download totals."
128 )