likes / app.py
Chris4K's picture
Update app.py
3b76691 verified
Raw
History Blame Contribute Delete
10.2 kB
"""
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()