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

added all files

Browse files
Files changed (3) hide show
  1. Dockerfile +42 -0
  2. app.py +442 -0
  3. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python runtime as base image
2
+ FROM python:3.9-slim
3
+
4
+ # Set working directory in container
5
+ WORKDIR /app
6
+
7
+ # Set environment variables
8
+ ENV PYTHONUNBUFFERED=1
9
+ ENV PYTHONDONTWRITEBYTECODE=1
10
+ ENV GRADIO_SERVER_NAME=0.0.0.0
11
+ ENV GRADIO_SERVER_PORT=7860
12
+
13
+ # Install system dependencies
14
+ RUN apt-get update && apt-get install -y \
15
+ gcc \
16
+ g++ \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # Copy requirements first for better Docker layer caching
20
+ COPY requirements.txt .
21
+
22
+ # Install Python dependencies
23
+ RUN pip install --no-cache-dir --upgrade pip && \
24
+ pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copy application code
27
+ COPY app.py .
28
+
29
+ # Create non-root user for security
30
+ RUN useradd --create-home --shell /bin/bash app && \
31
+ chown -R app:app /app
32
+ USER app
33
+
34
+ # Expose the port that Gradio will run on
35
+ EXPOSE 7860
36
+
37
+ # Health check
38
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
39
+ CMD curl -f http://localhost:7860/health || exit 1
40
+
41
+ # Run the application
42
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import sys
3
+ import io
4
+ import traceback
5
+ import ast
6
+ 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):
16
+ print(f"Received signal {signum}, shutting down gracefully...")
17
+ sys.exit(0)
18
+
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
+
25
+ def __init__(self):
26
+ self.reset()
27
+
28
+ def reset(self):
29
+ self.state = 'normal'
30
+ self.bracket_stack = []
31
+ self.in_string = False
32
+ self.string_delimiter = None
33
+ self.line_number = 0
34
+ self.errors = []
35
+ self.warnings = []
36
+
37
+ def validate_code(self, code: str) -> Dict[str, Any]:
38
+ """Validate Python code using FSM approach"""
39
+ self.reset()
40
+
41
+ if not code or not code.strip():
42
+ return {
43
+ 'valid': False,
44
+ 'errors': [{'line': 0, 'message': 'Empty code input', 'type': 'input_error'}],
45
+ 'warnings': [],
46
+ 'total_issues': 1
47
+ }
48
+
49
+ lines = code.split('\n')
50
+
51
+ for i, line in enumerate(lines):
52
+ self.line_number = i + 1
53
+ self.validate_line(line)
54
+
55
+ # Check for unclosed brackets at end
56
+ if self.bracket_stack:
57
+ self.errors.append({
58
+ 'line': self.line_number,
59
+ 'type': 'syntax_error',
60
+ 'message': f'Unclosed bracket: {self.bracket_stack[-1]}',
61
+ 'severity': 'error'
62
+ })
63
+
64
+ return {
65
+ 'valid': len(self.errors) == 0,
66
+ 'errors': self.errors,
67
+ 'warnings': self.warnings,
68
+ 'total_issues': len(self.errors) + len(self.warnings)
69
+ }
70
+
71
+ def validate_line(self, line: str):
72
+ """Validate a single line using FSM logic"""
73
+ if not line.strip():
74
+ return
75
+
76
+ # Check indentation
77
+ leading_spaces = len(line) - len(line.lstrip())
78
+ if leading_spaces % 4 != 0 and line.strip():
79
+ self.warnings.append({
80
+ 'line': self.line_number,
81
+ 'type': 'style_warning',
82
+ 'message': 'Inconsistent indentation (PEP 8 recommends 4 spaces)',
83
+ 'severity': 'warning'
84
+ })
85
+
86
+ # Process character by character for brackets and strings
87
+ i = 0
88
+ while i < len(line):
89
+ char = line[i]
90
+
91
+ # Handle string states
92
+ if self.in_string:
93
+ if char == self.string_delimiter and (i == 0 or line[i-1] != '\\'):
94
+ self.in_string = False
95
+ self.string_delimiter = None
96
+ i += 1
97
+ continue
98
+
99
+ # Handle comment detection
100
+ if char == '#':
101
+ break # Rest of line is comment
102
+
103
+ # Handle string detection
104
+ if char in ['"', "'"]:
105
+ # Check for triple quotes
106
+ if i + 2 < len(line) and line[i:i+3] == char * 3:
107
+ self.string_delimiter = char * 3
108
+ i += 3
109
+ else:
110
+ self.string_delimiter = char
111
+ i += 1
112
+ self.in_string = True
113
+ continue
114
+
115
+ # Handle brackets
116
+ if char in '([{':
117
+ self.bracket_stack.append(char)
118
+ elif char in ')]}':
119
+ if not self.bracket_stack:
120
+ self.errors.append({
121
+ 'line': self.line_number,
122
+ 'type': 'syntax_error',
123
+ 'message': f'Unmatched closing bracket: {char}',
124
+ 'severity': 'error'
125
+ })
126
+ else:
127
+ expected = {'(': ')', '[': ']', '{': '}'}
128
+ last_open = self.bracket_stack[-1]
129
+ if expected[last_open] != char:
130
+ self.errors.append({
131
+ 'line': self.line_number,
132
+ 'type': 'syntax_error',
133
+ 'message': f'Mismatched brackets: expected {expected[last_open]}, got {char}',
134
+ 'severity': 'error'
135
+ })
136
+ else:
137
+ self.bracket_stack.pop()
138
+
139
+ i += 1
140
+
141
+ # Check Python syntax patterns
142
+ stripped = line.strip()
143
+ control_keywords = ['def ', 'class ', 'if ', 'elif ', 'for ', 'while ', 'try:', 'except']
144
+
145
+ for keyword in control_keywords:
146
+ if stripped.startswith(keyword):
147
+ if not stripped.endswith(':') and keyword != 'except':
148
+ self.errors.append({
149
+ 'line': self.line_number,
150
+ 'type': 'syntax_error',
151
+ 'message': f'{keyword.strip()} statement must end with colon',
152
+ 'severity': 'error'
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
162
+ self.restricted_modules = {
163
+ 'os', 'sys', 'subprocess', 'socket', 'urllib', 'requests',
164
+ 'shutil', 'glob', 'pickle', 'marshal', 'shelve', 'dbm',
165
+ 'sqlite3', 'threading', 'multiprocessing', 'ctypes', 'importlib',
166
+ 'builtins', '__builtin__', 'imp', 'zipimport'
167
+ }
168
+
169
+ # Create safe builtins
170
+ self.safe_builtins = {
171
+ 'print': print,
172
+ 'len': len,
173
+ 'range': range,
174
+ 'str': str,
175
+ 'int': int,
176
+ 'float': float,
177
+ 'bool': bool,
178
+ 'list': list,
179
+ 'dict': dict,
180
+ 'tuple': tuple,
181
+ 'set': set,
182
+ 'frozenset': frozenset,
183
+ 'abs': abs,
184
+ 'max': max,
185
+ 'min': min,
186
+ 'sum': sum,
187
+ 'sorted': sorted,
188
+ 'reversed': reversed,
189
+ 'enumerate': enumerate,
190
+ 'zip': zip,
191
+ 'map': map,
192
+ 'filter': filter,
193
+ 'all': all,
194
+ 'any': any,
195
+ 'type': type,
196
+ 'isinstance': isinstance,
197
+ 'hasattr': hasattr,
198
+ 'getattr': getattr,
199
+ 'setattr': setattr,
200
+ 'delattr': delattr,
201
+ 'round': round,
202
+ 'pow': pow,
203
+ 'divmod': divmod,
204
+ 'chr': chr,
205
+ 'ord': ord,
206
+ 'hex': hex,
207
+ 'oct': oct,
208
+ 'bin': bin,
209
+ 'format': format,
210
+ 'repr': repr,
211
+ 'ascii': ascii,
212
+ 'iter': iter,
213
+ 'next': next,
214
+ 'slice': slice,
215
+ 'callable': callable,
216
+ 'id': id,
217
+ 'hash': hash,
218
+ 'vars': vars,
219
+ 'dir': dir,
220
+ 'help': help,
221
+ 'Exception': Exception,
222
+ 'ValueError': ValueError,
223
+ 'TypeError': TypeError,
224
+ 'IndexError': IndexError,
225
+ 'KeyError': KeyError,
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
+ }
248
+ }
249
+
250
+ # Pre-execution validation
251
+ fsm = PythonSyntaxFSM()
252
+ validation_result = fsm.validate_code(code)
253
+
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 {
270
+ 'success': result['success'],
271
+ 'output': result['output'],
272
+ 'error': result['error'],
273
+ 'execution_time': round(execution_time, 3),
274
+ 'validation': validation_result
275
+ }
276
+
277
+ def check_restricted_imports(self, code: str) -> Dict[str, Any]:
278
+ """Check for restricted module imports"""
279
+ try:
280
+ tree = ast.parse(code)
281
+ for node in ast.walk(tree):
282
+ if isinstance(node, ast.Import):
283
+ for alias in node.names:
284
+ if alias.name.split('.')[0] in self.restricted_modules:
285
+ return {'allowed': False, 'module': alias.name}
286
+ elif isinstance(node, ast.ImportFrom):
287
+ if node.module and node.module.split('.')[0] in self.restricted_modules:
288
+ return {'allowed': False, 'module': node.module}
289
+ return {'allowed': True, 'module': None}
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}")
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==4.44.0
2
+ fastapi==0.104.1
3
+ uvicorn==0.24.0
4
+ pydantic==2.5.0