Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/secchi/api/pubdev.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,27 @@ async def fetch_download_counts(self, name: str) -> DownloadCounts:

async def search(self, query: str, limit: int = 10) -> list[SearchResult]:
async with self._client_scope() as client:
names: list[str] = []
try:
response = await client.get(f"{PUB_API}/search", params={"q": query})
response.raise_for_status()
except httpx.HTTPError:
return []
names = [
entry.get("package", "")
for entry in response.json().get("packages", [])[:limit]
if entry.get("package")
]
names = [
entry.get("package", "")
for entry in response.json().get("packages", [])[:limit]
if entry.get("package")
]
except (httpx.HTTPError, ValueError, KeyError, TypeError):
# The search endpoint is useful for suggestions, but it is not
# authoritative for exact package resolution. Fall through to
# the package endpoint below so newly indexed or oddly ranked
# packages can still be opened directly.
pass

# An unqualified CLI package name must resolve even when pub.dev's
# search results omit the exact package (or the search endpoint is
# temporarily unavailable).
if query.casefold() not in {name.casefold() for name in names}:
names.insert(0, query)

async def describe(candidate: str) -> SearchResult | None:
try:
Expand All @@ -115,7 +126,7 @@ async def describe(candidate: str) -> SearchResult | None:
exact=resolved_name.casefold() == query.casefold(),
)

results = await asyncio.gather(*(describe(name) for name in names))
results = await asyncio.gather(*(describe(name) for name in names[:limit]))
return [result for result in results if result is not None]


Expand Down
10 changes: 5 additions & 5 deletions src/secchi/renderers/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

def render_summary(info: PackageInfo, derived: DerivedPackageData) -> str:
change = derived.downloads_30d_pct_change
adoption = (
"—" if change is None else f"{'▲' if change >= 0 else '▼'} {abs(change):.1f}%"
)
downloads = _format_count(derived.downloads_30d_total)
if change is not None:
downloads = f"{downloads} ({'▲' if change >= 0 else '▼'} {abs(change):.1f}%)"
stars = (
_format_count(info.github_stats.stars) if info.github_stats.resolved else "—"
)
Expand All @@ -22,7 +22,7 @@ def render_summary(info: PackageInfo, derived: DerivedPackageData) -> str:
"─" * max(20, len(info.name)),
f"Health Score {health} / 100",
f"Latest Version {info.latest_version or '—'}",
f"Downloads {adoption}",
f"Downloads {downloads}",
f"GitHub Stars {stars}",
f"Dependents {dependents}",
f"Security Advisories {advisories}",
Expand All @@ -31,7 +31,7 @@ def render_summary(info: PackageInfo, derived: DerivedPackageData) -> str:


def _format_count(value: int | None) -> str:
if value is None:
if not value:
return "—"
if value >= 1_000_000:
return f"{value / 1_000_000:.1f}M"
Expand Down
20 changes: 20 additions & 0 deletions tests/test_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,23 @@ async def exercise() -> None:
assert next(r for r in results if r.name == "demo_two").exact is False

run(exercise())


def test_pubdev_adapter_resolves_exact_package_when_search_omits_it() -> None:
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/api/search":
return json_response(request, {"packages": [{"package": "other"}]})
if request.url.path == "/api/packages/scanbot_sdk":
return json_response(request, _pubdev_package("scanbot_sdk"))
if request.url.path == "/api/packages/other":
return json_response(request, _pubdev_package("other"))
return httpx.Response(404, request=request)

async def exercise() -> None:
async with client_for(handler) as client:
results = await PubDevAdapter(client).search("scanbot_sdk")

assert [result.name for result in results if result.exact] == ["scanbot_sdk"]
assert results[0].registry is Registry.PUB

run(exercise())
5 changes: 4 additions & 1 deletion tests/test_config_and_renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,16 @@ def test_resolver_prefers_configured_registry_sources() -> None:

def test_summary_includes_primary_signals() -> None:
info = PackageInfo(name="duckdb", registry=Registry.PYPI, latest_version="1.5.5")
derived = DerivedPackageData(
health_score=HealthScore(total=92), downloads_30d_total=7_600
)
info.github_stats.resolved = True
info.github_stats.stars = 34_000
derived = DerivedPackageData(health_score=HealthScore(total=92))
output = render_summary(info, derived)
assert "Health Score 92 / 100" in output
assert "Latest Version 1.5.5" in output
assert "GitHub Stars 34.0k" in output
assert "Downloads 7.6k" in output
assert "Security Advisories 0" in output


Expand Down
Loading