more-compute 0.3.3__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 +9 -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 +107 -17
- {more_compute-0.3.3.dist-info → more_compute-0.4.0.dist-info}/METADATA +30 -28
- {more_compute-0.3.3.dist-info → more_compute-0.4.0.dist-info}/RECORD +25 -24
- morecompute/__version__.py +1 -1
- morecompute/execution/executor.py +113 -44
- morecompute/execution/worker.py +319 -107
- morecompute/notebook.py +65 -6
- morecompute/server.py +72 -40
- morecompute/utils/cell_magics.py +35 -4
- 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.3.dist-info → more_compute-0.4.0.dist-info}/WHEEL +0 -0
- {more_compute-0.3.3.dist-info → more_compute-0.4.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.3.3.dist-info → more_compute-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {more_compute-0.3.3.dist-info → more_compute-0.4.0.dist-info}/top_level.txt +0 -0
frontend/app/globals.css
CHANGED
|
@@ -643,7 +643,8 @@ body {
|
|
|
643
643
|
}
|
|
644
644
|
|
|
645
645
|
.markdown-rendered code {
|
|
646
|
-
background:
|
|
646
|
+
background: var(--mc-secondary);
|
|
647
|
+
color: var(--mc-text-color);
|
|
647
648
|
padding: 2px 4px;
|
|
648
649
|
border-radius: 3px;
|
|
649
650
|
font-family: 'Fira', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
@@ -651,7 +652,7 @@ body {
|
|
|
651
652
|
}
|
|
652
653
|
|
|
653
654
|
.markdown-rendered pre {
|
|
654
|
-
background:
|
|
655
|
+
background: var(--mc-secondary);
|
|
655
656
|
padding: 12px;
|
|
656
657
|
border-radius: 6px;
|
|
657
658
|
margin: 12px 0;
|
|
@@ -665,7 +666,7 @@ body {
|
|
|
665
666
|
}
|
|
666
667
|
|
|
667
668
|
.markdown-rendered blockquote {
|
|
668
|
-
border-left: 3px solid
|
|
669
|
+
border-left: 3px solid var(--mc-border);
|
|
669
670
|
padding-left: 12px;
|
|
670
671
|
margin: 12px 0;
|
|
671
672
|
color: var(--mc-markdown-paragraph-color);
|
|
@@ -684,7 +685,7 @@ body {
|
|
|
684
685
|
}
|
|
685
686
|
|
|
686
687
|
.markdown-rendered .empty-markdown {
|
|
687
|
-
color:
|
|
688
|
+
color: var(--mc-line-number-color);
|
|
688
689
|
font-style: italic;
|
|
689
690
|
}
|
|
690
691
|
|
|
@@ -697,19 +698,20 @@ body {
|
|
|
697
698
|
flex-direction: column;
|
|
698
699
|
align-items: center;
|
|
699
700
|
font-size: 11px;
|
|
700
|
-
color:
|
|
701
|
+
color: var(--mc-line-number-color);
|
|
701
702
|
width: 30px;
|
|
702
703
|
}
|
|
703
704
|
|
|
704
705
|
.execution-count {
|
|
705
706
|
font-family: 'Fira', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
706
707
|
font-size: 10px;
|
|
707
|
-
color:
|
|
708
|
+
color: var(--mc-line-number-color);
|
|
708
709
|
}
|
|
709
710
|
|
|
710
711
|
.execution-time {
|
|
711
712
|
font-size: 9px;
|
|
712
|
-
color:
|
|
713
|
+
color: var(--mc-line-number-color);
|
|
714
|
+
opacity: 0.7;
|
|
713
715
|
margin-top: 2px;
|
|
714
716
|
}
|
|
715
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
|
@@ -33,8 +33,8 @@ class NotebookLauncher:
|
|
|
33
33
|
def start_backend(self):
|
|
34
34
|
"""Start the FastAPI backend server"""
|
|
35
35
|
try:
|
|
36
|
-
# Force a stable port (default
|
|
37
|
-
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"))
|
|
38
38
|
self._ensure_port_available(chosen_port)
|
|
39
39
|
cmd = [
|
|
40
40
|
sys.executable,
|
|
@@ -406,21 +406,40 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
406
406
|
|
|
407
407
|
def ensure_notebook_exists(notebook_path: Path):
|
|
408
408
|
if notebook_path.exists():
|
|
409
|
-
|
|
409
|
+
# File exists, check extension
|
|
410
|
+
if notebook_path.suffix == '.ipynb':
|
|
410
411
|
raise ValueError(
|
|
411
|
-
f"Error:
|
|
412
|
-
f"
|
|
413
|
-
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"
|
|
414
423
|
)
|
|
415
424
|
return
|
|
416
425
|
|
|
417
|
-
|
|
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':
|
|
418
437
|
raise ValueError(
|
|
419
|
-
f"Error: '{notebook_path}' does not have the .
|
|
420
|
-
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"
|
|
421
440
|
f"Did you mean?\n"
|
|
422
|
-
f" more-compute {notebook_path}.
|
|
423
|
-
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"
|
|
424
443
|
f" more-compute new"
|
|
425
444
|
)
|
|
426
445
|
|
|
@@ -431,14 +450,18 @@ def ensure_notebook_exists(notebook_path: Path):
|
|
|
431
450
|
|
|
432
451
|
def print_help():
|
|
433
452
|
"""Print concise help message"""
|
|
434
|
-
print(f"""Usage: more-compute [OPTIONS] [NOTEBOOK]
|
|
453
|
+
print(f"""Usage: more-compute [OPTIONS] [COMMAND] [NOTEBOOK]
|
|
435
454
|
|
|
436
|
-
MoreCompute -
|
|
455
|
+
MoreCompute - Python notebooks with GPU compute
|
|
437
456
|
|
|
438
457
|
Getting started:
|
|
439
458
|
|
|
440
459
|
* more-compute new create a new notebook with timestamp
|
|
441
|
-
* more-compute notebook.
|
|
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
|
|
442
465
|
|
|
443
466
|
Options:
|
|
444
467
|
--version, -v Show version and exit
|
|
@@ -446,12 +469,72 @@ Options:
|
|
|
446
469
|
--help, -h Show this message and exit
|
|
447
470
|
|
|
448
471
|
Environment variables:
|
|
449
|
-
MORECOMPUTE_PORT Backend port (default:
|
|
472
|
+
MORECOMPUTE_PORT Backend port (default: 3141)
|
|
450
473
|
MORECOMPUTE_FRONTEND_PORT Frontend port (default: 2718)
|
|
451
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
|
|
452
478
|
""")
|
|
453
479
|
|
|
454
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
|
|
455
538
|
parser = build_parser()
|
|
456
539
|
args = parser.parse_args(argv)
|
|
457
540
|
|
|
@@ -462,10 +545,11 @@ def main(argv=None):
|
|
|
462
545
|
|
|
463
546
|
raw_notebook_path = args.notebook_path
|
|
464
547
|
|
|
548
|
+
# Handle "new" command
|
|
465
549
|
if raw_notebook_path == "new":
|
|
466
550
|
from datetime import datetime
|
|
467
551
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
468
|
-
raw_notebook_path = f"notebook_{timestamp}.
|
|
552
|
+
raw_notebook_path = f"notebook_{timestamp}.py"
|
|
469
553
|
print(f"Creating new notebook: {raw_notebook_path}")
|
|
470
554
|
|
|
471
555
|
notebook_path_env = os.getenv("MORECOMPUTE_NOTEBOOK_PATH")
|
|
@@ -477,7 +561,13 @@ def main(argv=None):
|
|
|
477
561
|
sys.exit(0)
|
|
478
562
|
|
|
479
563
|
notebook_path = Path(raw_notebook_path).expanduser().resolve()
|
|
480
|
-
|
|
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)
|
|
481
571
|
|
|
482
572
|
launcher = NotebookLauncher(
|
|
483
573
|
notebook_path=notebook_path,
|