from __future__ import annotations import gradio as gr from dotenv import load_dotenv from components import subscriptions import utils import theme import os load_dotenv() # Version marker for cache busting __version__ = "3.2.0" # Debug: Check OAuth environment variables print("=== OAuth Debug Info (Gradio 6.0.1) ===") print(f"OAUTH_CLIENT_ID set: {bool(os.getenv('OAUTH_CLIENT_ID'))}") print(f"OAUTH_CLIENT_SECRET set: {bool(os.getenv('OAUTH_CLIENT_SECRET'))}") print(f"OAUTH_SCOPES: {os.getenv('OAUTH_SCOPES')}") print(f"SPACE_HOST: {os.getenv('SPACE_HOST', 'not set')}") print("========================================") def main(): # Custom CSS for dataset cards custom_css = theme.css + """ /* Hide any share buttons globally */ .share-button, [class*="share"], button[title="Share"], .icon-buttons, .image-button-group { display: none !important; } /* Hero section styling */ .hero-section { justify-content: center !important; padding: 1.5rem 1rem 0.5rem !important; background: transparent !important; } .hero-logo { max-width: 200px !important; background: transparent !important; border: none !important; box-shadow: none !important; margin: 0 auto !important; } .hero-logo img { object-fit: contain !important; } .hero-subtitle { font-size: 1rem !important; color: var(--text-secondary, #98989d) !important; margin: 0 0 1rem 0 !important; font-weight: 400 !important; text-align: center !important; } /* Force center the subtitle container */ .hero-subtitle-wrapper { width: 100% !important; display: flex !important; justify-content: center !important; } .hero-subtitle-wrapper + div, .hero-subtitle-wrapper ~ div { width: 100%; } /* Dataset card styling */ .dataset-card-group { background: var(--bg-card, #1c1c1e) !important; border: 1px solid rgba(255,255,255,0.06) !important; border-radius: 16px !important; padding: 1.25rem 1.5rem !important; margin-bottom: 0.75rem !important; transition: all 0.2s ease !important; } .dataset-card-group:hover { border-color: rgba(255,255,255,0.12) !important; background: rgba(255,255,255,0.02) !important; } /* Card header */ .card-header-row { margin-bottom: 0.5rem !important; gap: 1rem !important; align-items: center !important; } .card-title-col { min-width: 0 !important; } .card-price-col { flex-shrink: 0 !important; min-width: auto !important; } .dataset-title { font-size: 1.125rem !important; font-weight: 600 !important; margin: 0 !important; color: var(--text-primary, #f5f5f7) !important; line-height: 1.4 !important; } .dataset-id { font-size: 0.6875rem !important; color: var(--text-tertiary, #6e6e73) !important; margin: 0.25rem 0 0 0 !important; font-family: 'JetBrains Mono', monospace !important; background: rgba(255,255,255,0.04) !important; padding: 0.15rem 0.4rem !important; border-radius: 4px !important; display: inline-block !important; } .dataset-desc { font-size: 0.875rem !important; color: var(--text-secondary, #98989d) !important; line-height: 1.5 !important; margin: 0 0 1rem 0 !important; } /* Price badges */ .price-badge { display: inline-flex !important; align-items: center !important; justify-content: center !important; padding: 0.4rem 0.9rem !important; border-radius: 20px !important; font-size: 0.8125rem !important; font-weight: 600 !important; white-space: nowrap !important; } .price-badge.paid { background: rgba(255,255,255,0.08) !important; color: var(--text-primary, #f5f5f7) !important; } .price-badge.free { background: rgba(52, 199, 89, 0.15) !important; color: #32d74b !important; } /* Card footer */ .card-footer-row { padding-top: 0.875rem !important; margin-top: 0.25rem !important; border-top: 1px solid rgba(255,255,255,0.06) !important; gap: 0 !important; } .card-footer-row > div { flex: 1 !important; } /* Subscribe buttons */ .subscribe-btn, .subscribe-btn button { width: 100% !important; background: linear-gradient(180deg, #f7d052 0%, #e9b93a 100%) !important; color: #1a1a1a !important; border: none !important; border-radius: 10px !important; padding: 0.75rem 1.5rem !important; font-weight: 600 !important; font-size: 0.9375rem !important; transition: all 0.15s ease !important; cursor: pointer !important; box-shadow: 0 1px 2px rgba(0,0,0,0.1), inset 0 1px 0 rgba(255,255,255,0.2) !important; } .subscribe-btn:hover, .subscribe-btn:hover button { background: linear-gradient(180deg, #fad965 0%, #f0c243 100%) !important; box-shadow: 0 2px 8px rgba(247, 208, 82, 0.3), inset 0 1px 0 rgba(255,255,255,0.25) !important; } .subscribe-btn:active, .subscribe-btn:active button { background: linear-gradient(180deg, #e5b835 0%, #d4a52e 100%) !important; box-shadow: inset 0 1px 2px rgba(0,0,0,0.1) !important; } /* Login hint */ .login-hint { font-size: 0.8125rem !important; color: var(--text-tertiary, #6e6e73) !important; font-style: italic !important; margin: 0 !important; padding: 0.5rem 0 !important; text-align: center !important; } /* Tighten up Gradio's default spacing */ .gradio-container .gr-group { gap: 0 !important; } .gradio-container .gr-block { padding: 0 !important; } """ with gr.Blocks(title="DataPass", theme=theme.get_theme(), css=custom_css) as demo: # State to track logged-in username (triggers re-render when changed) logged_in_user = gr.State(value=None) # Hero Section with logo and subtitle with gr.Row(elem_classes="hero-section"): with gr.Column(): gr.Image( value="datapass_logo.png", show_label=False, show_download_button=False, show_fullscreen_button=False, show_share_button=False, container=False, height=120, elem_classes="hero-logo" ) gr.HTML('

Your pass to private datasets.

') # Simple auth row with Gradio 6 LoginButton with gr.Row(): user_status = gr.Markdown() gr.LoginButton(size="sm") # Status message for subscription actions subscribe_status = gr.Markdown() # Main Tabs with gr.Tabs() as tabs: # How It Works Tab with gr.Tab("How It Works", id="about"): gr.HTML("""

Traditional Sharing vs DataPass

📥

Traditional

Share dataset User downloads everything

❌ You lose control of your data

🔐

DataPass

User asks question Gets only the answer

✅ Data stays private on HF

MCP Does the Heavy Lifting

When a subscriber asks a question, here's what happens behind the scenes:

1 User asks: "What are the top 10 categories?"
2 DataPass validates the access token
3 LLM converts question to SQL
4 DuckDB queries parquet files on HF
5 Only the result is returned to user

The user never sees the SQL, never touches DuckDB, never accesses HF directly.

Why DataPass?

For Dataset Owners

  • 🔒 Keep your data private
  • ⏱️ Grant time-limited access
  • 🚫 Revoke anytime
  • 💰 Monetize with Stripe

For Subscribers

  • 💬 Query with SQL or plain English
  • ⚡ No setup - just use MCP tools
  • 📊 Get answers, not giant files
  • 🔌 Works with Claude, Cursor, etc.

Security Model

🗄️ Dataset files never leave Hugging Face
🎫 DataPass validated on every request
📉 Results capped - no SELECT * dumps
""") # Catalog Tab with gr.Tab("Catalog", id="catalog"): # Use gr.render with State input to re-render when login state changes @gr.render(inputs=logged_in_user) def render_catalog(username): datasets = utils.get_catalog() or [] if not datasets: gr.HTML("""
📦
No datasets available
Check back soon for new data products.
""") return for dataset in datasets: dataset_id = dataset.get('dataset_id', '') display_name = dataset.get('display_name', dataset_id) description = dataset.get('description', 'No description available.') # Get pricing info plans = dataset.get("plans", []) is_free = False price = "10" if plans: plan = plans[0] price_id = plan.get("stripe_price_id", "") if price_id in ["free", "0", 0]: is_free = True else: price = plan.get("price", "10") price_class = "free" if is_free else "paid" price_text = "Free Trial" if is_free else f"${price}/mo" button_text = "Start Free Trial" if is_free else f"Subscribe - ${price}/mo" # Create card using gr.Group with gr.Group(elem_classes="dataset-card-group"): # Card header with title and price with gr.Row(elem_classes="card-header-row"): with gr.Column(scale=4, elem_classes="card-title-col"): gr.HTML(f'''

{display_name}

{dataset_id}

''') with gr.Column(scale=1, min_width=100, elem_classes="card-price-col"): gr.HTML(f'{price_text}') # Description gr.HTML(f'

{description}

') # Footer with action with gr.Row(elem_classes="card-footer-row"): if username: # User is logged in - show subscribe button btn = gr.Button( button_text, variant="primary", size="lg", key=f"subscribe-btn-{dataset_id}", elem_classes="subscribe-btn" ) # Define handler with frozen variables (critical for loops!) def make_subscribe_handler(ds_id, ds_name, ds_free): def handler(p: gr.OAuthProfile | None, t: gr.OAuthToken | None): if not p: return "Please sign in first to subscribe." hf_token = t.token if t else None if ds_free: result = utils.subscribe_free(ds_id, p.username, hf_token) if "error" in result: return f"Error: {result['error']}" return f"Your 24-hour DataPass for **{ds_name}** is active! Go to **My Subscriptions** to get your access token." else: result = utils.create_checkout_session(ds_id, p.username, hf_token) if "error" in result: return f"Error: {result['error']}" if "checkout_url" in result: return f"[Click here to complete payment]({result['checkout_url']})" return "Error creating checkout session." return handler btn.click( fn=make_subscribe_handler(dataset_id, display_name, is_free), outputs=[subscribe_status] ) else: # User not logged in - show hint gr.HTML('

Sign in with Hugging Face to subscribe

') # Subscriptions Tab with gr.Tab("My Subscriptions", id="subscriptions"): subscriptions_container = gr.HTML() # Footer gr.HTML("""

DataPass — Your pass to private datasets.

Powered by Hugging Face & MCP

""") # Load user status and update login state def load_user_status(profile: gr.OAuthProfile | None): if profile: # Return both status text AND username for the State return f"👤 Signed in as **{profile.username}**", profile.username return "Sign in to subscribe to datasets", None # Load subscriptions def load_subscriptions(profile: gr.OAuthProfile | None, token: gr.OAuthToken | None): if not profile: return """
🔐
Sign in required
Please sign in with your Hugging Face account to view your subscriptions.
""" # Pass HF token for authentication hf_token = token.token if token else None user_subs = utils.get_user_subscriptions(profile.username, hf_token) return subscriptions.create_subscriptions_html(user_subs) # Load on page load - update both user_status and logged_in_user state demo.load(fn=load_user_status, outputs=[user_status, logged_in_user]) demo.load(fn=load_subscriptions, outputs=[subscriptions_container]) return demo if __name__ == "__main__": demo = main() demo.launch()