Nanny7's picture
Add UI/UX rules documentation endpoint at /ui-rules
6d4f40e
from fastapi import FastAPI, HTTPException, status, UploadFile, File, Path as FastAPIPath
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, Response, HTMLResponse
import logging
from contextlib import asynccontextmanager
from typing import List
from sandbox.executor import SandboxExecutor
from sandbox.session_manager import SessionManager
from sandbox.file_manager import FileManager
from sandbox.container_builder import ContainerBuilder
from sandbox.models import (
ExecutionRequest, ExecutionResponse, SandboxConfig, Language,
CreateSessionRequest, SessionResponse, FileInfo, FileUploadResponse,
ExecuteInSessionRequest, ExecuteFileRequest
)
from sandbox.language_runners import LanguageRunner
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Global instances
executor: SandboxExecutor = None
session_manager: SessionManager = None
file_manager: FileManager = None
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize and cleanup resources"""
global executor, session_manager, file_manager
docker_available = False
try:
# Try to initialize Docker services
logger.info("Checking Docker availability...")
import docker
try:
docker_client = docker.from_env()
docker_client.ping()
docker_available = True
logger.info("Docker is available, initializing full services...")
except Exception as docker_error:
logger.warning(f"Docker is not available: {docker_error}")
logger.warning("Running in limited mode without Docker support")
logger.warning("Session management and file operations will be disabled")
docker_available = False
if docker_available:
# Build/verify devenv image
logger.info("Checking development environment image...")
try:
builder = ContainerBuilder()
if not builder.ensure_devenv_image():
logger.warning("Dev environment image not available, sessions will fail")
except Exception as e:
logger.warning(f"Could not check/build devenv image: {e}")
# Initialize services
logger.info("Initializing sandbox services...")
try:
executor = SandboxExecutor(SandboxConfig())
session_manager = SessionManager(SandboxConfig())
file_manager = FileManager()
logger.info("All services initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize some services: {e}")
logger.warning("Some features may be unavailable")
else:
logger.info("Skipping Docker-dependent service initialization")
yield
except Exception as e:
logger.error(f"Lifespan error: {e}", exc_info=True)
# Don't raise - allow app to start even with limited functionality
yield
finally:
# Cleanup on shutdown
if session_manager:
try:
logger.info("Shutting down session manager...")
session_manager.shutdown()
except:
pass
app = FastAPI(
title="isolated-sandbox",
description="Execute code in isolated containers with persistent VM-like sessions and file system operations",
version="2.0.0",
lifespan=lifespan
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def root():
"""Root endpoint with API information"""
docker_available = executor is not None and hasattr(executor, 'client') and executor.client is not None
if docker_available:
try:
executor.client.ping()
docker_available = True
except:
docker_available = False
return {
"name": "isolated-sandbox",
"version": "2.0.0",
"status": "running",
"docker_available": docker_available,
"features": {
"stateless_execution": "/execute" if docker_available else "unavailable (Docker required)",
"persistent_sessions": "/sessions" if docker_available else "unavailable (Docker required)",
"file_operations": docker_available,
"multi_language": docker_available
},
"supported_languages": ["python", "javascript", "bash"] if docker_available else [],
"endpoints": {
"execute": "/execute (stateless)" if docker_available else "unavailable",
"sessions": "/sessions (create/list)" if docker_available else "unavailable",
"session_detail": "/sessions/{session_id}" if docker_available else "unavailable",
"files": "/sessions/{session_id}/files" if docker_available else "unavailable",
"execute_in_session": "/sessions/{session_id}/execute" if docker_available else "unavailable",
"languages": "/languages",
"health": "/health",
"docs": "/docs",
"ui-rules": "/ui-rules"
},
"notice": "Docker is not available. Full functionality requires Docker. See /docs for API documentation." if not docker_available else None
}
@app.get("/health")
def health_check():
"""Health check endpoint"""
try:
docker_status = "unavailable"
session_count = 0
if executor and hasattr(executor, 'client') and executor.client:
try:
executor.client.ping()
docker_status = "connected"
# Check session manager
session_count = len(session_manager.sessions) if session_manager else 0
except Exception:
docker_status = "disconnected"
# App is healthy even without Docker
return {
"status": "healthy",
"docker": docker_status,
"active_sessions": session_count,
"features_available": docker_status == "connected"
}
except Exception as e:
logger.error(f"Health check failed: {e}")
# Still return healthy status, just with error info
return {
"status": "healthy",
"docker": "unknown",
"active_sessions": 0,
"features_available": False,
"warning": str(e)
}
@app.get("/languages")
def list_languages():
"""List all supported programming languages"""
return {
"languages": LanguageRunner.get_all_languages()
}
@app.get("/ui-rules", response_class=HTMLResponse)
def ui_rules_docs():
"""UI/UX Design Rules Documentation"""
html_content = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<title>UI/UX Rules - isolated-sandbox</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #ffffff;
--text: #1a1a1a;
--accent: #0066cc;
--border: #e0e0e0;
--code-bg: #f5f5f5;
--must: #c53030;
--should: #d69e2e;
--never: #742a2a;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #e0e0e0;
--accent: #4a9eff;
--border: #333;
--code-bg: #2a2a2a;
--must: #ff6b6b;
--should: #ffd93d;
--never: #ff8787;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: var(--text);
background: var(--bg);
padding: 2rem;
max-width: 1000px;
margin: 0 auto;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
scroll-margin-top: 2rem;
}
h2 {
font-size: 1.75rem;
margin-top: 3rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border);
scroll-margin-top: 2rem;
}
h3 {
font-size: 1.25rem;
margin-top: 2rem;
margin-bottom: 0.75rem;
scroll-margin-top: 2rem;
}
p {
margin-bottom: 1rem;
}
ul, ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
li {
margin-bottom: 0.5rem;
}
code {
background: var(--code-bg);
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9em;
}
pre {
background: var(--code-bg);
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
margin: 1rem 0;
}
pre code {
background: none;
padding: 0;
}
.must {
color: var(--must);
font-weight: 600;
}
.should {
color: var(--should);
font-weight: 600;
}
.never {
color: var(--never);
font-weight: 600;
}
.rule-item {
margin-bottom: 1rem;
padding-left: 1rem;
border-left: 3px solid var(--border);
}
.rule-item .must {
border-left-color: var(--must);
}
.rule-item .should {
border-left-color: var(--should);
}
.rule-item .never {
border-left-color: var(--never);
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover, a:focus {
text-decoration: underline;
outline: 2px solid var(--accent);
outline-offset: 2px;
}
a:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
nav {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
nav a {
margin-right: 1rem;
}
@media (max-width: 768px) {
body {
padding: 1rem;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/docs">API Docs</a>
<a href="/ui-rules">UI Rules</a>
</nav>
<main>
<h1>UI/UX Design Rules</h1>
<p>Concise rules for building accessible, fast, delightful UIs. Use <span class="must">MUST</span>/<span class="should">SHOULD</span>/<span class="never">NEVER</span> to guide decisions.</p>
<h2>Interactions</h2>
<h3>Keyboard</h3>
<ul>
<li class="rule-item"><span class="must">MUST:</span> Full keyboard support per <a href="https://www.w3.org/WAI/ARIA/apg/patterns/" target="_blank" rel="noopener">WAI-ARIA APG</a></li>
<li class="rule-item"><span class="must">MUST:</span> Visible focus rings (<code>:focus-visible</code>; group with <code>:focus-within</code>)</li>
<li class="rule-item"><span class="must">MUST:</span> Manage focus (trap, move, and return) per APG patterns</li>
</ul>
<h3>Targets &amp; Input</h3>
<ul>
<li class="rule-item"><span class="must">MUST:</span> Hit target ≥24px (mobile ≥44px) If visual &lt;24px, expand hit area</li>
<li class="rule-item"><span class="must">MUST:</span> Mobile <code>&lt;input&gt;</code> font-size ≥16px or set:
<pre><code>&lt;meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"&gt;</code></pre>
</li>
<li class="rule-item"><span class="never">NEVER:</span> Disable browser zoom</li>
<li class="rule-item"><span class="must">MUST:</span> <code>touch-action: manipulation</code> to prevent double-tap zoom; set <code>-webkit-tap-highlight-color</code> to match design</li>
</ul>
<h3>Inputs &amp; Forms (Behavior)</h3>
<ul>
<li class="rule-item"><span class="must">MUST:</span> Hydration-safe inputs (no lost focus/value)</li>
<li class="rule-item"><span class="never">NEVER:</span> Block paste in <code>&lt;input&gt;/&lt;textarea&gt;</code></li>
<li class="rule-item"><span class="must">MUST:</span> Loading buttons show spinner and keep original label</li>
<li class="rule-item"><span class="must">MUST:</span> Enter submits focused text input In <code>&lt;textarea&gt;</code>, ⌘/Ctrl+Enter submits; Enter adds newline</li>
<li class="rule-item"><span class="must">MUST:</span> Keep submit enabled until request starts; then disable, show spinner, use idempotency key</li>
<li class="rule-item"><span class="must">MUST:</span> Don't block typing; accept free text and validate after</li>
<li class="rule-item"><span class="must">MUST:</span> Allow submitting incomplete forms to surface validation</li>
<li class="rule-item"><span class="must">MUST:</span> Errors inline next to fields; on submit, focus first error</li>
<li class="rule-item"><span class="must">MUST:</span> <code>autocomplete</code> + meaningful <code>name</code>; correct <code>type</code> and <code>inputmode</code></li>
<li class="rule-item"><span class="should">SHOULD:</span> Disable spellcheck for emails/codes/usernames</li>
<li class="rule-item"><span class="should">SHOULD:</span> Placeholders end with ellipsis and show example pattern (eg, <code>+1 (123) 456-7890</code>, <code>sk-012345…</code>)</li>
<li class="rule-item"><span class="must">MUST:</span> Warn on unsaved changes before navigation</li>
<li class="rule-item"><span class="must">MUST:</span> Compatible with password managers &amp; 2FA; allow pasting one-time codes</li>
<li class="rule-item"><span class="must">MUST:</span> Trim values to handle text expansion trailing spaces</li>
<li class="rule-item"><span class="must">MUST:</span> No dead zones on checkboxes/radios; label+control share one generous hit target</li>
</ul>
<h3>State &amp; Navigation</h3>
<ul>
<li class="rule-item"><span class="must">MUST:</span> URL reflects state (deep-link filters/tabs/pagination/expanded panels) Prefer libs like <a href="https://nuqs.dev" target="_blank" rel="noopener">nuqs</a></li>
<li class="rule-item"><span class="must">MUST:</span> Back/Forward restores scroll</li>
<li class="rule-item"><span class="must">MUST:</span> Links are links—use <code>&lt;a&gt;/&lt;Link&gt;</code> for navigation (support Cmd/Ctrl/middle-click)</li>
</ul>
<h3>Feedback</h3>
<ul>
<li class="rule-item"><span class="should">SHOULD:</span> Optimistic UI; reconcile on response; on failure show error and rollback or offer Undo</li>
<li class="rule-item"><span class="must">MUST:</span> Confirm destructive actions or provide Undo window</li>
<li class="rule-item"><span class="must">MUST:</span> Use polite <code>aria-live</code> for toasts/inline validation</li>
<li class="rule-item"><span class="should">SHOULD:</span> Ellipsis (<code>…</code>) for options that open follow-ups (eg, "Rename…") and loading states (eg, "Loading…", "Saving…", "Generating…")</li>
</ul>
<h3>Touch/Drag/Scroll</h3>
<ul>
<li class="rule-item"><span class="must">MUST:</span> Design forgiving interactions (generous targets, clear affordances; avoid finickiness)</li>
<li class="rule-item"><span class="must">MUST:</span> Delay first tooltip in a group; subsequent peers no delay</li>
<li class="rule-item"><span class="must">MUST:</span> Intentional <code>overscroll-behavior: contain</code> in modals/drawers</li>
<li class="rule-item"><span class="must">MUST:</span> During drag, disable text selection and set <code>inert</code> on dragged element/containers</li>
<li class="rule-item"><span class="must">MUST:</span> No "dead-looking" interactive zones—if it looks clickable, it is</li>
</ul>
<h3>Autofocus</h3>
<ul>
<li class="rule-item"><span class="should">SHOULD:</span> Autofocus on desktop when there's a single primary input; rarely on mobile (to avoid layout shift)</li>
</ul>
<h2>Animation</h2>
<ul>
<li class="rule-item"><span class="must">MUST:</span> Honor <code>prefers-reduced-motion</code> (provide reduced variant)</li>
<li class="rule-item"><span class="should">SHOULD:</span> Prefer CSS &gt; Web Animations API &gt; JS libraries</li>
<li class="rule-item"><span class="must">MUST:</span> Animate compositor-friendly props (<code>transform</code>, <code>opacity</code>); avoid layout/repaint props (<code>top/left/width/height</code>)</li>
<li class="rule-item"><span class="should">SHOULD:</span> Animate only to clarify cause/effect or add deliberate delight</li>
<li class="rule-item"><span class="should">SHOULD:</span> Choose easing to match the change (size/distance/trigger)</li>
<li class="rule-item"><span class="must">MUST:</span> Animations are interruptible and input-driven (avoid autoplay)</li>
<li class="rule-item"><span class="must">MUST:</span> Correct <code>transform-origin</code> (motion starts where it "physically" should)</li>
</ul>
<h2>Layout</h2>
<ul>
<li class="rule-item"><span class="should">SHOULD:</span> Optical alignment; adjust by ±1px when perception beats geometry</li>
<li class="rule-item"><span class="must">MUST:</span> Deliberate alignment to grid/baseline/edges/optical centers—no accidental placement</li>
<li class="rule-item"><span class="should">SHOULD:</span> Balance icon/text lockups (stroke/weight/size/spacing/color)</li>
<li class="rule-item"><span class="must">MUST:</span> Verify mobile, laptop, ultra-wide (simulate ultra-wide at 50% zoom)</li>
<li class="rule-item"><span class="must">MUST:</span> Respect safe areas (use <code>env(safe-area-inset-*)</code>)</li>
<li class="rule-item"><span class="must">MUST:</span> Avoid unwanted scrollbars; fix overflows</li>
</ul>
<h2>Content &amp; Accessibility</h2>
<ul>
<li class="rule-item"><span class="should">SHOULD:</span> Inline help first; tooltips last resort</li>
<li class="rule-item"><span class="must">MUST:</span> Skeletons mirror final content to avoid layout shift</li>
<li class="rule-item"><span class="must">MUST:</span> <code>&lt;title&gt;</code> matches current context</li>
<li class="rule-item"><span class="must">MUST:</span> No dead ends; always offer next step/recovery</li>
<li class="rule-item"><span class="must">MUST:</span> Design empty/sparse/dense/error states</li>
<li class="rule-item"><span class="should">SHOULD:</span> Curly quotes (" "); avoid widows/orphans</li>
<li class="rule-item"><span class="must">MUST:</span> Tabular numbers for comparisons (<code>font-variant-numeric: tabular-nums</code> or a mono like Geist Mono)</li>
<li class="rule-item"><span class="must">MUST:</span> Redundant status cues (not color-only); icons have text labels</li>
<li class="rule-item"><span class="must">MUST:</span> Don't ship the schema—visuals may omit labels but accessible names still exist</li>
<li class="rule-item"><span class="must">MUST:</span> Use the ellipsis character <code>…</code> (not <code>...</code>)</li>
<li class="rule-item"><span class="must">MUST:</span> <code>scroll-margin-top</code> on headings for anchored links; include a "Skip to content" link; hierarchical <code>&lt;h1–h6&gt;</code></li>
<li class="rule-item"><span class="must">MUST:</span> Resilient to user-generated content (short/avg/very long)</li>
<li class="rule-item"><span class="must">MUST:</span> Locale-aware dates/times/numbers/currency</li>
<li class="rule-item"><span class="must">MUST:</span> Accurate names (<code>aria-label</code>), decorative elements <code>aria-hidden</code>, verify in the Accessibility Tree</li>
<li class="rule-item"><span class="must">MUST:</span> Icon-only buttons have descriptive <code>aria-label</code></li>
<li class="rule-item"><span class="must">MUST:</span> Prefer native semantics (<code>button</code>, <code>a</code>, <code>label</code>, <code>table</code>) before ARIA</li>
<li class="rule-item"><span class="should">SHOULD:</span> Right-clicking the nav logo surfaces brand assets</li>
<li class="rule-item"><span class="must">MUST:</span> Use non-breaking spaces to glue terms: <code>10&nbsp;MB</code>, <code>⌘&nbsp;+&nbsp;K</code>, <code>Vercel&nbsp;SDK</code></li>
</ul>
<h2>Performance</h2>
<ul>
<li class="rule-item"><span class="should">SHOULD:</span> Test iOS Low Power Mode and macOS Safari</li>
<li class="rule-item"><span class="must">MUST:</span> Measure reliably (disable extensions that skew runtime)</li>
<li class="rule-item"><span class="must">MUST:</span> Track and minimize re-renders (React DevTools/React Scan)</li>
<li class="rule-item"><span class="must">MUST:</span> Profile with CPU/network throttling</li>
<li class="rule-item"><span class="must">MUST:</span> Batch layout reads/writes; avoid unnecessary reflows/repaints</li>
<li class="rule-item"><span class="must">MUST:</span> Mutations (<code>POST/PATCH/DELETE</code>) target &lt;500 ms</li>
<li class="rule-item"><span class="should">SHOULD:</span> Prefer uncontrolled inputs; make controlled loops cheap (keystroke cost)</li>
<li class="rule-item"><span class="must">MUST:</span> Virtualize large lists (eg, <code>virtua</code>)</li>
<li class="rule-item"><span class="must">MUST:</span> Preload only above-the-fold images; lazy-load the rest</li>
<li class="rule-item"><span class="must">MUST:</span> Prevent CLS from images (explicit dimensions or reserved space)</li>
</ul>
<h2>Design</h2>
<ul>
<li class="rule-item"><span class="should">SHOULD:</span> Layered shadows (ambient + direct)</li>
<li class="rule-item"><span class="should">SHOULD:</span> Crisp edges via semi-transparent borders + shadows</li>
<li class="rule-item"><span class="should">SHOULD:</span> Nested radii: child ≤ parent; concentric</li>
<li class="rule-item"><span class="should">SHOULD:</span> Hue consistency: tint borders/shadows/text toward bg hue</li>
<li class="rule-item"><span class="must">MUST:</span> Accessible charts (color-blind-friendly palettes)</li>
<li class="rule-item"><span class="must">MUST:</span> Meet contrast—prefer <a href="https://apcacontrast.com/" target="_blank" rel="noopener">APCA</a> over WCAG 2</li>
<li class="rule-item"><span class="must">MUST:</span> Increase contrast on <code>:hover/:active/:focus</code></li>
<li class="rule-item"><span class="should">SHOULD:</span> Match browser UI to bg</li>
<li class="rule-item"><span class="should">SHOULD:</span> Avoid gradient banding (use masks when needed)</li>
</ul>
</main>
</body>
</html>"""
return html_content
# ========== Stateless Execution (backward compatible) ==========
@app.post("/execute", response_model=ExecutionResponse)
def execute_code(request: ExecutionRequest):
"""
Execute code in an isolated ephemeral container (stateless).
This is the original execution method - creates a fresh container,
executes code, and destroys the container immediately.
Note: Requires Docker to be available. On Hugging Face Spaces, Docker-in-Docker
is not supported, so this endpoint will not work.
"""
if not executor:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. Code execution requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
try:
logger.info(f"Stateless execution: {request.language} code")
result = executor.execute(request)
return result
except Exception as e:
logger.error(f"Execution failed: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Execution failed: {str(e)}"
)
# ========== Session Management ==========
@app.post("/sessions", response_model=SessionResponse, status_code=status.HTTP_201_CREATED)
def create_session(request: CreateSessionRequest):
"""
Create a new persistent VM-like session.
The session is a long-running container with persistent storage,
supporting file uploads and multiple code executions.
"""
if not session_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
try:
logger.info(f"Creating new session with metadata: {request.metadata}")
session = session_manager.create_session(request)
return session
except RuntimeError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@app.get("/sessions", response_model=List[SessionResponse])
def list_sessions():
"""List all active sessions"""
if not session_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
sessions = session_manager.list_sessions()
return sessions
@app.get("/sessions/{session_id}", response_model=SessionResponse)
def get_session(session_id: str = FastAPIPath(..., description="Session ID")):
"""Get session details by ID"""
if not session_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
session = session_manager.get_session(session_id)
if not session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session {session_id} not found"
)
# Update file count
if file_manager:
try:
files = file_manager.list_files(session.container_id)
session.files_count = len(files)
except:
pass
return session
@app.delete("/sessions/{session_id}")
def destroy_session(session_id: str = FastAPIPath(..., description="Session ID")):
"""Destroy a session and cleanup all resources"""
if not session_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
success = session_manager.destroy_session(session_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session {session_id} not found"
)
return {"message": f"Session {session_id} destroyed successfully"}
# ========== File Operations ==========
@app.post("/sessions/{session_id}/files", response_model=FileUploadResponse)
async def upload_file(
session_id: str = FastAPIPath(..., description="Session ID"),
file: UploadFile = File(..., description="File to upload")
):
"""Upload a file to session workspace"""
if not session_manager or not file_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. File operations require Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
# Get session
session = session_manager.get_session(session_id)
if not session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session {session_id} not found"
)
try:
# Read file data
file_data = await file.read()
# Upload to container
result = file_manager.upload_file(
container_id=session.container_id,
filename=file.filename,
file_data=file_data
)
# Update session activity
session_manager.update_activity(session_id)
return result
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error(f"File upload failed: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"File upload failed: {str(e)}"
)
@app.get("/sessions/{session_id}/files", response_model=List[FileInfo])
def list_files(session_id: str = FastAPIPath(..., description="Session ID")):
"""List files in session workspace"""
if not session_manager or not file_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. File operations require Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
# Get session
session = session_manager.get_session(session_id)
if not session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session {session_id} not found"
)
try:
files = file_manager.list_files(session.container_id)
return files
except Exception as e:
logger.error(f"File listing failed: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"File listing failed: {str(e)}"
)
@app.get("/sessions/{session_id}/files/{filepath:path}")
def download_file(
session_id: str = FastAPIPath(..., description="Session ID"),
filepath: str = FastAPIPath(..., description="File path relative to workspace")
):
"""Download a file from session workspace"""
if not session_manager or not file_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. File operations require Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
# Get session
session = session_manager.get_session(session_id)
if not session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session {session_id} not found"
)
try:
file_data = file_manager.download_file(session.container_id, filepath)
# Determine content type
import mimetypes
content_type, _ = mimetypes.guess_type(filepath)
return Response(
content=file_data,
media_type=content_type or "application/octet-stream",
headers={
"Content-Disposition": f"attachment; filename={filepath.split('/')[-1]}"
}
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
except Exception as e:
logger.error(f"File download failed: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"File download failed: {str(e)}"
)
# ========== Execute in Session ==========
@app.post("/sessions/{session_id}/execute", response_model=ExecutionResponse)
def execute_in_session(
session_id: str = FastAPIPath(..., description="Session ID"),
request: ExecuteInSessionRequest = None
):
"""Execute code in an existing session (persistent state)"""
if not session_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
# Get session
session = session_manager.get_session(session_id)
if not session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session {session_id} not found"
)
try:
import time
from docker.errors import DockerException
container = executor.client.containers.get(session.container_id)
runner_config = LanguageRunner.get_runner_config(request.language)
start_time = time.time()
# Execute command in running container
exec_result = container.exec_run(
cmd=runner_config["command"] + [request.code],
workdir=request.working_dir,
demux=True,
stream=False
)
execution_time = time.time() - start_time
# Parse output
stdout = exec_result.output[0].decode('utf-8', errors='replace') if exec_result.output[0] else ""
stderr = exec_result.output[1].decode('utf-8', errors='replace') if exec_result.output[1] else ""
# Update session activity
session_manager.update_activity(session_id)
return ExecutionResponse(
stdout=stdout,
stderr=stderr,
exit_code=exec_result.exit_code,
execution_time=round(execution_time, 3),
error=None if exec_result.exit_code == 0 else "Execution failed"
)
except DockerException as e:
logger.error(f"Docker error: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Execution failed: {str(e)}"
)
@app.post("/sessions/{session_id}/execute-file", response_model=ExecutionResponse)
def execute_file_in_session(
session_id: str = FastAPIPath(..., description="Session ID"),
request: ExecuteFileRequest = None
):
"""Execute an uploaded file in session"""
if not session_manager:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality."
)
# Get session
session = session_manager.get_session(session_id)
if not session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session {session_id} not found"
)
try:
import time
container = executor.client.containers.get(session.container_id)
runner_config = LanguageRunner.get_runner_config(request.language)
# Build command based on language
if request.language == Language.PYTHON:
cmd = ["python", request.filepath] + request.args
elif request.language == Language.JAVASCRIPT:
cmd = ["node", request.filepath] + request.args
elif request.language == Language.BASH:
cmd = ["bash", request.filepath] + request.args
else:
cmd = runner_config["command"] + [request.filepath] + request.args
start_time = time.time()
# Execute file
exec_result = container.exec_run(
cmd=cmd,
workdir="/workspace",
demux=True,
stream=False
)
execution_time = time.time() - start_time
# Parse output
stdout = exec_result.output[0].decode('utf-8', errors='replace') if exec_result.output[0] else ""
stderr = exec_result.output[1].decode('utf-8', errors='replace') if exec_result.output[1] else ""
# Update session activity
session_manager.update_activity(session_id)
return ExecutionResponse(
stdout=stdout,
stderr=stderr,
exit_code=exec_result.exit_code,
execution_time=round(execution_time, 3),
error=None if exec_result.exit_code == 0 else "Execution failed"
)
except Exception as e:
logger.error(f"File execution failed: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"File execution failed: {str(e)}"
)
@app.exception_handler(Exception)
async def global_exception_handler( request, exc):
"""Global exception handler"""
logger.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": "Internal server error",
"detail": str(exc)
}
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app:app",
host="0.0.0.0",
port=7860,
reload=False,
log_level="info"
)