alyxruntime 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/PKG-INFO +1 -1
- alyxruntime-0.4.0/alyxruntime/ui/index.html +651 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime.egg-info/PKG-INFO +1 -1
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/pyproject.toml +1 -1
- alyxruntime-0.3.0/alyxruntime/ui/index.html +0 -99
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/README.md +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/__init__.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/__main__.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/cli.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/client.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/config.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/hardware.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/server.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/tunnel.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime/webui.py +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime.egg-info/SOURCES.txt +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime.egg-info/dependency_links.txt +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime.egg-info/entry_points.txt +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime.egg-info/requires.txt +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/alyxruntime.egg-info/top_level.txt +0 -0
- {alyxruntime-0.3.0 → alyxruntime-0.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alyxruntime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Use any remote machine's CPU/GPU as a Jupyter runtime over a Cloudflare tunnel, Colab-style, from the command line or Python.
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset=utf-8>
|
|
5
|
+
<meta name=viewport content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>alyxruntime notebook</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{--bg:#fff;--fg:#202124;--mut:#5f6368;--bd:#dadce0;--code:#f8f9fa;--hov:#f1f3f4;--pri:#1a73e8;--err:#d93025}
|
|
9
|
+
|
|
10
|
+
body.dark{--bg:#1f1f1f;--fg:#e8eaed;--mut:#9aa0a6;--bd:#3c4043;--code:#2a2a2a;--hov:#303134;--pri:#8ab4f8;--err:#f28b82}
|
|
11
|
+
|
|
12
|
+
*{box-sizing:border-box}
|
|
13
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:14px "Google Sans",Roboto,system-ui,sans-serif;height:100vh;display:flex;flex-direction:column}
|
|
14
|
+
|
|
15
|
+
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;border-radius:4px;padding:4px 8px}
|
|
16
|
+
button:hover{background:var(--hov)}
|
|
17
|
+
|
|
18
|
+
header{padding:6px 12px;display:flex;align-items:center;gap:10px}
|
|
19
|
+
|
|
20
|
+
.logo{width:34px;height:34px;border-radius:50%;background:#f9ab00;color:#fff;display:grid;place-items:center;font-weight:700;font-size:18px}
|
|
21
|
+
|
|
22
|
+
#title{font-size:18px;border:0;background:none;color:var(--fg);width:260px;padding:2px 6px;border-radius:4px}
|
|
23
|
+
#title:hover,#title:focus{outline:1px solid var(--bd)}
|
|
24
|
+
|
|
25
|
+
.menus{display:flex;margin-left:46px;position:absolute;top:34px}
|
|
26
|
+
.m{position:relative}
|
|
27
|
+
.m>span{padding:3px 9px;display:block;cursor:default;border-radius:4px}
|
|
28
|
+
.m:hover>span{background:var(--hov)}
|
|
29
|
+
|
|
30
|
+
.dd{display:none;position:absolute;top:100%;left:0;background:var(--bg);border:1px solid var(--bd);border-radius:4px;box-shadow:0 2px 8px #0003;min-width:230px;z-index:9;padding:4px 0}
|
|
31
|
+
.m:hover .dd{display:block}
|
|
32
|
+
|
|
33
|
+
.dd button{display:block;width:100%;text-align:left;border-radius:0;padding:6px 16px}
|
|
34
|
+
|
|
35
|
+
#conn{margin-left:auto;border:1px solid var(--bd);border-radius:18px;padding:6px 14px}
|
|
36
|
+
.dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:#1e8e3e;margin-right:6px}
|
|
37
|
+
|
|
38
|
+
.bar{border-bottom:1px solid var(--bd);padding:4px 12px 4px 12px;margin-top:14px;display:flex;gap:6px}
|
|
39
|
+
.bar button{border:1px solid var(--bd)}
|
|
40
|
+
|
|
41
|
+
.wrap{flex:1;display:flex;min-height:0}
|
|
42
|
+
aside{width:260px;border-right:1px solid var(--bd);overflow:auto;padding:8px}
|
|
43
|
+
aside[hidden]{display:none}
|
|
44
|
+
|
|
45
|
+
aside li{list-style:none;padding:5px 8px;cursor:pointer;border-radius:4px}
|
|
46
|
+
aside li:hover{background:var(--hov)}
|
|
47
|
+
aside ul{margin:6px 0;padding:0}
|
|
48
|
+
|
|
49
|
+
#nb{flex:1;overflow:auto;padding:16px 24px 200px}
|
|
50
|
+
.cell{max-width:980px;margin:0 auto 10px}
|
|
51
|
+
|
|
52
|
+
.row{display:flex;gap:6px;align-items:flex-start}
|
|
53
|
+
.gut{width:62px;flex:none;text-align:center;color:var(--mut);font:12px monospace}
|
|
54
|
+
|
|
55
|
+
.play{width:34px;height:34px;border-radius:50%;background:var(--hov)}
|
|
56
|
+
.run .play{background:var(--pri);color:#fff;animation:p 1s infinite}
|
|
57
|
+
@keyframes p{50%{opacity:.5}}
|
|
58
|
+
|
|
59
|
+
.ed{flex:1;border:1px solid var(--bd);border-radius:4px;background:var(--code);min-width:0}
|
|
60
|
+
.cell:focus-within .ed{border-color:var(--pri)}
|
|
61
|
+
|
|
62
|
+
textarea{width:100%;border:0;background:none;color:var(--fg);font:13px/1.5 "Roboto Mono",Consolas,monospace;resize:none;padding:8px 10px;outline:0;display:block;overflow:hidden}
|
|
63
|
+
|
|
64
|
+
.text .ed{background:none;border-color:transparent}
|
|
65
|
+
.text:focus-within .ed{border-color:var(--pri)}
|
|
66
|
+
.text textarea{background:var(--code);border:1px solid var(--bd)}
|
|
67
|
+
.md{padding:4px 8px;line-height:1.6}
|
|
68
|
+
.md h1{font-size:28px;margin:.3em 0}
|
|
69
|
+
.md h2{font-size:22px;margin:.3em 0}
|
|
70
|
+
.md code{background:var(--code);padding:1px 4px;border-radius:3px}
|
|
71
|
+
|
|
72
|
+
.tools{display:flex;visibility:hidden}
|
|
73
|
+
.cell:hover .tools,.cell:focus-within .tools{visibility:visible}
|
|
74
|
+
|
|
75
|
+
.out{
|
|
76
|
+
display:none;
|
|
77
|
+
margin:4px 0 0 68px;
|
|
78
|
+
border:1px solid var(--bd);
|
|
79
|
+
border-radius:6px;
|
|
80
|
+
background:var(--code);
|
|
81
|
+
padding:8px 10px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.out.has{display:block}
|
|
85
|
+
|
|
86
|
+
.out pre{margin:0;white-space:pre-wrap;word-break:break-word;font:13px/1.45 "Roboto Mono",Consolas,monospace}
|
|
87
|
+
.out .stderr{background:#fce8e610}
|
|
88
|
+
.out .error{color:var(--err)}
|
|
89
|
+
.out img{max-width:100%}
|
|
90
|
+
|
|
91
|
+
dialog{border:1px solid var(--bd);border-radius:8px;background:var(--bg);color:var(--fg);width:440px}
|
|
92
|
+
dialog::backdrop{background:#0006}
|
|
93
|
+
dialog input[type=text],dialog input[type=password]{width:100%;padding:8px;margin:4px 0 12px;background:var(--code);color:var(--fg);border:1px solid var(--bd);border-radius:4px}
|
|
94
|
+
|
|
95
|
+
.pri{background:var(--pri);color:#fff}
|
|
96
|
+
.pri:hover{background:var(--pri);opacity:.9}
|
|
97
|
+
#err{color:var(--err);margin:8px 0}
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<header>
|
|
102
|
+
<div class=logo>α</div>
|
|
103
|
+
<div><input id=title value="Untitled.ipynb"></div>
|
|
104
|
+
|
|
105
|
+
<div class=menus>
|
|
106
|
+
<div class=m><span>File</span>
|
|
107
|
+
<div class=dd>
|
|
108
|
+
<button onclick="$('#of').click()">Open notebook…</button>
|
|
109
|
+
<button onclick=dl()>Download .ipynb</button>
|
|
110
|
+
<button onclick=dlpy()>Download .py</button>
|
|
111
|
+
<button onclick="if(confirm('Clear notebook?')){cells=[];add('code');}">New notebook</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class=m><span>Runtime</span>
|
|
116
|
+
<div class=dd>
|
|
117
|
+
<button onclick=runAll()>Run all</button>
|
|
118
|
+
<button onclick=intr()>Interrupt execution</button>
|
|
119
|
+
<button onclick=restart()>Restart runtime</button>
|
|
120
|
+
<button onclick="restart().then(runAll)">Restart and run all</button>
|
|
121
|
+
<button onclick="$('#dlg').showModal()">Change connection…</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class=m><span>View</span>
|
|
126
|
+
<div class=dd>
|
|
127
|
+
<button onclick="$('#files').hidden^=1;ls(cwd)">Toggle files</button>
|
|
128
|
+
<button onclick="document.body.classList.toggle('dark')">Toggle dark mode</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<button id=conn onclick="$('#dlg').showModal()">Connect</button>
|
|
134
|
+
</header>
|
|
135
|
+
|
|
136
|
+
<div class=bar>
|
|
137
|
+
<button onclick="add('code')">+ Code</button>
|
|
138
|
+
<button onclick="add('text')">+ Text</button>
|
|
139
|
+
<button onclick=runAll()>▶ Run all</button>
|
|
140
|
+
<button onclick="$('#files').hidden^=1;ls(cwd)">📁 Files</button>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div class=wrap>
|
|
144
|
+
<aside id=files hidden>
|
|
145
|
+
<b>Files</b> <button onclick="$('#up').click()">⬆ Upload</button>
|
|
146
|
+
<ul id=fl></ul>
|
|
147
|
+
<input id=up type=file multiple hidden>
|
|
148
|
+
</aside>
|
|
149
|
+
|
|
150
|
+
<div id=nb></div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<input id=of type=file accept=.ipynb hidden>
|
|
154
|
+
|
|
155
|
+
<dialog id=dlg>
|
|
156
|
+
<form method=dialog onsubmit="return false">
|
|
157
|
+
<h3>Connect to runtime</h3>
|
|
158
|
+
|
|
159
|
+
URL
|
|
160
|
+
<input type=text id=cu placeholder="https://xxxx.trycloudflare.com">
|
|
161
|
+
|
|
162
|
+
Token (if not in the URL)
|
|
163
|
+
<input type=password id=ct>
|
|
164
|
+
|
|
165
|
+
<label><input type=checkbox id=ci> Skip TLS verification</label>
|
|
166
|
+
|
|
167
|
+
<div id=err></div>
|
|
168
|
+
|
|
169
|
+
<div style="text-align:right">
|
|
170
|
+
<button onclick="$('#dlg').close()">Cancel</button>
|
|
171
|
+
<button class=pri onclick=conn()>Connect</button>
|
|
172
|
+
</div>
|
|
173
|
+
</form>
|
|
174
|
+
</dialog>
|
|
175
|
+
|
|
176
|
+
<script>
|
|
177
|
+
const $=s=>document.querySelector(s);
|
|
178
|
+
const esc=s=>s.replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
|
|
179
|
+
|
|
180
|
+
const K=new URLSearchParams(location.search).get('k')||sessionStorage.k;
|
|
181
|
+
sessionStorage.k=K;
|
|
182
|
+
|
|
183
|
+
const api=(p,o={})=>fetch('/api/'+p,{...o,headers:{'X-Alyx':K}});
|
|
184
|
+
|
|
185
|
+
let cells=[],q=Promise.resolve(),abort=false,all=false,cwd='';
|
|
186
|
+
|
|
187
|
+
if(matchMedia('(prefers-color-scheme: dark)').matches)document.body.classList.add('dark');
|
|
188
|
+
|
|
189
|
+
const save=()=>localStorage.alyx_nb=JSON.stringify({
|
|
190
|
+
t:$('#title').value,
|
|
191
|
+
c:cells.map(c=>({type:c.type,src:c.src,outs:c.outs,n:c.n}))
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
$('#title').oninput=save;
|
|
195
|
+
|
|
196
|
+
const fit=t=>{
|
|
197
|
+
t.style.height='auto';
|
|
198
|
+
t.style.height=t.scrollHeight+'px'
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const md=s=>esc(s).split('\n').map(l=>{
|
|
202
|
+
let m=l.match(/^(#{1,3}) (.*)/);
|
|
203
|
+
if(m)return`<h${m[1].length}>${m[2]}</h${m[1].length}>`;
|
|
204
|
+
if(/^[-*] /.test(l))return'• '+l.slice(2)+'<br>';
|
|
205
|
+
return l+'<br>'
|
|
206
|
+
}).join('')
|
|
207
|
+
.replace(/\*\*(.+?)\*\*/g,'<b>$1</b>')
|
|
208
|
+
.replace(/\*(.+?)\*/g,'<i>$1</i>')
|
|
209
|
+
.replace(/`(.+?)`/g,'<code>$1</code>')
|
|
210
|
+
.replace(/\[(.+?)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target=_blank>$1</a>');
|
|
211
|
+
|
|
212
|
+
function put(c,t,x,rec=true){
|
|
213
|
+
const o=c.el.querySelector('.out');
|
|
214
|
+
o.classList.add('has');
|
|
215
|
+
|
|
216
|
+
let l=o.lastElementChild;
|
|
217
|
+
|
|
218
|
+
if(t=='img'){
|
|
219
|
+
l=document.createElement('img');
|
|
220
|
+
l.src='data:image/png;base64,'+x;
|
|
221
|
+
o.appendChild(l)
|
|
222
|
+
} else {
|
|
223
|
+
x=x.replace(/\x1b\[[0-9;]*m/g,'');
|
|
224
|
+
|
|
225
|
+
if(!l||l.className!=t||l.tagName!='PRE'){
|
|
226
|
+
l=document.createElement('pre');
|
|
227
|
+
l.className=t;
|
|
228
|
+
o.appendChild(l)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
l.textContent+=x
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if(rec){
|
|
235
|
+
const p=c.outs[c.outs.length-1];
|
|
236
|
+
|
|
237
|
+
if(p&&p.t==t&&t!='img')p.x+=x;
|
|
238
|
+
else c.outs.push({t,x})
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function mk(c){
|
|
243
|
+
const d=c.el=document.createElement('div');
|
|
244
|
+
const code=c.type=='code';
|
|
245
|
+
|
|
246
|
+
d.className='cell '+c.type;
|
|
247
|
+
|
|
248
|
+
d.innerHTML=`
|
|
249
|
+
<div class=row>
|
|
250
|
+
<div class=gut>
|
|
251
|
+
${code?'<button class=play>▶</button><div class=n></div>':''}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div class=ed>
|
|
255
|
+
<textarea spellcheck=false rows=1></textarea>
|
|
256
|
+
${code?'':'<div class=md></div>'}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div class=tools>
|
|
260
|
+
<button data-a=-1>↑</button>
|
|
261
|
+
<button data-a=1>↓</button>
|
|
262
|
+
<button data-a=x>✕</button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class=out></div>
|
|
267
|
+
`;
|
|
268
|
+
|
|
269
|
+
const t=c.t=d.querySelector('textarea');
|
|
270
|
+
t.value=c.src;
|
|
271
|
+
|
|
272
|
+
const m=d.querySelector('.md');
|
|
273
|
+
|
|
274
|
+
const view=()=>{
|
|
275
|
+
if(m){
|
|
276
|
+
m.innerHTML=md(c.src);
|
|
277
|
+
m.hidden=false;
|
|
278
|
+
t.hidden=true
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const edit=()=>{
|
|
283
|
+
if(m){
|
|
284
|
+
m.hidden=true;
|
|
285
|
+
t.hidden=false
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
t.focus();
|
|
289
|
+
fit(t)
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
if(m){
|
|
293
|
+
m.ondblclick=edit;
|
|
294
|
+
c.src?view():(m.hidden=true)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
t.oninput=()=>{
|
|
298
|
+
c.src=t.value;
|
|
299
|
+
fit(t);
|
|
300
|
+
save()
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
t.onkeydown=e=>{
|
|
304
|
+
if(e.key=='Tab'){
|
|
305
|
+
e.preventDefault();
|
|
306
|
+
document.execCommand('insertText',false,' ')
|
|
307
|
+
} else if(e.key=='Enter'&&(e.shiftKey||e.ctrlKey)){
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
code?run(c):view();
|
|
310
|
+
|
|
311
|
+
if(e.shiftKey){
|
|
312
|
+
const n=cells[cells.indexOf(c)+1];
|
|
313
|
+
n?n.t.focus():add('code')
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if(code)d.querySelector('.play').onclick=()=>run(c);
|
|
319
|
+
|
|
320
|
+
d.querySelector('.tools').onclick=e=>{
|
|
321
|
+
const a=e.target.dataset.a;
|
|
322
|
+
if(!a)return;
|
|
323
|
+
|
|
324
|
+
const i=cells.indexOf(c);
|
|
325
|
+
|
|
326
|
+
if(a=='x')cells.splice(i,1);
|
|
327
|
+
else{
|
|
328
|
+
const j=i+ +a;
|
|
329
|
+
if(j<0||j>=cells.length)return;
|
|
330
|
+
|
|
331
|
+
cells.splice(i,1);
|
|
332
|
+
cells.splice(j,0,c)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
render();
|
|
336
|
+
save()
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
c.outs.forEach(o=>put(c,o.t,o.x,false));
|
|
340
|
+
|
|
341
|
+
if(c.n)d.querySelector('.n').textContent='['+c.n+']';
|
|
342
|
+
|
|
343
|
+
setTimeout(()=>{
|
|
344
|
+
fit(t)
|
|
345
|
+
},0);
|
|
346
|
+
|
|
347
|
+
return c
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function render(){
|
|
351
|
+
$('#nb').replaceChildren(...cells.map(c=>c.el))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function add(type,src='',outs=[],n=0){
|
|
355
|
+
const c=mk({type,src,outs,n});
|
|
356
|
+
cells.push(c);
|
|
357
|
+
render();
|
|
358
|
+
fit(c.t);
|
|
359
|
+
|
|
360
|
+
if(!src){
|
|
361
|
+
const mdEl=c.el.querySelector('.md');
|
|
362
|
+
|
|
363
|
+
if(mdEl)mdEl.hidden=type=='text'?true:false;
|
|
364
|
+
|
|
365
|
+
c.t.hidden=false;
|
|
366
|
+
c.t.focus()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
save();
|
|
370
|
+
return c
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function exec(c){
|
|
374
|
+
if(abort)return;
|
|
375
|
+
|
|
376
|
+
c.el.classList.add('run');
|
|
377
|
+
c.outs=[];
|
|
378
|
+
|
|
379
|
+
const out=c.el.querySelector('.out');
|
|
380
|
+
out.innerHTML='';
|
|
381
|
+
out.classList.remove('has');
|
|
382
|
+
|
|
383
|
+
try{
|
|
384
|
+
const r=await api('run',{
|
|
385
|
+
method:'POST',
|
|
386
|
+
body:JSON.stringify({code:c.src})
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if(!r.ok){
|
|
390
|
+
put(c,'error',(await r.json()).error);
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const rd=r.body.getReader();
|
|
395
|
+
const dec=new TextDecoder();
|
|
396
|
+
let buf='';
|
|
397
|
+
|
|
398
|
+
for(;;){
|
|
399
|
+
const{done,value}=await rd.read();
|
|
400
|
+
if(done)break;
|
|
401
|
+
|
|
402
|
+
buf+=dec.decode(value,{stream:true});
|
|
403
|
+
let i;
|
|
404
|
+
|
|
405
|
+
while((i=buf.indexOf('\n'))>=0){
|
|
406
|
+
const m=JSON.parse(buf.slice(0,i));
|
|
407
|
+
buf=buf.slice(i+1);
|
|
408
|
+
|
|
409
|
+
if(m.t=='done'){
|
|
410
|
+
c.n=m.n;
|
|
411
|
+
c.el.querySelector('.n').textContent=m.n?'['+m.n+']':'';
|
|
412
|
+
|
|
413
|
+
if(!m.ok&&all)abort=true
|
|
414
|
+
} else {
|
|
415
|
+
put(c,m.t,m.x)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}catch(e){
|
|
420
|
+
put(c,'error',String(e))
|
|
421
|
+
}finally{
|
|
422
|
+
c.el.classList.remove('run');
|
|
423
|
+
save()
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const run=c=>c.type=='code'?(q=q.then(()=>exec(c))):0;
|
|
428
|
+
|
|
429
|
+
async function runAll(){
|
|
430
|
+
abort=false;
|
|
431
|
+
all=true;
|
|
432
|
+
|
|
433
|
+
for(const c of cells)run(c);
|
|
434
|
+
|
|
435
|
+
await q;
|
|
436
|
+
|
|
437
|
+
all=false;
|
|
438
|
+
abort=false
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function intr(){
|
|
442
|
+
abort=true;
|
|
443
|
+
await api('interrupt',{method:'POST'});
|
|
444
|
+
|
|
445
|
+
q=q.then(()=>{
|
|
446
|
+
abort=false
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function restart(){
|
|
451
|
+
await api('restart',{method:'POST'});
|
|
452
|
+
|
|
453
|
+
cells.forEach(c=>{
|
|
454
|
+
c.n=0;
|
|
455
|
+
|
|
456
|
+
const n=c.el.querySelector('.n');
|
|
457
|
+
if(n)n.textContent=''
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function setConn(s){
|
|
462
|
+
const b=$('#conn');
|
|
463
|
+
|
|
464
|
+
if(s.connected){
|
|
465
|
+
const i=s.info||{};
|
|
466
|
+
|
|
467
|
+
b.innerHTML=`<i class=dot></i>Connected · ${
|
|
468
|
+
i.gpus&&i.gpus.length
|
|
469
|
+
?(i.gpus.length>1?i.gpus.length+'× ':'')+i.gpus[0].name
|
|
470
|
+
:i.cpu_count+' CPU'
|
|
471
|
+
}`;
|
|
472
|
+
|
|
473
|
+
b.title=`${s.url}\nRAM ${i.ram_gb} GB · ${i.cpu_count} cores${
|
|
474
|
+
(i.gpus||[]).map(g=>'\n#'+g.index+' '+g.name+' '+g.memory).join('')
|
|
475
|
+
}`
|
|
476
|
+
} else {
|
|
477
|
+
b.textContent='Connect';
|
|
478
|
+
$('#dlg').showModal()
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function conn(){
|
|
483
|
+
$('#err').textContent='Connecting…';
|
|
484
|
+
|
|
485
|
+
const r=await api('connect',{
|
|
486
|
+
method:'POST',
|
|
487
|
+
body:JSON.stringify({
|
|
488
|
+
url:$('#cu').value,
|
|
489
|
+
token:$('#ct').value,
|
|
490
|
+
insecure:$('#ci').checked
|
|
491
|
+
})
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const s=await r.json();
|
|
495
|
+
|
|
496
|
+
if(s.error){
|
|
497
|
+
$('#err').textContent=s.error;
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
$('#err').textContent='';
|
|
502
|
+
$('#dlg').close();
|
|
503
|
+
|
|
504
|
+
setConn(s);
|
|
505
|
+
ls('')
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function ls(p){
|
|
509
|
+
cwd=p;
|
|
510
|
+
|
|
511
|
+
if($('#files').hidden)return;
|
|
512
|
+
|
|
513
|
+
const r=await(await api('files?path='+encodeURIComponent(p))).json();
|
|
514
|
+
|
|
515
|
+
$('#fl').innerHTML=
|
|
516
|
+
(p?`<li data-p="${esc(p.split('/').slice(0,-1).join('/'))}" data-t=directory>⬑ ..</li>`:'')+
|
|
517
|
+
(r.error?esc(r.error):r.map(f=>`<li data-p="${esc(f.path)}" data-t="${f.type}">${f.type=='directory'?'📁':'📄'} ${esc(f.name)}</li>`).join(''))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
$('#fl').onclick=async e=>{
|
|
521
|
+
const li=e.target.closest('li');
|
|
522
|
+
if(!li)return;
|
|
523
|
+
|
|
524
|
+
const p=li.dataset.p;
|
|
525
|
+
|
|
526
|
+
if(li.dataset.t=='directory')return ls(p);
|
|
527
|
+
|
|
528
|
+
const a=document.createElement('a');
|
|
529
|
+
a.href=URL.createObjectURL(await(await api('download?path='+encodeURIComponent(p))).blob());
|
|
530
|
+
a.download=p.split('/').pop();
|
|
531
|
+
a.click()
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
$('#up').onchange=async e=>{
|
|
535
|
+
for(const f of e.target.files){
|
|
536
|
+
await api('upload?path='+encodeURIComponent((cwd?cwd+'/':'')+f.name),{
|
|
537
|
+
method:'POST',
|
|
538
|
+
body:f
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
ls(cwd)
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const file=(n,t)=>{
|
|
546
|
+
const a=document.createElement('a');
|
|
547
|
+
a.href=URL.createObjectURL(new Blob([t]));
|
|
548
|
+
a.download=n;
|
|
549
|
+
a.click()
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const nm=()=>$('#title').value.replace(/\.(ipynb|py)$/,'');
|
|
553
|
+
|
|
554
|
+
function dl(){
|
|
555
|
+
file(nm()+'.ipynb',JSON.stringify({
|
|
556
|
+
nbformat:4,
|
|
557
|
+
nbformat_minor:5,
|
|
558
|
+
metadata:{
|
|
559
|
+
kernelspec:{
|
|
560
|
+
name:'python3',
|
|
561
|
+
display_name:'Python 3'
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
cells:cells.map(c=>c.type=='code'?{
|
|
565
|
+
cell_type:'code',
|
|
566
|
+
metadata:{},
|
|
567
|
+
execution_count:c.n||null,
|
|
568
|
+
source:c.src,
|
|
569
|
+
outputs:c.outs.map(o=>
|
|
570
|
+
o.t=='stdout'||o.t=='stderr'
|
|
571
|
+
?{output_type:'stream',name:o.t,text:o.x}
|
|
572
|
+
:o.t=='img'
|
|
573
|
+
?{output_type:'display_data',metadata:{},data:{'image/png':o.x}}
|
|
574
|
+
:o.t=='result'
|
|
575
|
+
?{output_type:'execute_result',execution_count:c.n||null,metadata:{},data:{'text/plain':o.x}}
|
|
576
|
+
:{output_type:'error',ename:'Error',evalue:'',traceback:[o.x]}
|
|
577
|
+
)
|
|
578
|
+
}:{
|
|
579
|
+
cell_type:'markdown',
|
|
580
|
+
metadata:{},
|
|
581
|
+
source:c.src
|
|
582
|
+
})
|
|
583
|
+
},null,1))
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const dlpy=()=>file(
|
|
587
|
+
nm()+'.py',
|
|
588
|
+
cells.filter(c=>c.type=='code').map(c=>c.src).join('\n\n')
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
$('#of').onchange=async e=>{
|
|
592
|
+
const f=e.target.files[0];
|
|
593
|
+
if(!f)return;
|
|
594
|
+
|
|
595
|
+
const nb=JSON.parse(await f.text());
|
|
596
|
+
|
|
597
|
+
cells=[];
|
|
598
|
+
$('#title').value=f.name;
|
|
599
|
+
|
|
600
|
+
nb.cells.forEach(c=>{
|
|
601
|
+
const s=Array.isArray(c.source)?c.source.join(''):c.source;
|
|
602
|
+
|
|
603
|
+
if(c.cell_type=='code'||c.cell_type=='markdown'){
|
|
604
|
+
cells.push(mk({
|
|
605
|
+
type:c.cell_type=='code'?'code':'text',
|
|
606
|
+
src:s,
|
|
607
|
+
outs:[],
|
|
608
|
+
n:0
|
|
609
|
+
}))
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
render();
|
|
614
|
+
cells.forEach(c=>fit(c.t));
|
|
615
|
+
save()
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
(async()=>{
|
|
619
|
+
const s=localStorage.alyx_nb?JSON.parse(localStorage.alyx_nb):null;
|
|
620
|
+
|
|
621
|
+
if(s){
|
|
622
|
+
$('#title').value=s.t;
|
|
623
|
+
|
|
624
|
+
s.c.forEach(c=>cells.push(mk({
|
|
625
|
+
type:c.type,
|
|
626
|
+
src:c.src,
|
|
627
|
+
outs:c.outs||[],
|
|
628
|
+
n:c.n||0
|
|
629
|
+
})));
|
|
630
|
+
|
|
631
|
+
render()
|
|
632
|
+
} else {
|
|
633
|
+
add('code','!nvidia-smi')
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if(!cells.length)add('code');
|
|
637
|
+
|
|
638
|
+
cells.forEach(c=>fit(c.t));
|
|
639
|
+
|
|
640
|
+
try{
|
|
641
|
+
setConn(await(await api('state')).json())
|
|
642
|
+
}catch(e){
|
|
643
|
+
document.body.insertAdjacentHTML(
|
|
644
|
+
'afterbegin',
|
|
645
|
+
'<p style="color:red">Open the URL printed by <code>alyxruntime ui</code>.</p>'
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
})();
|
|
649
|
+
</script>
|
|
650
|
+
</body>
|
|
651
|
+
</html>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alyxruntime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Use any remote machine's CPU/GPU as a Jupyter runtime over a Cloudflare tunnel, Colab-style, from the command line or Python.
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alyxruntime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Use any remote machine's CPU/GPU as a Jupyter runtime over a Cloudflare tunnel, Colab-style, from the command line or Python."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
<!doctype html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>alyxruntime notebook</title>
|
|
2
|
-
<style>
|
|
3
|
-
:root{--bg:#fff;--fg:#202124;--mut:#5f6368;--bd:#dadce0;--code:#f8f9fa;--hov:#f1f3f4;--pri:#1a73e8;--err:#d93025}
|
|
4
|
-
body.dark{--bg:#1f1f1f;--fg:#e8eaed;--mut:#9aa0a6;--bd:#3c4043;--code:#2a2a2a;--hov:#303134;--pri:#8ab4f8;--err:#f28b82}
|
|
5
|
-
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:14px "Google Sans",Roboto,system-ui,sans-serif;height:100vh;display:flex;flex-direction:column}
|
|
6
|
-
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;border-radius:4px;padding:4px 8px}button:hover{background:var(--hov)}
|
|
7
|
-
header{padding:6px 12px;display:flex;align-items:center;gap:10px}
|
|
8
|
-
.logo{width:34px;height:34px;border-radius:50%;background:#f9ab00;color:#fff;display:grid;place-items:center;font-weight:700;font-size:18px}
|
|
9
|
-
#title{font-size:18px;border:0;background:none;color:var(--fg);width:260px;padding:2px 6px;border-radius:4px}#title:hover,#title:focus{outline:1px solid var(--bd)}
|
|
10
|
-
.menus{display:flex;margin-left:46px;position:absolute;top:34px}.m{position:relative}.m>span{padding:3px 9px;display:block;cursor:default;border-radius:4px}.m:hover>span{background:var(--hov)}
|
|
11
|
-
.dd{display:none;position:absolute;top:100%;left:0;background:var(--bg);border:1px solid var(--bd);border-radius:4px;box-shadow:0 2px 8px #0003;min-width:230px;z-index:9;padding:4px 0}.m:hover .dd{display:block}
|
|
12
|
-
.dd button{display:block;width:100%;text-align:left;border-radius:0;padding:6px 16px}
|
|
13
|
-
#conn{margin-left:auto;border:1px solid var(--bd);border-radius:18px;padding:6px 14px}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:#1e8e3e;margin-right:6px}
|
|
14
|
-
.bar{border-bottom:1px solid var(--bd);padding:4px 12px 4px 12px;margin-top:14px;display:flex;gap:6px}.bar button{border:1px solid var(--bd)}
|
|
15
|
-
.wrap{flex:1;display:flex;min-height:0}aside{width:260px;border-right:1px solid var(--bd);overflow:auto;padding:8px}aside[hidden]{display:none}
|
|
16
|
-
aside li{list-style:none;padding:5px 8px;cursor:pointer;border-radius:4px}aside li:hover{background:var(--hov)}aside ul{margin:6px 0;padding:0}
|
|
17
|
-
#nb{flex:1;overflow:auto;padding:16px 24px 200px}.cell{max-width:980px;margin:0 auto 10px}
|
|
18
|
-
.row{display:flex;gap:6px;align-items:flex-start}.gut{width:62px;flex:none;text-align:center;color:var(--mut);font:12px monospace}
|
|
19
|
-
.play{width:34px;height:34px;border-radius:50%;background:var(--hov)}.run .play{background:var(--pri);color:#fff;animation:p 1s infinite}@keyframes p{50%{opacity:.5}}
|
|
20
|
-
.ed{flex:1;border:1px solid var(--bd);border-radius:4px;background:var(--code);min-width:0}.cell:focus-within .ed{border-color:var(--pri)}
|
|
21
|
-
textarea{width:100%;border:0;background:none;color:var(--fg);font:13px/1.5 "Roboto Mono",Consolas,monospace;resize:none;padding:8px 10px;outline:0;display:block;overflow:hidden}
|
|
22
|
-
.text .ed{background:none;border-color:transparent}.text:focus-within .ed{border-color:var(--pri)}.text textarea{background:var(--code);border:1px solid var(--bd)}.md{padding:4px 8px;line-height:1.6}.md h1{font-size:28px;margin:.3em 0}.md h2{font-size:22px;margin:.3em 0}.md code{background:var(--code);padding:1px 4px;border-radius:3px}
|
|
23
|
-
.tools{display:flex;visibility:hidden}.cell:hover .tools,.cell:focus-within .tools{visibility:visible}
|
|
24
|
-
.out{margin:4px 0 0 68px}.out pre{margin:0;white-space:pre-wrap;word-break:break-word;font:13px/1.45 "Roboto Mono",Consolas,monospace}.out .stderr{background:#fce8e610}.out .error{color:var(--err)}.out img{max-width:100%}
|
|
25
|
-
dialog{border:1px solid var(--bd);border-radius:8px;background:var(--bg);color:var(--fg);width:440px}dialog::backdrop{background:#0006}dialog input[type=text],dialog input[type=password]{width:100%;padding:8px;margin:4px 0 12px;background:var(--code);color:var(--fg);border:1px solid var(--bd);border-radius:4px}
|
|
26
|
-
.pri{background:var(--pri);color:#fff}.pri:hover{background:var(--pri);opacity:.9}#err{color:var(--err);margin:8px 0}
|
|
27
|
-
</style></head><body>
|
|
28
|
-
<header><div class=logo>α</div><div><input id=title value="Untitled.ipynb"></div>
|
|
29
|
-
<div class=menus>
|
|
30
|
-
<div class=m><span>File</span><div class=dd><button onclick="$('#of').click()">Open notebook…</button><button onclick=dl()>Download .ipynb</button><button onclick=dlpy()>Download .py</button><button onclick="if(confirm('Clear notebook?')){cells=[];add('code');}">New notebook</button></div></div>
|
|
31
|
-
<div class=m><span>Runtime</span><div class=dd><button onclick=runAll()>Run all</button><button onclick=intr()>Interrupt execution</button><button onclick=restart()>Restart runtime</button><button onclick="restart().then(runAll)">Restart and run all</button><button onclick="$('#dlg').showModal()">Change connection…</button></div></div>
|
|
32
|
-
<div class=m><span>View</span><div class=dd><button onclick="$('#files').hidden^=1;ls(cwd)">Toggle files</button><button onclick="document.body.classList.toggle('dark')">Toggle dark mode</button></div></div></div>
|
|
33
|
-
<button id=conn onclick="$('#dlg').showModal()">Connect</button></header>
|
|
34
|
-
<div class=bar><button onclick="add('code')">+ Code</button><button onclick="add('text')">+ Text</button><button onclick=runAll()>▶ Run all</button><button onclick="$('#files').hidden^=1;ls(cwd)">📁 Files</button></div>
|
|
35
|
-
<div class=wrap><aside id=files hidden><b>Files</b> <button onclick="$('#up').click()">⬆ Upload</button><ul id=fl></ul><input id=up type=file multiple hidden></aside><div id=nb></div></div>
|
|
36
|
-
<input id=of type=file accept=.ipynb hidden>
|
|
37
|
-
<dialog id=dlg><form method=dialog onsubmit="return false"><h3>Connect to runtime</h3>URL<input type=text id=cu placeholder="https://xxxx.trycloudflare.com">Token (if not in the URL)<input type=password id=ct>
|
|
38
|
-
<label><input type=checkbox id=ci> Skip TLS verification</label><div id=err></div><div style="text-align:right"><button onclick="$('#dlg').close()">Cancel</button> <button class=pri onclick=conn()>Connect</button></div></form></dialog>
|
|
39
|
-
<script>
|
|
40
|
-
const $=s=>document.querySelector(s),esc=s=>s.replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
|
|
41
|
-
const K=new URLSearchParams(location.search).get('k')||sessionStorage.k;sessionStorage.k=K;
|
|
42
|
-
const api=(p,o={})=>fetch('/api/'+p,{...o,headers:{'X-Alyx':K}});
|
|
43
|
-
let cells=[],q=Promise.resolve(),abort=false,all=false,cwd='';
|
|
44
|
-
if(matchMedia('(prefers-color-scheme: dark)').matches)document.body.classList.add('dark');
|
|
45
|
-
const save=()=>localStorage.alyx_nb=JSON.stringify({t:$('#title').value,c:cells.map(c=>({type:c.type,src:c.src,outs:c.outs,n:c.n}))});
|
|
46
|
-
$('#title').oninput=save;
|
|
47
|
-
const fit=t=>{t.style.height='auto';t.style.height=t.scrollHeight+'px'};
|
|
48
|
-
const md=s=>esc(s).split('\n').map(l=>{let m=l.match(/^(#{1,3}) (.*)/);if(m)return`<h${m[1].length}>${m[2]}</h${m[1].length}>`;if(/^[-*] /.test(l))return'• '+l.slice(2)+'<br>';return l+'<br>'}).join('').replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/\*(.+?)\*/g,'<i>$1</i>').replace(/`(.+?)`/g,'<code>$1</code>').replace(/\[(.+?)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target=_blank>$1</a>');
|
|
49
|
-
function put(c,t,x,rec=true){const o=c.el.querySelector('.out');let l=o.lastElementChild;
|
|
50
|
-
if(t=='img'){l=document.createElement('img');l.src='data:image/png;base64,'+x;o.appendChild(l)}
|
|
51
|
-
else{x=x.replace(/\x1b\[[0-9;]*m/g,'');if(!l||l.className!=t||l.tagName!='PRE'){l=document.createElement('pre');l.className=t;o.appendChild(l)}l.textContent+=x}
|
|
52
|
-
if(rec){const p=c.outs[c.outs.length-1];if(p&&p.t==t&&t!='img')p.x+=x;else c.outs.push({t,x})}}
|
|
53
|
-
function mk(c){const d=c.el=document.createElement('div'),code=c.type=='code';d.className='cell '+c.type;
|
|
54
|
-
d.innerHTML=`<div class=row><div class=gut>${code?'<button class=play>▶</button><div class=n></div>':''}</div><div class=ed><textarea spellcheck=false rows=1></textarea>${code?'':'<div class=md></div>'}</div><div class=tools><button data-a=-1>↑</button><button data-a=1>↓</button><button data-a=x>✕</button></div></div><div class=out></div>`;
|
|
55
|
-
const t=c.t=d.querySelector('textarea');t.value=c.src;const m=d.querySelector('.md');
|
|
56
|
-
const view=()=>{if(m){m.innerHTML=md(c.src);m.hidden=false;t.hidden=true}};
|
|
57
|
-
const edit=()=>{if(m){m.hidden=true;t.hidden=false}t.focus();fit(t)};
|
|
58
|
-
if(m){m.ondblclick=edit;c.src?view():(m.hidden=true)}
|
|
59
|
-
t.oninput=()=>{c.src=t.value;fit(t);save()};
|
|
60
|
-
t.onkeydown=e=>{if(e.key=='Tab'){e.preventDefault();document.execCommand('insertText',false,' ')}
|
|
61
|
-
else if(e.key=='Enter'&&(e.shiftKey||e.ctrlKey)){e.preventDefault();code?run(c):view();if(e.shiftKey){const n=cells[cells.indexOf(c)+1];n?n.t.focus():add('code')}}};
|
|
62
|
-
if(code)d.querySelector('.play').onclick=()=>run(c);
|
|
63
|
-
d.querySelector('.tools').onclick=e=>{const a=e.target.dataset.a;if(!a)return;const i=cells.indexOf(c);
|
|
64
|
-
if(a=='x')cells.splice(i,1);else{const j=i+ +a;if(j<0||j>=cells.length)return;cells.splice(i,1);cells.splice(j,0,c)}render();save()};
|
|
65
|
-
c.outs.forEach(o=>put(c,o.t,o.x,false));if(c.n)d.querySelector('.n').textContent='['+c.n+']';
|
|
66
|
-
setTimeout(()=>{fit(t);if(!c.src&&!m)0},0);return c}
|
|
67
|
-
function render(){$('#nb').replaceChildren(...cells.map(c=>c.el))}
|
|
68
|
-
function add(type,src='',outs=[],n=0){const c=mk({type,src,outs,n});cells.push(c);render();fit(c.t);if(!src)(c.el.querySelector('.md')||{}).hidden=type=='text'?true:0,c.t.hidden=false,c.t.focus();save();return c}
|
|
69
|
-
async function exec(c){if(abort)return;c.el.classList.add('run');c.outs=[];c.el.querySelector('.out').innerHTML='';
|
|
70
|
-
try{const r=await api('run',{method:'POST',body:JSON.stringify({code:c.src})});
|
|
71
|
-
if(!r.ok){put(c,'error',(await r.json()).error);return}
|
|
72
|
-
const rd=r.body.getReader(),dec=new TextDecoder();let buf='';
|
|
73
|
-
for(;;){const{done,value}=await rd.read();if(done)break;buf+=dec.decode(value,{stream:true});let i;
|
|
74
|
-
while((i=buf.indexOf('\n'))>=0){const m=JSON.parse(buf.slice(0,i));buf=buf.slice(i+1);
|
|
75
|
-
if(m.t=='done'){c.n=m.n;c.el.querySelector('.n').textContent=m.n?'['+m.n+']':'';if(!m.ok&&all)abort=true}else put(c,m.t,m.x)}}
|
|
76
|
-
}catch(e){put(c,'error',String(e))}finally{c.el.classList.remove('run');save()}}
|
|
77
|
-
const run=c=>c.type=='code'?(q=q.then(()=>exec(c))):0;
|
|
78
|
-
async function runAll(){abort=false;all=true;for(const c of cells)run(c);await q;all=false;abort=false}
|
|
79
|
-
async function intr(){abort=true;await api('interrupt',{method:'POST'});q=q.then(()=>{abort=false})}
|
|
80
|
-
async function restart(){await api('restart',{method:'POST'});cells.forEach(c=>{c.n=0;c.el.querySelector('.n')&&(c.el.querySelector('.n').textContent='')})}
|
|
81
|
-
function setConn(s){const b=$('#conn');if(s.connected){const i=s.info||{};b.innerHTML=`<i class=dot></i>Connected · ${(i.gpus&&i.gpus.length?(i.gpus.length>1?i.gpus.length+'× ':'')+i.gpus[0].name:i.cpu_count+' CPU')}`;b.title=`${s.url}\nRAM ${i.ram_gb} GB · ${i.cpu_count} cores${(i.gpus||[]).map(g=>'\n#'+g.index+' '+g.name+' '+g.memory).join('')}`}else{b.textContent='Connect';$('#dlg').showModal()}}
|
|
82
|
-
async function conn(){$('#err').textContent='Connecting…';const r=await api('connect',{method:'POST',body:JSON.stringify({url:$('#cu').value,token:$('#ct').value,insecure:$('#ci').checked})});
|
|
83
|
-
const s=await r.json();if(s.error){$('#err').textContent=s.error;return}$('#err').textContent='';$('#dlg').close();setConn(s);ls('')}
|
|
84
|
-
async function ls(p){cwd=p;if($('#files').hidden)return;const r=await(await api('files?path='+encodeURIComponent(p))).json();
|
|
85
|
-
$('#fl').innerHTML=(p?`<li data-p="${esc(p.split('/').slice(0,-1).join('/'))}" data-t=directory>⬑ ..</li>`:'')+(r.error?esc(r.error):r.map(f=>`<li data-p="${esc(f.path)}" data-t="${f.type}">${f.type=='directory'?'📁':'📄'} ${esc(f.name)}</li>`).join(''))}
|
|
86
|
-
$('#fl').onclick=async e=>{const li=e.target.closest('li');if(!li)return;const p=li.dataset.p;if(li.dataset.t=='directory')return ls(p);
|
|
87
|
-
const a=document.createElement('a');a.href=URL.createObjectURL(await(await api('download?path='+encodeURIComponent(p))).blob());a.download=p.split('/').pop();a.click()};
|
|
88
|
-
$('#up').onchange=async e=>{for(const f of e.target.files)await api('upload?path='+encodeURIComponent((cwd?cwd+'/':'')+f.name),{method:'POST',body:f});ls(cwd)};
|
|
89
|
-
const file=(n,t)=>{const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([t]));a.download=n;a.click()};
|
|
90
|
-
const nm=()=>$('#title').value.replace(/\.(ipynb|py)$/,'');
|
|
91
|
-
function dl(){file(nm()+'.ipynb',JSON.stringify({nbformat:4,nbformat_minor:5,metadata:{kernelspec:{name:'python3',display_name:'Python 3'}},cells:cells.map(c=>c.type=='code'?{cell_type:'code',metadata:{},execution_count:c.n||null,source:c.src,outputs:c.outs.map(o=>o.t=='stdout'||o.t=='stderr'?{output_type:'stream',name:o.t,text:o.x}:o.t=='img'?{output_type:'display_data',metadata:{},data:{'image/png':o.x}}:o.t=='result'?{output_type:'execute_result',execution_count:c.n||null,metadata:{},data:{'text/plain':o.x}}:{output_type:'error',ename:'Error',evalue:'',traceback:[o.x]})}:{cell_type:'markdown',metadata:{},source:c.src})},null,1))}
|
|
92
|
-
const dlpy=()=>file(nm()+'.py',cells.filter(c=>c.type=='code').map(c=>c.src).join('\n\n'));
|
|
93
|
-
$('#of').onchange=async e=>{const f=e.target.files[0];if(!f)return;const nb=JSON.parse(await f.text());cells=[];$('#title').value=f.name;
|
|
94
|
-
nb.cells.forEach(c=>{const s=Array.isArray(c.source)?c.source.join(''):c.source;if(c.cell_type=='code'||c.cell_type=='markdown')cells.push(mk({type:c.cell_type=='code'?'code':'text',src:s,outs:[],n:0}))});render();cells.forEach(c=>fit(c.t));save()};
|
|
95
|
-
(async()=>{const s=localStorage.alyx_nb?JSON.parse(localStorage.alyx_nb):null;
|
|
96
|
-
if(s){$('#title').value=s.t;s.c.forEach(c=>cells.push(mk({type:c.type,src:c.src,outs:c.outs||[],n:c.n||0})));render()}else add('code','!nvidia-smi');
|
|
97
|
-
if(!cells.length)add('code');cells.forEach(c=>fit(c.t));
|
|
98
|
-
try{setConn(await(await api('state')).json())}catch(e){document.body.insertAdjacentHTML('afterbegin','<p style="color:red">Open the URL printed by <code>alyxruntime ui</code>.</p>')}})();
|
|
99
|
-
</script></body></html>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|