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 +10 -7
- frontend/app/layout.tsx +43 -1
- frontend/components/Notebook.tsx +22 -1
- frontend/components/cell/CellButton.tsx +5 -4
- frontend/components/cell/MonacoCell.tsx +42 -21
- frontend/components/output/ErrorDisplay.tsx +14 -1
- frontend/contexts/PodWebSocketContext.tsx +1 -1
- frontend/lib/websocket.ts +2 -2
- frontend/next.config.mjs +2 -2
- kernel_run.py +153 -18
- {more_compute-0.3.2.dist-info → more_compute-0.4.0.dist-info}/METADATA +34 -24
- {more_compute-0.3.2.dist-info → more_compute-0.4.0.dist-info}/RECORD +27 -26
- morecompute/__version__.py +1 -1
- morecompute/execution/executor.py +114 -44
- morecompute/execution/worker.py +319 -107
- morecompute/notebook.py +65 -6
- morecompute/server.py +76 -44
- morecompute/services/pod_manager.py +2 -2
- morecompute/utils/cell_magics.py +35 -4
- morecompute/utils/config_util.py +47 -31
- morecompute/utils/notebook_converter.py +129 -0
- morecompute/utils/py_percent_parser.py +190 -0
- morecompute/utils/special_commands.py +126 -49
- frontend/.DS_Store +0 -0
- {more_compute-0.3.2.dist-info → more_compute-0.4.0.dist-info}/WHEEL +0 -0
- {more_compute-0.3.2.dist-info → more_compute-0.4.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.3.2.dist-info → more_compute-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {more_compute-0.3.2.dist-info → more_compute-0.4.0.dist-info}/top_level.txt +0 -0
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
708
|
+
color: var(--mc-line-number-color);
|
|
707
709
|
}
|
|
708
710
|
|
|
709
711
|
.execution-time {
|
|
710
712
|
font-size: 9px;
|
|
711
|
-
color:
|
|
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
|
|
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
|
}
|
frontend/components/Notebook.tsx
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
273
|
-
onInterrupt(indexRef.current);
|
|
274
|
-
} else {
|
|
275
|
-
onExecute(indexRef.current);
|
|
276
|
-
}
|
|
273
|
+
onExecute(indexRef.current);
|
|
277
274
|
}
|
|
278
|
-
}, [cell.cell_type,
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
e
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
7
|
+
destination: 'http://localhost:3141/ws/:path*',
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
source: '/api/:path*',
|
|
11
|
-
destination: 'http://localhost:
|
|
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
|
|
36
|
-
chosen_port = int(os.getenv("MORECOMPUTE_PORT", "
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
409
|
+
# File exists, check extension
|
|
410
|
+
if notebook_path.suffix == '.ipynb':
|
|
394
411
|
raise ValueError(
|
|
395
|
-
f"Error:
|
|
396
|
-
f"
|
|
397
|
-
f"
|
|
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
|
-
|
|
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 .
|
|
404
|
-
f"Notebook files must end with .
|
|
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}.
|
|
407
|
-
f"Or to create a new notebook
|
|
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}.
|
|
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
|
-
|
|
560
|
+
print_help()
|
|
561
|
+
sys.exit(0)
|
|
433
562
|
|
|
434
563
|
notebook_path = Path(raw_notebook_path).expanduser().resolve()
|
|
435
|
-
|
|
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,
|