Sanyam400 commited on
Commit
f80d25b
·
verified ·
1 Parent(s): a24df4b

Create sandbox.py

Browse files
Files changed (1) hide show
  1. app/sandbox.py +312 -0
app/sandbox.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Real Python Sandbox
3
+ ====================
4
+ Executes arbitrary Python code with:
5
+ - Auto pip-install on ImportError (retries up to 3 times)
6
+ - Persistent package directory across requests
7
+ - Captures stdout, stderr, files, images, audio
8
+ - Returns everything as structured output
9
+ """
10
+ import os, sys, re, json, subprocess, tempfile, shutil, base64, time
11
+ from pathlib import Path
12
+
13
+ # Persistent dirs
14
+ HOME = Path(os.environ.get("HOME", "/home/user"))
15
+ PKG_DIR = HOME / ".praison_pkgs"
16
+ WORK_DIR = HOME / ".praison_work"
17
+ PKG_DIR.mkdir(parents=True, exist_ok=True)
18
+ WORK_DIR.mkdir(parents=True, exist_ok=True)
19
+
20
+ # Track installed packages this session
21
+ _installed: set = set()
22
+
23
+ # Common aliases: import name -> pip package name
24
+ _PIP_ALIASES = {
25
+ "cv2": "opencv-python-headless",
26
+ "PIL": "Pillow",
27
+ "bs4": "beautifulsoup4",
28
+ "sklearn": "scikit-learn",
29
+ "yaml": "pyyaml",
30
+ "dotenv": "python-dotenv",
31
+ "duckduckgo_search": "duckduckgo-search",
32
+ "telegram": "pyTelegramBotAPI",
33
+ "googlesearch": "googlesearch-python",
34
+ "forex_python": "forex-python",
35
+ "dateutil": "python-dateutil",
36
+ "Crypto": "pycryptodome",
37
+ "nacl": "PyNaCl",
38
+ "gi": "PyGObject",
39
+ "wx": "wxPython",
40
+ "usb": "pyusb",
41
+ "serial": "pyserial",
42
+ "pynput": "pynput",
43
+ "pyttsx3": "pyttsx3",
44
+ "speech_recognition":"SpeechRecognition",
45
+ "wikipedia": "wikipedia",
46
+ "tweepy": "tweepy",
47
+ "instaloader": "instaloader",
48
+ "yt_dlp": "yt-dlp",
49
+ "pytube": "pytube",
50
+ "moviepy": "moviepy",
51
+ "pdf2image": "pdf2image",
52
+ "docx": "python-docx",
53
+ "pptx": "python-pptx",
54
+ "xlrd": "xlrd",
55
+ "openpyxl": "openpyxl",
56
+ "qrcode": "qrcode",
57
+ "barcode": "python-barcode",
58
+ "cryptography": "cryptography",
59
+ "paramiko": "paramiko",
60
+ "ftplib": "ftplib",
61
+ "imaplib": "imaplib",
62
+ "smtplib": "smtplib",
63
+ "win32api": "pywin32",
64
+ "psutil": "psutil",
65
+ "GPUtil": "GPUtil",
66
+ "platform": "platform",
67
+ "distro": "distro",
68
+ "netifaces": "netifaces",
69
+ "scapy": "scapy",
70
+ "nmap": "python-nmap",
71
+ "shodan": "shodan",
72
+ "boto3": "boto3",
73
+ "google.cloud": "google-cloud",
74
+ "azure": "azure",
75
+ "openai": "openai",
76
+ "anthropic": "anthropic",
77
+ "langchain": "langchain",
78
+ "transformers": "transformers",
79
+ "torch": "torch",
80
+ "tensorflow": "tensorflow",
81
+ "keras": "keras",
82
+ "sklearn": "scikit-learn",
83
+ "xgboost": "xgboost",
84
+ "lightgbm": "lightgbm",
85
+ "catboost": "catboost",
86
+ "prophet": "prophet",
87
+ "statsmodels": "statsmodels",
88
+ "scipy": "scipy",
89
+ "sympy": "sympy",
90
+ "networkx": "networkx",
91
+ "igraph": "python-igraph",
92
+ "nltk": "nltk",
93
+ "spacy": "spacy",
94
+ "textblob": "textblob",
95
+ "gensim": "gensim",
96
+ "flair": "flair",
97
+ "sumy": "sumy",
98
+ "rake_nltk": "rake-nltk",
99
+ "wordcloud": "wordcloud",
100
+ "folium": "folium",
101
+ "plotly": "plotly",
102
+ "bokeh": "bokeh",
103
+ "altair": "altair",
104
+ "seaborn": "seaborn",
105
+ "matplotlib": "matplotlib",
106
+ "pandas": "pandas",
107
+ "numpy": "numpy",
108
+ "arrow": "arrow",
109
+ "pendulum": "pendulum",
110
+ "pytz": "pytz",
111
+ "babel": "Babel",
112
+ "pydantic": "pydantic",
113
+ "aiohttp": "aiohttp",
114
+ "httpx": "httpx",
115
+ "requests": "requests",
116
+ "flask": "Flask",
117
+ "fastapi": "fastapi",
118
+ "celery": "celery",
119
+ "redis": "redis",
120
+ "pymongo": "pymongo",
121
+ "sqlalchemy": "SQLAlchemy",
122
+ "psycopg2": "psycopg2-binary",
123
+ "pymysql": "PyMySQL",
124
+ "sqlite3": "sqlite3",
125
+ "gtts": "gTTS",
126
+ "playsound": "playsound",
127
+ "pydub": "pydub",
128
+ "librosa": "librosa",
129
+ "soundfile": "soundfile",
130
+ "pyaudio": "PyAudio",
131
+ "yfinance": "yfinance",
132
+ "alpha_vantage": "alpha-vantage",
133
+ "fredapi": "fredapi",
134
+ "quandl": "quandl",
135
+ "ccxt": "ccxt",
136
+ "ta": "ta",
137
+ "backtrader": "backtrader",
138
+ "zipline": "zipline-reloaded",
139
+ }
140
+
141
+
142
+ def _pip_name(import_name: str) -> str:
143
+ return _PIP_ALIASES.get(import_name, import_name)
144
+
145
+
146
+ def pip_install(packages: list) -> tuple[bool, str]:
147
+ """Actually install packages into PKG_DIR."""
148
+ to_install = []
149
+ for p in packages:
150
+ pip_p = _pip_name(p.strip())
151
+ norm = pip_p.lower().replace("-","_")
152
+ if norm not in _installed:
153
+ to_install.append(pip_p)
154
+ if not to_install:
155
+ return True, "Already installed"
156
+ cmd = [sys.executable, "-m", "pip", "install", "--quiet",
157
+ "--target", str(PKG_DIR), "--upgrade"] + to_install
158
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
159
+ if r.returncode == 0:
160
+ for p in to_install:
161
+ _installed.add(p.lower().replace("-","_"))
162
+ return True, f"Installed: {', '.join(to_install)}"
163
+ return False, r.stderr[-600:]
164
+
165
+
166
+ def _extract_missing_module(stderr: str) -> str | None:
167
+ """Extract missing module name from ImportError traceback."""
168
+ patterns = [
169
+ r"No module named '([^']+)'",
170
+ r"ModuleNotFoundError: No module named '([^']+)'",
171
+ r"ImportError: cannot import name .+ from '([^']+)'",
172
+ r"ImportError: No module named ([^\s]+)",
173
+ ]
174
+ for pat in patterns:
175
+ m = re.search(pat, stderr)
176
+ if m:
177
+ # Get root package (e.g. "google.cloud.storage" -> "google")
178
+ return m.group(1).split(".")[0]
179
+ return None
180
+
181
+
182
+ def _make_runner(code: str, work_dir: str) -> str:
183
+ """Wrap code so it runs with PKG_DIR in path and captures structured output."""
184
+ return f'''
185
+ import sys, os, json, base64, traceback, io, contextlib
186
+ sys.path.insert(0, {repr(str(PKG_DIR))})
187
+ os.chdir({repr(work_dir)})
188
+
189
+ _output_files = []
190
+ _stdout_buf = io.StringIO()
191
+ _stderr_buf = io.StringIO()
192
+
193
+ with contextlib.redirect_stdout(_stdout_buf), contextlib.redirect_stderr(_stderr_buf):
194
+ try:
195
+ exec(compile({repr(code)}, "<praison_tool>", "exec"), {{"__name__": "__main__"}})
196
+ _success = True
197
+ _error = ""
198
+ except SystemExit:
199
+ _success = True
200
+ _error = ""
201
+ except Exception as _e:
202
+ _success = False
203
+ _error = traceback.format_exc()
204
+
205
+ _stdout = _stdout_buf.getvalue()
206
+ _stderr = _stderr_buf.getvalue()
207
+ if _error:
208
+ _stderr += "\\n" + _error
209
+
210
+ # Collect any files written to work dir
211
+ _files = []
212
+ for _f in os.listdir({repr(work_dir)}):
213
+ _fp = os.path.join({repr(work_dir)}, _f)
214
+ if os.path.isfile(_fp):
215
+ try:
216
+ _size = os.path.getsize(_fp)
217
+ if _size < 10_000_000: # 10MB limit
218
+ with open(_fp, "rb") as _fh:
219
+ _b64 = base64.b64encode(_fh.read()).decode()
220
+ _ext = _f.rsplit(".", 1)[-1].lower() if "." in _f else ""
221
+ _files.append({{"name": _f, "size": _size, "b64": _b64, "ext": _ext}})
222
+ except Exception:
223
+ pass
224
+
225
+ print(json.dumps({{
226
+ "ok": _success,
227
+ "stdout": _stdout[:8000],
228
+ "stderr": _stderr[:3000],
229
+ "files": _files,
230
+ }}))
231
+ '''
232
+
233
+
234
+ def run(code: str, max_retries: int = 3, timeout: int = 60) -> dict:
235
+ """
236
+ Execute Python code. Auto-installs missing modules and retries.
237
+ Returns:
238
+ ok: bool
239
+ stdout: str
240
+ stderr: str
241
+ files: list of {name, size, b64, ext}
242
+ installs: list of installed packages
243
+ attempts: int
244
+ """
245
+ installs = []
246
+ work_dir = tempfile.mkdtemp(dir=WORK_DIR)
247
+
248
+ try:
249
+ for attempt in range(max_retries):
250
+ script = _make_runner(code, work_dir)
251
+ tmp_path = tempfile.NamedTemporaryFile(
252
+ mode="w", suffix=".py", delete=False, encoding="utf-8"
253
+ )
254
+ tmp_path.write(script)
255
+ tmp_path.close()
256
+
257
+ env = os.environ.copy()
258
+ env["PYTHONPATH"] = str(PKG_DIR) + os.pathsep + env.get("PYTHONPATH","")
259
+
260
+ try:
261
+ proc = subprocess.run(
262
+ [sys.executable, tmp_path.name],
263
+ capture_output=True, text=True,
264
+ timeout=timeout, env=env
265
+ )
266
+ except subprocess.TimeoutExpired:
267
+ return {"ok":False,"stdout":"","stderr":f"Timed out after {timeout}s",
268
+ "files":[],"installs":installs,"attempts":attempt+1}
269
+ finally:
270
+ try: os.unlink(tmp_path.name)
271
+ except: pass
272
+
273
+ raw = proc.stdout.strip()
274
+ stderr_raw = proc.stderr.strip()
275
+
276
+ # Parse output
277
+ if raw:
278
+ try:
279
+ last_line = [l for l in raw.split("\n") if l.strip()][-1]
280
+ result = json.loads(last_line)
281
+ result["installs"] = installs
282
+ result["attempts"] = attempt + 1
283
+ result["stderr"] = result.get("stderr","") + ("\n"+stderr_raw if stderr_raw else "")
284
+ # Check if we need to install something
285
+ missing = _extract_missing_module(result.get("stderr",""))
286
+ if missing and not result["ok"] and attempt < max_retries - 1:
287
+ ok, msg = pip_install([missing])
288
+ installs.append({"package": missing, "ok": ok, "msg": msg})
289
+ continue
290
+ return result
291
+ except (json.JSONDecodeError, IndexError):
292
+ pass
293
+
294
+ # No JSON output — raw stdout
295
+ combined_err = proc.stderr + "\n" + raw
296
+ missing = _extract_missing_module(combined_err)
297
+ if missing and attempt < max_retries - 1:
298
+ ok, msg = pip_install([missing])
299
+ installs.append({"package": missing, "ok": ok, "msg": msg})
300
+ continue
301
+
302
+ return {"ok": proc.returncode == 0,
303
+ "stdout": raw[:6000],
304
+ "stderr": combined_err[:2000],
305
+ "files": [],
306
+ "installs": installs,
307
+ "attempts": attempt + 1}
308
+
309
+ return {"ok":False,"stdout":"","stderr":"Max retries reached",
310
+ "files":[],"installs":installs,"attempts":max_retries}
311
+ finally:
312
+ shutil.rmtree(work_dir, ignore_errors=True)