smartpptbuilder / app.py
Knightxshade786's picture
Upload folder using huggingface_hub
0aa7d91 verified
Raw History Blame Contribute Delete
12.7 kB
"""
SmartPPT Builder — FastAPI Backend for HuggingFace Spaces.
This backend receives Python code, runs it in an isolated environment,
and returns the generated .pptx file.
Deploy to: https://huggingface.co/spaces/SmartPPTBuilder/SmartPPTbuilder1
"""
import os
import shutil
import subprocess
import sys
import tempfile
import ast
import hashlib
import json
from pathlib import Path
from typing import Optional
import firebase_admin
from firebase_admin import credentials, auth as firebase_auth
from fastapi import FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
# ── App Setup ────────────────────────────────────────────────────────────
app = FastAPI(
title="SmartPPT Builder API",
description="Generate PowerPoint presentations from Python code",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://smartpptbuilder.web.app",
"https://smartpptbuilder.firebaseapp.com",
"http://localhost:5173", # Dev server
"http://localhost:4173", # Preview server
],
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
)
# ── Firebase Admin Setup ──────────────────────────────────────────────────
# Requires a FIREBASE_SERVICE_ACCOUNT JSON string in Hugging Face Secrets
firebase_cred_json = os.environ.get("FIREBASE_SERVICE_ACCOUNT")
if firebase_cred_json:
try:
cred_dict = json.loads(firebase_cred_json)
cred = credentials.Certificate(cred_dict)
firebase_admin.initialize_app(cred)
except Exception as e:
print(f"Failed to initialize Firebase Admin: {e}")
else:
print("WARNING: FIREBASE_SERVICE_ACCOUNT not set in environment. Token verification will fail.")
# ── Config ───────────────────────────────────────────────────────────────
# Approved packages that can be imported
ALLOWED_PACKAGES = {
"pptx": "python-pptx",
"PIL": "Pillow",
"cv2": "opencv-python-headless",
"sklearn": "scikit-learn",
"skimage": "scikit-image",
"yaml": "pyyaml",
"bs4": "beautifulsoup4",
"lxml": "lxml",
"pandas": "pandas",
"numpy": "numpy",
"matplotlib": "matplotlib",
"seaborn": "seaborn",
"plotly": "plotly",
"requests": "requests",
"openpyxl": "openpyxl",
"docx": "python-docx",
"reportlab": "reportlab",
"xlrd": "xlrd",
"chardet": "chardet",
"tabulate": "tabulate",
"jinja2": "Jinja2",
"dateutil": "python-dateutil",
"pytz": "pytz",
"tqdm": "tqdm",
"colorama": "colorama",
"rich": "rich",
"click": "click",
}
# Dangerous modules/functions that are BLOCKED
BLOCKED_MODULES = {
"os", "sys", "subprocess", "shutil", "socket", "ctypes",
"importlib", "builtins", "code", "codeop", "compile",
"compileall", "py_compile", "zipimport", "pkgutil",
"signal", "multiprocessing", "threading", "_thread",
"webbrowser", "http", "urllib", "xmlrpc", "smtplib",
"ftplib", "telnetlib", "poplib", "imaplib", "nntplib",
"ssl", "tempfile", "glob", "fnmatch", "shlex",
}
BLOCKED_CALLS = {
"exec", "eval", "compile", "open", "__import__",
"globals", "locals", "dir", "vars", "getattr",
"setattr", "delattr", "breakpoint", "exit", "quit",
"input",
}
# Standard library modules that don't need pip install
STDLIB = {
"abc", "argparse", "array", "ast", "asyncio", "base64",
"bisect", "calendar", "cmath", "collections", "colorsys",
"configparser", "contextlib", "copy", "csv", "dataclasses",
"datetime", "decimal", "difflib", "enum", "fcntl",
"filecmp", "fractions", "functools", "gc", "getpass",
"gzip", "hashlib", "heapq", "html", "inspect",
"io", "itertools", "json", "keyword", "linecache",
"locale", "logging", "math", "mimetypes", "numbers",
"operator", "pathlib", "pdb", "pickle", "plistlib",
"pprint", "profile", "queue", "random", "re",
"secrets", "shelve", "sqlite3", "statistics", "string",
"struct", "textwrap", "time", "timeit", "token",
"tokenize", "trace", "traceback", "turtle", "types",
"typing", "unicodedata", "unittest", "urllib", "uuid",
"warnings", "weakref", "xml", "zipfile", "zlib",
}
# ── Models ───────────────────────────────────────────────────────────────
class GenerateRequest(BaseModel):
code: str
# ── Security Scanner ─────────────────────────────────────────────────────
def scan_code(code: str) -> tuple[bool, str, list[str]]:
"""
Scan code for dangerous operations and extract import names.
Returns (is_safe, error_message, list_of_imports).
"""
try:
tree = ast.parse(code)
except SyntaxError as e:
return False, f"Syntax error: {e}", []
imports = set()
for node in ast.walk(tree):
# Check imports
if isinstance(node, ast.Import):
for alias in node.names:
top = alias.name.split(".")[0]
if top in BLOCKED_MODULES:
return False, f"Blocked module: {top}", []
imports.add(top)
elif isinstance(node, ast.ImportFrom):
if node.module:
top = node.module.split(".")[0]
if top in BLOCKED_MODULES:
return False, f"Blocked module: {top}", []
imports.add(top)
# Check function calls
elif isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
if node.func.id in BLOCKED_CALLS:
return False, f"Blocked function: {node.func.id}()", []
elif isinstance(node.func, ast.Attribute):
if node.func.attr in {"system", "popen", "exec", "rmtree", "remove", "unlink"}:
return False, f"Blocked call: .{node.func.attr}()", []
return True, "", list(imports)
def get_pip_packages(imports: list[str]) -> list[str]:
"""Convert import names to pip package names."""
packages = []
for imp in imports:
if imp in STDLIB:
continue
pkg = ALLOWED_PACKAGES.get(imp, imp)
packages.append(pkg)
return list(set(packages))
# ── Environment Management ───────────────────────────────────────────────
ENV_CACHE_DIR = Path("/tmp/smartppt_envs")
ENV_CACHE_DIR.mkdir(parents=True, exist_ok=True)
JOBS_DIR = Path("/tmp/smartppt_jobs")
JOBS_DIR.mkdir(parents=True, exist_ok=True)
def get_or_create_venv(packages: list[str]) -> Path:
"""Get or create a cached virtual environment for the given packages."""
# Hash the package list for caching
pkg_key = hashlib.sha256("|".join(sorted(packages)).encode()).hexdigest()[:16]
venv_dir = ENV_CACHE_DIR / f"env_{pkg_key}"
if venv_dir.exists():
return venv_dir
# Create venv
subprocess.run(
[sys.executable, "-m", "venv", str(venv_dir)],
check=True,
capture_output=True,
timeout=60,
)
# Install packages
if packages:
pip_exe = venv_dir / "bin" / "pip"
if not pip_exe.exists():
pip_exe = venv_dir / "Scripts" / "pip.exe"
subprocess.run(
[str(pip_exe), "install", "--quiet", "--no-warn-script-location"] + packages,
check=True,
capture_output=True,
timeout=300,
)
return venv_dir
# ── Endpoints ────────────────────────────────────────────────────────────
@app.get("/health")
async def health():
return {"status": "ok", "message": "SmartPPT Backend is running"}
@app.get("/")
async def root():
return {
"app": "SmartPPT Builder API",
"version": "1.0.0",
"docs": "/docs",
}
@app.post("/api/generate")
async def generate(
request: GenerateRequest,
authorization: Optional[str] = Header(None),
):
"""Generate a .pptx file from Python code."""
code = request.code.strip()
if not code:
raise HTTPException(status_code=400, detail="No code provided")
# 0. Auth Verification
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
id_token = authorization.split("Bearer ")[1]
try:
# Verify token securely to prevent API abuse
decoded_token = firebase_auth.verify_id_token(id_token)
user_id = decoded_token['uid']
except Exception as e:
raise HTTPException(status_code=401, detail=f"Unauthorized: Invalid token ({str(e)})")
if len(code) > 50000:
raise HTTPException(status_code=400, detail="Code too long (max 50,000 chars)")
# 1. Security scan
is_safe, error, imports = scan_code(code)
if not is_safe:
raise HTTPException(status_code=400, detail=f"Security check failed: {error}")
# 2. Determine packages
packages = get_pip_packages(imports)
# Ensure python-pptx is always available
if "python-pptx" not in packages:
packages.append("python-pptx")
# 3. Get/create cached environment
try:
venv_dir = get_or_create_venv(packages)
except subprocess.TimeoutExpired:
raise HTTPException(status_code=504, detail="Environment setup timed out")
except subprocess.CalledProcessError as e:
raise HTTPException(status_code=500, detail=f"Failed to create environment: {e.stderr}")
# 4. Create job directory
job_dir = JOBS_DIR / hashlib.sha256(code.encode()).hexdigest()[:16]
if job_dir.exists():
shutil.rmtree(job_dir)
job_dir.mkdir(parents=True)
script_path = job_dir / "script.py"
script_path.write_text(code, encoding="utf-8")
# 5. Run the script
python_exe = venv_dir / "bin" / "python"
if not python_exe.exists():
python_exe = venv_dir / "Scripts" / "python.exe"
try:
result = subprocess.run(
[str(python_exe), str(script_path)],
cwd=str(job_dir),
capture_output=True,
text=True,
timeout=120,
)
except subprocess.TimeoutExpired:
raise HTTPException(status_code=504, detail="Script execution timed out (120s limit)")
if result.returncode != 0:
err = result.stderr.strip() or "(no error output)"
raise HTTPException(
status_code=400,
detail=f"Script failed (exit code {result.returncode}):\n{err}",
)
# 6. Find generated .pptx files
pptx_files = list(job_dir.glob("*.pptx"))
if not pptx_files:
raise HTTPException(
status_code=400,
detail="Script ran successfully but no .pptx file was produced. "
"Make sure your code calls prs.save('filename.pptx')",
)
# Return the last created .pptx
pptx_file = sorted(pptx_files, key=lambda p: p.stat().st_mtime)[-1]
return FileResponse(
path=str(pptx_file),
filename=pptx_file.name,
media_type="application/vnd.openxmlformats-officedocument.presentationml.presentation",
)
# ── Cleanup old jobs periodically ────────────────────────────────────────
@app.on_event("startup")
async def cleanup_old_jobs():
"""Clean up job directories older than 1 hour."""
import time
cutoff = time.time() - 3600
if JOBS_DIR.exists():
for job in JOBS_DIR.iterdir():
if job.is_dir() and job.stat().st_mtime < cutoff:
shutil.rmtree(job, ignore_errors=True)