Skip to content

Commit 2bbc3e1

Browse files
Add CodeRunner client package and container management
- Implement auto-container management with Docker integration - Create zero-config CodeRunner client with InstaVM-compatible interface - Add cloud migration support via import alias pattern - Implement comprehensive error handling and logging - Add test suite for core functionality - Update dependencies and add setup.py for package distribution - Support session management, async execution, and health monitoring
1 parent 4ef5f0c commit 2bbc3e1

File tree

8 files changed

+1505
-2
lines changed

8 files changed

+1505
-2
lines changed

__init__.py

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
"""
2+
CodeRunner - Local code execution without API keys or cloud setup
3+
4+
Zero-configuration local code execution with seamless cloud migration.
5+
6+
Basic Usage:
7+
from coderunner import CodeRunner
8+
9+
runner = CodeRunner() # Auto-starts container
10+
result = runner.execute("print('Hello World!')")
11+
print(result['stdout']) # "Hello World!"
12+
13+
Cloud Migration:
14+
from coderunner.cloud import InstaVM as CodeRunner
15+
16+
runner = CodeRunner(api_key="your-key") # Same interface!
17+
result = runner.execute("print('Hello Cloud!')")
18+
"""
19+
20+
import requests
21+
import logging
22+
from typing import Dict, Optional, Any
23+
24+
from .container_manager import ContainerManager
25+
from .core.exceptions import CodeRunnerError, SessionError, ExecutionError
26+
27+
__version__ = "1.0.0"
28+
29+
logger = logging.getLogger(__name__)
30+
31+
32+
class CodeRunner:
33+
"""Local code execution without API keys or cloud setup"""
34+
35+
def __init__(self, auto_start: bool = True, base_url: str = None):
36+
"""
37+
Initialize CodeRunner client
38+
39+
Args:
40+
auto_start: Automatically start container if not running
41+
base_url: Custom REST API base URL (for testing)
42+
"""
43+
self.base_url = base_url or "http://localhost:8223"
44+
self.session_id: Optional[str] = None
45+
self._session = requests.Session()
46+
47+
# Set reasonable timeouts
48+
self._session.timeout = 30
49+
50+
if auto_start:
51+
try:
52+
ContainerManager.ensure_running()
53+
except Exception as e:
54+
raise CodeRunnerError(f"Failed to start CodeRunner: {e}")
55+
56+
def execute(self, code: str, language: str = "python", timeout: int = 30) -> Dict[str, Any]:
57+
"""
58+
Execute code and return results (InstaVM compatible interface)
59+
60+
Args:
61+
code: Code to execute
62+
language: Programming language ("python", "bash", "shell", "javascript")
63+
timeout: Execution timeout in seconds
64+
65+
Returns:
66+
Dictionary with stdout, stderr, execution_time, etc.
67+
68+
Raises:
69+
ExecutionError: If execution fails
70+
CodeRunnerError: If container communication fails
71+
"""
72+
if not self.session_id:
73+
self.session_id = self._create_session()
74+
75+
try:
76+
response = self._session.post(
77+
f"{self.base_url}/execute",
78+
json={
79+
"command": code,
80+
"language": language,
81+
"timeout": timeout,
82+
"session_id": self.session_id
83+
},
84+
timeout=timeout + 10
85+
)
86+
87+
if response.status_code != 200:
88+
error_detail = "Unknown error"
89+
try:
90+
error_data = response.json()
91+
error_detail = error_data.get("detail", error_detail)
92+
except:
93+
error_detail = response.text
94+
95+
raise ExecutionError(f"Execution failed: {error_detail}")
96+
97+
return response.json()
98+
99+
except requests.exceptions.Timeout:
100+
raise ExecutionError(f"Execution timed out after {timeout} seconds")
101+
except requests.exceptions.ConnectionError:
102+
raise CodeRunnerError(
103+
"Cannot connect to CodeRunner. Is the container running? "
104+
"Try CodeRunner().start_container()"
105+
)
106+
except ExecutionError:
107+
raise
108+
except Exception as e:
109+
raise CodeRunnerError(f"Unexpected error: {e}")
110+
111+
def execute_async(self, code: str, language: str = "python", timeout: int = 30) -> str:
112+
"""
113+
Execute code asynchronously, return task_id (InstaVM compatible)
114+
115+
Args:
116+
code: Code to execute
117+
language: Programming language
118+
timeout: Execution timeout in seconds
119+
120+
Returns:
121+
task_id string for checking execution status
122+
123+
Raises:
124+
ExecutionError: If async execution setup fails
125+
"""
126+
if not self.session_id:
127+
self.session_id = self._create_session()
128+
129+
try:
130+
response = self._session.post(
131+
f"{self.base_url}/execute_async",
132+
json={
133+
"command": code,
134+
"language": language,
135+
"timeout": timeout,
136+
"session_id": self.session_id
137+
}
138+
)
139+
140+
if response.status_code != 200:
141+
error_detail = "Unknown error"
142+
try:
143+
error_data = response.json()
144+
error_detail = error_data.get("detail", error_detail)
145+
except:
146+
error_detail = response.text
147+
148+
raise ExecutionError(f"Async execution failed: {error_detail}")
149+
150+
result = response.json()
151+
return result["task_id"]
152+
153+
except requests.exceptions.ConnectionError:
154+
raise CodeRunnerError("Cannot connect to CodeRunner")
155+
except ExecutionError:
156+
raise
157+
except Exception as e:
158+
raise CodeRunnerError(f"Unexpected error: {e}")
159+
160+
def get_task_status(self, task_id: str) -> Dict[str, Any]:
161+
"""
162+
Get status of async task (InstaVM compatible)
163+
164+
Args:
165+
task_id: Task identifier from execute_async
166+
167+
Returns:
168+
Task status dictionary
169+
"""
170+
try:
171+
response = self._session.get(f"{self.base_url}/tasks/{task_id}")
172+
173+
if response.status_code == 404:
174+
raise ExecutionError("Task not found")
175+
elif response.status_code != 200:
176+
raise ExecutionError(f"Failed to get task status: {response.text}")
177+
178+
return response.json()
179+
180+
except requests.exceptions.ConnectionError:
181+
raise CodeRunnerError("Cannot connect to CodeRunner")
182+
except ExecutionError:
183+
raise
184+
except Exception as e:
185+
raise CodeRunnerError(f"Unexpected error: {e}")
186+
187+
def start_session(self) -> str:
188+
"""
189+
Start a new execution session (InstaVM compatible)
190+
191+
Returns:
192+
session_id string
193+
194+
Raises:
195+
SessionError: If session creation fails
196+
"""
197+
self.session_id = self._create_session()
198+
return self.session_id
199+
200+
def close_session(self):
201+
"""Close current session (InstaVM compatible)"""
202+
if self.session_id:
203+
try:
204+
self._session.delete(f"{self.base_url}/sessions/{self.session_id}")
205+
except Exception as e:
206+
logger.warning(f"Error closing session: {e}")
207+
finally:
208+
self.session_id = None
209+
210+
def is_session_active(self) -> bool:
211+
"""
212+
Check if current session is active (InstaVM compatible)
213+
214+
Returns:
215+
True if session is active, False otherwise
216+
"""
217+
if not self.session_id:
218+
return False
219+
220+
try:
221+
response = self._session.get(f"{self.base_url}/sessions/{self.session_id}")
222+
return response.status_code == 200
223+
except:
224+
return False
225+
226+
def list_sessions(self) -> Dict[str, Any]:
227+
"""
228+
List all active sessions
229+
230+
Returns:
231+
Dictionary with session information
232+
"""
233+
try:
234+
response = self._session.get(f"{self.base_url}/sessions")
235+
236+
if response.status_code != 200:
237+
raise CodeRunnerError(f"Failed to list sessions: {response.text}")
238+
239+
return response.json()
240+
241+
except requests.exceptions.ConnectionError:
242+
raise CodeRunnerError("Cannot connect to CodeRunner")
243+
except Exception as e:
244+
raise CodeRunnerError(f"Unexpected error: {e}")
245+
246+
def get_health(self) -> Dict[str, Any]:
247+
"""
248+
Get CodeRunner health status
249+
250+
Returns:
251+
Health status dictionary
252+
"""
253+
try:
254+
response = self._session.get(f"{self.base_url}/health")
255+
256+
if response.status_code != 200:
257+
raise CodeRunnerError(f"Health check failed: {response.text}")
258+
259+
return response.json()
260+
261+
except requests.exceptions.ConnectionError:
262+
raise CodeRunnerError("Cannot connect to CodeRunner")
263+
except Exception as e:
264+
raise CodeRunnerError(f"Unexpected error: {e}")
265+
266+
def get_supported_languages(self) -> Dict[str, Any]:
267+
"""
268+
Get list of supported programming languages
269+
270+
Returns:
271+
Dictionary with supported languages
272+
"""
273+
try:
274+
response = self._session.get(f"{self.base_url}/languages")
275+
276+
if response.status_code != 200:
277+
raise CodeRunnerError(f"Failed to get languages: {response.text}")
278+
279+
return response.json()
280+
281+
except requests.exceptions.ConnectionError:
282+
raise CodeRunnerError("Cannot connect to CodeRunner")
283+
except Exception as e:
284+
raise CodeRunnerError(f"Unexpected error: {e}")
285+
286+
def _create_session(self) -> str:
287+
"""
288+
Internal method to create new session
289+
290+
Returns:
291+
Session ID string
292+
293+
Raises:
294+
SessionError: If session creation fails
295+
"""
296+
try:
297+
response = self._session.post(f"{self.base_url}/sessions")
298+
299+
if response.status_code != 200:
300+
error_detail = "Unknown error"
301+
try:
302+
error_data = response.json()
303+
error_detail = error_data.get("detail", error_detail)
304+
except:
305+
error_detail = response.text
306+
307+
raise SessionError(f"Failed to create session: {error_detail}")
308+
309+
result = response.json()
310+
return result["session_id"]
311+
312+
except requests.exceptions.ConnectionError:
313+
raise CodeRunnerError(
314+
"Cannot connect to CodeRunner. Is the container running? "
315+
"Try CodeRunner().start_container()"
316+
)
317+
except SessionError:
318+
raise
319+
except Exception as e:
320+
raise SessionError(f"Unexpected error creating session: {e}")
321+
322+
# Container management convenience methods
323+
324+
def start_container(self) -> bool:
325+
"""Start CodeRunner container manually"""
326+
return ContainerManager.ensure_running()
327+
328+
def stop_container(self) -> bool:
329+
"""Stop CodeRunner container"""
330+
return ContainerManager.stop_container()
331+
332+
def get_container_status(self) -> Dict[str, Any]:
333+
"""Get container status information"""
334+
return ContainerManager.get_container_status()
335+
336+
# Context manager support (InstaVM compatible)
337+
def __enter__(self):
338+
return self
339+
340+
def __exit__(self, exc_type, exc_val, exc_tb):
341+
self.close_session()
342+
343+
344+
# Import cloud migration support
345+
try:
346+
from . import cloud
347+
InstaVM = cloud.InstaVM
348+
except ImportError:
349+
# Create placeholder for better error messages
350+
class InstaVM:
351+
def __init__(self, *args, **kwargs):
352+
raise ImportError(
353+
"Cloud provider not available. Install with:\n"
354+
" pip install coderunner[cloud]\n\n"
355+
"Or use local execution:\n"
356+
" from coderunner import CodeRunner"
357+
)
358+
359+
__all__ = ["CodeRunner", "InstaVM"]

0 commit comments

Comments
 (0)