From 2ecdbc40766db5596d73b92c21fdf13c4fad4677 Mon Sep 17 00:00:00 2001 From: Seth Troisi Date: Tue, 10 Nov 2026 10:37:08 -0800 Subject: [PATCH] fix: serialize format=json downloads --- docs/03_file_management.md | 4 ++-- src/colab_cli/contents.py | 3 +++ tests/test_contents.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/03_file_management.md b/docs/03_file_management.md index be44c9e3..0a14d302 100644 --- a/docs/03_file_management.md +++ b/docs/03_file_management.md @@ -31,7 +31,7 @@ File management on the Colab VM will be implemented using the Jupyter Contents A ### 3. Downloading Files (`colab download`) - **API**: `GET /api/contents/?content=1` (as seen in HAR). - **Response**: JSON with `content` field. -- **Handling**: Decodes content based on `format` (text or base64) and saves it locally. +- **Handling**: Decodes content based on `format` (`base64`, `json`, or plain `text`) and saves it locally. ### 4. Deleting Files (`colab rm`) - **API**: `DELETE /api/contents/`. @@ -59,4 +59,4 @@ TDD is mandatory for all file management features. ### 2. Error Cases - **Test Case**: Verify 404 responses are correctly caught and presented as a "File not found" error to the user. -- **Test Case**: Verify correct handling of large file uploads exceeding API limits via kernel streaming. \ No newline at end of file +- **Test Case**: Verify correct handling of large file uploads exceeding API limits via kernel streaming. diff --git a/src/colab_cli/contents.py b/src/colab_cli/contents.py index 72d6d06c..d794b5cb 100644 --- a/src/colab_cli/contents.py +++ b/src/colab_cli/contents.py @@ -13,6 +13,7 @@ # limitations under the License. import base64 +import json from urllib.parse import quote import requests @@ -82,6 +83,8 @@ def download(self, remote_path: str, local_path: str): if fmt == "base64": content_bytes = base64.b64decode(content) + elif fmt == "json": + content_bytes = json.dumps(content, indent=2).encode("utf-8") else: # Assume text if it's not base64 explicitly encoded content_bytes = str(content).encode("utf-8") diff --git a/tests/test_contents.py b/tests/test_contents.py index 4c910c88..26d0f3c1 100644 --- a/tests/test_contents.py +++ b/tests/test_contents.py @@ -13,6 +13,7 @@ # limitations under the License. import base64 +import json from unittest.mock import MagicMock, patch import pytest @@ -123,6 +124,42 @@ def test_download_file(mock_request, client, tmp_path): assert local_file.read_bytes() == content_bytes +@patch("colab_cli.contents.requests.request") +def test_download_notebook_json_format(mock_request, client, tmp_path): + """Downloading a format=json payload serializes valid 2-space indented JSON.""" + notebook_doc = { + "cells": [ + {"cell_type": "markdown", "metadata": {}, "source": ["# Demo"]}, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["x = 42\n", "print(x)"], + }, + ], + "metadata": {"kernelspec": {"display_name": "Python 3", "name": "python3"}}, + "nbformat": 4, + "nbformat_minor": 5, + } + mock_resp = MagicMock(spec=Response) + mock_resp.status_code = 200 + mock_resp.json.return_value = { + "name": "demo.ipynb", + "type": "notebook", + "format": "json", + "content": notebook_doc, + } + mock_request.return_value = mock_resp + + local_file = tmp_path / "demo.ipynb" + client.download("content/demo.ipynb", str(local_file)) + + raw = local_file.read_text(encoding="utf-8") + assert json.loads(raw) == notebook_doc + assert raw == json.dumps(notebook_doc, indent=2) + + @patch("colab_cli.contents.requests.request") def test_upload_file(mock_request, client, tmp_path): mock_resp = MagicMock(spec=Response)