feat: add "Multiple on PyPI" badge for SDK monorepos

azure-sdk-for-python and google-cloud-python were rendering "Not on
PyPI", which is misleading. Both do ship on PyPI, just as many
per-service packages (azure-identity, azure-storage-blob,
google-cloud-storage, etc.) rather than under the repo name.

pypi_name_overrides.json already recorded that distinction in its
reason field; those two entries now carry an optional "badge" value
that build.py reads into the PyPI Downloads column. The other sixteen
no-count entries (cpython, renpy, agent skill repos, etc.) keep
"Not on PyPI" since that remains accurate for them.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vinta Chen
2026-08-23 01:28:33 +08:00
co-authored by Claude
parent c07324b24f
commit 0f677911f0
6 changed files with 40 additions and 9 deletions
+16 -1
View File
@@ -11,7 +11,7 @@ from datetime import UTC, datetime
from pathlib import Path
from typing import TypedDict
from fetch_pypi_downloads_via_clickpy import normalize
from fetch_pypi_downloads_via_clickpy import OVERRIDES_FILE, normalize
from jinja2 import Environment, FileSystemLoader
from readme_parser import AlsoSee, ParsedGroup, ParsedSection, parse_readme, parse_sponsors, slugify
@@ -60,6 +60,7 @@ class TemplateEntry(TypedDict):
last_commit_at: str | None
source_type: str | None
bundled: bool
badge: str | None
also_see: list[AlsoSee]
@@ -101,6 +102,17 @@ def load_stars(path: Path) -> dict[str, dict]:
return {}
def load_pypi_badges() -> dict[str, str]:
"""Load the badge each entry shows in place of a download count.
Keyed by normalized README name. Only entries whose missing count needs
explaining beyond "Not on PyPI" carry one, e.g. the SDK monorepos that do
ship on PyPI, just as many per-service packages.
"""
raw = json.loads(OVERRIDES_FILE.read_text())
return {name: entry["badge"] for name, entry in raw.items() if entry.get("badge")}
def load_downloads(path: Path) -> dict[str, int]:
"""Load last-30-day download counts from the TSV cache, keyed by normalized README name.
@@ -524,6 +536,7 @@ def extract_entries(
last_commit_at=None,
source_type=detect_source_type(entry["url"]),
bundled=bool(BUNDLED_PREFIX_RE.match(entry["description"])),
badge=None,
also_see=entry["also_see"],
)
seen[key] = existing
@@ -576,6 +589,7 @@ def build(repo_root: Path) -> None:
stars_data = load_stars(website / "data" / "github_stars.json")
downloads_data = load_downloads(website / "data" / "pypi_downloads.tsv")
pypi_badges = load_pypi_badges()
repo_self = stars_data.get("vinta/awesome-python", {})
repo_stars = None
@@ -595,6 +609,7 @@ def build(repo_root: Path) -> None:
# Stdlib entries would hit same-named PyPI backports (e.g. asyncio), not the stdlib.
if entry.get("source_type") != BUILTIN_FILTER:
entry["downloads"] = downloads_data.get(normalize(entry["name"]))
entry["badge"] = pypi_badges.get(normalize(entry["name"]))
entries = sort_entries(entries)
category_urls = {cat["name"]: category_path(cat) for cat in categories}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"autobahn-python": { "package": "autobahn", "reason": null },
"azure-sdk-for-python": { "package": null, "reason": "monorepo umbrella; ships as many azure-* packages, no single package represents it" },
"azure-sdk-for-python": { "package": null, "reason": "monorepo umbrella; ships as many azure-* packages, no single package represents it", "badge": "Multiple on PyPI" },
"cloud-init": { "package": null, "reason": "installed via distro images, not pip" },
"concurrent-futures": { "package": null, "reason": "stdlib module" },
"cpython": { "package": null, "reason": "not pip-installable; the cpython PyPI name was squatted" },
@@ -12,7 +12,7 @@
"django-rules": { "package": "rules", "reason": "PyPI django-rules is an abandoned unrelated fork; dfunckt's project publishes as rules" },
"fasthtml": { "package": "python-fasthtml", "reason": null },
"geodjango": { "package": null, "reason": "part of Django (django.contrib.gis), not a package" },
"google-cloud-python": { "package": null, "reason": "monorepo umbrella; ships as many google-cloud-* packages" },
"google-cloud-python": { "package": null, "reason": "monorepo umbrella; ships as many google-cloud-* packages", "badge": "Multiple on PyPI" },
"graphify": { "package": null, "reason": "GitHub-only so far; young project, remap if it publishes" },
"httpx-url": { "package": null, "reason": "class in the httpx package, not a package" },
"jinja": { "package": "jinja2", "reason": "PyPI jinja is Jinja1; current releases publish as jinja2" },
+4 -2
View File
@@ -56,8 +56,10 @@ def normalize(name: str) -> str:
def load_overrides() -> dict[str, str | None]:
"""Load curated overrides: normalized README name -> PyPI package to measure (None = never query).
Each file entry is {"package": str | null, "reason": str | null}; the reason is
documentation only — required for null packages, optional for remaps.
Each file entry is {"package": str | null, "reason": str | null} plus an
optional "badge" the website renders in place of a download count; the
reason is documentation only — required for null packages, optional for
remaps.
"""
raw = json.loads(OVERRIDES_FILE.read_text())
return {name: entry["package"] for name, entry in raw.items()}
+3 -2
View File
@@ -157,8 +157,9 @@
{% if entry.downloads is not none %}{{
"{:,}".format(entry.downloads) }}{% elif entry.source_type ==
'Stdlib' %}<span class="source-badge">Stdlib</span>{% elif
entry.bundled %}<span class="source-badge">Bundled</span>{% else
%}<span class="source-badge">Not on PyPI</span>{% endif %}
entry.bundled %}<span class="source-badge">Bundled</span>{% elif
entry.badge %}<span class="source-badge">{{ entry.badge }}</span>{%
else %}<span class="source-badge">Not on PyPI</span>{% endif %}
</td>
<td class="col-stars">
{% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{%
+3 -2
View File
@@ -194,8 +194,9 @@
{% if entry.downloads is not none %}{{
"{:,}".format(entry.downloads) }}{% elif entry.source_type ==
'Stdlib' %}<span class="source-badge">Stdlib</span>{% elif
entry.bundled %}<span class="source-badge">Bundled</span>{% else
%}<span class="source-badge">Not on PyPI</span>{% endif %}
entry.bundled %}<span class="source-badge">Bundled</span>{% elif
entry.badge %}<span class="source-badge">{{ entry.badge }}</span>{%
else %}<span class="source-badge">Not on PyPI</span>{% endif %}
</td>
<td class="col-stars">
{% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{%
+12
View File
@@ -18,6 +18,7 @@ from build import (
extract_entries,
extract_github_repo,
load_downloads,
load_pypi_badges,
load_stars,
sort_entries,
subcategory_path,
@@ -1012,6 +1013,16 @@ class TestLoadStars:
# ---------------------------------------------------------------------------
class TestLoadPypiBadges:
def test_only_entries_with_a_badge_are_returned(self):
badges = load_pypi_badges()
assert badges["azure-sdk-for-python"] == "Multiple on PyPI"
assert badges["google-cloud-python"] == "Multiple on PyPI"
# Entries whose missing count needs no explaining stay off the map
assert "cpython" not in badges
assert "tomllib" not in badges
def _template_entry(name: str, stars: int | None, source_type: str | None = None) -> TemplateEntry:
return TemplateEntry(
name=name,
@@ -1026,6 +1037,7 @@ def _template_entry(name: str, stars: int | None, source_type: str | None = None
last_commit_at=None,
source_type=source_type,
bundled=False,
badge=None,
also_see=[],
)