Skip to content

Commit 87d62f6

Browse files
Add custom API route for .deepnote files
Signed-off-by: Andy Jakubowski <[email protected]>
1 parent 7f654bc commit 87d62f6

File tree

6 files changed

+43
-92
lines changed

6 files changed

+43
-92
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ jlpm run watch
114114

115115
The `jlpm` command is JupyterLab's pinned version of
116116
[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
117-
`yarn` or `npm` in lieu of `jlpm` below.
117+
`yarn` or `npm` instead of `jlpm` below.
118118

119-
In a separate terminal, run `jupyter lab` with the `--config` option to register our custom file contents manager for the `.deepnote` extension. The `--debug` option lets you see HTTP requests in the logs, which is helpful for debugging.
119+
In a separate terminal, run `jupyter lab`. You can add the `--debug` option to see HTTP requests in the logs, which can be helpful for debugging.
120120

121121
```shell
122-
jupyter lab --debug --config="$(pwd)/jupyter-config/server-config/jupyter_server_config.json"
122+
jupyter lab --debug
123123
```
124124

125125
You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.

jupyter-config/server-config/jupyter_server_config.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

jupyterlab_deepnote/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
warnings.warn("Importing 'jupyterlab_deepnote' outside a proper installation.")
1010
__version__ = "dev"
11-
from jupyterlab_deepnote.contents import DeepnoteContentsManager
1211
from .handlers import setup_handlers
1312

1413

jupyterlab_deepnote/contents.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

jupyterlab_deepnote/handlers.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
1+
from datetime import datetime
12
import json
23

34
from jupyter_server.base.handlers import APIHandler
45
from jupyter_server.utils import url_path_join
6+
from jupyter_core.utils import ensure_async
57
import tornado
68

9+
710
class RouteHandler(APIHandler):
811
# The following decorator should be present on all verb methods (head, get, post,
912
# patch, put, delete, options) to ensure only authorized user can request the
1013
# Jupyter server
1114
@tornado.web.authenticated
12-
def get(self):
13-
self.finish(json.dumps({
14-
"data": "This is /jupyterlab-deepnote/get-example endpoint!"
15-
}))
15+
async def get(self):
16+
path = self.get_query_argument("path")
17+
# Use Jupyter Server’s contents_manager, not direct filesystem access.
18+
model = await ensure_async(
19+
self.contents_manager.get(path, type="file", format="text", content=True)
20+
)
21+
# Convert datetimes to strings so JSON can handle them
22+
for key in ("created", "last_modified"):
23+
if isinstance(model.get(key), datetime):
24+
model[key] = model[key].isoformat()
25+
26+
# Return everything, including YAML content
27+
result = {"deepnoteFileModel": model}
28+
29+
self.finish(json.dumps(result))
1630

1731

1832
def setup_handlers(web_app):
1933
host_pattern = ".*$"
2034

2135
base_url = web_app.settings["base_url"]
22-
route_pattern = url_path_join(base_url, "jupyterlab-deepnote", "get-example")
36+
route_pattern = url_path_join(base_url, "jupyterlab-deepnote", "file")
2337
handlers = [(route_pattern, RouteHandler)]
2438
web_app.add_handlers(host_pattern, handlers)

src/deepnote-content-provider.ts

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,42 @@
11
import { Contents, RestContentProvider } from '@jupyterlab/services';
2-
import { z } from 'zod';
32
import { transformDeepnoteYamlToNotebookContent } from './transform-deepnote-yaml-to-notebook-content';
3+
import { requestAPI } from './handler';
44

55
export const deepnoteContentProviderName = 'deepnote-content-provider';
6-
7-
const deepnoteFileFromServerSchema = z.object({
8-
cells: z.array(z.any()), // or refine further with nbformat
9-
metadata: z.object({
10-
deepnote: z.object({
11-
rawYamlString: z.string()
12-
})
13-
}),
14-
nbformat: z.number(),
15-
nbformat_minor: z.number()
16-
});
17-
186
export class DeepnoteContentProvider extends RestContentProvider {
197
async get(
208
localPath: string,
219
options?: Contents.IFetchOptions
2210
): Promise<Contents.IModel> {
23-
const model = await super.get(localPath, options);
24-
const isDeepnoteFile =
25-
localPath.endsWith('.deepnote') && model.type === 'notebook';
11+
const isDeepnoteFile = localPath.endsWith('.deepnote');
2612

2713
if (!isDeepnoteFile) {
2814
// Not a .deepnote file, return as-is
29-
return model;
15+
const nonDeepnoteModel = await super.get(localPath, options);
16+
return nonDeepnoteModel;
3017
}
3118

32-
const validatedModelContent = deepnoteFileFromServerSchema.safeParse(
33-
model.content
34-
);
35-
36-
if (!validatedModelContent.success) {
37-
console.error(
38-
'Invalid .deepnote file content:',
39-
validatedModelContent.error
40-
);
41-
// Return an empty notebook instead of throwing an error
42-
model.content.cells = [];
43-
return model;
44-
}
19+
// Call custom API route to fetch the Deepnote file content
20+
const data = await requestAPI<any>(`file?path=${localPath}`);
21+
const modelData = data.deepnoteFileModel;
4522

4623
// Transform the Deepnote YAML to Jupyter notebook content
47-
const transformedModelContent =
48-
await transformDeepnoteYamlToNotebookContent(
49-
validatedModelContent.data.metadata.deepnote.rawYamlString
50-
);
24+
const notebookContent = await transformDeepnoteYamlToNotebookContent(
25+
modelData.content
26+
);
5127

52-
const transformedModel = {
53-
...model,
54-
content: transformedModelContent
28+
const model: Contents.IModel = {
29+
name: modelData.name,
30+
path: modelData.path,
31+
type: 'notebook',
32+
writable: false,
33+
created: modelData.created,
34+
last_modified: modelData.last_modified,
35+
mimetype: 'application/x-ipynb+json',
36+
format: 'json',
37+
content: notebookContent
5538
};
5639

57-
return transformedModel;
40+
return model;
5841
}
5942
}

0 commit comments

Comments
 (0)