""" Hackathon Engagement Helper — a Gradio Space ============================================ For a given Hugging Face org this Space lets you: 1. **Add every Space to a Collection in one pass** — the supported, non-gated way to register support for all of them at once, on your profile, no clicking. (Collections are a real write API; liking is NOT — HF blocks scripted liking to prevent spam, so there is no bulk-like and this tool does not pretend otherwise.) 2. **See which Spaces you haven't liked yet** — as a table or a link list, so you can like the ones you care about yourself in the browser. 3. **List every participant (Space creator)** — resolved from first commits. Read actions need no token (public data). Building a Collection needs a WRITE token, used only for that request, never stored. """ import gradio as gr from huggingface_hub import HfApi, list_liked_repos import requests api = HfApi() DEFAULT_ORG = "build-small-hackathon" _CACHE = {"org": None, "spaces": None, "creators": {}} def get_org_spaces(org: str): if _CACHE["org"] != org or _CACHE["spaces"] is None: _CACHE["org"] = org _CACHE["spaces"] = [s.id for s in api.list_spaces(author=org)] _CACHE["creators"] = {} return _CACHE["spaces"] def space_url(repo_id): return f"https://huggingface.co/spaces/{repo_id}" def user_url(username): return f"https://huggingface.co/{username}" def _token_role(token): try: r = requests.get( "https://huggingface.co/api/whoami-v2", headers={"Authorization": f"Bearer {token}"}, timeout=15, ) j = r.json() return j.get("name"), j.get("auth", {}).get("accessToken", {}).get("role") except Exception: return None, None def build_collection(org, token, title, private, progress=gr.Progress()): org = (org or DEFAULT_ORG).strip().strip("/") token = (token or "").strip() title = (title or f"{org} — all spaces").strip() if not token: return "Paste a **write** token to build a Collection (read tabs need no token)." name, role = _token_role(token) if not name: return "Couldn't validate that token (network error or bad token)." if role != "write": return (f"That token's role is **{role}**, not write. Create a *write* token at " "https://huggingface.co/settings/tokens (a Classic write token is simplest).") try: spaces = get_org_spaces(org) except Exception as e: return f"Error listing spaces: {e}" try: progress(0.02, desc="Creating / finding collection…") coll = api.create_collection( title=title, namespace=name, private=private, exists_ok=True, token=token, ) except Exception as e: return f"Error creating collection: {e}" added, skipped, failed = 0, 0, [] for i, repo_id in enumerate(spaces): progress((i + 1) / len(spaces), desc=f"Adding {i+1}/{len(spaces)}") try: api.add_collection_item( coll.slug, item_id=repo_id, item_type="space", exists_ok=True, token=token, ) added += 1 except Exception as e: msg = str(e).lower() if "already" in msg or "exists" in msg: skipped += 1 else: failed.append(f"{repo_id}: {e}") coll_url = f"https://huggingface.co/collections/{coll.slug}" out = (f"✅ Collection ready: [{title}]({coll_url})\n\n" f"- added/confirmed: **{added}**\n" f"- already there: **{skipped}**\n" f"- failed: **{len(failed)}**") if failed: out += "\n\n
failures\n\n" + \ "\n".join(f"- {f}" for f in failed[:50]) + "\n
" return out def scan_unliked(org, username, progress=gr.Progress()): org = (org or DEFAULT_ORG).strip().strip("/") username = (username or "").strip().strip("/") if not username: return "Enter your username first.", gr.update(value=None), "" try: progress(0.2, desc="Listing spaces…") spaces = get_org_spaces(org) progress(0.7, desc="Fetching your likes…") liked = set(list_liked_repos(username).spaces) except Exception as e: return f"Error: {e}", gr.update(value=None), "" unliked = [s for s in spaces if s not in liked] summary = (f"**{org}** — {len(spaces)} spaces · " f"liked **{len(liked & set(spaces))}** · " f"**{len(unliked)}** left.") rows = [[s, space_url(s)] for s in unliked] links = _link_list([(s, space_url(s)) for s in unliked], "🎉 Nothing left — you've liked them all.") return summary, gr.update(value=rows), links def resolve_creators(org, max_spaces, progress=gr.Progress()): org = (org or DEFAULT_ORG).strip().strip("/") try: spaces = get_org_spaces(org) except Exception as e: return f"Error: {e}", gr.update(value=None), "" todo = [s for s in spaces if s not in _CACHE["creators"]] if max_spaces and max_spaces > 0: todo = todo[: int(max_spaces)] for i, repo_id in enumerate(todo): progress((i + 1) / max(len(todo), 1), desc=f"Resolving {i+1}/{len(todo)}") try: commits = api.list_repo_commits(repo_id, repo_type="space") first = min(commits, key=lambda c: c.created_at) if commits else None _CACHE["creators"][repo_id] = ( first.authors[0] if first and first.authors else None) except Exception: _CACHE["creators"][repo_id] = None unique = sorted({c for c in _CACHE["creators"].values() if c and c.lower() != org.lower()}, key=str.lower) resolved = len(_CACHE["creators"]) summary = (f"Resolved **{resolved}/{len(spaces)}** spaces → " f"**{len(unique)}** creators." + ("" if resolved >= len(spaces) else " _Raise the limit and run again for more._")) rows = [[u, user_url(u)] for u in unique] links = _link_list([(u, user_url(u)) for u in unique], "None resolved yet.") return summary, gr.update(value=rows), links def _link_list(items, empty): if not items: return f"

{empty}

" lis = "".join( f'
  • {lbl}
  • ' for lbl, u in items) return (f"
    " f"
      {lis}
    ") with gr.Blocks(title="Hackathon Engagement Helper", theme=gr.themes.Soft()) as demo: gr.Markdown( "# 🤗 Hackathon Engagement Helper\n" "Register support for a whole org's Spaces **at once** via a Collection, " "and find what's left to like / who to follow.\n\n" "*Heads up: liking and following can't be scripted — HF blocks it to " "prevent spam. A **Collection** is the supported one-pass way to back all " "of them; this tool uses that instead of faking likes.*" ) with gr.Row(): org_in = gr.Textbox(label="Org", value=DEFAULT_ORG, scale=2) user_in = gr.Textbox(label="Your HF username", placeholder="Chris4K", scale=2) with gr.Tab("Add ALL to a Collection (one pass)"): gr.Markdown( "Adds every Space in the org to a Collection on your profile in a " "single run — the supported 'all at once'. Needs a **write** token " "(used only for this; never stored). Don't paste tokens into chats." ) with gr.Row(): token_in = gr.Textbox(label="Write token (hf_...)", type="password", scale=3) title_in = gr.Textbox(label="Collection title", placeholder="build-small-hackathon — all spaces", scale=3) private_in = gr.Checkbox(label="Make collection private", value=False) coll_btn = gr.Button("Build collection from all spaces", variant="primary") coll_out = gr.Markdown() coll_btn.click(build_collection, inputs=[org_in, token_in, title_in, private_in], outputs=[coll_out]) with gr.Tab("Spaces to like (table)"): gr.Markdown("Which Spaces you haven't liked yet. Like them yourself in " "the browser — that part HF intentionally keeps manual.") scan_btn = gr.Button("Scan", variant="primary") scan_summary = gr.Markdown() scan_table = gr.Dataframe(headers=["space", "url"], wrap=True, interactive=False, label="Un-liked spaces") with gr.Accordion("Link list (click to open individually)", open=False): scan_links = gr.HTML() scan_btn.click(scan_unliked, inputs=[org_in, user_in], outputs=[scan_summary, scan_table, scan_links]) with gr.Tab("Creators to follow (table)"): gr.Markdown("One commit-log read per space, so slow for a big org — " "start small; results cache, raise the limit to do more.") with gr.Row(): max_in = gr.Slider(10, 1000, value=50, step=10, label="Max spaces to resolve this run") creators_btn = gr.Button("Resolve creators", variant="primary") creators_summary = gr.Markdown() creators_table = gr.Dataframe(headers=["creator", "profile_url"], wrap=True, interactive=False, label="Unique creators") with gr.Accordion("Link list", open=False): creators_links = gr.HTML() creators_btn.click(resolve_creators, inputs=[org_in, max_in], outputs=[creators_summary, creators_table, creators_links]) gr.Markdown( "---\n*Reads only public APIs (`list_spaces`, `list_liked_repos`, " "`list_repo_commits`). The only write it makes is building a Collection, " "and only with the token you provide. It never likes or follows for you.*" ) if __name__ == "__main__": demo.queue().launch()