more-compute 0.3.2__py3-none-any.whl → 0.4.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.
frontend/app/globals.css CHANGED
@@ -111,6 +111,7 @@ body {
111
111
  display: flex;
112
112
  align-items: center;
113
113
  gap: 6px;
114
+ pointer-events: none; /* Allow clicks to pass through */
114
115
  }
115
116
 
116
117
  .connection-banner.provisioning {
@@ -642,7 +643,8 @@ body {
642
643
  }
643
644
 
644
645
  .markdown-rendered code {
645
- background: #f3f4f6;
646
+ background: var(--mc-secondary);
647
+ color: var(--mc-text-color);
646
648
  padding: 2px 4px;
647
649
  border-radius: 3px;
648
650
  font-family: 'Fira', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
@@ -650,7 +652,7 @@ body {
650
652
  }
651
653
 
652
654
  .markdown-rendered pre {
653
- background: #f9fafb;
655
+ background: var(--mc-secondary);
654
656
  padding: 12px;
655
657
  border-radius: 6px;
656
658
  margin: 12px 0;
@@ -664,7 +666,7 @@ body {
664
666
  }
665
667
 
666
668
  .markdown-rendered blockquote {
667
- border-left: 3px solid #d1d5db;
669
+ border-left: 3px solid var(--mc-border);
668
670
  padding-left: 12px;
669
671
  margin: 12px 0;
670
672
  color: var(--mc-markdown-paragraph-color);
@@ -683,7 +685,7 @@ body {
683
685
  }
684
686
 
685
687
  .markdown-rendered .empty-markdown {
686
- color: #9ca3af;
688
+ color: var(--mc-line-number-color);
687
689
  font-style: italic;
688
690
  }
689
691
 
@@ -696,19 +698,20 @@ body {
696
698
  flex-direction: column;
697
699
  align-items: center;
698
700
  font-size: 11px;
699
- color: #6b7280;
701
+ color: var(--mc-line-number-color);
700
702
  width: 30px;
701
703
  }
702
704
 
703
705
  .execution-count {
704
706
  font-family: 'Fira', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
705
707
  font-size: 10px;
706
- color: #9ca3af;
708
+ color: var(--mc-line-number-color);
707
709
  }
708
710
 
709
711
  .execution-time {
710
712
  font-size: 9px;
711
- color: #d1d5db;
713
+ color: var(--mc-line-number-color);
714
+ opacity: 0.7;
712
715
  margin-top: 2px;
713
716
  }
714
717
 
frontend/app/layout.tsx CHANGED
@@ -8,6 +8,7 @@ import PackagesPopup from "@/components/popups/PackagesPopup";
8
8
  import ComputePopup from "@/components/popups/ComputePopup";
9
9
  import MetricsPopup from "@/components/popups/MetricsPopup";
10
10
  import SettingsPopup from "@/components/popups/SettingsPopup";
11
+ import ConfirmModal from "@/components/modals/ConfirmModal";
11
12
  import { ConnectionBanner } from "@/components/layout/ConnectionBanner";
12
13
  import {
13
14
  PodWebSocketProvider,
@@ -22,6 +23,7 @@ const POLL_MS = 3000;
22
23
  function AppContent({ children }: { children: React.ReactNode }) {
23
24
  const [appSettings, setAppSettings] = useState<NotebookSettings>(() => loadSettings());
24
25
  const [activePopup, setActivePopup] = useState<string | null>(null);
26
+ const [showRestartModal, setShowRestartModal] = useState(false);
25
27
  const { connectionState, gpuPods, connectingPodId } = usePodWebSocket();
26
28
 
27
29
  // Persistent metrics collection
@@ -84,6 +86,15 @@ function AppContent({ children }: { children: React.ReactNode }) {
84
86
  setActivePopup(null);
85
87
  };
86
88
 
89
+ const handleRestartKernel = () => {
90
+ // Send reset kernel command via WebSocket
91
+ const ws = new WebSocket('ws://127.0.0.1:3141/ws');
92
+ ws.onopen = () => {
93
+ ws.send(JSON.stringify({ type: 'reset_kernel' }));
94
+ setTimeout(() => ws.close(), 100);
95
+ };
96
+ };
97
+
87
98
  const renderPopup = () => {
88
99
  if (!activePopup) return null;
89
100
 
@@ -176,7 +187,28 @@ function AppContent({ children }: { children: React.ReactNode }) {
176
187
  id="kernel-status-dot"
177
188
  className="status-dot connecting"
178
189
  ></span>
179
- <span id="kernel-status-text" className="status-text">
190
+ <span
191
+ id="kernel-status-text"
192
+ className="status-text"
193
+ data-original-text="Connecting..."
194
+ style={{
195
+ cursor: 'pointer',
196
+ transition: 'all 0.15s ease',
197
+ }}
198
+ onMouseEnter={(e) => {
199
+ const originalText = e.currentTarget.textContent || '';
200
+ e.currentTarget.setAttribute('data-original-text', originalText);
201
+ e.currentTarget.textContent = 'Restart Kernel';
202
+ e.currentTarget.style.color = 'var(--mc-primary)';
203
+ }}
204
+ onMouseLeave={(e) => {
205
+ const originalText = e.currentTarget.getAttribute('data-original-text') || 'Connecting...';
206
+ e.currentTarget.textContent = originalText;
207
+ e.currentTarget.style.color = '';
208
+ }}
209
+ onClick={() => setShowRestartModal(true)}
210
+ title="Click to restart kernel"
211
+ >
180
212
  Connecting...
181
213
  </span>
182
214
  </div>
@@ -197,6 +229,16 @@ function AppContent({ children }: { children: React.ReactNode }) {
197
229
  />
198
230
  </div>
199
231
  </div>
232
+ <ConfirmModal
233
+ isOpen={showRestartModal}
234
+ onClose={() => setShowRestartModal(false)}
235
+ onConfirm={handleRestartKernel}
236
+ title="Restart Kernel"
237
+ message="Are you sure you want to restart the kernel? All variables will be lost."
238
+ confirmLabel="Restart"
239
+ cancelLabel="Cancel"
240
+ isDangerous={true}
241
+ />
200
242
  </>
201
243
  );
202
244
  }
@@ -276,10 +276,18 @@ function notebookReducer(
276
276
  case "EXECUTION_COMPLETE": {
277
277
  const payload = action.payload || {};
278
278
  const cell_index = payload.cell_index;
279
+
280
+ console.log(`[EXECUTION_COMPLETE] Processing completion for cell ${cell_index}`, payload);
281
+
279
282
  // Support both shapes: { result: {...} } and flat payload {...}
280
283
  const result = payload && payload.result ? payload.result : payload || {};
284
+
285
+ console.log(`[EXECUTION_COMPLETE] result.status=${result.status}, result.error=`, result.error);
286
+
287
+ // ALWAYS remove cell from executingCells when completion arrives
281
288
  const newExecuting = new Set(state.executingCells);
282
289
  newExecuting.delete(cell_index);
290
+
283
291
  return {
284
292
  ...state,
285
293
  executingCells: newExecuting,
@@ -310,8 +318,13 @@ function notebookReducer(
310
318
 
311
319
  case "EXECUTION_ERROR": {
312
320
  const { cell_index, error } = action.payload;
321
+
322
+ console.log(`[EXECUTION_ERROR] Processing error for cell ${cell_index}`);
323
+
324
+ // ALWAYS remove cell from executingCells when error arrives
313
325
  const newExecuting = new Set(state.executingCells);
314
326
  newExecuting.delete(cell_index);
327
+
315
328
  const normalizedError = normalizeError(error);
316
329
  return {
317
330
  ...state,
@@ -348,6 +361,8 @@ function notebookReducer(
348
361
  ...cell,
349
362
  outputs: [],
350
363
  execution_count: null,
364
+ execution_time: null,
365
+ error: null,
351
366
  })),
352
367
  };
353
368
 
@@ -435,6 +450,10 @@ export const Notebook: React.FC<NotebookProps> = ({
435
450
  dispatch({ type: "NOTEBOOK_UPDATED", payload: data });
436
451
  }, []);
437
452
 
453
+ const handleKernelRestarted = useCallback(() => {
454
+ dispatch({ type: "RESET_KERNEL" });
455
+ }, []);
456
+
438
457
  const handleHeartbeat = useCallback((data: any) => {
439
458
  // Heartbeat received - execution still in progress
440
459
  // Cell spinner already showing via executingCells set
@@ -473,7 +492,7 @@ export const Notebook: React.FC<NotebookProps> = ({
473
492
  wsRef.current = ws;
474
493
  handleKernelStatusUpdate("connecting");
475
494
 
476
- ws.connect("ws://127.0.0.1:8000/ws")
495
+ ws.connect("ws://127.0.0.1:3141/ws")
477
496
  .then(() => {
478
497
  ws.loadNotebook(notebookName || "default");
479
498
  })
@@ -486,6 +505,7 @@ export const Notebook: React.FC<NotebookProps> = ({
486
505
  ws.on("disconnect", () => handleKernelStatusUpdate("disconnected"));
487
506
  ws.on("notebook_loaded", handleNotebookLoaded);
488
507
  ws.on("notebook_updated", handleNotebookUpdate);
508
+ ws.on("kernel_restarted", handleKernelRestarted);
489
509
  ws.on("execution_start", handleExecutionStart);
490
510
  ws.on("stream_output", handleStreamOutput);
491
511
  ws.on("execution_complete", handleExecutionComplete);
@@ -499,6 +519,7 @@ export const Notebook: React.FC<NotebookProps> = ({
499
519
  handleKernelStatusUpdate,
500
520
  handleNotebookLoaded,
501
521
  handleNotebookUpdate,
522
+ handleKernelRestarted,
502
523
  handleExecutionStart,
503
524
  handleStreamOutput,
504
525
  handleExecuteResult,
@@ -22,7 +22,8 @@ export const CellButton: React.FC<CellButtonProps> = ({
22
22
  const [isHovered, setIsHovered] = useState(false);
23
23
 
24
24
  const handleClick = (e: React.MouseEvent) => {
25
- if (onClick && !disabled && !isLoading) {
25
+ if (onClick && !disabled) {
26
+ // Allow clicks even when loading (for stop/interrupt functionality)
26
27
  onClick(e);
27
28
  }
28
29
  };
@@ -34,7 +35,7 @@ export const CellButton: React.FC<CellButtonProps> = ({
34
35
  onClick={handleClick}
35
36
  onMouseEnter={() => setIsHovered(true)}
36
37
  onMouseLeave={() => setIsHovered(false)}
37
- disabled={disabled || isLoading}
38
+ disabled={disabled}
38
39
  title={title}
39
40
  style={{
40
41
  width: '28px',
@@ -45,9 +46,9 @@ export const CellButton: React.FC<CellButtonProps> = ({
45
46
  display: 'flex',
46
47
  alignItems: 'center',
47
48
  justifyContent: 'center',
48
- cursor: disabled || isLoading ? 'not-allowed' : 'pointer',
49
+ cursor: disabled ? 'not-allowed' : 'pointer',
49
50
  transition: 'background-color 0.15s ease',
50
- opacity: disabled ? 0.5 : 1
51
+ opacity: disabled ? 0.5 : isLoading ? 0.8 : 1
51
52
  }}
52
53
  >
53
54
  {icon}
@@ -12,6 +12,7 @@ import {
12
12
  UpdateIcon,
13
13
  LinkBreak2Icon,
14
14
  PlayIcon,
15
+ StopIcon,
15
16
  ChevronUpIcon,
16
17
  ChevronDownIcon,
17
18
  } from "@radix-ui/react-icons";
@@ -269,13 +270,13 @@ export const MonacoCell: React.FC<CellProps> = ({
269
270
  onExecute(indexRef.current);
270
271
  setIsEditing(false);
271
272
  } else {
272
- if (isExecuting) {
273
- onInterrupt(indexRef.current);
274
- } else {
275
- onExecute(indexRef.current);
276
- }
273
+ onExecute(indexRef.current);
277
274
  }
278
- }, [cell.cell_type, isExecuting, onExecute, onInterrupt]);
275
+ }, [cell.cell_type, onExecute]);
276
+
277
+ const handleInterrupt = useCallback(() => {
278
+ onInterrupt(indexRef.current);
279
+ }, [onInterrupt]);
279
280
 
280
281
  const handleCellClick = () => {
281
282
  onSetActive(indexRef.current);
@@ -583,15 +584,26 @@ export const MonacoCell: React.FC<CellProps> = ({
583
584
  <div className="cell-hover-controls">
584
585
  <div className="cell-actions-right">
585
586
  {!isMarkdownWithContent && (
586
- <CellButton
587
- icon={<PlayIcon className="w-6 h-6" />}
588
- onClick={(e) => {
589
- e.stopPropagation();
590
- handleExecute();
591
- }}
592
- title={isExecuting ? "Stop execution" : "Run cell"}
593
- isLoading={isExecuting}
594
- />
587
+ <>
588
+ <CellButton
589
+ icon={<PlayIcon className="w-6 h-6" />}
590
+ onClick={(e) => {
591
+ e.stopPropagation();
592
+ handleExecute();
593
+ }}
594
+ title="Run cell"
595
+ disabled={isExecuting}
596
+ />
597
+ <CellButton
598
+ icon={<StopIcon className="w-6 h-6" />}
599
+ onClick={(e) => {
600
+ e.stopPropagation();
601
+ handleInterrupt();
602
+ }}
603
+ title="Stop execution"
604
+ disabled={!isExecuting}
605
+ />
606
+ </>
595
607
  )}
596
608
  <CellButton
597
609
  icon={<ChevronUpIcon className="w-6 h-6" />}
@@ -599,8 +611,8 @@ export const MonacoCell: React.FC<CellProps> = ({
599
611
  e.stopPropagation();
600
612
  onMoveUp(indexRef.current);
601
613
  }}
602
- title="Move cell up"
603
- disabled={index === 0}
614
+ title={isExecuting ? "Cannot move while executing" : "Move cell up"}
615
+ disabled={isExecuting || index === 0}
604
616
  />
605
617
  <CellButton
606
618
  icon={<ChevronDownIcon className="w-6 h-6" />}
@@ -608,16 +620,25 @@ export const MonacoCell: React.FC<CellProps> = ({
608
620
  e.stopPropagation();
609
621
  onMoveDown(indexRef.current);
610
622
  }}
611
- title="Move cell down"
612
- disabled={index === totalCells - 1}
623
+ title={isExecuting ? "Cannot move while executing" : "Move cell down"}
624
+ disabled={isExecuting || index === totalCells - 1}
613
625
  />
614
626
  <CellButton
615
627
  icon={<LinkBreak2Icon className="w-5 h-5" />}
616
628
  onClick={(e) => {
617
629
  e.stopPropagation();
618
- onDelete(indexRef.current);
630
+ // If cell is executing, interrupt it first
631
+ if (isExecuting) {
632
+ onInterrupt(indexRef.current);
633
+ // Wait a bit for interrupt to complete, then delete
634
+ setTimeout(() => {
635
+ onDelete(indexRef.current);
636
+ }, 500);
637
+ } else {
638
+ onDelete(indexRef.current);
639
+ }
619
640
  }}
620
- title="Delete cell"
641
+ title={isExecuting ? "Stop and delete cell" : "Delete cell"}
621
642
  />
622
643
  </div>
623
644
  </div>
@@ -165,7 +165,20 @@ const TypedErrorDisplay: FC<{ error: ErrorOutput }> = ({ error }) => {
165
165
  margin: 0
166
166
  }}
167
167
  >
168
- {error.traceback?.join('\n') || ''}
168
+ {/* Always show error name and message */}
169
+ {error.ename && (
170
+ <div style={{ fontWeight: 600, marginBottom: '4px' }}>
171
+ {error.ename}: {error.evalue}
172
+ </div>
173
+ )}
174
+ {/* Show traceback if available */}
175
+ {error.traceback && error.traceback.length > 0 && (
176
+ <div>{error.traceback.join('\n')}</div>
177
+ )}
178
+ {/* Fallback if no traceback */}
179
+ {(!error.traceback || error.traceback.length === 0) && !error.ename && (
180
+ <div>An unknown error occurred</div>
181
+ )}
169
182
  </div>
170
183
  </div>
171
184
  </div>
@@ -95,7 +95,7 @@ export const PodWebSocketProvider: React.FC<PodWebSocketProviderProps> = ({ chil
95
95
  wsRef.current = null;
96
96
  }
97
97
 
98
- const wsUrl = 'ws://127.0.0.1:8000/ws';
98
+ const wsUrl = 'ws://127.0.0.1:3141/ws';
99
99
  const ws = new WebSocket(wsUrl);
100
100
 
101
101
  ws.onopen = () => {
frontend/lib/websocket.ts CHANGED
@@ -53,12 +53,12 @@ export class WebSocketService {
53
53
  };
54
54
  }
55
55
 
56
- connect(url: string = 'ws://localhost:8000'): Promise<void> {
56
+ connect(url: string = 'ws://localhost:3141'): Promise<void> {
57
57
  return new Promise((resolve, reject) => {
58
58
  // For development, connect directly to the backend WebSocket
59
59
  const wsUrl = process.env.NODE_ENV === 'production'
60
60
  ? '/ws'
61
- : 'ws://localhost:8000/ws';
61
+ : 'ws://localhost:3141/ws';
62
62
 
63
63
  // Use native WebSocket for FastAPI compatibility
64
64
  const ws = new WebSocket(wsUrl);
frontend/next.config.mjs CHANGED
@@ -4,11 +4,11 @@ const nextConfig = {
4
4
  return [
5
5
  {
6
6
  source: '/ws/:path*',
7
- destination: 'http://localhost:8000/ws/:path*',
7
+ destination: 'http://localhost:3141/ws/:path*',
8
8
  },
9
9
  {
10
10
  source: '/api/:path*',
11
- destination: 'http://localhost:8000/api/:path*',
11
+ destination: 'http://localhost:3141/api/:path*',
12
12
  },
13
13
  ];
14
14
  },
kernel_run.py CHANGED
@@ -25,6 +25,7 @@ class NotebookLauncher:
25
25
  self.notebook_path = notebook_path
26
26
  self.is_windows = platform.system() == "Windows"
27
27
  self.cleaning_up = False # Flag to prevent multiple cleanup calls
28
+ self.frontend_port = int(os.getenv("MORECOMPUTE_FRONTEND_PORT", "2718"))
28
29
  root_dir = notebook_path.parent if notebook_path.parent != Path('') else Path.cwd()
29
30
  os.environ["MORECOMPUTE_ROOT"] = str(root_dir.resolve())
30
31
  os.environ["MORECOMPUTE_NOTEBOOK_PATH"] = str(self.notebook_path)
@@ -32,8 +33,8 @@ class NotebookLauncher:
32
33
  def start_backend(self):
33
34
  """Start the FastAPI backend server"""
34
35
  try:
35
- # Force a stable port (default 8000); if busy, ask to free it
36
- chosen_port = int(os.getenv("MORECOMPUTE_PORT", "8000"))
36
+ # Force a stable port (default 3141); if busy, ask to free it
37
+ chosen_port = int(os.getenv("MORECOMPUTE_PORT", "3141"))
37
38
  self._ensure_port_available(chosen_port)
38
39
  cmd = [
39
40
  sys.executable,
@@ -244,9 +245,14 @@ class NotebookLauncher:
244
245
  fe_stdout = None if self.debug else subprocess.DEVNULL
245
246
  fe_stderr = None if self.debug else subprocess.DEVNULL
246
247
 
248
+ # Set PORT environment variable for Next.js
249
+ frontend_env = os.environ.copy()
250
+ frontend_env["PORT"] = str(self.frontend_port)
251
+
247
252
  self.frontend_process = subprocess.Popen(
248
253
  [npm_cmd, "run", "dev"],
249
254
  cwd=frontend_dir,
255
+ env=frontend_env,
250
256
  stdout=fe_stdout,
251
257
  stderr=fe_stderr,
252
258
  shell=self.is_windows, # CRITICAL for Windows
@@ -256,7 +262,7 @@ class NotebookLauncher:
256
262
 
257
263
  # Wait a bit then open browser
258
264
  time.sleep(3)
259
- webbrowser.open("http://localhost:3000")
265
+ webbrowser.open(f"http://localhost:{self.frontend_port}")
260
266
 
261
267
  except Exception as e:
262
268
  print(f"Failed to start frontend: {e}")
@@ -316,7 +322,7 @@ class NotebookLauncher:
316
322
  def run(self):
317
323
  """Main run method"""
318
324
  print("\n Edit notebook in your browser!\n")
319
- print(" ➜ URL: http://localhost:3000\n")
325
+ print(f" ➜ URL: http://localhost:{self.frontend_port}\n")
320
326
 
321
327
  # Track Ctrl+C presses
322
328
  interrupt_count = [0] # Use list to allow modification in nested function
@@ -366,7 +372,11 @@ class NotebookLauncher:
366
372
 
367
373
 
368
374
  def build_parser() -> argparse.ArgumentParser:
369
- parser = argparse.ArgumentParser(description="Launch the MoreCompute notebook")
375
+ parser = argparse.ArgumentParser(
376
+ prog="more-compute",
377
+ description="MoreCompute - Jupyter notebooks with GPU compute",
378
+ add_help=False,
379
+ )
370
380
  parser.add_argument(
371
381
  "--version",
372
382
  "-v",
@@ -383,28 +393,53 @@ def build_parser() -> argparse.ArgumentParser:
383
393
  "-debug",
384
394
  "--debug",
385
395
  action="store_true",
386
- help="Show backend/frontend logs (hidden by default)",
396
+ help="Show backend/frontend logs",
397
+ )
398
+ parser.add_argument(
399
+ "-h",
400
+ "--help",
401
+ action="store_true",
402
+ help="Show this help message",
387
403
  )
388
404
  return parser
389
405
 
390
406
 
391
407
  def ensure_notebook_exists(notebook_path: Path):
392
408
  if notebook_path.exists():
393
- if notebook_path.suffix != '.ipynb':
409
+ # File exists, check extension
410
+ if notebook_path.suffix == '.ipynb':
394
411
  raise ValueError(
395
- f"Error: '{notebook_path}' is not a notebook file.\n"
396
- f"Notebook files must have a .ipynb extension.\n"
397
- f"Example: more-compute {notebook_path}.ipynb"
412
+ f"Error: MoreCompute only supports .py notebooks.\n\n"
413
+ f"Convert your notebook with:\n"
414
+ f" more-compute convert {notebook_path.name} -o {notebook_path.stem}.py\n\n"
415
+ f"Then open with:\n"
416
+ f" more-compute {notebook_path.stem}.py"
417
+ )
418
+ elif notebook_path.suffix != '.py':
419
+ raise ValueError(
420
+ f"Error: '{notebook_path}' is not a Python notebook file.\n"
421
+ f"Notebook files must have a .py extension.\n"
422
+ f"Example: more-compute {notebook_path.stem}.py"
398
423
  )
399
424
  return
400
425
 
401
- if notebook_path.suffix != '.ipynb':
426
+ # File doesn't exist, create it
427
+ if notebook_path.suffix == '.ipynb':
428
+ raise ValueError(
429
+ f"Error: MoreCompute only supports .py notebooks.\n\n"
430
+ f"Convert your notebook with:\n"
431
+ f" more-compute convert {notebook_path.name} -o {notebook_path.stem}.py\n\n"
432
+ f"Or create a new notebook:\n"
433
+ f" more-compute new"
434
+ )
435
+
436
+ if notebook_path.suffix != '.py':
402
437
  raise ValueError(
403
- f"Error: '{notebook_path}' does not have the .ipynb extension.\n"
404
- f"Notebook files must end with .ipynb\n\n"
438
+ f"Error: '{notebook_path}' does not have the .py extension.\n"
439
+ f"Notebook files must end with .py\n\n"
405
440
  f"Did you mean?\n"
406
- f" more-compute {notebook_path}.ipynb\n\n"
407
- f"Or to create a new notebook with timestamp:\n"
441
+ f" more-compute {notebook_path}.py\n\n"
442
+ f"Or to create a new notebook:\n"
408
443
  f" more-compute new"
409
444
  )
410
445
 
@@ -413,15 +448,108 @@ def ensure_notebook_exists(notebook_path: Path):
413
448
  notebook.save_to_file(str(notebook_path))
414
449
 
415
450
 
451
+ def print_help():
452
+ """Print concise help message"""
453
+ print(f"""Usage: more-compute [OPTIONS] [COMMAND] [NOTEBOOK]
454
+
455
+ MoreCompute - Python notebooks with GPU compute
456
+
457
+ Getting started:
458
+
459
+ * more-compute new create a new notebook with timestamp
460
+ * more-compute notebook.py open or create notebook.py
461
+
462
+ Commands:
463
+ convert NOTEBOOK -o OUTPUT convert .ipynb to .py format
464
+ Example: more-compute convert notebook.ipynb -o notebook.py
465
+
466
+ Options:
467
+ --version, -v Show version and exit
468
+ --debug Show backend/frontend logs
469
+ --help, -h Show this message and exit
470
+
471
+ Environment variables:
472
+ MORECOMPUTE_PORT Backend port (default: 3141)
473
+ MORECOMPUTE_FRONTEND_PORT Frontend port (default: 2718)
474
+ MORECOMPUTE_NOTEBOOK_PATH Default notebook path
475
+
476
+ Note: MoreCompute uses .py notebooks (not .ipynb). Convert existing notebooks with:
477
+ more-compute convert notebook.ipynb -o notebook.py
478
+ """)
479
+
416
480
  def main(argv=None):
481
+ # Handle convert command before argparse (to avoid parsing -o flag)
482
+ argv_to_check = argv if argv is not None else sys.argv[1:]
483
+ if len(argv_to_check) > 0 and argv_to_check[0] == "convert":
484
+ # Parse convert arguments
485
+ if len(sys.argv) < 3:
486
+ print("Error: convert command requires input file")
487
+ print("\nUsage:")
488
+ print(" more-compute convert notebook.ipynb # -> notebook.py")
489
+ print(" more-compute convert notebook.py # -> notebook.ipynb")
490
+ print(" more-compute convert notebook.ipynb -o out.py")
491
+ sys.exit(1)
492
+
493
+ input_file = Path(sys.argv[2])
494
+ output_file = None
495
+
496
+ # Parse -o flag
497
+ if len(sys.argv) >= 5 and sys.argv[3] == "-o":
498
+ output_file = Path(sys.argv[4])
499
+ else:
500
+ # Auto-detect output extension based on input
501
+ if input_file.suffix == '.ipynb':
502
+ output_file = input_file.with_suffix('.py')
503
+ elif input_file.suffix == '.py':
504
+ output_file = input_file.with_suffix('.ipynb')
505
+ else:
506
+ print(f"Error: Unsupported file type: {input_file.suffix}")
507
+ print("Supported: .ipynb, .py")
508
+ sys.exit(1)
509
+
510
+ if not input_file.exists():
511
+ print(f"Error: File not found: {input_file}")
512
+ sys.exit(1)
513
+
514
+ # Perform conversion based on input type
515
+ if input_file.suffix == '.ipynb':
516
+ from morecompute.utils.notebook_converter import convert_ipynb_to_py
517
+ try:
518
+ convert_ipynb_to_py(input_file, output_file)
519
+ sys.exit(0)
520
+ except Exception as e:
521
+ print(f"Error converting notebook: {e}")
522
+ sys.exit(1)
523
+ elif input_file.suffix == '.py':
524
+ from morecompute.utils.notebook_converter import convert_py_to_ipynb
525
+ try:
526
+ convert_py_to_ipynb(input_file, output_file)
527
+ sys.exit(0)
528
+ except Exception as e:
529
+ print(f"Error converting notebook: {e}")
530
+ sys.exit(1)
531
+ else:
532
+ print(f"Error: Can only convert .ipynb or .py files")
533
+ print(f"Got: {input_file.suffix}")
534
+ sys.exit(1)
535
+
536
+
537
+ # Parse arguments for non-convert commands
417
538
  parser = build_parser()
418
539
  args = parser.parse_args(argv)
540
+
541
+ # Show help if requested or no arguments provided
542
+ if args.help or (args.notebook_path is None and os.getenv("MORECOMPUTE_NOTEBOOK_PATH") is None):
543
+ print_help()
544
+ sys.exit(0)
545
+
419
546
  raw_notebook_path = args.notebook_path
420
547
 
548
+ # Handle "new" command
421
549
  if raw_notebook_path == "new":
422
550
  from datetime import datetime
423
551
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
424
- raw_notebook_path = f"notebook_{timestamp}.ipynb"
552
+ raw_notebook_path = f"notebook_{timestamp}.py"
425
553
  print(f"Creating new notebook: {raw_notebook_path}")
426
554
 
427
555
  notebook_path_env = os.getenv("MORECOMPUTE_NOTEBOOK_PATH")
@@ -429,10 +557,17 @@ def main(argv=None):
429
557
  raw_notebook_path = notebook_path_env
430
558
 
431
559
  if raw_notebook_path is None:
432
- raw_notebook_path = DEFAULT_NOTEBOOK_NAME
560
+ print_help()
561
+ sys.exit(0)
433
562
 
434
563
  notebook_path = Path(raw_notebook_path).expanduser().resolve()
435
- ensure_notebook_exists(notebook_path)
564
+
565
+ try:
566
+ ensure_notebook_exists(notebook_path)
567
+ except ValueError as e:
568
+ # Print clean error message without traceback
569
+ print(str(e), file=sys.stderr)
570
+ sys.exit(1)
436
571
 
437
572
  launcher = NotebookLauncher(
438
573
  notebook_path=notebook_path,