more-compute 0.2.4__py3-none-any.whl → 0.3.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.
@@ -11,17 +11,39 @@ import {
11
11
 
12
12
  const POLL_MS = 3000;
13
13
 
14
- const MetricsPopup: React.FC<{ onClose?: () => void }> = ({ onClose }) => {
14
+ interface MetricsPopupProps {
15
+ onClose?: () => void;
16
+ sharedHistory?: MetricsSnapshot[]; // Passed from parent when in persistent mode
17
+ }
18
+
19
+ const MetricsPopup: React.FC<MetricsPopupProps> = ({ onClose, sharedHistory }) => {
15
20
  const [metrics, setMetrics] = useState<MetricsSnapshot | null>(null);
16
- const [history, setHistory] = useState<MetricsSnapshot[]>([]);
21
+ const [localHistory, setLocalHistory] = useState<MetricsSnapshot[]>([]);
17
22
  const intervalRef = useRef<number | null>(null);
18
23
 
24
+ // Use shared history if provided (persistent mode), otherwise use local history (on-demand mode)
25
+ const history = sharedHistory ?? localHistory;
26
+ const isPersistentMode = sharedHistory !== undefined;
27
+
28
+ // Update current metrics from shared history
19
29
  useEffect(() => {
30
+ if (isPersistentMode && sharedHistory && sharedHistory.length > 0) {
31
+ setMetrics(sharedHistory[sharedHistory.length - 1]);
32
+ }
33
+ }, [isPersistentMode, sharedHistory]);
34
+
35
+ // On-demand mode: poll metrics when popup is open
36
+ useEffect(() => {
37
+ if (isPersistentMode) {
38
+ // Don't poll in persistent mode - use shared history
39
+ return;
40
+ }
41
+
20
42
  const load = async () => {
21
43
  try {
22
44
  const snap = await fetchMetrics();
23
45
  setMetrics(snap);
24
- setHistory((prev) => {
46
+ setLocalHistory((prev) => {
25
47
  const arr = [...prev, snap];
26
48
  return arr.slice(-100);
27
49
  });
@@ -32,7 +54,7 @@ const MetricsPopup: React.FC<{ onClose?: () => void }> = ({ onClose }) => {
32
54
  return () => {
33
55
  if (intervalRef.current) window.clearInterval(intervalRef.current);
34
56
  };
35
- }, []);
57
+ }, [isPersistentMode]);
36
58
 
37
59
  const hasGPU = metrics?.gpu && metrics.gpu.length > 0;
38
60
 
@@ -137,7 +159,7 @@ const BigValue: React.FC<{ value: string; subtitle?: string }> = ({
137
159
  );
138
160
 
139
161
  const MiniChart: React.FC<{ data: number[] }> = ({ data }) => {
140
- const width = 220;
162
+ const width = 100;
141
163
  const height = 48;
142
164
  const max = Math.max(100, ...data);
143
165
  const points = data
@@ -148,8 +170,21 @@ const MiniChart: React.FC<{ data: number[] }> = ({ data }) => {
148
170
  })
149
171
  .join(" ");
150
172
  return (
151
- <svg width={width} height={height} className="mini-chart">
152
- <polyline points={points} fill="none" stroke="var(--mc-primary)" strokeWidth="2" />
173
+ <svg
174
+ width="100%"
175
+ height={height}
176
+ viewBox={`0 0 ${width} ${height}`}
177
+ preserveAspectRatio="none"
178
+ className="mini-chart"
179
+ style={{ display: 'block' }}
180
+ >
181
+ <polyline
182
+ points={points}
183
+ fill="none"
184
+ stroke="var(--mc-primary)"
185
+ strokeWidth="2"
186
+ vectorEffect="non-scaling-stroke"
187
+ />
153
188
  </svg>
154
189
  );
155
190
  };
@@ -19,7 +19,8 @@ const PackagesPopup: React.FC<PackagesPopupProps> = ({ onClose }) => {
19
19
  const [query, setQuery] = useState('');
20
20
 
21
21
  useEffect(() => {
22
- loadPackages();
22
+ // Always force refresh on initial load to ensure fresh data
23
+ loadPackages(true);
23
24
  const handler = () => loadPackages(true); // Force refresh when packages updated
24
25
  if (typeof window !== 'undefined') {
25
26
  window.addEventListener('mc:packages-updated', handler as EventListener);
@@ -46,6 +46,9 @@ const SettingsPopup: React.FC<{ onSettingsChange?: (settings: NotebookSettings)
46
46
 
47
47
  return (
48
48
  <div className="settings-container">
49
+ <div style={{ fontSize: '12px', marginBottom: '8px', color: 'var(--mc-text-secondary, #6b7280)' }}>
50
+ See all themes at <a href="https://github.com/DannyMang/more-compute/blob/main/frontend/styling_README.md" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--mc-link-color, #3b82f6)', textDecoration: 'underline' }}>styling_README.md</a>
51
+ </div>
49
52
  <textarea
50
53
  className="settings-editor"
51
54
  value={settingsJson}
frontend/lib/api.ts CHANGED
@@ -51,8 +51,12 @@ export interface PackagesResponse {
51
51
  }
52
52
 
53
53
  export async function fetchInstalledPackages(forceRefresh: boolean = false): Promise<PackageInfo[]> {
54
- const url = forceRefresh ? `/api/packages?force_refresh=true` : `/api/packages`;
55
- const response = await fetch(url);
54
+ // Add timestamp to prevent browser caching
55
+ const timestamp = forceRefresh ? `&t=${Date.now()}` : '';
56
+ const url = forceRefresh ? `/api/packages?force_refresh=true${timestamp}` : `/api/packages`;
57
+ const response = await fetch(url, {
58
+ cache: forceRefresh ? 'no-store' : 'default'
59
+ });
56
60
  if (!response.ok) {
57
61
  throw new Error(`Failed to load packages: ${response.status}`);
58
62
  }
frontend/lib/settings.ts CHANGED
@@ -106,12 +106,16 @@ export const THEMES: Record<string, ThemeDefinition> = (() => {
106
106
  return themes;
107
107
  })();
108
108
 
109
+ export type MetricsCollectionMode = 'persistent' | 'on-demand';
110
+
109
111
  export type NotebookSettings = {
110
112
  theme: keyof typeof THEMES;
113
+ metricsCollectionMode: MetricsCollectionMode;
111
114
  };
112
115
 
113
116
  export const DEFAULT_SETTINGS: NotebookSettings = {
114
117
  theme: 'light',
118
+ metricsCollectionMode: 'on-demand', // Only collect metrics when popup is open to save memory
115
119
  };
116
120
 
117
121
  const STORAGE_KEY = 'morecompute-settings';
@@ -127,6 +131,9 @@ export function loadSettings(): NotebookSettings {
127
131
  // Return only valid settings fields (remove old auto_save, font_size, font_family, etc.)
128
132
  const cleanSettings: NotebookSettings = {
129
133
  theme,
134
+ metricsCollectionMode: parsed.metricsCollectionMode === 'on-demand' || parsed.metricsCollectionMode === 'persistent'
135
+ ? parsed.metricsCollectionMode
136
+ : DEFAULT_SETTINGS.metricsCollectionMode,
130
137
  };
131
138
 
132
139
  // Save cleaned settings back to localStorage
@@ -62,6 +62,9 @@ export class WebSocketService {
62
62
  case 'execution_start':
63
63
  this.emit('execution_start', messageData);
64
64
  break;
65
+ case 'heartbeat':
66
+ this.emit('heartbeat', messageData);
67
+ break;
65
68
  case 'stream_output':
66
69
  this.emit('stream_output', messageData);
67
70
  break;
@@ -1,18 +1,23 @@
1
- (for settings.json)
1
+ # For settings.json
2
2
 
3
- Possible Themes:
3
+ Available themes: `light`, `dark`, `tokyo-night`, `tokyo-night-storm`, `tokyo-night-light`, `night-owl`, `night-owl-light`, `synthwave-84`, `one-dark-pro`
4
4
 
5
- light
6
- dark
7
- tokyo-night
8
- tokyo-night-storm
9
- tokyo-night-light
10
- night-owl
11
- night-owl-light
12
- synthwave-84
13
- one-dark-pro
5
+ - light
6
+ - dark
7
+ - tokyo-night
8
+ - tokyo-night-storm
9
+ - tokyo-night-light
10
+ - night-owl
11
+ - night-owl-light
12
+ - synthwave-84
13
+ - one-dark-pro
14
+
15
+ Available metricsCollectionMode: `on-demand` (only collect when popup open), `persistent` (collect continuously in background, uses more memory (obviously)
14
16
 
15
17
  Example settings.json:
18
+ ```json
16
19
  {
17
- "theme": "dark"
20
+ "theme": "dark",
21
+ "metricsCollectionMode": "on-demand"
18
22
  }
23
+ ```
kernel_run.py CHANGED
@@ -305,13 +305,19 @@ class NotebookLauncher:
305
305
  print("\n Edit notebook in your browser!\n")
306
306
  print(" ➜ URL: http://localhost:3000\n")
307
307
 
308
+ # Track Ctrl+C presses
309
+ interrupt_count = [0] # Use list to allow modification in nested function
310
+
308
311
  # Set up signal handlers
309
312
  def signal_handler(signum, frame):
310
- # Shutdown immediately on Ctrl+C
311
- print("\nREMINDER: Any running GPU pods will continue to incur costs until you terminate them in the Compute popup.")
312
- print("\n Thanks for using MoreCompute!\n")
313
- self.cleanup()
314
- sys.exit(0)
313
+ interrupt_count[0] += 1
314
+ if interrupt_count[0] == 1:
315
+ print("\n\nREMINDER: Any running GPU pods will continue to incur costs until you terminate them in the Compute popup.")
316
+ print("[CTRL-C AGAIN TO EXIT]")
317
+ else:
318
+ print("\n Thanks for using MoreCompute!\n")
319
+ self.cleanup()
320
+ sys.exit(0)
315
321
 
316
322
  # Windows signal handling is different
317
323
  if not self.is_windows:
@@ -341,6 +347,7 @@ class NotebookLauncher:
341
347
  time.sleep(1)
342
348
 
343
349
  except KeyboardInterrupt:
350
+ # This shouldn't be reached due to signal handler, but keep as fallback
344
351
  print("\n\n Thanks for using MoreCompute!\n")
345
352
  self.cleanup()
346
353
 
@@ -371,11 +378,22 @@ def build_parser() -> argparse.ArgumentParser:
371
378
  def ensure_notebook_exists(notebook_path: Path):
372
379
  if notebook_path.exists():
373
380
  if notebook_path.suffix != '.ipynb':
374
- raise ValueError("Notebook path must be a .ipynb file")
381
+ raise ValueError(
382
+ f"Error: '{notebook_path}' is not a notebook file.\n"
383
+ f"Notebook files must have a .ipynb extension.\n"
384
+ f"Example: more-compute {notebook_path}.ipynb"
385
+ )
375
386
  return
376
387
 
377
388
  if notebook_path.suffix != '.ipynb':
378
- raise ValueError("Notebook path must end with .ipynb")
389
+ raise ValueError(
390
+ f"Error: '{notebook_path}' does not have the .ipynb extension.\n"
391
+ f"Notebook files must end with .ipynb\n\n"
392
+ f"Did you mean?\n"
393
+ f" more-compute {notebook_path}.ipynb\n\n"
394
+ f"Or to create a new notebook with timestamp:\n"
395
+ f" more-compute new"
396
+ )
379
397
 
380
398
  notebook_path.parent.mkdir(parents=True, exist_ok=True)
381
399
  notebook = Notebook()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: more-compute
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: An interactive notebook environment for local and GPU computing
5
5
  Home-page: https://github.com/DannyMang/MORECOMPUTE
6
6
  Author: MoreCompute Team
@@ -44,8 +44,7 @@ Dynamic: requires-python
44
44
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
45
45
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
46
46
 
47
- Interactive notebook environment for local Python development. Works with standard `.ipynb` files,
48
- similar to Jupyter Lab but more awesome.
47
+ An interactive notebook environment, similar to Marimo and Google Colab, that runs locally. It works with standard `.ipynb` files, similar to Jupyter Lab but more awesome.
49
48
 
50
49
  ## Installation
51
50
 
@@ -1,4 +1,4 @@
1
- kernel_run.py,sha256=-lyN0b77fPIhhX1KoxwMofnY6TyvZ227Pbega7oL4_w,15070
1
+ kernel_run.py,sha256=XAgUmZO9EVPXMlQe8RO8w4s_NveB9U7J6S-CyF12D0o,15847
2
2
  frontend/.DS_Store,sha256=uQeHnkKyuTF1AVax3NPqtN0uCH6XNXAxL9Nkb6Q9cGw,8196
3
3
  frontend/.gitignore,sha256=IH4mX_SQH5rZ-W2M4IUw4E-fxgCBVHKmbQpEYJbWVM0,480
4
4
  frontend/README.md,sha256=YLVf9995r3JZD5UkII5GZCvDK9wXXNrUE0loHA4vlY8,1450
@@ -10,38 +10,37 @@ frontend/next.config.ts,sha256=OL_rEfTIZxsB_B5R9JX2AxYXgC0Fc_XiDoRlOGEDEpk,368
10
10
  frontend/package-lock.json,sha256=pGL_J72WHNTz7iLx2MOwHwnDGfbKzN-r_qnnbzfGbEU,334369
11
11
  frontend/package.json,sha256=nUVMqlXGT5m431ArOzzs_geSfmq0AKfqUB40zIAeZ5s,1395
12
12
  frontend/postcss.config.mjs,sha256=jEBxSXR5tLs0bQ67U7G961Vb62WVEqqg1piNDYPd7tU,105
13
- frontend/styling_README.md,sha256=RAQ4q-axGqugym8L22KpxB2ReBbEtg67YCEHLvTVvAw,196
13
+ frontend/styling_README.md,sha256=jvoXKUzJv0rEIU40gz23Z6Xugy4To8P0N9szg_hKrqw,561
14
14
  frontend/tailwind.config.ts,sha256=eP9nVaAuyYo46vGQfCyWbo25_pr2hW830fs1Itcix9Q,620
15
15
  frontend/tsconfig.json,sha256=7SvBlRBYmuXAlAteRQTGwEE7ooWuNaPUrZ219dOo61E,598
16
16
  frontend/app/favicon.ico,sha256=K4rS0zRVqPc2_DqOv48L3qiEitTA20iigzvQ-c13WTI,25931
17
- frontend/app/globals.css,sha256=imFZBoEmnHEOpo7Jegj8F71iPwSwNQivtH0ylpCKyWE,35682
18
- frontend/app/layout.tsx,sha256=R1vUDTImQzUJIW51rdvBUIsvejSbjfLrwgbLSjrmcMM,6035
17
+ frontend/app/globals.css,sha256=zx2VGZhPHqrFSwCsW667gJ80BkEwL8E7J_IJNoMuYGQ,33738
18
+ frontend/app/layout.tsx,sha256=VMkhvePu0hV0w-huEmTocTr7lZ6DF_7eidrhzCCMwzc,7604
19
19
  frontend/app/page.tsx,sha256=p-DgDv8xDnwcRfDJY4rtfSQ2VdYwbnP3G-otWqDR1l0,256
20
- frontend/components/Cell.tsx,sha256=JeXn5Z_jzezYOtrgl291b6HBKJdfhQ88dTTDZjFoWmk,12237
21
- frontend/components/Notebook.tsx,sha256=tJt45gyNch3ifWjnKUDoyvik7snrgRqt55gQgiYEnlc,21528
20
+ frontend/components/Notebook.tsx,sha256=JImlThoX4F363yShOqdYDjIIO3i1uqfCLRkDlzurinQ,21851
22
21
  frontend/components/cell/AddCellButton.tsx,sha256=ZC2Vck0JIRDxGYhYv3LPYAdKDo13U6008WG_-XoPlIM,1012
23
- frontend/components/cell/CellButton.tsx,sha256=BjfazBKzlybA5Syx6tXGTJa1U_jwL8C_IeQKbcHlkyk,1364
24
- frontend/components/cell/MonacoCell.tsx,sha256=11jSlfGcUpxvbRDJr1GGocNtzZIarioDdgg7WLuZs9U,24334
22
+ frontend/components/cell/CellButton.tsx,sha256=j3lRStluVtRXjZOhi9isS3XKsRmM0WTn5SaFDM6GuYg,1386
23
+ frontend/components/cell/MonacoCell.tsx,sha256=YunwOlyeRsFqx-sHowhctqMV0RmhcQ1glSl5LO0s7NY,23914
25
24
  frontend/components/layout/ConnectionBanner.tsx,sha256=-m77wKCFpeRJ_AQwnM38jLwCY5vfpqE846xeXmT3p8A,870
26
25
  frontend/components/layout/Sidebar.tsx,sha256=kxgO2mXX11xI6rX478cUqqQ3xB40jlby21y27GTJSLU,1551
27
26
  frontend/components/modals/ConfirmModal.tsx,sha256=3WgXy_wmR8FHZPwzMevfZHFXa0blR4v_SbKG3d5McT4,3652
28
27
  frontend/components/modals/ErrorModal.tsx,sha256=kkGHQvgyMYlScKwni04-V_Dq6a0-lg0WodglLARR-uA,3536
29
28
  frontend/components/modals/SuccessModal.tsx,sha256=7NVg0MFPVvsBGDOHPVyZTNx4kmZLHgxdhZKKtTD9FTU,3385
30
- frontend/components/output/CellOutput.tsx,sha256=D1ZIvOmQvFn_4Y1ahGM5rp0Yr5_Zp_loNJaEUA-GCrA,2073
31
- frontend/components/output/ErrorDisplay.tsx,sha256=d6I2WVyDLPsonyDuqsrrN8sc_KHg0VTAW86DfcqfqL0,5978
29
+ frontend/components/output/CellOutput.tsx,sha256=KLRzwEchvBoeoai8TbabKEwO4tHmog3EKeTJAXtmveI,3665
30
+ frontend/components/output/ErrorDisplay.tsx,sha256=kYfip5zikcFIuN7WW47RVjoqwDujC1q_yMbgHrHl0E0,4996
32
31
  frontend/components/output/MarkdownRenderer.tsx,sha256=RtZ5yNRxDXIh_NkNsNiy30wMGIW7V1gfhsjecgMdc80,3341
33
32
  frontend/components/popups/ComputePopup.tsx,sha256=B7AwwjpJ9_nNI5qSN53jdR2tAulB5yl7spjMXjH4yDM,44021
34
33
  frontend/components/popups/FilterPopup.tsx,sha256=4kx9txg8cWeC6XHlh0pu0-BAfZkLTDYEU93fMdzn86M,13818
35
34
  frontend/components/popups/FolderPopup.tsx,sha256=V2tDAbztvNIUyPWtFiwjeIoCmFGQyDosQgST_JsAzLo,5215
36
- frontend/components/popups/MetricsPopup.tsx,sha256=V4Srat-RRr7B046Ezg5w99_E_t9j0P3Ch5DO9rF7Nx0,5028
37
- frontend/components/popups/PackagesPopup.tsx,sha256=K_9kGlQ-y-njugOLrahbv0KHRh_mXIlzuMg0giRsTb8,3606
38
- frontend/components/popups/SettingsPopup.tsx,sha256=etwDuB4HHfadzN7CZf9omZ0uNl_4vhwJ92C5p3rleuY,2395
35
+ frontend/components/popups/MetricsPopup.tsx,sha256=ublYbOQ-pSU_w48F10uFjDYctysEwmri9lrji5YHIC4,6034
36
+ frontend/components/popups/PackagesPopup.tsx,sha256=faeXL7hGFIEUBU6PUrTIr9P5Lw-81GGKWK1ldnIEDcc,3675
37
+ frontend/components/popups/SettingsPopup.tsx,sha256=rGwYxjkADnyTtpP0MjVpFPBsGoT1eENN2z7nWjLuzFE,2773
39
38
  frontend/contexts/PodWebSocketContext.tsx,sha256=QyEg1Msyum7h3T6F3j6pn4Ds5sXFT_9WYu5_0YZ2IUI,8256
40
- frontend/lib/api.ts,sha256=0N4PCSC5pfbq7GvR_6aOdPZ3JGujzi1Rz4V51g8sHP8,10852
39
+ frontend/lib/api.ts,sha256=L6Js3K7RcPCLfVVUz-bKX5hLAVFKCfUSHAKSHk2keAk,11026
41
40
  frontend/lib/monaco-themes.ts,sha256=jh_pZAmSMKjY_belbMbZX2WpFBN7baRxvJp9shUDYgk,5396
42
- frontend/lib/settings.ts,sha256=ZAdwLErjPITkIc87D3DTcHYhhUP3fBI-LpDYsH9eVE4,5842
41
+ frontend/lib/settings.ts,sha256=klHVyB2n-wTc8YrjKCIf2BEmrR-JLVHrRYIXkDxv9-A,6263
43
42
  frontend/lib/themes.json,sha256=mk6IGy6o_DCOerBH3QmfXozTHEiy-alsTLTTIaba7No,292018
44
- frontend/lib/websocket-native.ts,sha256=6QUSLSONnCImPp9hpFo_XdXME5X7DOptW1mrtG7E99A,5962
43
+ frontend/lib/websocket-native.ts,sha256=KcokVZAl08iWRAcBOfyt3uej352esOa_-YwLk7M5iSU,6046
45
44
  frontend/lib/websocket.ts,sha256=V2Y7sktt2dm1G-ZILggcqIWccsh-xoAG9dLHpNVqIqs,4500
46
45
  frontend/public/file.svg,sha256=K2eBLDJcGZoCU2zb7qDFk6cvcH0yO3LuPgjbqwZ1O9Q,391
47
46
  frontend/public/globe.svg,sha256=thS5vxg5JZV2YayFFJj-HYAp_UOmL7_thvniYkpX588,1035
@@ -65,17 +64,17 @@ frontend/public/fonts/Fira.ttf,sha256=dbSM4W7Drd9n_EkfXq8P31KuxbjZ1wtuFiZ8aFebvT
65
64
  frontend/public/fonts/Tiempos.woff2,sha256=h83bJKvAK301wXCMIvK7ZG5j0H2K3tzAfgo0yk4z8OE,13604
66
65
  frontend/public/fonts/VeraMono.ttf,sha256=2kKB3H2xej385kpiztkodcWJU0AFXsi6JKORTrl7NJ0,49224
67
66
  frontend/types/notebook.ts,sha256=v23RaZe6H3lU5tq6sqnJDPxC2mu0NZFDCJfiN0mgvSs,1359
68
- more_compute-0.2.4.dist-info/licenses/LICENSE,sha256=0Ot-XIetYt06iay6IhtpJkruD-cLZtjyv7_aIEE-oSc,1073
67
+ more_compute-0.3.0.dist-info/licenses/LICENSE,sha256=0Ot-XIetYt06iay6IhtpJkruD-cLZtjyv7_aIEE-oSc,1073
69
68
  morecompute/__init__.py,sha256=pcMVq8Q7qb42AOn7tqgoZJOi3epDDBnEriiv2WVKnXY,87
70
- morecompute/__version__.py,sha256=SBl2EPFW-ltPvQ7vbVWItyAsz3aKYIpjO7vcfr84GkU,22
69
+ morecompute/__version__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
71
70
  morecompute/cli.py,sha256=kVvzvPBqF8xO6UuhU_-TBn99nKwJ405R2mAS6zU0KBc,734
72
71
  morecompute/notebook.py,sha256=KEcv0eOEh9N7bPVGoRuKJb47G9MmpQ5zz1B6Dm58w18,4651
73
72
  morecompute/process_worker.py,sha256=KsE3r-XpkYGuyO4w3t54VKkD51LfNHAZc3TYattMtrg,7185
74
- morecompute/server.py,sha256=W_EfX4NiSYZgK3ZZFP-LPO78_Sjzuyo3eiKprnAxsfI,40144
73
+ morecompute/server.py,sha256=mdN80iBUixq4KUQfu53aoVsNZ6DM9RaBXc-lwt4bxFk,40285
75
74
  morecompute/execution/__init__.py,sha256=jPmBmq8BZWbUEY9XFSpqt5FkgX04uNS10WnUlr7Rnms,134
76
75
  morecompute/execution/__main__.py,sha256=pAWB_1bn99u8Gb-tVMSMI-NYvbYbDiwbn40L0a0djeA,202
77
- morecompute/execution/executor.py,sha256=v8OKWuHykuLvhnvsRydFECcgqRqfCFM__FAHb22ARcg,20128
78
- morecompute/execution/worker.py,sha256=9z1-3ruT5DbsqRJWw54J8VFVOnm_X1Vwcp6GCV5JP4o,12465
76
+ morecompute/execution/executor.py,sha256=BesGdx10HSGj8PyPrekdQZzc0tXoqidznEaFITOq8JM,20554
77
+ morecompute/execution/worker.py,sha256=4yZc_Z4A_nhCv_Lv8MaseSe7KYdXPYK5yDeQeDZoVwM,16004
79
78
  morecompute/models/__init__.py,sha256=VLJ5GWi2uTNiZBdvl-ipSbmA6EL8FZHZ5oq-rJmm9z0,640
80
79
  morecompute/models/api_models.py,sha256=-ydvi9SeTfdoY9oVPNQS4by-kQGSknx6BHhGD8E2tpo,4553
81
80
  morecompute/services/data_manager.py,sha256=c4GKucetMM-VPNbHyzce6bZRvFfmz8kTd5RppLjoLVc,14329
@@ -86,15 +85,18 @@ morecompute/services/prime_intellect.py,sha256=b705rHv3RPRsgWedRlHwoP_S-TxxZtMSy
86
85
  morecompute/static/styles.css,sha256=el_NtrUMbAUNSiMVBn1xlG70m3iPv7dyaIbWQMexhsY,19277
87
86
  morecompute/utils/__init__.py,sha256=VIxCL3S1pnjEs4cjKGZqZB68_P8FegdeMIqBjJhI5jQ,419
88
87
  morecompute/utils/cache_util.py,sha256=lVlXudHvtyvSo_kCSxORJrI85Jod8FrQLbI2f_JOIbA,661
88
+ morecompute/utils/cell_magics.py,sha256=XUxy6lIsrx-9r0mWn0smI1gvT3-kv0rhN5km9cs3D48,27278
89
89
  morecompute/utils/config_util.py,sha256=fGTQll7Zh05ZHrW8LuQNTJGziGnIfvKIU3azbrY-I-s,1793
90
90
  morecompute/utils/error_utils.py,sha256=e50WLFdD6ngIC30xAgrzdTYtD8tPOIFkKAAh_sPbK0I,11667
91
+ morecompute/utils/line_magics.py,sha256=kTutYBPAWoURY_pk8HXQ38IP712M2rBBfUg3oN8VrP0,33740
91
92
  morecompute/utils/notebook_util.py,sha256=3hH94dtXvhizRVTU9a2b38m_51Y4igoXpkjAXUqpVBQ,1353
92
93
  morecompute/utils/python_environment_util.py,sha256=l8WWWPwKbypknw8GwL22NXCji5i1FOy1vWG47J6og4g,7441
93
- morecompute/utils/special_commands.py,sha256=JTc9II2EitmivwTTdnydEefShasiTa-7w8tNyYVIenw,19104
94
+ morecompute/utils/shell_utils.py,sha256=fGFLhQLZU-lmMGALbbS-fKPkhQtmMhZ1FkgQ3TeoFhA,1917
95
+ morecompute/utils/special_commands.py,sha256=IyF9MTINMNNo3P_f56Q6yS_P2HNjA9vohYVxEV7KYnc,17331
94
96
  morecompute/utils/system_environment_util.py,sha256=32mQRubo0i4X61o-825T7m-eUSidcEp07qkInP1sWZA,4774
95
97
  morecompute/utils/zmq_util.py,sha256=tx7-iS04UN69OFtBzkxcEnRhT7xtI9EzRnrZ_nsH_O0,1889
96
- more_compute-0.2.4.dist-info/METADATA,sha256=9apuFd8BacFWNrlul4rCFXSLNthj23Jm9m0qomhueSo,3599
97
- more_compute-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
98
- more_compute-0.2.4.dist-info/entry_points.txt,sha256=xp7z9eRPNRM4oxkZZVlyXkhkSjN1AjoYI_B7qpDJ1bI,49
99
- more_compute-0.2.4.dist-info/top_level.txt,sha256=Tamm6ADzjwaQa1z27O7Izcyhyt9f0gVjMv1_tC810aI,32
100
- more_compute-0.2.4.dist-info/RECORD,,
98
+ more_compute-0.3.0.dist-info/METADATA,sha256=86In89hNjbgE5pKBac7gAQ9YKQD-hVbL3b9IecnskMo,3631
99
+ more_compute-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
100
+ more_compute-0.3.0.dist-info/entry_points.txt,sha256=xp7z9eRPNRM4oxkZZVlyXkhkSjN1AjoYI_B7qpDJ1bI,49
101
+ more_compute-0.3.0.dist-info/top_level.txt,sha256=Tamm6ADzjwaQa1z27O7Izcyhyt9f0gVjMv1_tC810aI,32
102
+ more_compute-0.3.0.dist-info/RECORD,,
@@ -1 +1 @@
1
- __version__ = "0.2.4"
1
+ __version__ = "0.3.0"
@@ -120,10 +120,17 @@ class NextZmqExecutor:
120
120
  if handler is not None:
121
121
  normalized_source = handler._coerce_source_to_text(source_code) # type: ignore[reportPrivateUsage]
122
122
  if handler.is_special_command(normalized_source):
123
- # If connected to remote pod, send special commands to remote worker
124
- # Otherwise they execute locally which gives wrong results
125
- if not self.is_remote:
126
- # Execute special command locally
123
+ # Check if this is a PURE special command (starts with !, %%, or %)
124
+ # vs a MIXED command (contains shell commands but also has Python code)
125
+ stripped = normalized_source.strip()
126
+ is_pure_special = (stripped.startswith('!') or
127
+ stripped.startswith('%%') or
128
+ stripped.startswith('%'))
129
+
130
+ # Only execute PURE special commands locally
131
+ # Mixed commands must go to worker for proper streaming
132
+ if not self.is_remote and is_pure_special:
133
+ # Execute pure special command locally
127
134
  execution_count = getattr(self, 'execution_count', 0) + 1
128
135
  self.execution_count = execution_count
129
136
  start_time = time.time()
@@ -143,7 +150,7 @@ class NextZmqExecutor:
143
150
  if websocket:
144
151
  await websocket.send_json({'type': 'execution_complete', 'data': {'cell_index': cell_index, 'result': result}})
145
152
  return result
146
- # For remote execution, fall through to send via ZMQ
153
+ # For remote execution OR mixed commands, fall through to send via ZMQ
147
154
 
148
155
  execution_count = getattr(self, 'execution_count', 0) + 1
149
156
  self.execution_count = execution_count
@@ -10,6 +10,91 @@ import matplotlib
10
10
  matplotlib.use('Agg')
11
11
  import matplotlib.pyplot as plt
12
12
  import re
13
+ import subprocess
14
+ import shlex
15
+ import platform
16
+
17
+ # Import shared shell command utilities
18
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
+ from utils.shell_utils import prepare_shell_command, prepare_shell_environment
20
+
21
+ def _preprocess_shell_commands(code: str) -> str:
22
+ """
23
+ Preprocess code to transform IPython-style shell commands (!cmd) into Python function calls.
24
+ Returns transformed code with shell commands converted to _run_shell_command() calls.
25
+ """
26
+ lines = code.split('\n')
27
+ transformed_lines = []
28
+
29
+ for line in lines:
30
+ # Match shell commands: " !pip install pandas"
31
+ shell_match = re.match(r'^(\s*)!(.+)$', line)
32
+
33
+ if shell_match:
34
+ indent = shell_match.group(1)
35
+ shell_cmd = shell_match.group(2).strip()
36
+ # Use repr() for proper escaping
37
+ shell_cmd_repr = repr(shell_cmd)
38
+ # Transform to function call
39
+ transformed = f"{indent}_run_shell_command({shell_cmd_repr})"
40
+ transformed_lines.append(transformed)
41
+ else:
42
+ transformed_lines.append(line)
43
+
44
+ return '\n'.join(transformed_lines)
45
+
46
+ def _inject_shell_command_function(globals_dict: dict):
47
+ """Inject the _run_shell_command function into globals if not present."""
48
+ if '_run_shell_command' not in globals_dict:
49
+ def _run_shell_command(cmd: str):
50
+ """Execute a shell command synchronously with streaming output"""
51
+ # Prepare command and environment (using shared utilities)
52
+ shell_cmd = prepare_shell_command(cmd)
53
+ env = prepare_shell_environment(cmd)
54
+
55
+ # Use Popen for real-time streaming
56
+ process = subprocess.Popen(
57
+ shell_cmd,
58
+ stdout=subprocess.PIPE,
59
+ stderr=subprocess.PIPE,
60
+ text=True,
61
+ bufsize=1, # Line buffered
62
+ env=env
63
+ )
64
+
65
+ # Stream output line by line
66
+ import threading
67
+
68
+ def read_stream(stream, output_type):
69
+ try:
70
+ for line in iter(stream.readline, ''):
71
+ if not line:
72
+ break
73
+ if output_type == 'stdout':
74
+ print(line, end='')
75
+ sys.stdout.flush()
76
+ else:
77
+ print(line, end='', file=sys.stderr)
78
+ sys.stderr.flush()
79
+ except Exception:
80
+ pass
81
+ finally:
82
+ stream.close()
83
+
84
+ stdout_thread = threading.Thread(target=read_stream, args=(process.stdout, 'stdout'))
85
+ stderr_thread = threading.Thread(target=read_stream, args=(process.stderr, 'stderr'))
86
+ stdout_thread.daemon = True
87
+ stderr_thread.daemon = True
88
+ stdout_thread.start()
89
+ stderr_thread.start()
90
+
91
+ return_code = process.wait()
92
+ stdout_thread.join()
93
+ stderr_thread.join()
94
+
95
+ return return_code
96
+
97
+ globals_dict['_run_shell_command'] = _run_shell_command
13
98
 
14
99
  def _setup_signals():
15
100
  def _handler(signum, frame):
@@ -232,7 +317,14 @@ def worker_main():
232
317
  error_payload = None
233
318
  start = time.time()
234
319
  try:
235
- compiled = compile(code, '<cell>', 'exec')
320
+ # Preprocess shell commands (!cmd) to Python function calls
321
+ # This allows code like "import os; !pip install pandas" to work
322
+ preprocessed_code = _preprocess_shell_commands(code)
323
+
324
+ # Inject shell command function into globals if needed
325
+ _inject_shell_command_function(g)
326
+
327
+ compiled = compile(preprocessed_code, '<cell>', 'exec')
236
328
  exec(compiled, g, l)
237
329
 
238
330
  # Try to evaluate last expression for display (like Jupyter)
morecompute/server.py CHANGED
@@ -119,6 +119,10 @@ async def list_installed_packages(force_refresh: bool = False):
119
119
  global pod_manager
120
120
  cache_key = "packages_list"
121
121
 
122
+ # Clear cache if force refresh is requested
123
+ if force_refresh and cache_key in packages_cache:
124
+ del packages_cache[cache_key]
125
+
122
126
  # Check cache first unless force refresh is requested
123
127
  if not force_refresh and cache_key in packages_cache:
124
128
  return packages_cache[cache_key]