Skip to content

Commit f70e4f5

Browse files
committed
Start integration with jupyterlite
1 parent b028851 commit f70e4f5

File tree

4 files changed

+313
-78
lines changed

4 files changed

+313
-78
lines changed

README-JUPYTERLITE.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
Install:
2+
3+
pip install jupyterlite-core
4+
pip install jupyterlite-pyodide-kernel
5+
6+
Generate static files:
7+
8+
cd caterva2/services/static
9+
jupyter lite build --output-dir jupyterlite
10+
11+
Usage:
12+
13+
import js
14+
js.fetch(...)
15+
16+
%pip install -q caterva2
17+
import caterva2
18+
19+
20+
http://localhost:8000/static/jupyterlite/notebooks/index.html?path=01.ndarray-basics.ipynb
21+
22+
23+
/static/jupyterlite/
24+
notebooks/index.html?path=01.ndarray-basics.ipynb
25+
api/contents/all.json
26+
api/contents/blosc/all.json
27+
overrides.json
28+
29+
30+
31+
32+
33+
http://localhost:8000/static/jupyterlite/api/contents/all.json
34+
35+
http://127.0.0.1:8000/api/contents/all.json
36+
37+
{
38+
"content": [
39+
{
40+
"content": null,
41+
"created": "2025-01-21T12:25:19.741116Z",
42+
"format": null,
43+
"hash": null,
44+
"hash_algorithm": null,
45+
"last_modified": "2025-01-21T12:23:38.424439Z",
46+
"mimetype": null,
47+
"name": "02.02-The-Basics-Of-NumPy-Arrays.ipynb",
48+
"path": "02.02-The-Basics-Of-NumPy-Arrays.ipynb",
49+
"size": 32798,
50+
"type": "notebook",
51+
"writable": true
52+
},
53+
{
54+
"content": null,
55+
"created": "2025-01-21T12:25:19.741116Z",
56+
"format": null,
57+
"hash": null,
58+
"hash_algorithm": null,
59+
"last_modified": "2025-01-21T12:25:19.741116Z",
60+
"mimetype": null,
61+
"name": "blosc",
62+
"path": "blosc",
63+
"size": null,
64+
"type": "directory",
65+
"writable": true
66+
}
67+
],
68+
"created": "2025-01-21T12:25:19.741116Z",
69+
"format": "json",
70+
"hash": null,
71+
"hash_algorithm": null,
72+
"last_modified": "2025-01-21T12:25:19.741116Z",
73+
"mimetype": null,
74+
"name": "",
75+
"path": "",
76+
"size": null,
77+
"type": "directory",
78+
"writable": true
79+
}
80+
81+
http://127.0.0.1:8000/api/contents/blosc/all.json
82+
83+
{
84+
"content": [
85+
{
86+
"content": null,
87+
"created": "2025-01-21T12:25:19.741116Z",
88+
"format": null,
89+
"hash": null,
90+
"hash_algorithm": null,
91+
"last_modified": "2025-01-21T11:58:04.096061Z",
92+
"mimetype": null,
93+
"name": "01.ndarray-basics.ipynb",
94+
"path": "blosc/01.ndarray-basics.ipynb",
95+
"size": 24683,
96+
"type": "notebook",
97+
"writable": true
98+
}
99+
],
100+
"created": "2025-01-21T12:25:19.741116Z",
101+
"format": "json",
102+
"hash": null,
103+
"hash_algorithm": null,
104+
"last_modified": "2025-01-21T12:25:19.741116Z",
105+
"mimetype": null,
106+
"name": "blosc",
107+
"path": "blosc",
108+
"size": null,
109+
"type": "directory",
110+
"writable": true
111+
}
112+
113+
114+
http://127.0.0.1:8000/files/01.ndarray-basics.ipynb

caterva2/services/sub.py

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,6 @@ def url(path: str) -> str:
501501
return f"{settings.urlbase}/{path}"
502502

503503

504-
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
505504
templates = Jinja2Templates(directory=BASE_DIR / "templates")
506505
templates.env.filters["filesizeformat"] = custom_filesizeformat
507506
templates.env.globals["url"] = url
@@ -1426,6 +1425,21 @@ async def htmx_root_list(
14261425
return templates.TemplateResponse(request, "root_list.html", context)
14271426

14281427

1428+
def _get_rootdir(user, root):
1429+
if user and root == "@personal":
1430+
return settings.personal / str(user.id)
1431+
elif user and root == "@shared":
1432+
return settings.shared
1433+
elif root == "@public":
1434+
return settings.public
1435+
else:
1436+
if not get_root(root).subscribed:
1437+
follow(root)
1438+
return settings.cache / root
1439+
1440+
return None
1441+
1442+
14291443
@app.get("/htmx/path-list/", response_class=HTMLResponse)
14301444
async def htmx_path_list(
14311445
request: Request,
@@ -1450,20 +1464,6 @@ def get_names():
14501464

14511465
names = get_names()
14521466

1453-
def get_rootdir(root):
1454-
if user and root == "@personal":
1455-
rootdir = settings.personal / str(user.id)
1456-
elif user and root == "@shared":
1457-
rootdir = settings.shared
1458-
elif root == "@public":
1459-
rootdir = settings.public
1460-
else:
1461-
if not get_root(root).subscribed:
1462-
follow(root)
1463-
rootdir = settings.cache / root
1464-
1465-
return rootdir
1466-
14671467
datasets = []
14681468
query = {"roots": roots, "search": search}
14691469

@@ -1479,7 +1479,7 @@ def add_dataset(path, abspath):
14791479
)
14801480

14811481
for root in roots:
1482-
rootdir = get_rootdir(root)
1482+
rootdir = _get_rootdir(user, root)
14831483
for abspath, relpath in utils.walk_files(rootdir):
14841484
if relpath.suffix == ".b2":
14851485
relpath = relpath.with_suffix("")
@@ -1497,7 +1497,7 @@ def add_dataset(path, abspath):
14971497
break
14981498
else:
14991499
root = segments[1]
1500-
rootdir = get_rootdir(root)
1500+
rootdir = _get_rootdir(user, root)
15011501
relpath = pathlib.Path(*segments[2:])
15021502
abspath = rootdir / relpath
15031503
if abspath.suffix not in {".b2", ".b2nd", ".b2frame"}:
@@ -2112,7 +2112,10 @@ async def html_display(
21122112
return f'<object data="{data}" type="application/pdf" class="w-100" style="height: 768px"></object>'
21132113
elif mimetype == "application/vnd.jupyter":
21142114
src = f"{url('api/preview/')}{path}"
2115-
return f'<iframe src="{src}" class="w-100" height="768px"></iframe>'
2115+
return (
2116+
f'<a href="/static/jupyterlite/notebooks/index.html?path={path}" target="_blank">Run</a><br>'
2117+
f'<iframe src="{src}" class="w-100" height="768px"></iframe>'
2118+
)
21162119
elif mimetype == "text/markdown":
21172120
content = await get_file_content(path, user)
21182121
return markdown.markdown(content.decode("utf-8"))
@@ -2138,6 +2141,97 @@ async def html_display(
21382141
return "Format not supported"
21392142

21402143

2144+
#
2145+
# For Jupyterlite
2146+
#
2147+
2148+
2149+
@app.get("/static/jupyterlite/api/contents/{path:path}")
2150+
def jupyterlite_contents(
2151+
request: Request,
2152+
# Path parameters
2153+
path: pathlib.Path,
2154+
user: db.User = Depends(optional_user),
2155+
):
2156+
parts = path.parts
2157+
if parts[-1] != "all.json":
2158+
raise fastapi.HTTPException(status_code=404) # NotFound
2159+
2160+
content = []
2161+
2162+
def directory(path):
2163+
return {
2164+
"name": pathlib.Path(path).name,
2165+
"path": path,
2166+
"size": None,
2167+
"type": "directory",
2168+
}
2169+
2170+
parts = parts[:-1]
2171+
if len(parts) == 0:
2172+
# TODO pub/sub roots: settings.database.roots.values()
2173+
if user:
2174+
content.append(directory("@personal"))
2175+
content.append(directory("@shared"))
2176+
2177+
content.append(directory("@public"))
2178+
else:
2179+
root = parts[0]
2180+
rootdir = _get_rootdir(user, root)
2181+
if rootdir is None:
2182+
raise fastapi.HTTPException(status_code=404) # NotFound
2183+
2184+
for abspath, relpath in utils.iterdir(rootdir):
2185+
if abspath.is_file():
2186+
if relpath.suffix == ".b2":
2187+
relpath = relpath.with_suffix("")
2188+
2189+
if relpath.suffix == ".ipynb":
2190+
type = "notebook"
2191+
else:
2192+
type = "file" # XXX Is this the correct type?
2193+
2194+
stat = abspath.stat()
2195+
content.append(
2196+
{
2197+
"created": utils.epoch_to_iso(stat.st_ctime),
2198+
"last_modified": utils.epoch_to_iso(stat.st_mtime),
2199+
"name": relpath.name,
2200+
"path": relpath,
2201+
"size": stat.st_size, # XXX Return the uncompressed size?
2202+
"type": type,
2203+
}
2204+
)
2205+
else:
2206+
content.append(directory(relpath))
2207+
2208+
return {
2209+
"content": content,
2210+
}
2211+
2212+
2213+
@app.get("/static/jupyterlite/files/{path:path}")
2214+
def jupyterlite_files(
2215+
request: Request,
2216+
# Path parameters
2217+
path: pathlib.Path,
2218+
user: db.User = Depends(optional_user),
2219+
):
2220+
async def downloader():
2221+
yield await get_file_content(path, user)
2222+
2223+
mimetype = guess_type(path)
2224+
# mimetype = 'application/json'
2225+
return responses.StreamingResponse(downloader(), media_type=mimetype)
2226+
2227+
2228+
#
2229+
# Static
2230+
#
2231+
2232+
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
2233+
2234+
21412235
#
21422236
# Command line interface
21432237
#

caterva2/services/templates/home.html

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,17 @@
5555

5656
<template id="alert-error-template">{% include 'error.html' %}</template>
5757

58-
<a href="https://ironarray.io/caterva2" target="blank_" class="float-end m-3 text-decoration-none" style="font-size: 1.1rem">
59-
<i class="fa-regular fa-circle-question"></i> Help
60-
</a>
58+
<span class="d-flex gap-3 float-end m-3 " style="font-size: 1.1rem">
59+
{#
60+
<a href="/static/jupyterlite/repl/index.html" target="blank_" class="text-decoration-none">
61+
<i class="fa-solid fa-terminal"></i> Repl
62+
</a>
63+
#}
64+
65+
<a href="https://ironarray.io/caterva2" target="blank_" class="text-decoration-none">
66+
<i class="fa-regular fa-circle-question"></i> Help
67+
</a>
68+
</span>
6169

6270
<!-- Main section -->
6371
<div id="page">

0 commit comments

Comments
 (0)