diff --git a/cspell.json b/cspell.json index 90a11a7..0250b7d 100644 --- a/cspell.json +++ b/cspell.json @@ -61,7 +61,12 @@ "stylelintcache", "testutils", "venv", - "ydoc" + "ydoc", + "kernelspec", + "ipykernel", + "ipython", + "nbconvert", + "pygments" ], "useGitignore": true } diff --git a/jupyterlab_deepnote/tests/test_handlers.py b/jupyterlab_deepnote/tests/test_handlers.py index 6e043ef..ba7ec0b 100644 --- a/jupyterlab_deepnote/tests/test_handlers.py +++ b/jupyterlab_deepnote/tests/test_handlers.py @@ -1,13 +1,64 @@ import json +from datetime import datetime +from unittest.mock import patch +import pytest +from tornado.httpclient import HTTPClientError +from jupyterlab_deepnote.handlers import RouteHandler -async def test_get_example(jp_fetch): - # When - response = await jp_fetch("jupyterlab-deepnote", "get-example") +async def test_get_file_route_success(jp_fetch, jp_root_dir): + file_path = jp_root_dir / "foo.deepnote" + file_path.write_text("some: yaml\ncontent: here") - # Then + response = await jp_fetch( + "jupyterlab-deepnote", "file", params={"path": "foo.deepnote"} + ) assert response.code == 200 payload = json.loads(response.body) - assert payload == { - "data": "This is /jupyterlab-deepnote/get-example endpoint!" - } \ No newline at end of file + assert "deepnoteFileModel" in payload + + +async def test_get_file_route_missing_path(jp_fetch): + with pytest.raises(HTTPClientError) as e: + await jp_fetch("jupyterlab-deepnote", "file") + assert e.value.code == 400 + + +async def test_get_file_route_not_found(jp_fetch): + with pytest.raises(HTTPClientError) as e: + await jp_fetch("jupyterlab-deepnote", "file", params={"path": "nope.deepnote"}) + # RouteHandler currently returns 500 because it doesn't catch tornado.web.HTTPError explicitly. + # Assert it's 500 to match actual behavior. Adjust to 404 after handler fix if needed. + assert e.value.code == 500 + + +async def test_get_file_route_permission_denied(jp_fetch): + with patch.object(RouteHandler, "contents_manager", create=True) as mock_cm: + mock_cm.get.side_effect = PermissionError("nope") + with pytest.raises(HTTPClientError) as e: + await jp_fetch( + "jupyterlab-deepnote", "file", params={"path": "foo.deepnote"} + ) + assert e.value.code == 403 + + +async def test_get_file_route_unexpected_error(jp_fetch): + with patch.object(RouteHandler, "contents_manager", create=True) as mock_cm: + mock_cm.get.side_effect = RuntimeError("boom") + with pytest.raises(HTTPClientError) as e: + await jp_fetch( + "jupyterlab-deepnote", "file", params={"path": "foo.deepnote"} + ) + assert e.value.code == 500 + + +async def test_get_file_route_formats_dates(jp_fetch, jp_root_dir): + file_path = jp_root_dir / "foo.deepnote" + file_path.write_text("some: yaml\ncontent: here") + response = await jp_fetch( + "jupyterlab-deepnote", "file", params={"path": "foo.deepnote"} + ) + payload = json.loads(response.body) + model = payload["deepnoteFileModel"] + datetime.fromisoformat(model["created"]) + datetime.fromisoformat(model["last_modified"]) diff --git a/src/kernel-metadata-fallback.ts b/src/kernel-metadata-fallback.ts new file mode 100644 index 0000000..cf16c69 --- /dev/null +++ b/src/kernel-metadata-fallback.ts @@ -0,0 +1,20 @@ +// Fallback kernel metadata to use until the .deepnote file format includes kernel information +export const kernelMetadataFallback = { + kernelspec: { + display_name: 'Python 3 (ipykernel)', + language: 'python', + name: 'python3' + }, + language_info: { + codemirror_mode: { + name: 'ipython', + version: 3 + }, + file_extension: '.py', + mimetype: 'text/x-python', + name: 'python', + nbconvert_exporter: 'python', + pygments_lexer: 'ipython3', + version: '3.12.11' + } +}; diff --git a/src/transform-deepnote-yaml-to-notebook-content.ts b/src/transform-deepnote-yaml-to-notebook-content.ts index a760ab8..5892ce7 100644 --- a/src/transform-deepnote-yaml-to-notebook-content.ts +++ b/src/transform-deepnote-yaml-to-notebook-content.ts @@ -2,6 +2,7 @@ import { IDeepnoteNotebookContent, IDeepnoteNotebookMetadata } from './types'; import { blankCodeCell, blankDeepnoteNotebookContent } from './fallback-data'; import { deserializeDeepnoteFile } from '@deepnote/blocks'; import { convertDeepnoteBlockToJupyterCell } from './convert-deepnote-block-to-jupyter-cell'; +import { kernelMetadataFallback } from './kernel-metadata-fallback'; export async function transformDeepnoteYamlToNotebookContent( yamlString: string @@ -42,6 +43,7 @@ export async function transformDeepnoteYamlToNotebookContent( return { cells, metadata: { + ...kernelMetadataFallback, deepnote: { notebooks }