Files
hassio-addons/.github/generate_map.py
2025-06-16 07:06:58 +00:00

175 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Generate a static PNG world map colour-coded by the percentage of your
stargazers that come from each country. The script maintains a CSV
in ".github/stargazer_countries.csv" cache so that locations are only looked
up once (unless the country entry is blank).
"""
import csv
import os
import sys
import time
from collections import Counter
from pathlib import Path
import plotly.express as px
import pycountry
import requests
from geopy.geocoders import Nominatim
# ---- Configuration ----------------------------------------------------------
REPO = os.getenv("REPO") # expected "owner/repo"
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") # provided by workflow
CSV_PATH = Path(".github/stargazer_countries.csv")
PNG_PATH = Path(".github/stargazer_map.png")
HEADERS = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
}
GEOL = Nominatim(user_agent="gh-stargazer-map")
# -----------------------------------------------------------------------------
def github_paginated(url):
page = 1
while True:
resp = requests.get(f"{url}?per_page=100&page={page}", headers=HEADERS)
resp.raise_for_status()
data = resp.json()
if not data:
break
yield from data
page += 1
def fetch_stargazer_usernames():
url = f"https://api.github.com/repos/{REPO}/stargazers"
return [s["login"] for s in github_paginated(url)]
def load_cache():
if not CSV_PATH.exists():
return {}
with CSV_PATH.open(newline="", encoding="utf-8") as f:
return {row["username"]: row["country"] for row in csv.DictReader(f)}
def save_cache(cache):
CSV_PATH.parent.mkdir(parents=True, exist_ok=True)
with CSV_PATH.open("w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["username", "country"])
for user, country in sorted(cache.items()):
w.writerow([user, country or ""])
def username_to_country(login):
"""Return readable country name or '' if unknown."""
resp = requests.get(f"https://api.github.com/users/{login}", headers=HEADERS)
resp.raise_for_status()
loc = (resp.json() or {}).get("location") or ""
if not loc.strip():
return ""
try:
g = GEOL.geocode(loc, language="en", timeout=10)
except Exception:
return ""
if not g or "display_name" not in g.raw:
return ""
# take the last comma-separated component that matches a country
for part in reversed(g.raw["display_name"].split(",")):
part = part.strip()
try:
country = pycountry.countries.lookup(part).name
return country
except LookupError:
pass
return ""
def build_choropleth(percent_by_iso):
iso, vals = zip(*percent_by_iso.items())
fig = px.choropleth(
locations=list(iso),
locationmode="ISO-3",
color=list(vals),
color_continuous_scale="Greens",
range_color=(0, max(vals) if vals else 1),
)
fig.update_layout(
coloraxis_colorbar=dict(
title="% stargazers",
orientation="h", # <-- échelle horizontale
x=0.5, # <-- centré
y=0, # <-- tout en bas
xanchor="center",
yanchor="bottom",
thickness=15,
len=0.7, # <-- longueur de l'échelle, ajustable
),
margin=dict(l=0, r=0, t=0, b=0),
)
PNG_PATH.parent.mkdir(parents=True, exist_ok=True)
fig.write_image(str(PNG_PATH), scale=2)
def main():
if not REPO or not GITHUB_TOKEN:
sys.exit("REPO and GITHUB_TOKEN env vars are required")
print("Fetching stargazer list…")
users = fetch_stargazer_usernames()
print(f"Total stargazers: {len(users)}")
cache = load_cache()
# Determine which usernames need a lookup
to_lookup = [u for u in users if cache.get(u, "") == ""]
print(f"Need geocode for {len(to_lookup)} users")
for i, login in enumerate(to_lookup, 1):
country = username_to_country(login)
cache[login] = country
print(f"{i}/{len(to_lookup)}: {login:<20} -> {country}")
# Nominatim polite usage
time.sleep(1)
# Ensure all stargazers are in cache (even those with blank location)
for u in users:
cache.setdefault(u, "")
save_cache(cache)
# Build stats
countries = [c for c in cache.values() if c]
counts = Counter(countries)
total = sum(counts.values()) or 1
pct_by_country = {c: v / total for c, v in counts.items()}
# convert to ISO-3 for plotly
pct_by_iso = {}
for c, pct in pct_by_country.items():
try:
iso = pycountry.countries.lookup(c).alpha_3
pct_by_iso[iso] = pct * 100 # plotly wants numeric
except LookupError:
print("Skip unknown country:", c)
print("Rendering PNG map…")
build_choropleth(pct_by_iso)
print(
"Done files saved:",
CSV_PATH.relative_to("."),
PNG_PATH.relative_to("."),
sep="\n",
)
if __name__ == "__main__":
main()