Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ jlpm run watch

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

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.
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.

```shell
jupyter lab --debug --config="$(pwd)/jupyter-config/server-config/jupyter_server_config.json"
jupyter lab --debug
```

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.
Expand Down
5 changes: 0 additions & 5 deletions jupyter-config/server-config/jupyter_server_config.json

This file was deleted.

2 changes: 0 additions & 2 deletions jupyterlab_deepnote/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

warnings.warn("Importing 'jupyterlab_deepnote' outside a proper installation.")
__version__ = "dev"
from jupyterlab_deepnote.contents import DeepnoteContentsManager
from .handlers import setup_handlers


Expand All @@ -31,4 +30,3 @@ def _load_jupyter_server_extension(server_app):
setup_handlers(server_app.web_app)
name = "jupyterlab_deepnote"
server_app.log.info(f"Registered {name} server extension")
server_app.contents_manager = DeepnoteContentsManager(parent=server_app)
40 changes: 0 additions & 40 deletions jupyterlab_deepnote/contents.py

This file was deleted.

54 changes: 49 additions & 5 deletions jupyterlab_deepnote/handlers.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,68 @@
from datetime import datetime
import json

from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join
from jupyter_core.utils import ensure_async
import tornado


class RouteHandler(APIHandler):
# The following decorator should be present on all verb methods (head, get, post,
# patch, put, delete, options) to ensure only authorized user can request the
# Jupyter server
@tornado.web.authenticated
def get(self):
self.finish(json.dumps({
"data": "This is /jupyterlab-deepnote/get-example endpoint!"
}))
async def get(self):
path = self.get_query_argument("path", default=None)
if not path:
self.set_status(400)
self.set_header("Content-Type", "application/json")
self.finish(
json.dumps(
{
"code": 400,
"message": "Missing required 'path' parameter",
}
)
)
return
try:
model = await ensure_async(
self.contents_manager.get(
path, type="file", format="text", content=True
)
)
except FileNotFoundError as e:
self.set_status(404)
self.set_header("Content-Type", "application/json")
self.finish(json.dumps({"code": 404, "message": "File not found"}))
return
except PermissionError as e:
self.set_status(403)
self.set_header("Content-Type", "application/json")
self.finish(json.dumps({"code": 403, "message": "Permission denied"}))
return
except Exception as e:
self.log.exception("Error retrieving file")
self.set_status(500)
self.set_header("Content-Type", "application/json")
self.finish(json.dumps({"code": 500, "message": "Internal server error"}))
return
# Convert datetimes to strings so JSON can handle them
for key in ("created", "last_modified"):
if isinstance(model.get(key), datetime):
model[key] = model[key].isoformat()

# Return everything, including YAML content
result = {"deepnoteFileModel": model}

self.finish(json.dumps(result))


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

base_url = web_app.settings["base_url"]
route_pattern = url_path_join(base_url, "jupyterlab-deepnote", "get-example")
route_pattern = url_path_join(base_url, "jupyterlab-deepnote", "file")
handlers = [(route_pattern, RouteHandler)]
web_app.add_handlers(host_pattern, handlers)
69 changes: 34 additions & 35 deletions src/deepnote-content-provider.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,58 @@
import { Contents, RestContentProvider } from '@jupyterlab/services';
import { z } from 'zod';
import { transformDeepnoteYamlToNotebookContent } from './transform-deepnote-yaml-to-notebook-content';
import { requestAPI } from './handler';

export const deepnoteContentProviderName = 'deepnote-content-provider';

const deepnoteFileFromServerSchema = z.object({
cells: z.array(z.any()), // or refine further with nbformat
metadata: z.object({
deepnote: z.object({
rawYamlString: z.string()
})
}),
nbformat: z.number(),
nbformat_minor: z.number()
});

export class DeepnoteContentProvider extends RestContentProvider {
async get(
localPath: string,
options?: Contents.IFetchOptions
): Promise<Contents.IModel> {
const model = await super.get(localPath, options);
const isDeepnoteFile =
localPath.endsWith('.deepnote') && model.type === 'notebook';
const isDeepnoteFile = localPath.endsWith('.deepnote');

if (!isDeepnoteFile) {
// Not a .deepnote file, return as-is
return model;
const nonDeepnoteModel = await super.get(localPath, options);
return nonDeepnoteModel;
}

const validatedModelContent = deepnoteFileFromServerSchema.safeParse(
model.content
);
// Call custom API route to fetch the Deepnote file content
let data: any;

if (!validatedModelContent.success) {
console.error(
'Invalid .deepnote file content:',
validatedModelContent.error
try {
data = await requestAPI<any>(
`file?path=${encodeURIComponent(localPath)}`
);
// Return an empty notebook instead of throwing an error
model.content.cells = [];
return model;
} catch (error) {
console.error(`Failed to fetch Deepnote file: ${localPath}`, error);
throw new Error(`Failed to fetch .deepnote file: ${error}`);
}

// Transform the Deepnote YAML to Jupyter notebook content
const transformedModelContent =
await transformDeepnoteYamlToNotebookContent(
validatedModelContent.data.metadata.deepnote.rawYamlString
if (!data.deepnoteFileModel) {
throw new Error(
`Invalid API response: missing deepnoteFileModel for ${localPath}`
);
}

const modelData = data.deepnoteFileModel;

// Transform the Deepnote YAML to Jupyter notebook content
const notebookContent = await transformDeepnoteYamlToNotebookContent(
modelData.content
);

const transformedModel = {
...model,
content: transformedModelContent
const model: Contents.IModel = {
name: modelData.name,
path: modelData.path,
type: 'notebook',
writable: false,
created: modelData.created,
last_modified: modelData.last_modified,
mimetype: 'application/x-ipynb+json',
format: 'json',
content: notebookContent
};

return transformedModel;
return model;
}
}