|
|
"""Kaggle integration — push and run merge notebooks on free T4 GPUs.""" |
|
|
|
|
|
import json |
|
|
import os |
|
|
import tempfile |
|
|
import requests |
|
|
from typing import Optional |
|
|
|
|
|
|
|
|
KAGGLE_API_URL = "https://www.kaggle.com/api/v1" |
|
|
|
|
|
|
|
|
def _kaggle_headers(username: str, api_key: str) -> dict: |
|
|
"""Create auth headers for Kaggle API (Basic auth).""" |
|
|
import base64 |
|
|
creds = base64.b64encode(f"{username}:{api_key}".encode()).decode() |
|
|
return { |
|
|
"Authorization": f"Basic {creds}", |
|
|
"Content-Type": "application/json", |
|
|
} |
|
|
|
|
|
|
|
|
def push_and_run_kernel( |
|
|
notebook_json: str, |
|
|
kernel_title: str, |
|
|
kaggle_username: str, |
|
|
kaggle_key: str, |
|
|
enable_gpu: bool = True, |
|
|
enable_internet: bool = True, |
|
|
) -> dict: |
|
|
"""Push a notebook to Kaggle and auto-run it. |
|
|
|
|
|
Args: |
|
|
notebook_json: The notebook content as JSON string |
|
|
kernel_title: Title for the Kaggle kernel |
|
|
kaggle_username: Kaggle username |
|
|
kaggle_key: Kaggle API key |
|
|
enable_gpu: Enable T4 GPU (free tier) |
|
|
enable_internet: Enable internet access (needed for HF downloads) |
|
|
|
|
|
Returns: |
|
|
dict with status, url, and any errors |
|
|
""" |
|
|
if not kaggle_username or not kaggle_key: |
|
|
return { |
|
|
"success": False, |
|
|
"error": ( |
|
|
"**Kaggle credentials required**\n\n" |
|
|
"1. Go to [kaggle.com/settings](https://www.kaggle.com/settings)\n" |
|
|
"2. Scroll to **API** section\n" |
|
|
"3. Click **Create New Token** (downloads `kaggle.json`)\n" |
|
|
"4. Copy your username and key from that file" |
|
|
), |
|
|
} |
|
|
|
|
|
|
|
|
slug = kernel_title.lower().replace(" ", "-") |
|
|
slug = "".join(c for c in slug if c.isalnum() or c == "-")[:50] |
|
|
kernel_slug = f"{kaggle_username}/{slug}" |
|
|
|
|
|
headers = _kaggle_headers(kaggle_username, kaggle_key) |
|
|
|
|
|
|
|
|
|
|
|
push_data = { |
|
|
"id": kernel_slug, |
|
|
"title": kernel_title[:50], |
|
|
"code_file_name": f"{slug}.ipynb", |
|
|
"code_file_content": notebook_json, |
|
|
"language": "python", |
|
|
"kernel_type": "notebook", |
|
|
"is_private": True, |
|
|
"enable_gpu": enable_gpu, |
|
|
"enable_internet": enable_internet, |
|
|
"dataset_sources": [], |
|
|
"competition_sources": [], |
|
|
"kernel_sources": [], |
|
|
"category_ids": [], |
|
|
} |
|
|
|
|
|
try: |
|
|
|
|
|
resp = requests.post( |
|
|
f"{KAGGLE_API_URL}/kernels/push", |
|
|
headers=headers, |
|
|
json=push_data, |
|
|
timeout=30, |
|
|
) |
|
|
|
|
|
if resp.status_code == 200: |
|
|
result = resp.json() |
|
|
kernel_url = f"https://www.kaggle.com/code/{kernel_slug}" |
|
|
return { |
|
|
"success": True, |
|
|
"url": kernel_url, |
|
|
"edit_url": f"{kernel_url}/edit", |
|
|
"message": ( |
|
|
f"**Kernel pushed and running!**\n\n" |
|
|
f"Your merge is now executing on Kaggle's free T4 GPU.\n\n" |
|
|
f"- **View & Edit:** [{kernel_slug}]({kernel_url}/edit)\n" |
|
|
f"- **Status:** [Check output]({kernel_url})\n\n" |
|
|
f"The kernel will run automatically. Check back in ~15-30 min for 7B models.\n\n" |
|
|
f"*Tip: Kaggle gives you 30 hours/week of free GPU time.*" |
|
|
), |
|
|
"ref": result.get("ref", ""), |
|
|
"version": result.get("versionNumber", 1), |
|
|
} |
|
|
|
|
|
elif resp.status_code == 401: |
|
|
return { |
|
|
"success": False, |
|
|
"error": "Invalid Kaggle credentials. Check your username and API key.", |
|
|
} |
|
|
elif resp.status_code == 403: |
|
|
return { |
|
|
"success": False, |
|
|
"error": "Kaggle API access forbidden. Make sure your API token has kernel permissions.", |
|
|
} |
|
|
else: |
|
|
error_detail = "" |
|
|
try: |
|
|
error_detail = resp.json().get("message", resp.text[:200]) |
|
|
except Exception: |
|
|
error_detail = resp.text[:200] |
|
|
return { |
|
|
"success": False, |
|
|
"error": f"Kaggle API error ({resp.status_code}): {error_detail}", |
|
|
} |
|
|
|
|
|
except requests.exceptions.Timeout: |
|
|
return {"success": False, "error": "Request timed out. Try again."} |
|
|
except Exception as e: |
|
|
return {"success": False, "error": f"Error: {str(e)}"} |
|
|
|
|
|
|
|
|
def check_kernel_status( |
|
|
kernel_slug: str, |
|
|
kaggle_username: str, |
|
|
kaggle_key: str, |
|
|
) -> dict: |
|
|
"""Check the execution status of a Kaggle kernel. |
|
|
|
|
|
Args: |
|
|
kernel_slug: Full kernel slug (username/kernel-name) |
|
|
kaggle_username: Kaggle username |
|
|
kaggle_key: Kaggle API key |
|
|
|
|
|
Returns: |
|
|
dict with status info |
|
|
""" |
|
|
headers = _kaggle_headers(kaggle_username, kaggle_key) |
|
|
|
|
|
try: |
|
|
resp = requests.get( |
|
|
f"{KAGGLE_API_URL}/kernels/status", |
|
|
headers=headers, |
|
|
params={"userName": kernel_slug.split("/")[0], "kernelSlug": kernel_slug.split("/")[1]}, |
|
|
timeout=15, |
|
|
) |
|
|
|
|
|
if resp.status_code == 200: |
|
|
data = resp.json() |
|
|
status = data.get("status", "unknown") |
|
|
|
|
|
status_emoji = { |
|
|
"queued": "⏳", |
|
|
"running": "🔄", |
|
|
"complete": "✅", |
|
|
"error": "❌", |
|
|
"cancelAcknowledged": "🚫", |
|
|
}.get(status, "❓") |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"status": status, |
|
|
"display": f"{status_emoji} **{status.upper()}**", |
|
|
"failure_message": data.get("failureMessage", ""), |
|
|
} |
|
|
else: |
|
|
return {"success": False, "error": f"API error: {resp.status_code}"} |
|
|
|
|
|
except Exception as e: |
|
|
return {"success": False, "error": str(e)} |
|
|
|
|
|
|
|
|
def generate_kaggle_notebook( |
|
|
merge_notebook: dict, |
|
|
hf_token_secret: bool = True, |
|
|
) -> str: |
|
|
"""Adapt a merge notebook for Kaggle execution. |
|
|
|
|
|
Modifies the notebook to: |
|
|
- Use Kaggle's GPU environment |
|
|
- Reference HF token from Kaggle secrets (if enabled) |
|
|
- Add Kaggle-specific output handling |
|
|
|
|
|
Args: |
|
|
merge_notebook: The notebook dict from notebook_generator |
|
|
hf_token_secret: If True, use Kaggle Secrets for HF token |
|
|
|
|
|
Returns: |
|
|
Notebook as JSON string |
|
|
""" |
|
|
nb = json.loads(json.dumps(merge_notebook)) |
|
|
|
|
|
|
|
|
kaggle_setup = { |
|
|
"cell_type": "code", |
|
|
"metadata": {}, |
|
|
"source": [ |
|
|
"# Kaggle Environment Setup\n", |
|
|
"import os\n", |
|
|
"\n", |
|
|
"# Use Kaggle Secrets for HF token (add in Kaggle Settings > Secrets)\n", |
|
|
"from kaggle_secrets import UserSecretsClient\n", |
|
|
"try:\n", |
|
|
" secrets = UserSecretsClient()\n", |
|
|
" hf_token = secrets.get_secret('HF_TOKEN')\n", |
|
|
" os.environ['HF_TOKEN'] = hf_token\n", |
|
|
" os.environ['HUGGING_FACE_HUB_TOKEN'] = hf_token\n", |
|
|
" print('✅ HF Token loaded from Kaggle Secrets')\n", |
|
|
"except Exception:\n", |
|
|
" print('⚠️ No HF_TOKEN secret found. Add it in Settings > Secrets if needed.')\n", |
|
|
"\n", |
|
|
"# Verify GPU\n", |
|
|
"import torch\n", |
|
|
"if torch.cuda.is_available():\n", |
|
|
" print(f'✅ GPU: {torch.cuda.get_device_name(0)}')\n", |
|
|
" print(f' VRAM: {torch.cuda.get_device_properties(0).total_mem / 1024**3:.1f} GB')\n", |
|
|
"else:\n", |
|
|
" print('⚠️ No GPU detected. Enable GPU in kernel settings.')\n", |
|
|
], |
|
|
"outputs": [], |
|
|
"execution_count": None, |
|
|
} |
|
|
|
|
|
|
|
|
if len(nb["cells"]) > 0: |
|
|
nb["cells"].insert(1, kaggle_setup) |
|
|
|
|
|
|
|
|
for i, cell in enumerate(nb["cells"]): |
|
|
if cell["cell_type"] == "code": |
|
|
source = "".join(cell["source"]) if isinstance(cell["source"], list) else cell["source"] |
|
|
if "notebook_login" in source: |
|
|
nb["cells"][i]["source"] = [ |
|
|
"# HF Authentication (using Kaggle Secrets)\n", |
|
|
"from huggingface_hub import login\n", |
|
|
"import os\n", |
|
|
"\n", |
|
|
"hf_token = os.environ.get('HF_TOKEN', '')\n", |
|
|
"if hf_token:\n", |
|
|
" login(token=hf_token)\n", |
|
|
" print('✅ Logged in to HuggingFace Hub')\n", |
|
|
"else:\n", |
|
|
" print('⚠️ No HF token. Add HF_TOKEN to Kaggle Secrets for gated models.')\n", |
|
|
] |
|
|
|
|
|
|
|
|
nb["metadata"]["kaggle"] = { |
|
|
"accelerator": "gpu", |
|
|
"dataSources": [], |
|
|
"isGpuEnabled": True, |
|
|
"isInternetEnabled": True, |
|
|
} |
|
|
|
|
|
return json.dumps(nb, indent=2, ensure_ascii=False) |