datapass / app.py
waroca's picture
Upload folder using huggingface_hub
4d2d85d verified
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('<p style="text-align:center;color:#98989d;margin:0.5rem 0 1rem 0;font-size:1.25rem;">Your pass to private datasets.</p>')
# 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("""
<div class="about-section">
<!-- Traditional vs DataPass Comparison -->
<div class="comparison-container">
<h2 class="section-heading">Traditional Sharing vs DataPass</h2>
<div class="comparison-grid">
<div class="comparison-card traditional">
<div class="comparison-icon">📥</div>
<h3>Traditional</h3>
<div class="comparison-flow">
<span class="flow-step">Share dataset</span>
<span class="flow-arrow">→</span>
<span class="flow-step">User downloads everything</span>
</div>
<p class="comparison-result bad">❌ You lose control of your data</p>
</div>
<div class="comparison-card datapass">
<div class="comparison-icon">🔐</div>
<h3>DataPass</h3>
<div class="comparison-flow">
<span class="flow-step">User asks question</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Gets only the answer</span>
</div>
<p class="comparison-result good">✅ Data stays private on HF</p>
</div>
</div>
</div>
<!-- How MCP Works -->
<div class="mcp-section">
<h2 class="section-heading">MCP Does the Heavy Lifting</h2>
<p class="section-desc">When a subscriber asks a question, here's what happens behind the scenes:</p>
<div class="mcp-flow">
<div class="mcp-step">
<span class="step-num">1</span>
<span class="step-text">User asks: <em>"What are the top 10 categories?"</em></span>
</div>
<div class="mcp-step">
<span class="step-num">2</span>
<span class="step-text">DataPass validates the access token</span>
</div>
<div class="mcp-step">
<span class="step-num">3</span>
<span class="step-text">LLM converts question to SQL</span>
</div>
<div class="mcp-step">
<span class="step-num">4</span>
<span class="step-text">DuckDB queries parquet files on HF</span>
</div>
<div class="mcp-step">
<span class="step-num">5</span>
<span class="step-text">Only the result is returned to user</span>
</div>
</div>
<p class="mcp-note">The user never sees the SQL, never touches DuckDB, never accesses HF directly.</p>
</div>
<!-- Value Props Table -->
<div class="value-section">
<h2 class="section-heading">Why DataPass?</h2>
<div class="value-grid">
<div class="value-column">
<h3>For Dataset Owners</h3>
<ul class="value-list">
<li>🔒 Keep your data private</li>
<li>⏱️ Grant time-limited access</li>
<li>🚫 Revoke anytime</li>
<li>💰 Monetize with Stripe</li>
</ul>
</div>
<div class="value-column">
<h3>For Subscribers</h3>
<ul class="value-list">
<li>💬 Query with SQL or plain English</li>
<li>⚡ No setup - just use MCP tools</li>
<li>📊 Get answers, not giant files</li>
<li>🔌 Works with Claude, Cursor, etc.</li>
</ul>
</div>
</div>
</div>
<!-- Security Model -->
<div class="security-section">
<h2 class="section-heading">Security Model</h2>
<div class="security-grid">
<div class="security-item">
<span class="security-icon">🗄️</span>
<span>Dataset files never leave Hugging Face</span>
</div>
<div class="security-item">
<span class="security-icon">🎫</span>
<span>DataPass validated on every request</span>
</div>
<div class="security-item">
<span class="security-icon">📉</span>
<span>Results capped - no SELECT * dumps</span>
</div>
</div>
</div>
</div>
<style>
.about-section {
max-width: 900px;
margin: 0 auto;
padding: 1rem 0;
}
.section-heading {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 1rem 0;
color: var(--text-primary);
}
.section-desc {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
/* Comparison */
.comparison-container {
margin-bottom: 3rem;
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 640px) {
.comparison-grid { grid-template-columns: 1fr; }
}
.comparison-card {
padding: 1.5rem;
border-radius: 12px;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
}
.comparison-card h3 {
margin: 0.5rem 0 1rem 0;
font-size: 1.125rem;
}
.comparison-icon {
font-size: 2rem;
}
.comparison-flow {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.flow-step {
background: var(--bg-tertiary);
padding: 0.375rem 0.75rem;
border-radius: 6px;
}
.flow-arrow {
color: var(--text-tertiary);
}
.comparison-result {
margin: 0;
font-weight: 500;
}
.comparison-result.bad { color: #ff6b6b; }
.comparison-result.good { color: #51cf66; }
.comparison-card.datapass {
border-color: #51cf66;
background: rgba(81, 207, 102, 0.05);
}
/* MCP Flow */
.mcp-section {
margin-bottom: 3rem;
}
.mcp-flow {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.mcp-step {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.875rem 1rem;
background: var(--bg-secondary);
border-radius: 8px;
border-left: 3px solid #007AFF;
}
.step-num {
width: 28px;
height: 28px;
background: #007AFF;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.875rem;
flex-shrink: 0;
}
.step-text {
color: var(--text-secondary);
}
.step-text em {
color: var(--text-primary);
font-style: normal;
background: var(--bg-tertiary);
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-family: monospace;
}
.mcp-note {
margin-top: 1rem;
padding: 1rem;
background: rgba(0, 122, 255, 0.1);
border-radius: 8px;
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Value Props */
.value-section {
margin-bottom: 3rem;
}
.value-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 640px) {
.value-grid { grid-template-columns: 1fr; }
}
.value-column {
padding: 1.5rem;
background: var(--bg-secondary);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.value-column h3 {
margin: 0 0 1rem 0;
font-size: 1rem;
color: var(--text-primary);
}
.value-list {
list-style: none;
padding: 0;
margin: 0;
}
.value-list li {
padding: 0.5rem 0;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
}
.value-list li:last-child {
border-bottom: none;
}
/* Security */
.security-section {
margin-bottom: 2rem;
}
.security-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
@media (max-width: 768px) {
.security-grid { grid-template-columns: 1fr; }
}
.security-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: var(--bg-secondary);
border-radius: 8px;
font-size: 0.9rem;
color: var(--text-secondary);
}
.security-icon {
font-size: 1.25rem;
}
</style>
""")
# 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("""
<div class="empty-state">
<div class="empty-state-icon">📦</div>
<div class="empty-state-title">No datasets available</div>
<div class="empty-state-text">Check back soon for new data products.</div>
</div>
""")
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'''
<h3 class="dataset-title">{display_name}</h3>
<p class="dataset-id">{dataset_id}</p>
''')
with gr.Column(scale=1, min_width=100, elem_classes="card-price-col"):
gr.HTML(f'<span class="price-badge {price_class}">{price_text}</span>')
# Description
gr.HTML(f'<p class="dataset-desc">{description}</p>')
# 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('<p class="login-hint">Sign in with Hugging Face to subscribe</p>')
# Subscriptions Tab
with gr.Tab("My Subscriptions", id="subscriptions"):
subscriptions_container = gr.HTML()
# Footer
gr.HTML("""
<div style="text-align: center; padding: 2rem 1rem; margin-top: 2rem;">
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 0.25rem;">
<strong>DataPass</strong> — Your pass to private datasets.
</p>
<p style="font-size: 0.75rem; color: var(--text-tertiary);">
Powered by Hugging Face & MCP
</p>
</div>
""")
# 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 """
<div class="empty-state">
<div class="empty-state-icon">🔐</div>
<div class="empty-state-title">Sign in required</div>
<div class="empty-state-text">Please sign in with your Hugging Face account to view your subscriptions.</div>
</div>
"""
# 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()