controlla-statistiche.py confronta stelle, fork, licenza, versione, installazioni e controlli di sicurezza delle schede con i dati attuali di GitHub e skills.sh, senza modificare nulla. CLAUDE.md documenta lo script, le convenzioni delle schede e gli argomenti Ufficio e Second brain. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
171 lines
7.4 KiB
Python
171 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Confronta le statistiche scritte nelle schede con quelle attuali.
|
|
|
|
Sola lettura: non modifica nessuna scheda, mostra solo che cosa è cambiato.
|
|
Per ogni scheda controlla, quando la scheda li riporta:
|
|
- stelle, fork, licenza e ultima versione del repository (GitHub, tramite `gh`)
|
|
- installazioni e controlli di sicurezza (pagina della skill su skills.sh)
|
|
|
|
Uso:
|
|
python3 scripts/controlla-statistiche.py # tutte le schede, solo le differenze
|
|
python3 scripts/controlla-statistiche.py --tutto # mostra anche i valori invariati
|
|
python3 scripts/controlla-statistiche.py --soglia 5 pdf # scarto minimo 5%, solo la scheda pdf
|
|
|
|
Serve `gh` con l'accesso effettuato (`gh auth login`). Uscita 1 se c'è qualcosa da aggiornare.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import urllib.request
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
|
|
SCHEDE = Path(__file__).resolve().parent.parent / "skills"
|
|
AUDITOR = ["Gen Agent Trust Hub", "Socket", "Snyk"]
|
|
MESI = ["gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio",
|
|
"agosto", "settembre", "ottobre", "novembre", "dicembre"]
|
|
|
|
|
|
def num_it(cifre, milioni=None):
|
|
"""'35.200' -> 35200; '3,9' + 'milioni' -> 3900000."""
|
|
if milioni:
|
|
return round(float(cifre.replace(",", ".")) * 1_000_000)
|
|
return int(cifre.replace(".", "").replace(",", ""))
|
|
|
|
|
|
def num_sksh(testo):
|
|
"""'219.7K' -> 219700; '1.4M' -> 1400000; '649' -> 649."""
|
|
mult = {"K": 1_000, "M": 1_000_000}.get(testo[-1].upper(), 1)
|
|
return round(float(testo.rstrip("KkMm")) * mult)
|
|
|
|
|
|
def fmt(n):
|
|
return f"{n:,}".replace(",", ".")
|
|
|
|
|
|
def gh(percorso):
|
|
r = subprocess.run(["gh", "api", percorso], capture_output=True, text=True)
|
|
return json.loads(r.stdout) if r.returncode == 0 else None
|
|
|
|
|
|
def pagina(url):
|
|
try:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
html = urllib.request.urlopen(req, timeout=25).read().decode("utf-8", "replace")
|
|
except Exception:
|
|
return None
|
|
html = re.sub(r"<(script|style).*?</\1>", " ", html, flags=re.S)
|
|
return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", html))
|
|
|
|
|
|
def audit_scheda(testo):
|
|
"""Esiti dei controlli come li dice la scheda: {'Socket': 'pass' | 'warn'}."""
|
|
m = re.search(r"\*\*Controlli di sicurezza su skills\.sh:\*\*(.*)", testo)
|
|
if not m:
|
|
return {}
|
|
riga = re.sub(r"\([^)]*\)", "", m.group(1))
|
|
esiti = {}
|
|
for clausola in re.split(r"[;.]", riga):
|
|
nomi = [a for a in AUDITOR if a in clausola]
|
|
if not nomi:
|
|
continue
|
|
if re.search(r"avviso|segnala|non superat", clausola):
|
|
stato = "warn"
|
|
elif "supera" in clausola:
|
|
stato = "pass"
|
|
else:
|
|
continue
|
|
esiti.update({a: stato for a in nomi})
|
|
return esiti
|
|
|
|
|
|
def confronta(righe, nome, scheda, ora, soglia):
|
|
"""Aggiunge una riga numerica; segnala se lo scarto supera la soglia."""
|
|
scarto = (ora - scheda) / scheda * 100 if scheda else 100.0
|
|
esito = "DA AGGIORNARE" if abs(scarto) >= soglia else "ok"
|
|
righe.append((nome, fmt(scheda), fmt(ora), f"{scarto:+.1f}%", esito))
|
|
|
|
|
|
def controlla(percorso, soglia):
|
|
testo = percorso.read_text(encoding="utf-8")
|
|
righe = []
|
|
repo = re.search(r"\*\*Repository:\*\*\s*\[[^\]]*\]\(https://github\.com/([^/\s)]+/[^/\s)]+)\)", testo)
|
|
riga_repo = re.search(r"\*\*Repository:\*\*(.*)", testo)
|
|
riga_repo = riga_repo.group(1) if riga_repo else ""
|
|
|
|
# GitHub: stelle, fork, licenza, versione
|
|
if repo:
|
|
info = gh(f"repos/{repo.group(1)}")
|
|
if info is None:
|
|
righe.append(("repository", repo.group(1), "non raggiungibile", "", "DA CONTROLLARE"))
|
|
else:
|
|
if m := re.search(r"circa ([\d.,]+)( milioni)? stelle", testo):
|
|
confronta(righe, "stelle", num_it(*m.groups()), info["stargazers_count"], soglia)
|
|
if m := re.search(r"([\d.,]+)( milioni)? fork", testo):
|
|
confronta(righe, "fork", num_it(*m.groups()), info["forks_count"], soglia)
|
|
if m := re.search(r"licenza ([A-Za-z0-9.\-]+)", riga_repo):
|
|
ora = (info.get("license") or {}).get("spdx_id") or "nessuna"
|
|
esito = "ok" if ora.lower() == m.group(1).lower() else "DA AGGIORNARE"
|
|
righe.append(("licenza", m.group(1), ora, "", esito))
|
|
if m := re.search(r"versione v?(\d+(?:\.\d+)+)", riga_repo):
|
|
rel = gh(f"repos/{repo.group(1)}/releases/latest")
|
|
if rel:
|
|
ora = rel["tag_name"].lstrip("v")
|
|
esito = "ok" if ora == m.group(1) else "DA AGGIORNARE"
|
|
righe.append(("versione", m.group(1), ora, "", esito))
|
|
if info.get("archived"):
|
|
righe.append(("repository", "", "archiviato", "", "DA CONTROLLARE"))
|
|
|
|
# skills.sh: installazioni e controlli di sicurezza
|
|
sksh = re.search(r"\*\*Pagina su skills\.sh:\*\*\s*\[[^\]]*\]\((https://skills\.sh/[^)\s]+)\)", testo)
|
|
if sksh:
|
|
pg = pagina(sksh.group(1))
|
|
if pg is None:
|
|
righe.append(("skills.sh", sksh.group(1), "non raggiungibile", "", "DA CONTROLLARE"))
|
|
else:
|
|
m_sc = re.search(r"circa ([\d.,]+)( milioni)? installazioni", testo)
|
|
m_pg = re.search(r"([\d.,]+[KM]?) total installs", pg) or re.search(r"\bInstalls ([\d.,]+[KM]?)(?=\s)", pg)
|
|
if m_sc and m_pg:
|
|
confronta(righe, "installazioni", num_it(*m_sc.groups()), num_sksh(m_pg.group(1)), soglia)
|
|
attesi = audit_scheda(testo)
|
|
visti = {a: ("pass" if s == "Pass" else "warn")
|
|
for a, s in re.findall(r"(Gen Agent Trust Hub|Socket|Snyk) (Pass|Warn|Fail)", pg)}
|
|
for a in AUDITOR:
|
|
if a in attesi and a in visti:
|
|
esito = "ok" if attesi[a] == visti[a] else "DA AGGIORNARE"
|
|
righe.append((f"sicurezza: {a}", attesi[a], visti[a], "", esito))
|
|
return righe
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("schede", nargs="*", help="nomi delle schede da controllare (predefinito: tutte)")
|
|
ap.add_argument("--soglia", type=float, default=10.0, help="scarto %% oltre il quale segnalare (predefinito 10)")
|
|
ap.add_argument("--tutto", action="store_true", help="mostra anche i valori invariati")
|
|
args = ap.parse_args()
|
|
|
|
elenco = [p for p in sorted(SCHEDE.glob("*.md")) if not args.schede or p.stem in args.schede]
|
|
if not elenco:
|
|
sys.exit("nessuna scheda trovata")
|
|
with ThreadPoolExecutor(max_workers=6) as pool:
|
|
risultati = list(pool.map(lambda p: controlla(p, args.soglia), elenco))
|
|
|
|
da_aggiornare = 0
|
|
for percorso, righe in zip(elenco, risultati):
|
|
mostra = [r for r in righe if args.tutto or r[4] != "ok"]
|
|
da_aggiornare += sum(r[4] != "ok" for r in righe)
|
|
if not mostra:
|
|
continue
|
|
rilevazione = re.search(r"Diffusione \(([^)]+)\)", percorso.read_text(encoding="utf-8"))
|
|
print(f"{percorso.stem} (rilevazione: {rilevazione.group(1) if rilevazione else 'non indicata'})")
|
|
for nome, scheda, ora, scarto, esito in mostra:
|
|
print(f" {nome:<24} scheda {scheda:>12} ora {ora:>12} {scarto:>7} {esito}")
|
|
print(f"\n{len(elenco)} schede controllate, {da_aggiornare} voci da aggiornare (soglia {args.soglia:g}%)")
|
|
sys.exit(1 if da_aggiornare else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|