Srevarshan1502 commited on
Commit
a85d4b0
·
1 Parent(s): 6521e91

added API for run time input

Browse files
Files changed (2) hide show
  1. app.py +230 -120
  2. requirements.txt +0 -1
app.py CHANGED
@@ -1,4 +1,3 @@
1
- import gradio as gr
2
  import sys
3
  import io
4
  import traceback
@@ -7,9 +6,13 @@ import time
7
  import json
8
  import os
9
  from contextlib import redirect_stdout, redirect_stderr
10
- from typing import Dict, Any
11
  import threading
 
12
  import signal
 
 
 
13
 
14
  # Set up proper signal handling for Docker
15
  def signal_handler(signum, frame):
@@ -19,6 +22,83 @@ def signal_handler(signum, frame):
19
  signal.signal(signal.SIGTERM, signal_handler)
20
  signal.signal(signal.SIGINT, signal_handler)
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  class PythonSyntaxFSM:
23
  """Finite State Machine for Python syntax validation"""
24
 
@@ -153,9 +233,10 @@ class PythonSyntaxFSM:
153
  })
154
  break
155
 
 
156
 
157
  class SecurePythonExecutor:
158
- """Secure Python code executor with sandboxing and timeout"""
159
 
160
  def __init__(self, timeout: int = 10):
161
  self.timeout = timeout
@@ -167,6 +248,7 @@ class SecurePythonExecutor:
167
  }
168
 
169
  # Create safe builtins
 
170
  self.safe_builtins = {
171
  'print': print,
172
  'len': len,
@@ -226,22 +308,25 @@ class SecurePythonExecutor:
226
  'AttributeError': AttributeError,
227
  'NameError': NameError,
228
  'ZeroDivisionError': ZeroDivisionError,
 
229
  }
230
 
231
- def execute_code(self, code: str) -> Dict[str, Any]:
232
- """Execute Python code in a secure environment"""
233
  start_time = time.time()
234
 
235
  # Input validation
236
  if not code or not code.strip():
 
 
237
  return {
238
  'success': False,
239
  'output': '',
240
- 'error': 'No code provided',
241
  'execution_time': 0,
242
  'validation': {
243
  'valid': False,
244
- 'errors': [{'message': 'Empty code input', 'line': 0}],
245
  'warnings': [],
246
  'total_issues': 1
247
  }
@@ -254,16 +339,18 @@ class SecurePythonExecutor:
254
  # Check for restricted imports
255
  restricted_check = self.check_restricted_imports(code)
256
  if not restricted_check['allowed']:
 
 
257
  return {
258
  'success': False,
259
  'output': '',
260
- 'error': f"Security violation: Restricted module '{restricted_check['module']}' is not allowed",
261
  'execution_time': 0,
262
  'validation': validation_result
263
  }
264
 
265
  # Execute code with timeout
266
- result = self.run_with_timeout(code)
267
  execution_time = time.time() - start_time
268
 
269
  return {
@@ -290,153 +377,176 @@ class SecurePythonExecutor:
290
  except Exception:
291
  return {'allowed': True, 'module': None} # If parsing fails, let execution handle it
292
 
293
- def run_with_timeout(self, code: str) -> Dict[str, Any]:
294
- """Run code with timeout protection"""
295
- output_buffer = io.StringIO()
296
- error_buffer = io.StringIO()
297
  result = {'success': False, 'output': '', 'error': ''}
298
 
 
 
 
 
 
 
 
 
299
  def target():
 
300
  try:
301
- with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
302
- # Create restricted execution environment
303
- restricted_globals = {
304
- '__builtins__': self.safe_builtins,
305
- '__name__': '__main__',
306
- '__doc__': None,
307
- }
308
-
309
- # Execute the code
310
- exec(code, restricted_globals)
311
- result['success'] = True
312
-
 
 
 
 
 
 
 
 
313
  except Exception as e:
314
- error_msg = f"{type(e).__name__}: {str(e)}"
315
- error_buffer.write(error_msg)
 
 
 
 
 
316
  result['success'] = False
317
-
318
- # Run with timeout
 
 
 
 
 
 
319
  thread = threading.Thread(target=target)
320
- thread.daemon = True
321
  thread.start()
 
 
322
  thread.join(timeout=self.timeout)
323
 
324
  if thread.is_alive():
325
  result['error'] = f"Code execution timed out after {self.timeout} seconds"
326
  result['success'] = False
 
 
 
 
 
327
  else:
328
- result['output'] = output_buffer.getvalue()
329
- if error_buffer.getvalue():
330
- result['error'] = error_buffer.getvalue()
 
331
  result['success'] = False
332
  else:
333
  result['success'] = True
334
-
 
 
 
 
 
 
 
 
335
  return result
336
 
337
 
338
  # Initialize the executor
339
  executor = SecurePythonExecutor(timeout=10)
340
 
341
- def execute_python_code_api(code: str) -> Dict[str, Any]:
342
- """
343
- API function to execute Python code.
344
- This function is directly exposed as an API endpoint.
345
- """
346
- try:
347
- result = executor.execute_code(code)
348
- return result
349
- except Exception as e:
350
- return {
351
- 'success': False,
352
- 'output': '',
353
- 'error': f"Execution engine error: {str(e)}",
354
- 'execution_time': 0,
355
- 'validation': {
356
- 'valid': False,
357
- 'errors': [{'message': str(e), 'line': 0, 'type': 'engine_error'}],
358
- 'warnings': [],
359
- 'total_issues': 1
360
- }
361
- }
362
 
363
- def health_check_api() -> Dict[str, Any]:
364
- """
365
- Health check API endpoint.
366
- """
367
- try:
368
- test_result = execute_python_code_api("print('Health check OK')")
369
- return {
370
- 'status': 'healthy' if test_result['success'] else 'unhealthy',
371
- 'timestamp': time.time(),
372
- 'test_execution_success': test_result['success']
373
- }
374
- except Exception as e:
375
- return {
376
- 'status': 'unhealthy',
377
- 'timestamp': time.time(),
378
- 'error': str(e),
379
- 'test_execution_success': False
380
- }
381
 
382
- # Create the Gradio interface specifically for API exposure
383
- # We use gr.Interface for direct API endpoint mapping, not gr.Blocks for this scenario.
384
- # We also create a separate interface for the health check.
 
 
 
385
 
386
- # Main code execution API
387
- api_code_executor = gr.Interface(
388
- fn=execute_python_code_api,
389
- inputs=[
390
- gr.Code(label="Python Code", language="python", lines=10, value="print('Hello from API!')")
391
- ],
392
- outputs=[
393
- gr.JSON(label="Execution Result")
394
- ],
395
- # To enable direct API calls without relying on implicit UI interactions
396
- api_name="predict",
397
- title="Python Code Executor API (Main)",
398
- description="Submits Python code for secure execution and returns the result."
399
- )
400
 
401
- # Health check API
402
- api_health_check = gr.Interface(
403
- fn=health_check_api,
404
- inputs=[],
405
- outputs=[
406
- gr.JSON(label="Health Status")
407
- ],
408
- # You can give this a different API name if you want a separate endpoint
409
- api_name="health",
410
- title="Service Health Check",
411
- description="Checks the health and basic functionality of the API service."
412
- )
 
 
413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
 
 
415
  if __name__ == "__main__":
416
- print("Starting Python Code Executor API...")
 
417
  print(f"Python version: {sys.version}")
418
  print(f"Working directory: {os.getcwd()}")
419
 
420
- server_name = os.getenv('GRADIO_SERVER_NAME', '0.0.0.0')
421
- server_port = int(os.getenv('GRADIO_SERVER_PORT', 7860))
422
 
423
  print(f"Starting server on {server_name}:{server_port}")
424
 
425
  try:
426
- # Launch the main API interface
427
- # Gradio will automatically create API endpoints for functions defined in gr.Interface
428
- # The /api/predict endpoint will now directly map to execute_python_code_api
429
- api_code_executor.launch(
430
- server_name=server_name,
431
- server_port=server_port,
432
- share=False,
433
- show_error=True,
434
- quiet=False,
435
- # show_api=True is default for Interface, and the API_NAME makes it explicit
436
  )
437
- # Note: If you launch multiple interfaces, they might conflict on the same port
438
- # For simplicity for this API-only case, we'll focus on the primary endpoint.
439
- # If you truly need both, you'd integrate FastAPI more deeply or use separate ports.
440
-
441
  except Exception as e:
442
- print(f"Failed to start server: {e}")
 
 
 
 
1
  import sys
2
  import io
3
  import traceback
 
6
  import json
7
  import os
8
  from contextlib import redirect_stdout, redirect_stderr
9
+ from typing import Dict, Any, Optional
10
  import threading
11
+ import asyncio
12
  import signal
13
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
14
+ from pydantic import BaseModel
15
+ from starlette.responses import HTMLResponse
16
 
17
  # Set up proper signal handling for Docker
18
  def signal_handler(signum, frame):
 
22
  signal.signal(signal.SIGTERM, signal_handler)
23
  signal.signal(signal.SIGINT, signal_handler)
24
 
25
+ # --- Custom IO Redirection for Interactive Execution ---
26
+
27
+ class WebSocketInputOutput:
28
+ """
29
+ Redirects stdin/stdout/stderr to/from a WebSocket connection.
30
+ Handles interactive input by waiting for messages from the client.
31
+ """
32
+ def __init__(self, websocket: WebSocket):
33
+ self.websocket = websocket
34
+ self.input_queue = asyncio.Queue()
35
+ self.output_buffer = io.StringIO()
36
+ self.error_buffer = io.StringIO()
37
+ self.closed = False
38
+
39
+ async def write(self, s: str):
40
+ """Writes to stdout/stderr and sends to WebSocket."""
41
+ self.output_buffer.write(s)
42
+ try:
43
+ # Send output in chunks or lines to prevent large messages
44
+ await self.websocket.send_text(json.dumps({"type": "output", "content": s}))
45
+ except WebSocketDisconnect:
46
+ self.closed = True
47
+ print("WebSocket disconnected during write.")
48
+ except Exception as e:
49
+ print(f"Error sending output over WebSocket: {e}")
50
+ self.closed = True
51
+
52
+ def flush(self):
53
+ """Flushes the buffer (no-op for now, as we send immediately)."""
54
+ pass
55
+
56
+ async def readline(self) -> str:
57
+ """Reads a line from stdin, waiting for input from WebSocket."""
58
+ try:
59
+ await self.websocket.send_text(json.dumps({"type": "input_request"}))
60
+ line = await self.input_queue.get() # Wait for input from client
61
+ return line
62
+ except WebSocketDisconnect:
63
+ self.closed = True
64
+ print("WebSocket disconnected during readline.")
65
+ raise EOFError("Input stream closed due to WebSocket disconnect")
66
+ except Exception as e:
67
+ print(f"Error requesting input over WebSocket: {e}")
68
+ self.closed = True
69
+ raise EOFError(f"Input stream error: {e}")
70
+
71
+ # For compatibility with sys.stdin, which expects a file-like object
72
+ def _get_input_line(self):
73
+ """Synchronous wrapper for readline for use with exec."""
74
+ # This is a bit tricky: exec is synchronous, but WebSocket is async.
75
+ # We need to bridge this. A simple way is to run the async part
76
+ # in the event loop, but it's usually better to refactor the executor
77
+ # to be fully async if input() is truly interactive.
78
+ # For this example, we'll use a blocking call to asyncio.run
79
+ # which is generally discouraged in a running event loop,
80
+ # but works for simple cases or if the exec is in a separate thread.
81
+ # A more robust solution involves a custom Future/Event loop integration.
82
+ try:
83
+ # This will block the current thread until input is available
84
+ return asyncio.run(self.readline())
85
+ except Exception as e:
86
+ print(f"Synchronous input wrapper error: {e}")
87
+ raise
88
+
89
+ def read(self, n=-1):
90
+ """Reads n characters. For simplicity, we'll treat it as readline."""
91
+ return self._get_input_line() # Or implement more complex buffering
92
+
93
+ def __getattr__(self, name):
94
+ """Delegate other attributes if needed, or raise AttributeError."""
95
+ # This is a placeholder; real implementation would be more robust
96
+ # For now, we only need write, flush, and readline.
97
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
98
+
99
+
100
+ # --- Python Syntax Validation FSM (unchanged) ---
101
+
102
  class PythonSyntaxFSM:
103
  """Finite State Machine for Python syntax validation"""
104
 
 
233
  })
234
  break
235
 
236
+ # --- Secure Python Executor (modified for WebSockets) ---
237
 
238
  class SecurePythonExecutor:
239
+ """Secure Python code executor with sandboxing and timeout, now interactive."""
240
 
241
  def __init__(self, timeout: int = 10):
242
  self.timeout = timeout
 
248
  }
249
 
250
  # Create safe builtins
251
+ # IMPORTANT: 'input' is now included and will be redirected!
252
  self.safe_builtins = {
253
  'print': print,
254
  'len': len,
 
308
  'AttributeError': AttributeError,
309
  'NameError': NameError,
310
  'ZeroDivisionError': ZeroDivisionError,
311
+ 'input': input # Now included for interactive use
312
  }
313
 
314
+ async def execute_code(self, code: str, websocket_io: WebSocketInputOutput) -> Dict[str, Any]:
315
+ """Execute Python code in a secure environment with WebSocket IO."""
316
  start_time = time.time()
317
 
318
  # Input validation
319
  if not code or not code.strip():
320
+ error_msg = 'No code provided'
321
+ await websocket_io.write(f"Error: {error_msg}\n")
322
  return {
323
  'success': False,
324
  'output': '',
325
+ 'error': error_msg,
326
  'execution_time': 0,
327
  'validation': {
328
  'valid': False,
329
+ 'errors': [{'message': error_msg, 'line': 0}],
330
  'warnings': [],
331
  'total_issues': 1
332
  }
 
339
  # Check for restricted imports
340
  restricted_check = self.check_restricted_imports(code)
341
  if not restricted_check['allowed']:
342
+ error_msg = f"Security violation: Restricted module '{restricted_check['module']}' is not allowed"
343
+ await websocket_io.write(f"Error: {error_msg}\n")
344
  return {
345
  'success': False,
346
  'output': '',
347
+ 'error': error_msg,
348
  'execution_time': 0,
349
  'validation': validation_result
350
  }
351
 
352
  # Execute code with timeout
353
+ result = await self.run_with_timeout(code, websocket_io) # Pass websocket_io
354
  execution_time = time.time() - start_time
355
 
356
  return {
 
377
  except Exception:
378
  return {'allowed': True, 'module': None} # If parsing fails, let execution handle it
379
 
380
+ async def run_with_timeout(self, code: str, websocket_io: WebSocketInputOutput) -> Dict[str, Any]:
381
+ """Run code with timeout protection, interacting via WebSocketIO."""
 
 
382
  result = {'success': False, 'output': '', 'error': ''}
383
 
384
+ # Store original stdin/stdout/stderr
385
+ original_stdin = sys.stdin
386
+ original_stdout = sys.stdout
387
+ original_stderr = sys.stderr
388
+
389
+ # Use a threading.Event to signal completion from the execution thread
390
+ execution_finished_event = threading.Event()
391
+
392
  def target():
393
+ nonlocal result
394
  try:
395
+ # Redirect sys.stdin, sys.stdout, sys.stderr for this thread
396
+ sys.stdin = websocket_io
397
+ sys.stdout = websocket_io
398
+ sys.stderr = websocket_io
399
+
400
+ # Create restricted execution environment
401
+ restricted_globals = {
402
+ '__builtins__': self.safe_builtins,
403
+ '__name__': '__main__',
404
+ '__doc__': None,
405
+ }
406
+
407
+ # Execute the code
408
+ exec(code, restricted_globals)
409
+ result['success'] = True
410
+
411
+ except EOFError as e:
412
+ # This happens if the WebSocket disconnects during input()
413
+ result['error'] = f"Input stream closed: {str(e)}"
414
+ result['success'] = False
415
  except Exception as e:
416
+ error_msg = f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}"
417
+ # Write error to WebSocket and internal buffer
418
+ try:
419
+ asyncio.run(websocket_io.write(f"Error: {error_msg}\n"))
420
+ except Exception as write_e:
421
+ print(f"Failed to write error to WebSocket: {write_e}")
422
+ websocket_io.error_buffer.write(error_msg)
423
  result['success'] = False
424
+ finally:
425
+ # Restore original stdin/stdout/stderr for the thread pool (if applicable)
426
+ sys.stdin = original_stdin
427
+ sys.stdout = original_stdout
428
+ sys.stderr = original_stderr
429
+ execution_finished_event.set() # Signal completion
430
+
431
+ # Run the execution in a separate thread to allow for timeout
432
  thread = threading.Thread(target=target)
433
+ thread.daemon = True # Allow the program to exit even if thread is running
434
  thread.start()
435
+
436
+ # Wait for the thread to finish or timeout
437
  thread.join(timeout=self.timeout)
438
 
439
  if thread.is_alive():
440
  result['error'] = f"Code execution timed out after {self.timeout} seconds"
441
  result['success'] = False
442
+ # Attempt to send timeout message to client
443
+ try:
444
+ await websocket_io.write(f"Error: {result['error']}\n")
445
+ except Exception:
446
+ pass # Ignore if websocket is already closed
447
  else:
448
+ # Execution finished within timeout
449
+ result['output'] = websocket_io.output_buffer.getvalue()
450
+ if websocket_io.error_buffer.getvalue():
451
+ result['error'] = websocket_io.error_buffer.getvalue()
452
  result['success'] = False
453
  else:
454
  result['success'] = True
455
+
456
+ # Send a final message to the client indicating execution is complete
457
+ try:
458
+ await websocket_io.send_text(json.dumps({"type": "execution_complete", "result": result}))
459
+ except WebSocketDisconnect:
460
+ pass # Ignore if websocket is already closed
461
+ except Exception as e:
462
+ print(f"Error sending execution_complete message: {e}")
463
+
464
  return result
465
 
466
 
467
  # Initialize the executor
468
  executor = SecurePythonExecutor(timeout=10)
469
 
470
+ # Initialize FastAPI app
471
+ app = FastAPI(
472
+ title="Interactive Python Code Executor API",
473
+ description="A secure WebSocket API for executing Python code interactively.",
474
+ version="1.0.0",
475
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
+ # Serve the HTML frontend
478
+ @app.get("/", response_class=HTMLResponse)
479
+ async def get_index():
480
+ """Serves the interactive frontend."""
481
+ # This assumes index.html is in the same directory as app.py
482
+ with open("index.html", "r") as f:
483
+ return HTMLResponse(content=f.read())
 
 
 
 
 
 
 
 
 
 
 
484
 
485
+ # WebSocket endpoint for interactive execution
486
+ @app.websocket("/ws")
487
+ async def websocket_endpoint(websocket: WebSocket):
488
+ await websocket.accept()
489
+ websocket_io = WebSocketInputOutput(websocket)
490
+ print(f"WebSocket connection established: {websocket.client}")
491
 
492
+ try:
493
+ while True:
494
+ message = await websocket.receive_text()
495
+ data = json.loads(message)
 
 
 
 
 
 
 
 
 
 
496
 
497
+ if data["type"] == "code":
498
+ code = data["content"]
499
+ print(f"Received code from {websocket.client}: {code[:50]}...")
500
+ # Execute code asynchronously in the background
501
+ asyncio.create_task(executor.execute_code(code, websocket_io))
502
+ elif data["type"] == "input":
503
+ user_input = data["content"]
504
+ print(f"Received input from {websocket.client}: {user_input.strip()[:50]}...")
505
+ await websocket_io.input_queue.put(user_input + "\n") # Add newline for readline
506
+ elif data["type"] == "ping":
507
+ # Respond to pings to keep connection alive
508
+ await websocket.send_text(json.dumps({"type": "pong"}))
509
+ else:
510
+ await websocket_io.write(f"Unknown message type: {data['type']}\n")
511
 
512
+ except WebSocketDisconnect:
513
+ print(f"WebSocket disconnected: {websocket.client}")
514
+ except json.JSONDecodeError:
515
+ print(f"Received invalid JSON from {websocket.client}: {message}")
516
+ except Exception as e:
517
+ print(f"WebSocket error for {websocket.client}: {e}")
518
+ finally:
519
+ websocket_io.closed = True
520
+ # Clean up any pending input requests if the client disconnects
521
+ while not websocket_io.input_queue.empty():
522
+ try:
523
+ websocket_io.input_queue.get_nowait()
524
+ except asyncio.QueueEmpty:
525
+ pass
526
+ print(f"WebSocket connection closed for {websocket.client}")
527
 
528
+ # This block is for local development using `uvicorn`
529
+ # Hugging Face Spaces will use `uvicorn` directly via the Dockerfile CMD
530
  if __name__ == "__main__":
531
+ import uvicorn
532
+ print("Starting Interactive Python Code Executor API (FastAPI)...")
533
  print(f"Python version: {sys.version}")
534
  print(f"Working directory: {os.getcwd()}")
535
 
536
+ server_name = os.getenv('FASTAPI_SERVER_NAME', '0.0.0.0')
537
+ server_port = int(os.getenv('FASTAPI_SERVER_PORT', 7860))
538
 
539
  print(f"Starting server on {server_name}:{server_port}")
540
 
541
  try:
542
+ uvicorn.run(
543
+ "app:app", # app:app means the `app` object in `app.py`
544
+ host=server_name,
545
+ port=server_port,
546
+ reload=False, # Set to True for local dev to auto-reload on code changes
547
+ log_level="info"
 
 
 
 
548
  )
 
 
 
 
549
  except Exception as e:
550
+ print(f"Failed to start server: {e}")
551
+ sys.exit(1)
552
+
requirements.txt CHANGED
@@ -1,4 +1,3 @@
1
- gradio==4.44.0
2
  fastapi==0.104.1
3
  uvicorn==0.24.0
4
  pydantic==2.5.0
 
 
1
  fastapi==0.104.1
2
  uvicorn==0.24.0
3
  pydantic==2.5.0