more-compute 0.4.2__py3-none-any.whl → 0.4.4__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.
@@ -74,6 +74,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
74
74
  const [connectionHealth, setConnectionHealth] = useState<"healthy" | "unhealthy" | "unknown">("unknown");
75
75
  const [searchQuery, setSearchQuery] = useState("");
76
76
  const [discoveredPod, setDiscoveredPod] = useState<GPUPod | null>(null);
77
+ const [showApiKeyInput, setShowApiKeyInput] = useState(false);
77
78
 
78
79
  // Filter popup state
79
80
  const [showFilterPopup, setShowFilterPopup] = useState(false);
@@ -218,6 +219,16 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
218
219
  setPods(pods);
219
220
  } catch (err) {
220
221
  console.error("Failed to load GPU pods:", err);
222
+ // If 401 error, API key is invalid - allow user to reset it
223
+ if (err instanceof Error && err.message.includes("401")) {
224
+ setApiConfigured(false);
225
+ setShowApiKeyInput(true);
226
+ setErrorModal({
227
+ isOpen: true,
228
+ title: "Invalid API Key",
229
+ message: "Your API key is invalid or has expired. Please enter a new API key to continue.",
230
+ });
231
+ }
221
232
  } finally {
222
233
  setLoading(false);
223
234
  }
@@ -561,8 +572,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
561
572
  try {
562
573
  await setGpuApiKey(apiKey);
563
574
  setApiConfigured(true);
575
+ setShowApiKeyInput(false);
564
576
  setApiKey("");
565
577
  await loadGPUPods();
578
+ await loadAvailableGPUs();
566
579
  } catch (err) {
567
580
  setSaveError(
568
581
  err instanceof Error ? err.message : "Failed to save API key",
@@ -681,26 +694,43 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
681
694
  <h3 className="runtime-section-title" style={{ fontSize: "12px", fontWeight: 500 }}>
682
695
  Remote GPU Pods
683
696
  </h3>
684
- {apiConfigured && (
685
- <button
686
- className="runtime-btn runtime-btn-secondary"
687
- onClick={handleConnectToPrimeIntellect}
688
- style={{
689
- fontSize: "11px",
690
- padding: "6px 12px",
691
- backgroundColor: "#000",
692
- color: "white",
693
- border: "none",
694
- borderRadius: "8px",
695
- cursor: "pointer"
696
- }}
697
- >
698
- Manage
699
- </button>
697
+ {apiConfigured && !showApiKeyInput && (
698
+ <div style={{ display: "flex", gap: "6px" }}>
699
+ <button
700
+ className="runtime-btn runtime-btn-secondary"
701
+ onClick={() => setShowApiKeyInput(true)}
702
+ style={{
703
+ fontSize: "11px",
704
+ padding: "6px 12px",
705
+ backgroundColor: "var(--text-secondary)",
706
+ color: "white",
707
+ border: "none",
708
+ borderRadius: "8px",
709
+ cursor: "pointer"
710
+ }}
711
+ >
712
+ Reset API Key
713
+ </button>
714
+ <button
715
+ className="runtime-btn runtime-btn-secondary"
716
+ onClick={handleConnectToPrimeIntellect}
717
+ style={{
718
+ fontSize: "11px",
719
+ padding: "6px 12px",
720
+ backgroundColor: "var(--mc-text-color)",
721
+ color: "var(--mc-cell-background)",
722
+ border: "none",
723
+ borderRadius: "8px",
724
+ cursor: "pointer"
725
+ }}
726
+ >
727
+ Manage
728
+ </button>
729
+ </div>
700
730
  )}
701
731
  </div>
702
732
 
703
- {apiConfigured === false ? (
733
+ {(apiConfigured === false || showApiKeyInput) ? (
704
734
  <div className="runtime-empty-state" style={{ padding: "6px" }}>
705
735
  <p
706
736
  style={{
@@ -750,8 +780,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
750
780
  flex: 1,
751
781
  fontSize: "11px",
752
782
  padding: "6px 12px",
753
- backgroundColor: "#000",
754
- color: "white",
783
+ backgroundColor: "var(--mc-text-color)",
784
+ color: "var(--mc-cell-background)",
755
785
  border: "none",
756
786
  borderRadius: "8px",
757
787
  cursor: "pointer"
@@ -765,8 +795,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
765
795
  style={{
766
796
  fontSize: "11px",
767
797
  padding: "6px 12px",
768
- backgroundColor: "#000",
769
- color: "white",
798
+ backgroundColor: "var(--mc-text-color)",
799
+ color: "var(--mc-cell-background)",
770
800
  border: "none",
771
801
  borderRadius: "8px",
772
802
  cursor: "pointer"
@@ -788,19 +818,19 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
788
818
  ) : !connectedPodId && discoveredPod ? (
789
819
  <div style={{ padding: "8px 0" }}>
790
820
  <div style={{
791
- backgroundColor: "#fff3cd",
792
- border: "1px solid #ffc107",
821
+ backgroundColor: "rgba(251, 191, 36, 0.1)",
822
+ border: "1px solid rgba(251, 191, 36, 0.3)",
793
823
  borderRadius: "8px",
794
824
  padding: "12px",
795
825
  marginBottom: "8px"
796
826
  }}>
797
- <div style={{ fontSize: "11px", fontWeight: 600, color: "#856404", marginBottom: "4px" }}>
827
+ <div style={{ fontSize: "11px", fontWeight: 600, color: "var(--mc-text-color)", marginBottom: "4px" }}>
798
828
  ⚠️ Running Pod Detected
799
829
  </div>
800
- <div style={{ fontSize: "10px", color: "#856404", marginBottom: "8px" }}>
830
+ <div style={{ fontSize: "10px", color: "var(--text-secondary)", marginBottom: "8px" }}>
801
831
  Found a running pod but not connected (backend may have restarted). This pod is still costing money!
802
832
  </div>
803
- <div style={{ fontSize: "10px", marginBottom: "8px" }}>
833
+ <div style={{ fontSize: "10px", marginBottom: "8px", color: "var(--mc-text-color)" }}>
804
834
  <span style={{ fontWeight: 500 }}>{discoveredPod.name}</span> • {discoveredPod.gpuType} • ${discoveredPod.costPerHour.toFixed(2)}/hour
805
835
  </div>
806
836
  <div style={{ display: "flex", gap: "6px" }}>
@@ -811,8 +841,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
811
841
  flex: 1,
812
842
  fontSize: "10px",
813
843
  padding: "6px 12px",
814
- backgroundColor: "#ffc107",
815
- color: "#000",
844
+ backgroundColor: "rgba(251, 191, 36, 0.8)",
845
+ color: "var(--mc-text-color)",
816
846
  border: "none",
817
847
  borderRadius: "8px",
818
848
  cursor: "pointer",
@@ -828,7 +858,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
828
858
  style={{
829
859
  fontSize: "10px",
830
860
  padding: "6px 12px",
831
- backgroundColor: "#dc2626",
861
+ backgroundColor: "rgba(220, 38, 38, 0.9)",
832
862
  color: "white",
833
863
  border: "none",
834
864
  borderRadius: "8px",
@@ -855,7 +885,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
855
885
  <span style={{
856
886
  marginLeft: "6px",
857
887
  fontSize: "9px",
858
- backgroundColor: "#10b981",
888
+ backgroundColor: "rgba(16, 185, 129, 0.9)",
859
889
  color: "white",
860
890
  padding: "2px 6px",
861
891
  borderRadius: "4px"
@@ -877,8 +907,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
877
907
  style={{
878
908
  fontSize: "10px",
879
909
  padding: "6px 12px",
880
- backgroundColor: "#000",
881
- color: "white",
910
+ backgroundColor: "var(--mc-text-color)",
911
+ color: "var(--mc-cell-background)",
882
912
  border: "none",
883
913
  borderRadius: "8px",
884
914
  cursor: "pointer"
@@ -893,7 +923,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
893
923
  style={{
894
924
  fontSize: "10px",
895
925
  padding: "6px 12px",
896
- backgroundColor: "#dc2626",
926
+ backgroundColor: "rgba(220, 38, 38, 0.9)",
897
927
  color: "white",
898
928
  border: "none",
899
929
  borderRadius: "8px",
@@ -912,7 +942,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
912
942
  style={{
913
943
  fontSize: "10px",
914
944
  padding: "6px 12px",
915
- backgroundColor: "#10b981",
945
+ backgroundColor: "rgba(16, 185, 129, 0.9)",
916
946
  color: "white",
917
947
  border: "none",
918
948
  borderRadius: "8px",
@@ -928,7 +958,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
928
958
  style={{
929
959
  fontSize: "10px",
930
960
  padding: "6px 12px",
931
- backgroundColor: "#dc2626",
961
+ backgroundColor: "rgba(220, 38, 38, 0.9)",
932
962
  color: "white",
933
963
  border: "none",
934
964
  borderRadius: "8px",
@@ -975,10 +1005,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
975
1005
  flex: 1,
976
1006
  padding: "6px 12px",
977
1007
  fontSize: "11px",
978
- border: "1px solid #d1d5db",
1008
+ border: "1px solid var(--mc-border)",
979
1009
  borderRadius: "8px",
980
- backgroundColor: "var(--background)",
981
- color: "var(--text)",
1010
+ backgroundColor: "var(--mc-background)",
1011
+ color: "var(--mc-text-color)",
982
1012
  outline: "none",
983
1013
  }}
984
1014
  />
@@ -989,8 +1019,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
989
1019
  padding: "6px 12px",
990
1020
  fontSize: "11px",
991
1021
  position: "relative",
992
- backgroundColor: "#000",
993
- color: "white",
1022
+ backgroundColor: "var(--mc-text-color)",
1023
+ color: "var(--mc-cell-background)",
994
1024
  border: "none",
995
1025
  borderRadius: "8px",
996
1026
  cursor: "pointer"
@@ -1119,8 +1149,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
1119
1149
  fontSize: "10px",
1120
1150
  padding: "6px 16px",
1121
1151
  whiteSpace: "nowrap",
1122
- backgroundColor: "#000",
1123
- color: "white",
1152
+ backgroundColor: "var(--mc-text-color)",
1153
+ color: "var(--mc-cell-background)",
1124
1154
  border: "none",
1125
1155
  borderRadius: "8px",
1126
1156
  cursor: "pointer"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: more-compute
3
- Version: 0.4.2
3
+ Version: 0.4.4
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
@@ -28,7 +28,7 @@ frontend/components/modals/SuccessModal.tsx,sha256=7NVg0MFPVvsBGDOHPVyZTNx4kmZLH
28
28
  frontend/components/output/CellOutput.tsx,sha256=KLRzwEchvBoeoai8TbabKEwO4tHmog3EKeTJAXtmveI,3665
29
29
  frontend/components/output/ErrorDisplay.tsx,sha256=va_jMO9mZnrjOWxVZ7nwL1_7-Ii9I5JVwEwAZR-jsNU,5535
30
30
  frontend/components/output/MarkdownRenderer.tsx,sha256=RtZ5yNRxDXIh_NkNsNiy30wMGIW7V1gfhsjecgMdc80,3341
31
- frontend/components/popups/ComputePopup.tsx,sha256=B7AwwjpJ9_nNI5qSN53jdR2tAulB5yl7spjMXjH4yDM,44021
31
+ frontend/components/popups/ComputePopup.tsx,sha256=JPdJdl9VVIdVf6cRQjiH56TfqmFIyV5b1ZtXNP6ofik,45742
32
32
  frontend/components/popups/FilterPopup.tsx,sha256=KRIzRvVxEF47ELWIAvrqv7BZQYmEo_zOX8Nbpz0zEik,14133
33
33
  frontend/components/popups/FolderPopup.tsx,sha256=V2tDAbztvNIUyPWtFiwjeIoCmFGQyDosQgST_JsAzLo,5215
34
34
  frontend/components/popups/MetricsPopup.tsx,sha256=ublYbOQ-pSU_w48F10uFjDYctysEwmri9lrji5YHIC4,6034
@@ -63,11 +63,11 @@ frontend/public/fonts/Fira.ttf,sha256=dbSM4W7Drd9n_EkfXq8P31KuxbjZ1wtuFiZ8aFebvT
63
63
  frontend/public/fonts/Tiempos.woff2,sha256=h83bJKvAK301wXCMIvK7ZG5j0H2K3tzAfgo0yk4z8OE,13604
64
64
  frontend/public/fonts/VeraMono.ttf,sha256=2kKB3H2xej385kpiztkodcWJU0AFXsi6JKORTrl7NJ0,49224
65
65
  frontend/types/notebook.ts,sha256=v23RaZe6H3lU5tq6sqnJDPxC2mu0NZFDCJfiN0mgvSs,1359
66
- more_compute-0.4.2.dist-info/licenses/LICENSE,sha256=UQuJtJ2v98w8mXozOPmE8nkNc_8BFY-SVVBgTA327z0,1067
66
+ more_compute-0.4.4.dist-info/licenses/LICENSE,sha256=UQuJtJ2v98w8mXozOPmE8nkNc_8BFY-SVVBgTA327z0,1067
67
67
  morecompute/__init__.py,sha256=pcMVq8Q7qb42AOn7tqgoZJOi3epDDBnEriiv2WVKnXY,87
68
- morecompute/__version__.py,sha256=6hfVa12Q-nXyUEXr6SyKpqPEDJW6vlRHyPxlA27PfTs,22
68
+ morecompute/__version__.py,sha256=6G_giX6Ucuweo7w5OiftoXmbNLoqiU_soXJoU8aiLmY,22
69
69
  morecompute/cli.py,sha256=kVvzvPBqF8xO6UuhU_-TBn99nKwJ405R2mAS6zU0KBc,734
70
- morecompute/notebook.py,sha256=RubIXps925vCFgHznkW0QvvW0I11p6IGDDOrhnZ4jYs,6824
70
+ morecompute/notebook.py,sha256=KGHORGAAZahT-TyX9HYuSc-b6TmfGgoi92FhRMK--Yg,7730
71
71
  morecompute/process_worker.py,sha256=KsE3r-XpkYGuyO4w3t54VKkD51LfNHAZc3TYattMtrg,7185
72
72
  morecompute/server.py,sha256=mcJwIvT9QHfpKuAq5jP2SW5hPjFB_rySevgQvOHuud8,41689
73
73
  morecompute/execution/__init__.py,sha256=jPmBmq8BZWbUEY9XFSpqt5FkgX04uNS10WnUlr7Rnms,134
@@ -88,16 +88,16 @@ morecompute/utils/cell_magics.py,sha256=YLxltTBprCA8jfgsjqf7Shnk4NCmGKIp3aV5_CYk
88
88
  morecompute/utils/config_util.py,sha256=I90om4Wf4BODc5Gy9u8Tu34AcqpkfkGAKQO6JE_NMzU,1940
89
89
  morecompute/utils/error_utils.py,sha256=e50WLFdD6ngIC30xAgrzdTYtD8tPOIFkKAAh_sPbK0I,11667
90
90
  morecompute/utils/line_magics.py,sha256=kTutYBPAWoURY_pk8HXQ38IP712M2rBBfUg3oN8VrP0,33740
91
- morecompute/utils/notebook_converter.py,sha256=aI9JdgPO8zpE0vYcBGlMEvQT2rLmZtjP_VD5bUn80bY,4229
91
+ morecompute/utils/notebook_converter.py,sha256=_V-iG060Rr04oqk__cg1LbfV3C7uR1Rsh504WNZ218Q,9756
92
92
  morecompute/utils/notebook_util.py,sha256=3hH94dtXvhizRVTU9a2b38m_51Y4igoXpkjAXUqpVBQ,1353
93
- morecompute/utils/py_percent_parser.py,sha256=_CVB7uP8Qu8X_yIGKv-k3Lhpw7saH67VofYvRDfGGw8,5087
93
+ morecompute/utils/py_percent_parser.py,sha256=c2AUOcXjGxmBWaYy0kk-pbNVzs-6YFagMVuVGQlYjY0,5388
94
94
  morecompute/utils/python_environment_util.py,sha256=l8WWWPwKbypknw8GwL22NXCji5i1FOy1vWG47J6og4g,7441
95
95
  morecompute/utils/shell_utils.py,sha256=fGFLhQLZU-lmMGALbbS-fKPkhQtmMhZ1FkgQ3TeoFhA,1917
96
96
  morecompute/utils/special_commands.py,sha256=nf2nVea5SyFqpulNYrnRx7anU4-G-e5_kLJ0PLtEi3Q,22030
97
97
  morecompute/utils/system_environment_util.py,sha256=32mQRubo0i4X61o-825T7m-eUSidcEp07qkInP1sWZA,4774
98
98
  morecompute/utils/zmq_util.py,sha256=tx7-iS04UN69OFtBzkxcEnRhT7xtI9EzRnrZ_nsH_O0,1889
99
- more_compute-0.4.2.dist-info/METADATA,sha256=_1hYq-pUsMBcYIn1zmoEZJFU1DLsk_0cuCjU32iJeSs,3869
100
- more_compute-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
101
- more_compute-0.4.2.dist-info/entry_points.txt,sha256=xp7z9eRPNRM4oxkZZVlyXkhkSjN1AjoYI_B7qpDJ1bI,49
102
- more_compute-0.4.2.dist-info/top_level.txt,sha256=Tamm6ADzjwaQa1z27O7Izcyhyt9f0gVjMv1_tC810aI,32
103
- more_compute-0.4.2.dist-info/RECORD,,
99
+ more_compute-0.4.4.dist-info/METADATA,sha256=Lnqd5Wjvhts-cYxKqupUNzzh0ZphJJutprJ0QX3ycy8,3869
100
+ more_compute-0.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
101
+ more_compute-0.4.4.dist-info/entry_points.txt,sha256=xp7z9eRPNRM4oxkZZVlyXkhkSjN1AjoYI_B7qpDJ1bI,49
102
+ more_compute-0.4.4.dist-info/top_level.txt,sha256=Tamm6ADzjwaQa1z27O7Izcyhyt9f0gVjMv1_tC810aI,32
103
+ more_compute-0.4.4.dist-info/RECORD,,
@@ -1 +1 @@
1
- __version__ = "0.4.2"
1
+ __version__ = "0.4.4"
morecompute/notebook.py CHANGED
@@ -3,6 +3,7 @@ from pathlib import Path
3
3
  from typing import List, Dict, Any
4
4
  from uuid import uuid4
5
5
  from .utils.py_percent_parser import parse_py_percent, generate_py_percent
6
+ from .utils.notebook_converter import detect_colab_format, parse_colab_py
6
7
 
7
8
  class Notebook:
8
9
  """Manages the state of a notebook's cells."""
@@ -92,11 +93,29 @@ class Notebook:
92
93
 
93
94
  # Check file extension
94
95
  if path.suffix == '.py':
95
- # Load .py file with py:percent format
96
+ # Load .py file - auto-detect format
96
97
  with open(file_path, 'r', encoding='utf-8') as f:
97
98
  content = f.read()
98
99
 
99
- data = parse_py_percent(content)
100
+ # Detect if it's Colab format (docstrings) or py:percent format (# %%)
101
+ if detect_colab_format(content):
102
+ print(f" Detected Colab format - auto-converting to py:percent")
103
+ # Parse Colab format (docstrings as markdown)
104
+ loaded_cells = parse_colab_py(content)
105
+ data = {
106
+ 'cells': loaded_cells,
107
+ 'metadata': {
108
+ 'kernelspec': {
109
+ 'display_name': 'Python 3',
110
+ 'language': 'python',
111
+ 'name': 'python3'
112
+ }
113
+ }
114
+ }
115
+ else:
116
+ # Parse py:percent format (# %% markers)
117
+ data = parse_py_percent(content)
118
+
100
119
  loaded_cells = data.get('cells', [])
101
120
 
102
121
  # Ensure stable IDs for all cells
@@ -3,7 +3,7 @@
3
3
  import json
4
4
  import re
5
5
  from pathlib import Path
6
- from typing import List, Set
6
+ from typing import List, Set, Dict
7
7
  from .py_percent_parser import generate_py_percent, parse_py_percent
8
8
 
9
9
 
@@ -97,10 +97,157 @@ def convert_ipynb_to_py(ipynb_path: Path, output_path: Path, include_uv_deps: bo
97
97
  print(f" Run with: more-compute {output_path.name}")
98
98
 
99
99
 
100
+ def detect_colab_format(content: str) -> bool:
101
+ """
102
+ Detect if a .py file is in Colab export format (docstrings as markdown).
103
+
104
+ Args:
105
+ content: Raw .py file content
106
+
107
+ Returns:
108
+ True if appears to be Colab format, False otherwise
109
+ """
110
+ # Check for actual cell markers (# %% at start of line)
111
+ # Must be ONLY # %% optionally followed by [markdown] or whitespace
112
+ # NOT # %%capture, # %%time, etc. (IPython magics)
113
+ has_cell_markers = bool(re.search(r'^\s*# %%\s*(?:\[markdown\])?\s*$', content, re.MULTILINE))
114
+
115
+ # If it has # %% markers, it's NOT Colab format (it's py:percent)
116
+ # Even if it has a Colab header comment!
117
+ if has_cell_markers:
118
+ return False
119
+
120
+ # Check for multi-line docstrings (Colab's markdown format)
121
+ has_docstrings = '"""' in content
122
+
123
+ # Colab format: has docstrings and no cell markers
124
+ return has_docstrings
125
+
126
+
127
+ def parse_colab_py(content: str) -> List[Dict]:
128
+ """
129
+ Parse Colab-exported .py file into cell structure.
130
+
131
+ Colab format uses:
132
+ - Multi-line docstrings ('''..''' or \"\"\"...\"\"\") for markdown cells
133
+ - Regular Python code for code cells
134
+
135
+ Args:
136
+ content: Raw .py file content from Colab export
137
+
138
+ Returns:
139
+ List of cell dicts with 'cell_type' and 'source'
140
+ """
141
+ cells = []
142
+
143
+ # Split on docstring boundaries
144
+ # Pattern matches both ''' and """ with optional content
145
+ pattern = r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\')'
146
+ parts = re.split(pattern, content)
147
+
148
+ for part in parts:
149
+ part = part.strip()
150
+ if not part:
151
+ continue
152
+
153
+ # Check if this is a docstring (markdown)
154
+ if (part.startswith('"""') and part.endswith('"""')) or \
155
+ (part.startswith("'''") and part.endswith("'''")):
156
+ # It's a markdown cell
157
+ # Remove the triple quotes
158
+ markdown_content = part[3:-3].strip()
159
+
160
+ # Skip empty markdown cells
161
+ if markdown_content:
162
+ cells.append({
163
+ 'cell_type': 'markdown',
164
+ 'source': markdown_content,
165
+ 'metadata': {}
166
+ })
167
+ else:
168
+ # It's a code cell
169
+ # Skip commented out code and special markers
170
+ if part and not part.startswith('# Commented out IPython magic'):
171
+ cells.append({
172
+ 'cell_type': 'code',
173
+ 'source': part,
174
+ 'metadata': {},
175
+ 'execution_count': None,
176
+ 'outputs': []
177
+ })
178
+
179
+ return cells
180
+
181
+
182
+ def convert_colab_py_to_py_percent(input_path: Path, output_path: Path, include_uv_deps: bool = True) -> None:
183
+ """
184
+ Convert Colab-exported .py file to py:percent format (# %% markers).
185
+
186
+ Args:
187
+ input_path: Path to Colab .py file
188
+ output_path: Path to output .py file with # %% markers
189
+ include_uv_deps: Whether to extract and add UV inline script dependencies
190
+ """
191
+ # Read Colab .py file
192
+ with open(input_path, 'r', encoding='utf-8') as f:
193
+ content = f.read()
194
+
195
+ # Detect format
196
+ if not detect_colab_format(content):
197
+ print(f"Warning: {input_path.name} doesn't appear to be in Colab format")
198
+ print(f" (Already has # %% markers or missing docstrings)")
199
+ return
200
+
201
+ # Parse Colab format into cells
202
+ cells = parse_colab_py(content)
203
+
204
+ if not cells:
205
+ print(f"Error: No cells found in {input_path.name}")
206
+ return
207
+
208
+ # Extract dependencies if requested
209
+ header_lines = []
210
+ if include_uv_deps:
211
+ # Create a temporary notebook structure to extract dependencies
212
+ temp_notebook = {'cells': cells}
213
+ dependencies = extract_pip_dependencies(temp_notebook)
214
+
215
+ if dependencies:
216
+ header_lines.append('# /// script')
217
+ header_lines.append('# dependencies = [')
218
+ for dep in sorted(dependencies):
219
+ header_lines.append(f'# "{dep}",')
220
+ header_lines.append('# ]')
221
+ header_lines.append('# ///')
222
+ header_lines.append('')
223
+
224
+ # Generate py:percent format
225
+ py_content = generate_py_percent(cells)
226
+
227
+ # Combine header and content
228
+ if header_lines:
229
+ final_content = '\n'.join(header_lines) + '\n' + py_content
230
+ else:
231
+ final_content = py_content
232
+
233
+ # Write output
234
+ with open(output_path, 'w', encoding='utf-8') as f:
235
+ f.write(final_content)
236
+
237
+ print(f"✓ Converted Colab format {input_path.name} → {output_path.name}")
238
+ print(f" Format: Colab docstrings → py:percent (# %%) markers")
239
+
240
+ if include_uv_deps and dependencies:
241
+ print(f" Found dependencies: {', '.join(sorted(dependencies))}")
242
+ print(f" Run with: more-compute {output_path.name}")
243
+
244
+
100
245
  def convert_py_to_ipynb(py_path: Path, output_path: Path) -> None:
101
246
  """
102
247
  Convert .py notebook to .ipynb format.
103
248
 
249
+ Automatically detects format (Colab, VSCode, JupyterLab).
250
+
104
251
  Args:
105
252
  py_path: Path to input .py file
106
253
  output_path: Path to output .ipynb file
@@ -109,8 +256,29 @@ def convert_py_to_ipynb(py_path: Path, output_path: Path) -> None:
109
256
  with open(py_path, 'r', encoding='utf-8') as f:
110
257
  py_content = f.read()
111
258
 
112
- # Parse py:percent format to notebook structure
113
- notebook_data = parse_py_percent(py_content)
259
+ # Detect format and parse accordingly
260
+ if detect_colab_format(py_content):
261
+ # Parse Colab format (docstrings as markdown)
262
+ cells = parse_colab_py(py_content)
263
+ notebook_data = {
264
+ 'cells': cells,
265
+ 'metadata': {
266
+ 'kernelspec': {
267
+ 'display_name': 'Python 3',
268
+ 'language': 'python',
269
+ 'name': 'python3'
270
+ },
271
+ 'language_info': {
272
+ 'name': 'python',
273
+ 'version': '3.8.0'
274
+ }
275
+ },
276
+ 'nbformat': 4,
277
+ 'nbformat_minor': 4
278
+ }
279
+ else:
280
+ # Parse py:percent format (# %% or # In[N]:)
281
+ notebook_data = parse_py_percent(py_content)
114
282
 
115
283
  # Ensure source is in list format (Jupyter notebook standard)
116
284
  for cell in notebook_data.get('cells', []):
@@ -8,6 +8,10 @@ def parse_py_percent(content: str) -> Dict:
8
8
  """
9
9
  Parse py:percent format Python file into notebook structure.
10
10
 
11
+ Supports multiple formats:
12
+ # %% - VSCode/PyCharm format
13
+ # In[N]: - JupyterLab export format
14
+
11
15
  Format:
12
16
  # %%
13
17
  code cell content
@@ -23,9 +27,15 @@ def parse_py_percent(content: str) -> Dict:
23
27
  """
24
28
  cells = []
25
29
 
26
- # Split by cell markers (# %%)
27
- # Keep the marker in the split to determine cell type
28
- parts = re.split(r'(# %%.*?\n)', content)
30
+ # Check if using JupyterLab In[] format
31
+ has_in_markers = bool(re.search(r'# In\[\d+\]:', content))
32
+
33
+ if has_in_markers:
34
+ # Parse JupyterLab # In[N]: format
35
+ parts = re.split(r'(# In\[\d+\]:.*?\n)', content)
36
+ else:
37
+ # Parse VSCode # %% format
38
+ parts = re.split(r'(# %%.*?\n)', content)
29
39
 
30
40
  # First part before any cell marker (usually imports/metadata)
31
41
  if parts[0].strip():