more-compute 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,458 @@
1
+ import os
2
+ import io
3
+ import sys
4
+ import asyncio
5
+ import subprocess
6
+ import time
7
+ import shlex
8
+ from contextlib import redirect_stdout, redirect_stderr
9
+ from typing import Dict, Any, Optional, Tuple, Union
10
+ from fastapi import WebSocket
11
+
12
+
13
+ # this file is not tested that all functions work, need to write a test file / manually check
14
+ # to-do
15
+
16
+ class AsyncSpecialCommandHandler:
17
+ """Handles all special commands asynchronously with streaming support: shell (!), line magics (%), and cell magics (%%)"""
18
+
19
+ def __init__(self, globals_dict: dict):
20
+ self.globals_dict = globals_dict
21
+ self.captured_outputs = {} # Store captured outputs from %%capture
22
+
23
+ def is_special_command(self, source_code: Union[str, list, tuple]) -> bool:
24
+ """Check if the source code is a special command"""
25
+ text = self._coerce_source_to_text(source_code)
26
+ stripped = text.strip()
27
+ return (stripped.startswith('!') or
28
+ stripped.startswith('%%') or
29
+ stripped.startswith('%'))
30
+
31
+ async def execute_special_command(self, source_code: Union[str, list, tuple], result: Dict[str, Any],
32
+ start_time: float, execution_count: int,
33
+ websocket: Optional[WebSocket] = None,
34
+ cell_index: Optional[int] = None) -> Dict[str, Any]:
35
+ """Execute a special command and return the result"""
36
+ text = self._coerce_source_to_text(source_code)
37
+ stripped = text.strip()
38
+
39
+ if stripped.startswith('!'):
40
+ return await self._execute_shell_command(stripped[1:], result, start_time, websocket, cell_index)
41
+ elif stripped.startswith('%%'):
42
+ return await self._execute_cell_magic(text, result, start_time, execution_count, websocket)
43
+ elif stripped.startswith('%'):
44
+ return await self._execute_line_magic(stripped[1:], result, start_time, websocket)
45
+ else:
46
+ raise ValueError("Not a special command")
47
+
48
+ def _coerce_source_to_text(self, source_code: Union[str, list, tuple]) -> str:
49
+ """Normalize incoming source to a single text string"""
50
+ try:
51
+ if isinstance(source_code, str):
52
+ return source_code
53
+ if isinstance(source_code, (list, tuple)):
54
+ return "".join(source_code)
55
+ return str(source_code)
56
+ except Exception:
57
+ return ""
58
+
59
+ async def _execute_shell_command(self, command: str, result: Dict[str, Any],
60
+ start_time: float, websocket: Optional[WebSocket] = None,
61
+ cell_index: Optional[int] = None) -> Dict[str, Any]:
62
+ """Execute a shell command with real-time streaming output"""
63
+ try:
64
+ # Prepare environment and command for streaming
65
+ env = self._prepare_streaming_environment(command)
66
+ cmd_parts = self._prepare_command_parts(command)
67
+
68
+ # Send execution start notification
69
+ if websocket:
70
+ await websocket.send_json({
71
+ "type": "execution_start",
72
+ "data": {
73
+ "command": f"!{command}",
74
+ **({"cell_index": cell_index} if cell_index is not None else {})
75
+ }
76
+ })
77
+
78
+ # Create subprocess with streaming
79
+ process = await asyncio.create_subprocess_exec(
80
+ *cmd_parts,
81
+ stdout=asyncio.subprocess.PIPE,
82
+ stderr=asyncio.subprocess.PIPE,
83
+ env=env,
84
+ cwd=os.getcwd()
85
+ )
86
+
87
+ # Stream output concurrently
88
+ stdout_task = asyncio.create_task(
89
+ self._stream_output(process.stdout, "stdout", result, websocket, cell_index)
90
+ )
91
+ stderr_task = asyncio.create_task(
92
+ self._stream_output(process.stderr, "stderr", result, websocket, cell_index)
93
+ )
94
+
95
+ # Wait for both streams to complete
96
+ await asyncio.gather(stdout_task, stderr_task)
97
+
98
+ # Wait for process completion
99
+ return_code = await process.wait()
100
+
101
+ # Send completion notification
102
+ if websocket:
103
+ await websocket.send_json({
104
+ "type": "execution_complete",
105
+ "data": {
106
+ "return_code": return_code,
107
+ "status": "error" if return_code != 0 else "ok",
108
+ **({"cell_index": cell_index} if cell_index is not None else {})
109
+ }
110
+ })
111
+
112
+ # If pip install/uninstall occurred, notify clients to refresh packages
113
+ try:
114
+ if websocket and (command.startswith('pip install') or command.startswith('pip uninstall') or 'pip install' in command or 'pip uninstall' in command):
115
+ await websocket.send_json({
116
+ "type": "packages_updated",
117
+ "data": {"action": "pip"}
118
+ })
119
+ except Exception:
120
+ pass
121
+
122
+ # Check if command failed
123
+ if return_code != 0:
124
+ result["status"] = "error"
125
+ result["error"] = {
126
+ "ename": "ShellCommandError",
127
+ "evalue": f"Command failed with return code {return_code}",
128
+ "traceback": [f"Shell command failed: {command}"]
129
+ }
130
+
131
+ except Exception as e:
132
+ result["status"] = "error"
133
+ result["error"] = {
134
+ "ename": type(e).__name__,
135
+ "evalue": str(e),
136
+ "traceback": [f"Shell command error: {str(e)}"]
137
+ }
138
+
139
+ if websocket:
140
+ await websocket.send_json({
141
+ "type": "execution_error",
142
+ "data": {
143
+ "error": result["error"]
144
+ }
145
+ })
146
+
147
+ # Calculate execution time
148
+ result["execution_time"] = f"{(time.time() - start_time) * 1000:.1f}ms"
149
+ return result
150
+
151
+ async def interrupt(self):
152
+ # Placeholder for future process-based interruption logic
153
+ return
154
+
155
+ def _prepare_streaming_environment(self, shell_cmd: str) -> dict:
156
+ """Prepare environment variables for unbuffered output"""
157
+ env = os.environ.copy()
158
+
159
+ # Always set unbuffered Python
160
+ env['PYTHONUNBUFFERED'] = '1'
161
+ env['PYTHONDONTWRITEBYTECODE'] = '1'
162
+
163
+ # Additional settings for specific commands
164
+ if 'pip install' in shell_cmd:
165
+ env['PIP_DISABLE_PIP_VERSION_CHECK'] = '1'
166
+ env['PIP_NO_CACHE_DIR'] = '1'
167
+
168
+ return env
169
+
170
+ def _prepare_command_parts(self, shell_cmd: str) -> list:
171
+ """Convert shell command to subprocess-compatible format"""
172
+
173
+ if shell_cmd.startswith('pip '):
174
+ # Route pip through Python module for better control
175
+ parts = ['python', '-m'] + shlex.split(shell_cmd)
176
+ # Add progress bar control for pip
177
+ if 'install' in shell_cmd and '--progress-bar' not in shell_cmd:
178
+ parts.extend(['--progress-bar', 'off'])
179
+ return parts
180
+
181
+ elif shell_cmd.startswith('python '):
182
+ # Add unbuffered flag to python commands
183
+ parts = shlex.split(shell_cmd)
184
+ parts.insert(1, '-u') # Add -u after 'python'
185
+ return parts
186
+
187
+ else:
188
+ # For other shell commands, use shell execution
189
+ return ['/bin/zsh', '-c', shell_cmd] # macOS with zsh
190
+
191
+ async def _stream_output(self, stream, stream_type: str, result: Dict[str, Any],
192
+ websocket: Optional[WebSocket] = None,
193
+ cell_index: Optional[int] = None):
194
+ """Read from a stream and send to websocket, while capturing the output."""
195
+
196
+ output_text = ""
197
+ while True:
198
+ try:
199
+ line = await stream.readline()
200
+ if not line:
201
+ break
202
+
203
+ decoded_line = line.decode('utf-8')
204
+ output_text += decoded_line
205
+
206
+ if websocket:
207
+ await websocket.send_json({
208
+ "type": "stream_output",
209
+ "data": {
210
+ "stream": stream_type,
211
+ "text": decoded_line,
212
+ **({"cell_index": cell_index} if cell_index is not None else {})
213
+ }
214
+ })
215
+ except asyncio.CancelledError:
216
+ break
217
+ except Exception as e:
218
+ # Handle potential errors during streaming
219
+ error_message = f"Error reading stream: {e}\n"
220
+ output_text += error_message
221
+ if websocket:
222
+ await websocket.send_json({
223
+ "type": "stream_output",
224
+ "data": {
225
+ "stream": "stderr",
226
+ "text": error_message,
227
+ **({"cell_index": cell_index} if cell_index is not None else {})
228
+ }
229
+ })
230
+ break
231
+
232
+ # Add the captured text to the final result object
233
+ if output_text:
234
+ # Look for an existing stream output of the same type to append to
235
+ existing_output = next((o for o in result["outputs"] if o.get("name") == stream_type), None)
236
+ if existing_output:
237
+ existing_output["text"] += output_text
238
+ else:
239
+ result["outputs"].append({
240
+ "output_type": "stream",
241
+ "name": stream_type,
242
+ "text": output_text
243
+ })
244
+
245
+ async def _execute_cell_magic(self, source_code: str, result: Dict[str, Any],
246
+ start_time: float, execution_count: int,
247
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
248
+ """Execute a cell magic command"""
249
+ lines = source_code.strip().split('\n')
250
+ magic_line = lines[0] # e.g., "%%capture", "%%time"
251
+ cell_content = '\n'.join(lines[1:]) if len(lines) > 1 else ""
252
+
253
+ # Parse magic command and arguments
254
+ magic_parts = shlex.split(magic_line)
255
+ magic_name = magic_parts[0][2:] # Remove %%
256
+ magic_args = magic_parts[1:] if len(magic_parts) > 1 else []
257
+
258
+ try:
259
+ if magic_name == "capture":
260
+ return await self._handle_capture_magic(magic_args, cell_content, result, start_time, execution_count, websocket)
261
+ elif magic_name == "time":
262
+ return await self._handle_time_magic(cell_content, result, start_time, execution_count, websocket)
263
+ elif magic_name == "writefile":
264
+ return await self._handle_writefile_magic(magic_args, cell_content, result, start_time, websocket)
265
+ else:
266
+ result["status"] = "error"
267
+ result["error"] = {
268
+ "ename": "UnknownMagicError",
269
+ "evalue": f"Unknown cell magic: %%{magic_name}",
270
+ "traceback": [f"Cell magic %%{magic_name} is not implemented"]
271
+ }
272
+
273
+ if websocket:
274
+ await websocket.send_json({
275
+ "type": "execution_error",
276
+ "data": {
277
+ "error": result["error"]
278
+ }
279
+ })
280
+ except Exception as e:
281
+ result["status"] = "error"
282
+ result["error"] = {
283
+ "ename": type(e).__name__,
284
+ "evalue": str(e),
285
+ "traceback": [f"Cell magic error: {str(e)}"]
286
+ }
287
+
288
+ if websocket:
289
+ await websocket.send_json({
290
+ "type": "execution_error",
291
+ "data": {
292
+ "error": result["error"]
293
+ }
294
+ })
295
+
296
+ result["execution_time"] = f"{(time.time() - start_time) * 1000:.1f}ms"
297
+ return result
298
+
299
+ async def _execute_line_magic(self, magic_line: str, result: Dict[str, Any],
300
+ start_time: float, websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
301
+ """Execute a line magic command"""
302
+ # Parse magic command and arguments
303
+ parts = shlex.split(magic_line)
304
+ magic_name = parts[0]
305
+ magic_args = parts[1:] if len(parts) > 1 else []
306
+
307
+ try:
308
+ if magic_name == "pwd":
309
+ return await self._handle_pwd_magic(result, start_time, websocket)
310
+ elif magic_name == "cd":
311
+ return await self._handle_cd_magic(magic_args, result, start_time, websocket)
312
+ elif magic_name == "ls":
313
+ return await self._handle_ls_magic(magic_args, result, start_time, websocket)
314
+ elif magic_name == "env":
315
+ return await self._handle_env_magic(magic_args, result, start_time, websocket)
316
+ elif magic_name == "who":
317
+ return await self._handle_who_magic(result, start_time, websocket)
318
+ elif magic_name == "whos":
319
+ return await self._handle_whos_magic(result, start_time, websocket)
320
+ else:
321
+ result["status"] = "error"
322
+ result["error"] = {
323
+ "ename": "UnknownMagicError",
324
+ "evalue": f"Unknown line magic: %{magic_name}",
325
+ "traceback": [f"Line magic %{magic_name} is not implemented"]
326
+ }
327
+
328
+ if websocket:
329
+ await websocket.send_json({
330
+ "type": "execution_error",
331
+ "data": {
332
+ "error": result["error"]
333
+ }
334
+ })
335
+ except Exception as e:
336
+ result["status"] = "error"
337
+ result["error"] = {
338
+ "ename": type(e).__name__,
339
+ "evalue": str(e),
340
+ "traceback": [f"Line magic error: {str(e)}"]
341
+ }
342
+
343
+ if websocket:
344
+ await websocket.send_json({
345
+ "type": "execution_error",
346
+ "data": {
347
+ "error": result["error"]
348
+ }
349
+ })
350
+
351
+ result["execution_time"] = f"{(time.time() - start_time) * 1000:.1f}ms"
352
+ return result
353
+
354
+ # Cell Magic Implementations
355
+
356
+ async def _handle_capture_magic(self, args: list, cell_content: str, result: Dict[str, Any],
357
+ start_time: float, execution_count: int,
358
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
359
+ """Handle %%capture magic - capture stdout/stderr without displaying"""
360
+ output_var = args[0] if args else None
361
+ no_stdout = "--no-stdout" in args
362
+ no_stderr = "--no-stderr" in args
363
+
364
+ # Capture outputs
365
+ stdout_capture = None if no_stdout else io.StringIO()
366
+ stderr_capture = None if no_stderr else io.StringIO()
367
+
368
+ try:
369
+ # Execute the cell content with output capture
370
+ if stdout_capture or stderr_capture:
371
+ with redirect_stdout(stdout_capture or sys.stdout), \
372
+ redirect_stderr(stderr_capture or sys.stderr):
373
+ compiled_code = compile(cell_content, '<cell>', 'exec')
374
+ exec(compiled_code, self.globals_dict)
375
+ else:
376
+ compiled_code = compile(cell_content, '<cell>', 'exec')
377
+ exec(compiled_code, self.globals_dict)
378
+
379
+ # Store captured output in a variable if specified
380
+ if output_var:
381
+ captured_data = {
382
+ 'stdout': stdout_capture.getvalue() if stdout_capture else '',
383
+ 'stderr': stderr_capture.getvalue() if stderr_capture else ''
384
+ }
385
+ self.globals_dict[output_var] = captured_data
386
+ self.captured_outputs[output_var] = captured_data
387
+
388
+ # Don't add outputs to result (they're captured, not displayed)
389
+ if websocket:
390
+ await websocket.send_json({
391
+ "type": "execution_complete",
392
+ "data": {
393
+ "status": "ok",
394
+ "message": "Output captured" + (f" in variable '{output_var}'" if output_var else "")
395
+ }
396
+ })
397
+
398
+ except Exception as e:
399
+ result["status"] = "error"
400
+ result["error"] = {
401
+ "ename": type(e).__name__,
402
+ "evalue": str(e),
403
+ "traceback": [f"Capture magic error: {str(e)}"]
404
+ }
405
+
406
+ if websocket:
407
+ await websocket.send_json({
408
+ "type": "execution_error",
409
+ "data": {
410
+ "error": result["error"]
411
+ }
412
+ })
413
+
414
+ return result
415
+
416
+ # Add other magic method implementations here...
417
+ # (Time magic, writefile magic, line magics like pwd, cd, ls, etc.)
418
+ # I'll implement a few key ones to keep this focused:
419
+
420
+ async def _handle_pwd_magic(self, result: Dict[str, Any], start_time: float,
421
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
422
+ """Handle %pwd magic - print working directory"""
423
+ try:
424
+ pwd = os.getcwd()
425
+ output_data = {
426
+ "output_type": "execute_result",
427
+ "execution_count": None,
428
+ "data": {
429
+ "text/plain": f"'{pwd}'"
430
+ }
431
+ }
432
+ result["outputs"].append(output_data)
433
+
434
+ if websocket:
435
+ await websocket.send_json({
436
+ "type": "execute_result",
437
+ "data": {
438
+ "data": output_data["data"]
439
+ }
440
+ })
441
+
442
+ except Exception as e:
443
+ result["status"] = "error"
444
+ result["error"] = {
445
+ "ename": type(e).__name__,
446
+ "evalue": str(e),
447
+ "traceback": [f"PWD magic error: {str(e)}"]
448
+ }
449
+
450
+ if websocket:
451
+ await websocket.send_json({
452
+ "type": "execution_error",
453
+ "data": {
454
+ "error": result["error"]
455
+ }
456
+ })
457
+
458
+ return result
@@ -0,0 +1,134 @@
1
+ import os
2
+ import time
3
+ import shutil
4
+ import subprocess
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import psutil # type: ignore
8
+
9
+
10
+ class DeviceMetrics:
11
+ """Collects system metrics with graceful fallbacks.
12
+
13
+ - Uses psutil for CPU, memory, disk, network and process metrics
14
+ - Uses nvidia-smi when available for basic GPU metrics
15
+ """
16
+
17
+ def get_all_devices(self) -> Dict[str, Any]:
18
+ return {
19
+ "timestamp": time.time(),
20
+ "cpu": self.get_cpu_metrics(),
21
+ "memory": self.get_memory_metrics(),
22
+ "gpu": self.get_gpu_metrics(),
23
+ "storage": self.get_storage_metrics(),
24
+ "network": self.get_network_metrics(),
25
+ "process": self.get_process_metrics(),
26
+ }
27
+
28
+ def get_cpu_metrics(self) -> Dict[str, Any]:
29
+ try:
30
+ cpu_percent = psutil.cpu_percent(interval=None)
31
+ freq = psutil.cpu_freq()
32
+ return {
33
+ "percent": cpu_percent,
34
+ "frequency_mhz": freq.current if freq else None,
35
+ "cores": psutil.cpu_count(logical=True),
36
+ }
37
+ except Exception:
38
+ return {"percent": None, "frequency_mhz": None, "cores": None}
39
+
40
+ def get_memory_metrics(self) -> Dict[str, Any]:
41
+ try:
42
+ vm = psutil.virtual_memory()
43
+ sm = psutil.swap_memory()
44
+ return {
45
+ "total": vm.total,
46
+ "available": vm.available,
47
+ "used": vm.used,
48
+ "percent": vm.percent,
49
+ "swap_total": sm.total,
50
+ "swap_used": sm.used,
51
+ "swap_percent": sm.percent,
52
+ }
53
+ except Exception:
54
+ return {"total": None, "available": None, "used": None, "percent": None}
55
+
56
+ def get_gpu_metrics(self) -> Optional[List[Dict[str, Any]]]:
57
+ """Return list of GPU dicts or None when no GPU available.
58
+
59
+ Uses nvidia-smi if present. If not found or fails, returns None.
60
+ """
61
+ if shutil.which("nvidia-smi") is None:
62
+ return None
63
+ try:
64
+ # Query a few basic metrics; avoid JSON for broader compat
65
+ query = "power.draw,clocks.sm,temperature.gpu,utilization.gpu,memory.used,memory.total"
66
+ cmd = [
67
+ "nvidia-smi",
68
+ f"--query-gpu={query}",
69
+ "--format=csv,noheader,nounits",
70
+ ]
71
+ out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT, timeout=3)
72
+ gpus: List[Dict[str, Any]] = []
73
+ for line in out.strip().splitlines():
74
+ parts = [p.strip() for p in line.split(",")]
75
+ if len(parts) >= 6:
76
+ gpus.append(
77
+ {
78
+ "power_w": _to_float(parts[0]),
79
+ "clock_sm_mhz": _to_float(parts[1]),
80
+ "temperature_c": _to_float(parts[2]),
81
+ "util_percent": _to_float(parts[3]),
82
+ "mem_used_mb": _to_float(parts[4]),
83
+ "mem_total_mb": _to_float(parts[5]),
84
+ }
85
+ )
86
+ return gpus or None
87
+ except Exception:
88
+ return None
89
+
90
+ def get_storage_metrics(self) -> Dict[str, Any]:
91
+ try:
92
+ usage = psutil.disk_usage("/")
93
+ io = psutil.disk_io_counters()
94
+ return {
95
+ "total": usage.total,
96
+ "used": usage.used,
97
+ "percent": usage.percent,
98
+ "read_bytes": getattr(io, "read_bytes", None),
99
+ "write_bytes": getattr(io, "write_bytes", None),
100
+ }
101
+ except Exception:
102
+ return {"total": None, "used": None, "percent": None}
103
+
104
+ def get_network_metrics(self) -> Dict[str, Any]:
105
+ try:
106
+ net = psutil.net_io_counters()
107
+ return {
108
+ "bytes_sent": net.bytes_sent,
109
+ "bytes_recv": net.bytes_recv,
110
+ "packets_sent": net.packets_sent,
111
+ "packets_recv": net.packets_recv,
112
+ }
113
+ except Exception:
114
+ return {"bytes_sent": None, "bytes_recv": None}
115
+
116
+ def get_process_metrics(self) -> Dict[str, Any]:
117
+ try:
118
+ p = psutil.Process(os.getpid())
119
+ mem = p.memory_info()
120
+ return {
121
+ "rss": mem.rss,
122
+ "vms": getattr(mem, "vms", None),
123
+ "threads": p.num_threads(),
124
+ "cpu_percent": p.cpu_percent(interval=None),
125
+ }
126
+ except Exception:
127
+ return {"rss": None, "threads": None}
128
+
129
+
130
+ def _to_float(value: str) -> Optional[float]:
131
+ try:
132
+ return float(value)
133
+ except Exception:
134
+ return None