feat(governance): implement publication eligibility gates, proportional dynamic redistribution, CI workflow, and 20/20 QA suite
HIFFI QA & Governance Pipeline / governance-tests (push) Successful in 1m43s

This commit is contained in:
Srilekha committed 2026-10-26 12:39:08 +05:30
1 parent 05da5050df
commit 0b36fff7b1
12 files changed
+1325 -806

No files matched your search

+50 -10
View File
@@ -3,11 +3,13 @@ collectors/kworb_spotify_collector.py
Audited public streaming telemetry collector.
Extracts verified Spotify Monthly Listeners and Daily Change (+/-) from Kworb.net tables.
Enforces strict null governance (unlisted artists remain None/NaN).
Includes stylized hip-hop stage name normalization and alias resolution.
"""
import requests
from bs4 import BeautifulSoup
import re
from typing import Dict, Optional, Tuple
from bs4 import BeautifulSoup
import requests
KWORB_LISTENERS_URLS = [
"https://kworb.net/spotify/listeners.html",
@@ -23,11 +25,43 @@ HEADERS = {
class KworbStreamingCollector:
# Curated canonical alias map for stylized hip-hop stage names
STAGE_ALIASES: Dict[str, str] = {
"a$ap rocky": "asap rocky",
"a$ap ferg": "asap ferg",
"a$ap mob": "asap mob",
"joey bada$$": "joey badass",
"6ix9ine": "sixnine",
"curren$y": "currensy",
"ty dolla $ign": "ty dolla sign",
"kool g rap": "kool g rap",
"t-pain": "tpain",
"jay-z": "jay z",
"e-40": "e40",
}
def __init__(self):
# Maps lowercase artist_name -> (monthly_listeners, daily_change)
# Maps normalized artist_name -> (monthly_listeners, daily_change)
self.cached_metrics: Dict[str, Tuple[int, Optional[int]]] = {}
self.is_loaded = False
@classmethod
def normalize_artist_name(cls, name: str) -> str:
"""
Normalizes artist names by resolving currency symbols, stripping
extraneous punctuation, and mapping known stage aliases.
"""
if not name:
return ""
norm = name.strip().lower()
if norm in cls.STAGE_ALIASES:
norm = cls.STAGE_ALIASES[norm]
# Standardize common hip-hop typography substitutions
norm = norm.replace("$", "s").replace("&", "and")
norm = re.sub(r"[^\w\s]", "", norm)
return re.sub(r"\s+", " ", norm).strip()
def load_aggregator_tables(self, max_pages: int = 2):
"""Scrapes and indexes artist listener counts and daily velocity from public tables."""
print(f"[KworbCollector] Indexing public streaming metrics across {max_pages} tables...")
@@ -40,8 +74,9 @@ class KworbStreamingCollector:
for row in rows:
cols = row.find_all("td")
if len(cols) >= 4:
# Col 1: Artist
artist_name = cols[1].get_text(strip=True).lower()
# Col 1: Raw Artist Name
raw_artist = cols[1].get_text(strip=True)
artist_norm = self.normalize_artist_name(raw_artist)
# Col 2: Monthly Listeners
raw_listeners = cols[2].get_text(strip=True).replace(",", "")
@@ -57,7 +92,8 @@ class KworbStreamingCollector:
except ValueError:
daily_change = None
self.cached_metrics[artist_name] = (listeners, daily_change)
if artist_norm:
self.cached_metrics[artist_norm] = (listeners, daily_change)
except Exception as e:
print(f"[KworbCollector] Error reading {url}: {e}")
continue
@@ -66,10 +102,14 @@ class KworbStreamingCollector:
print(f"[KworbCollector] Loaded {len(self.cached_metrics):,} artist telemetry records.")
def get_artist_metrics(self, artist_name: str) -> Tuple[Optional[int], Optional[int]]:
"""Returns (monthly_listeners, daily_change) or (None, None)."""
"""
Returns (monthly_listeners, daily_change) or (None, None).
Searches both the direct normalization and alias substitutions.
"""
if not self.is_loaded:
self.load_aggregator_tables(max_pages=2)
return self.cached_metrics.get(artist_name.strip().lower(), (None, None))
norm_key = self.normalize_artist_name(artist_name)
return self.cached_metrics.get(norm_key, (None, None))
def get_monthly_listeners(self, artist_name: str) -> Optional[int]:
return self.get_artist_metrics(artist_name)[0]
@@ -83,9 +123,9 @@ if __name__ == "__main__":
collector.load_aggregator_tables(max_pages=2)
test_cohort = [
"Drake", "Kendrick Lamar", "SZA", "Future",
"Cardi B", "Russ Millions", "Larry June", "Rylo Rodriguez"
"Cardi B", "A$AP Rocky", "Joey Bada$$", "6ix9ine", "Ty Dolla $ign"
]
print("\nValidating Kworb Stream & Daily Velocity Extraction:")
print("\nValidating Kworb Stream & Daily Velocity Extraction with Aliases:")
print(f"{'Artist':20} | {'Monthly Listeners':>18} | {'Daily +/- Trend':>16}")
print("-" * 60)
for artist in test_cohort: