| """ |
| 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<details><summary>failures</summary>\n\n" + \ |
| "\n".join(f"- {f}" for f in failed[:50]) + "\n</details>" |
| 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"<p style='opacity:.7'>{empty}</p>" |
| lis = "".join( |
| f'<li><a href="{u}" target="_blank" rel="noopener">{lbl}</a></li>' |
| for lbl, u in items) |
| return (f"<div style='max-height:420px;overflow:auto'>" |
| f"<ol style='padding-left:1.4em'>{lis}</ol></div>") |
|
|
|
|
| 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() |