import os from datetime import datetime import random import requests from io import BytesIO from datetime import date import tempfile from PIL import Image, ImageDraw, ImageFont from huggingface_hub import upload_file import pandas as pd from huggingface_hub import HfApi, hf_hub_download, Repository from huggingface_hub.repocard import metadata_load import gradio as gr from datasets import load_dataset, Dataset from huggingface_hub import whoami import asyncio from functools import partial EXAM_DATASET_ID = os.getenv("EXAM_DATASET_ID") or "agents-course/unit_1_quiz" EXAM_MAX_QUESTIONS = os.getenv("EXAM_MAX_QUESTIONS") or 1 EXAM_PASSING_SCORE = os.getenv("EXAM_PASSING_SCORE") or 0.8 CERTIFYING_ORG_LINKEDIN_ID = os.getenv("CERTIFYING_ORG_LINKEDIN_ID", "000000") COURSE_TITLE = os.getenv("COURSE_TITLE", "Fundamentals of MCP") ds = load_dataset(EXAM_DATASET_ID, split="train") DATASET_REPO_URL = "https://huggingface.co/datasets/mcp-course/certificates" # Convert dataset to a list of dicts and randomly sort quiz_data = ds.to_pandas().to_dict("records") random.shuffle(quiz_data) # Limit to max questions if specified if EXAM_MAX_QUESTIONS: quiz_data = quiz_data[: int(EXAM_MAX_QUESTIONS)] def on_user_logged_in(token: gr.OAuthToken | None): """ If the user has a valid token, show Start button. Otherwise, keep the login button visible. """ if token is not None: return [ gr.update(visible=False), # login_btn gr.update(visible=True), # start_btn gr.update(visible=False), # next_btn gr.update(visible=False), # submit_btn "", # question_text gr.update(choices=[], visible=False), # radio_choices "Click 'Start' to begin the quiz", # status_text 0, # question_idx [], # user_answers gr.update(visible=False), # certificate_img gr.update( visible=True, value="""
Pass the quiz to add your certificate to LinkedIn!
"""
return message
async def upload_certificate_to_hub(username: str, certificate_img) -> str:
"""Upload certificate to the dataset hub and return the URL asynchronously."""
# Save image to temporary file
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
certificate_img.save(tmp.name)
try:
# Run upload in a thread pool since upload_file is blocking
loop = asyncio.get_event_loop()
upload_func = partial(
upload_file,
path_or_fileobj=tmp.name,
path_in_repo=f"certificates/{username}/{date.today()}.png",
repo_id="mcp-course/certificates",
repo_type="dataset",
token=os.getenv("HF_TOKEN"),
)
await loop.run_in_executor(None, upload_func)
# Construct the URL to the image
cert_url = (
f"https://huggingface.co/datasets/mcp-course/certificates/"
f"resolve/main/certificates/{username}/{date.today()}.png"
)
# Clean up temp file
os.unlink(tmp.name)
return cert_url
except Exception as e:
print(f"Error uploading certificate: {e}")
os.unlink(tmp.name)
return None
async def push_results_to_hub(
user_answers,
custom_name: str | None,
token: gr.OAuthToken | None,
profile: gr.OAuthProfile | None,
):
"""Handle quiz completion and certificate generation."""
if token is None or profile is None:
gr.Warning("Please log in to Hugging Face before submitting!")
return (
gr.update(visible=True, value="Please login first"),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False), # hide custom name input
)
# Calculate grade
correct_count = sum(1 for answer in user_answers if answer["is_correct"])
total_questions = len(user_answers)
grade = correct_count / total_questions if total_questions > 0 else 0
if grade < float(EXAM_PASSING_SCORE):
return (
gr.update(visible=True, value=f"You scored {grade:.1%}..."),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False), # hide custom name input
)
try:
# Use custom name if provided, otherwise use profile name
name = (
custom_name.strip() if custom_name and custom_name.strip() else profile.name
)
# Generate certificate
certificate_img, _ = generate_certificate(
name=name, profile_url=profile.picture
)
# Start certificate upload asynchronously
gr.Info("Uploading your certificate...")
cert_url = await upload_certificate_to_hub(profile.username, certificate_img)
if cert_url is None:
gr.Warning("Certificate upload failed, but you still passed!")
cert_url = "https://huggingface.co/mcp-course"
# Create LinkedIn button
linkedin_button = create_linkedin_button(profile.username, cert_url)
result_message = f"""
🎉 Congratulations! You passed with a score of {grade:.1%}!
{linkedin_button}
"""
return (
gr.update(visible=True, value=result_message),
gr.update(visible=True, value=certificate_img),
gr.update(visible=True),
gr.update(visible=True), # show custom name input
)
except Exception as e:
print(f"Error generating certificate: {e}")
return (
gr.update(visible=True, value=f"🎉 You passed with {grade:.1%}!"),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False), # hide custom name input
)
def handle_quiz(
question_idx,
user_answers,
selected_answer,
is_start,
token: gr.OAuthToken | None,
profile: gr.OAuthProfile | None,
):
"""Handle quiz state transitions and store answers"""
if token is None or profile is None:
gr.Warning("Please log in to Hugging Face before starting the quiz!")
return [
"", # question_text
gr.update(choices=[], visible=False), # radio choices
"Please login first", # status_text
question_idx, # question_idx
user_answers, # user_answers
gr.update(visible=True), # start button
gr.update(visible=False), # next button
gr.update(visible=False), # submit button
gr.update(visible=False), # certificate image
gr.update(
visible=True,
value="""
Please log in with your Hugging Face account to access the quiz and earn your LinkedIn certificate!
Great! Click "Get your certificate" above to unlock the LinkedIn button.
You need a higher score to earn the LinkedIn certificate. Please retake the quiz!
Complete the quiz and pass to unlock your LinkedIn certificate!
Please log in with your Hugging Face account to access the quiz and earn your LinkedIn certificate!